반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리


반응형
지난 시간에는 New 버튼을 달고 더미 데이터를 넣어서 List 를 출력해 봤습니다.
잘 되셨나요?

지난주까지 진행한 소스코드 아래에 있습니다.
필요하신 분은 받아서 보세요.


오늘은 Note Editor 부분을 하겠습니다.
Notes List 에서 Note Editor 화면으로 전환하고 새 데이터를 넣고 저장하고 이것을 List 로 새로 출력하는 기능을 할 겁니다.
이 과정에서 validation도 체크하는 기능도 다룰거구요.

아래 오늘의 내용을 보시죠.

Writing a Sencha Touch Application, Part 3

Part 1,2 에서는 Notes 앱 중에 Notes List view 부분을 다뤘습니다. 오늘은 Note Editor view를 다루겠습니다.
타이틀은 Edit Note 가 될 텐데요. Note의 생성, update, 삭제 기능이 있을 겁니다.
아래 Mock up 이미지와 완성 화면이 있습니다.








***** Creating a Sencha Touch Form Panel

Note Editor mockup을 보면 우리는 툴바와 필드들을 만들어야 합니다.
우선 form field들 부터 만들겠습니다.

NotesApp.views.noteEditor = new Ext.form.FormPanel({
    id: 'noteEditor',
    items: [
        {
            xtype: 'textfield',
            name: 'title',
            label: 'Title',
            required: true
        },
        {
            xtype: 'textareafield',
            name: 'narrative',
            label: 'Narrative'
        }
    ]
});


이 필드들을 만들기 위해 Ext.form.FormPanel 클래스를 사용했습니다. 센차터치에서 form 필드들을 만들 때 가장 손쉽게 만들 수 있는 방법입니다.
제목은 textfield를 사용하고 내용을 넣는 공간은 Text field를 사용합니다.
title 부분에 required 를 true로 했습니다. 반드시 이곳에 값을 넣어야 한다는 겁니다. 디비의 not null 이죠. 나중에 save 하는 단계에서 이곳에 값을 넣지 않으면 어떻게 되는지 보실 수 있습니다.

툴바를 넣기 전에 이 form들의 모습이 어떤지 미리 보면 좋겠죠? viewport 부분을 약간 수정해서 이 form이 보이도록 해 보죠.
viewport의 items 부분을 수정해 아래처럼 수정해 보세요. 그러면 지난번에 만들었던 list가 아니라 edit form이 나올겁니다.

NotesApp.views.viewport = new Ext.Panel({
    fullscreen: true,
    layout: 'card',
    cardAnimation: 'slide',
    items: [NotesApp.views.noteEditor] // TODO: revert to [NotesApp.views.notesListContainer]
});

제대로 됐다면 아래처럼 보일겁니다.




***** Adding top and bottom toolbars to a Sencha Touch Panel

이제 툴바를 만들 차례입니다.
첫번째 툴바는 Home 과 Save 버튼이 있는 위쪽 툴바입니다.

NotesApp.views.noteEditorTopToolbar = new Ext.Toolbar({
    title: 'Edit Note',
    items: [
        {
            text: 'Home',
            ui: 'back',
            handler: function () {
                // TODO: Transition to the notes list view.
            }
        },
        { xtype: 'spacer' },
        {
            text: 'Save',
            ui: 'action',
            handler: function () {
                // TODO: Save current note.
            }
        }
    ]
});

위 소스 콛를 보시면 툴바를 만들면서 ui config 옵션을 사용하신 걸 보실 수 있을 겁니다. (back, action 으로 정의했죠?) home 버튼에는 back 이라고 정의했습니다. 왜냐하면 이 버튼을 누르면 back 해서 main view 로 갈 것이기 때문입니다. 그리고 save 버튼에는 action이라고 정의했는데요. 이것은 유저가  이 버튼을 누르면 note를 save 할 거라는 겁니다. 이 기능이 가장 중요한 기능 중 하나죠?



화면 아래에 있는 툴바에는 휴지통 버튼이 들어갈 겁니다. 이 버튼을 누르면 해당 note 가 delete 되겠죠?
NotesApp.views.noteEditorBottomToolbar = new Ext.Toolbar({
    dock: 'bottom',
    items: [
        { xtype: 'spacer' },
        {
            iconCls: 'trash',
            iconMask: true,
            handler: function () {
                // TODO: Delete current note.
            }
        }
    ]
});

여기에 몇가지 주의하실 부분이 있습니다. 첫번째로 dock config를 어떻게 사용했는지 보세요. 화면 아래에 배치하기 위해 bottom을 사용했습니다. 그리고 iconCls와 iconMask config 옵션을 보세요. 이 기능들은 버튼 안에 trash icon을 render 하기 위한 옵션들입니다.



이제 툴바를 add 하실 수 있습니다.

NotesApp.views.noteEditor = new Ext.form.FormPanel({
    id: 'noteEditor',
    items: [
        {
            xtype: 'textfield',
            name: 'title',
            label: 'Title',
            required: true
        },
        {
            xtype: 'textareafield',
            name: 'narrative',
            label: 'Narrative'
        }
    ],
    dockedItems: [
            NotesApp.views.noteEditorTopToolbar,
            NotesApp.views.noteEditorBottomToolbar
        ]
});

약간 코드를 수정해서 지금까지 한 작업을 화면으로 볼 수 있도록 해 보죠.

NotesApp.views.viewport = new Ext.Panel({
    fullscreen: true,
    layout: 'card',
    cardAnimation: 'slide',
    items: [NotesApp.views.noteEditor] // TODO: revert to [NotesApp.views.notesListContainer]
});

성공했으면 아래와 같은 화면을 보실 수 있을 겁니다.




아주 훌륭하죠?

지금까지 우리는 Notes List와 Note Editor 뷰를 만들었습니다. 이제 필요한 것은 이 앱의 workflow 입니다. 뷰 이외에 다른 비지니스 로직을 어떻게 구현하느냐를 말하는 것이죠. 우선 첫번째 기억할 비지니스 로직(Navigate screen) 은 Notes List 에서 New 버튼을 누르면 Notes Edit View 로 간다는 겁니다.



****** How to change views in a Sencha Touch application

이제 Notes List view 로 가 볼까요? 이제 New 버튼의 handler 부분을 다룰 차례입니다. 이 버튼이 눌려지면 새로운 note를 만들 수 있어야 합니다. 그러니까 이 버튼을 누르면 Note Editor를 화면에 보이도록 하면 되곘죠.

NotesApp.views.notesListToolbar = new Ext.Toolbar({
    id: 'notesListToolbar',
    title: 'My Notes',
    layout: 'hbox',
    items: [
        { xtype: 'spacer' },
        {
            id: 'newNoteButton',
            text: 'New',
            ui: 'action',
            handler: function () {

                var now = new Date();
                var noteId = now.getTime();
                var note = Ext.ModelMgr.create(
                    { id: noteId, date: now, title: '', narrative: '' },
                    'Note'
                );

                NotesApp.views.noteEditor.load(note);
                NotesApp.views.viewport.setActiveItem('noteEditor', {type: 'slide', direction: 'left'});
            }
        }
    ]
});

순서대로 한번 볼까요?
첫번째로 ModelMgr 클래스의 create()메소드를 사용해서 새로운 note를 생성합니다.

var now = new Date();
var noteId = now.getTime();
var note = Ext.ModelMgr.create(
    { id: noteId, date: now, title: '', narrative: '' },
    'Note'
);

그리고 나서 이 새 note를 Note Editor에 전해줍니다. FormPanel에서 이 부분이 보이도록 하기 위해 load 메소드를 사용했습니다. 이 load 메소드는 필드들을 보여줄겁니다. 이때 value 값들이 있으면 그 value 값들도 보여 줄 거구요.

NotesApp.views.noteEditor.load(note);

다음으로는 우리가 viewport에서 card layout을 사용했듯이 viewport이 setActiveItem()메소드를 사용해서 Note Editor를 보이도록 할 겁니다.
이 setActiveItem()을 call 하면 우리는 우리가 active 시키려고 하는 card의 id를 전달해 주고 그 객체가 transition 하는 animation 기능을 사용할 수 있습니다.

NotesApp.views.viewport.setActiveItem('noteEditor', {type: 'slide', direction: 'left'});

이 New 버튼이 어떻게 작동하는지 확인하기 전에 viewport의 items array를 약간 수정해야 합니다.

NotesApp.views.viewport = new Ext.Panel({
    fullscreen: true,
    layout: 'card',
    cardAnimation: 'slide',
    items: [
        NotesApp.views.notesListContainer,
        NotesApp.views.noteEditor
    ]
});



여기까지 완성 됐나요? 그러면 이제 user가 작성한 note를 save 할 수 있도록 하시면 됩니다.
이 기능은 당연히 Note Editor의 Save 버튼에 달아야 겠죠.

***** Validating a data model in Sencha Touch

save 버튼을 누르게 되면 우선 체크해야 할 것들이 몇가지 있습니다.

1. form field의 title과 narrative  정보는 Note Model 인스턴스에서 capture 됩니다.
2. note의 title의 value 가 없으면 유저에게 alert 화면을 띄울 겁니다.
3. note 가 new일 경우 이 note를 cache에 add 할 겁니다. 이 note 가 이전에 저장 됐던 것이면 cache에 있는 해당 note를 update 할 겁니다.
4. 그리고 나서 Notes List 를 refresh 해서 보여줄 겁니다.

자 이 기능들을 구현해 볼까요?
save 버튼의 tap 핸들러를 구현하기 전에 Note data model을 수정해서 validation 부분을 구현하는데 좀 더 편하도록 해 봅시다. 우리가 할 것은 model의 title 필드의 validation 함수를 사용해서 message 프로퍼티를 override 할 겁니다. 이 기능은 이 필드가 invalid 할 경우 메세지를 띄우는 기능입니다.

Ext.regModel('Note', {
    idProperty: 'id',
    fields: [
        { name: 'id', type: 'int' },
        { name: 'date', type: 'date', dateFormat: 'c' },
        { name: 'title', type: 'string' },
        { name: 'narrative', type: 'string' }
    ],
    validations: [
        { type: 'presence', field: 'id' },
        { type: 'presence', field: 'title', message: 'Please enter a title for this note.' }
    ]
});

이 메세지는 우리가 Note Editor view 에서 note의 validation을 할 때 사용할 겁니다.
이제 save 버튼의 tap 핸들러를 구현해서 이 validation 이 어떻게 이뤄지는지 확인해 보겠습니다.

NotesApp.views.noteEditorTopToolbar = new Ext.Toolbar({
    title: 'Edit Note',
    items: [
        {
            text: 'Home',
            ui: 'back',
            handler: function () {
                NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });
            }
        },
        { xtype: 'spacer' },
        {
            text: 'Save',
            ui: 'action',
            handler: function () {

                var noteEditor = NotesApp.views.noteEditor;

                var currentNote = noteEditor.getRecord();
                // Update the note with the values in the form fields.
                noteEditor.updateRecord(currentNote);

                var errors = currentNote.validate();
                if (!errors.isValid()) {
                    currentNote.reject();
                    Ext.Msg.alert('Wait!', errors.getByField('title')[0].message, Ext.emptyFn);
                    return;
                }

                var notesList = NotesApp.views.notesList;
                var notesStore = notesList.getStore();

                if (notesStore.findRecord('id', currentNote.data.id) === null) {
                    notesStore.add(currentNote);
                } else {
                    currentNote.setDirty();
                }

                notesStore.sync();
  notesStore.sort([{ property: 'date', direction: 'DESC'}]);

                notesList.refresh();

                NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });

            }
        }
    ]
});

이 handler 함수에서 첫번째로 form panel의 getRecord() 메소드를 사용했습니다. 이 메소드는 form에 Note model이 로드되는 reference를 가질 수 있도록 해 줍니다. 그 다음에는 updateRecord() 메소드를 사용합니다. 이 메소드는 이 model reference로 폼 필드로부터 value들을 가져와서 전달해 줍니다.
validation dms Note data model에서 validate()를 call 함으로서 Errors 객체를 return 하도록 합니다. isValid()는 이 과정에서 에러가 있는지 없는지 알 수 있게 해 줍니다. 지난 시간에 했듯이 Note Editor 에서는 title 만이 not null입니다. 그래서 이 앱에서는 그 부분만 체크할 겁니다. 에러를 확인하기 위해서는 model의 validations config 옵션에서 정의했듯이 errors.getByField('title')[0] 메세지를 통해서 확인할 수 있습니다.

이 validation을 통화 하면 이 note를 cache에 add 해야 합니다. 대신 이 데이터가 이전에 있는지 확인하는 기능이 있어야 합니다.
아래 findRecord()메소드를 통해서 이 작업을 간단하게 해 주시구요.

var notesStore = notesList.getStore();
if (notesStore.findRecord('id', currentNote.data.id) === null) {
    notesStore.add(currentNote);
}


update 시키신 후에는 sync() 메소드를 이용해서 완전히 이 데이터를 저장하시면 됩니다. 오라클에서 commit; 기능하고 비슷합니다.
그 다음에 sort() 메소드를 사용해서 note를 날짜 순서로 정렬합니다. 그러면 notes list에 render 할 준비가 끝나는 겁니다.


notesList.refresh();

NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });

type을 보시면 slide와 right가 있을 겁니다. 이것은 화면 전환 할 때 된쪽에서 오른쪽으로 slide하게 화면이 바뀌도록 합니다.

이제 남은 것은 Home button에 handler를 구현하는 겁니다.
이 홈 버튼을 누르면 단순히 Notes List로 돌아가기만 하면 됩니다.

{
    text: 'Home',
    ui: 'back',
    handler: function () {
        NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });'s next
    }
}

한번 화면으로 테스트 해 보고 싶으신가요? 이제 실행 해 보시면 save하고 새 note를 create하는 것들이 다 가능할 겁니다.

***** What’s next
다음에 우리가 구현 할 것은 기존에 존재하는 notes를 수정하고 지우는 기능입니다.
이 기능은 다음 시간에 구현해 보겠습니다.

오늘까지 한 js 소스는 아래와 같습니다.

var App = new Ext.Application({
    name: 'NotesApp',
    useLoadMask: true,
    launch: function () {

        Ext.regModel('Note', {
            idProperty: 'id',
            fields: [
                { name: 'id', type: 'int' },
                { name: 'date', type: 'date', dateFormat: 'c' },
                { name: 'title', type: 'string' },
                { name: 'narrative', type: 'string' }
            ],
            validations: [
                { type: 'presence', field: 'id' },
                { type: 'presence', field: 'title', message: 'Please enter a title for this note.' }
            ]
        });

        Ext.regStore('NotesStore', {
            model: 'Note',
            sorters: [{
                property: 'date',
                direction: 'DESC'
            }],
            proxy: {
                type: 'localstorage',
                id: 'notes-app-store'
            }
        });

        NotesApp.views.noteEditorTopToolbar = new Ext.Toolbar({
            title: 'Edit Note',
            items: [
                {
                    text: 'Home',
                    ui: 'back',
                    handler: function () {
                        NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });
                    }
                },
                { xtype: 'spacer' },
                {
                    text: 'Save',
                    ui: 'action',
                    handler: function () {

                        var noteEditor = NotesApp.views.noteEditor;

                        var currentNote = noteEditor.getRecord();
                        // Update the note with the values in the form fields.
                        noteEditor.updateRecord(currentNote);

                        var errors = currentNote.validate();
                        if (!errors.isValid()) {
                            currentNote.reject();
                            Ext.Msg.alert('Wait!', errors.getByField('title')[0].message, Ext.emptyFn);
                            return;
                        }

                        var notesList = NotesApp.views.notesList;
                        var notesStore = notesList.getStore();

                        if (notesStore.findRecord('id', currentNote.data.id) === null) {
                            notesStore.add(currentNote);
                        } else {
                           currentNote.setDirty();
                        }

                        notesStore.sync();
                        notesStore.sort([{ property: 'date', direction: 'DESC'}]);

                        notesList.refresh();

                        NotesApp.views.viewport.setActiveItem('notesListContainer', { type: 'slide', direction: 'right' });

                    }
                }
            ]
        });

        NotesApp.views.noteEditorBottomToolbar = new Ext.Toolbar({
            dock: 'bottom',
            items: [
                { xtype: 'spacer' },
                {
                    iconCls: 'trash',
                    iconMask: true,
                    handler: function () {
                        // TODO: Delete current note.
                    }
                }
            ]
        });

        NotesApp.views.noteEditor = new Ext.form.FormPanel({
            id: 'noteEditor',
            items: [
                {
                    xtype: 'textfield',
                    name: 'title',
                    label: 'Title',
                    required: true
                },
                {
                    xtype: 'textareafield',
                    name: 'narrative',
                    label: 'Narrative'
                }
            ],
            dockedItems: [
                    NotesApp.views.noteEditorTopToolbar,
                    NotesApp.views.noteEditorBottomToolbar
                ]
        });

        NotesApp.views.notesList = new Ext.List({
            id: 'notesList',
            store: 'NotesStore',
            itemTpl: '
<div class="list-item-title">{title}</div>
' +
                '
<div class="list-item-narrative">{narrative}</div>
',
            onItemDisclosure: function (record) {
                // TODO: Render the selected note in the note editor.
            },
            listeners: {
                'render': function (thisComponent) {
                    thisComponent.getStore().load();
                }
            }
        });

        NotesApp.views.notesListToolbar = new Ext.Toolbar({
            id: 'notesListToolbar',
            title: 'My Notes',
            layout: 'hbox',
            items: [
                { xtype: 'spacer' },
                {
                    id: 'newNoteButton',
                    text: 'New',
                    ui: 'action',
                    handler: function () {

                        var now = new Date();
                        var noteId = now.getTime();
                        var note = Ext.ModelMgr.create(
                            { id: noteId, date: now, title: '', narrative: '' },
                            'Note'
                        );

                        NotesApp.views.noteEditor.load(note);
                        NotesApp.views.viewport.setActiveItem('noteEditor', { type: 'slide', direction: 'left' });
                    }
                }
            ]
        });

        NotesApp.views.notesListContainer = new Ext.Panel({
            id: 'notesListContainer',
            layout: 'fit',
            html: 'This is the notes list container',
            dockedItems: [NotesApp.views.notesListToolbar],
            items: [NotesApp.views.notesList]
        });

        NotesApp.views.viewport = new Ext.Panel({
            fullscreen: true,
            layout: 'card',
            cardAnimation: 'slide',
            items: [
                NotesApp.views.notesListContainer,
                NotesApp.views.noteEditor
            ]
        });
    }
});

이제 저도 Sencha Touch 에 대해 대충 감이 잡히네요.
다음시간에 첫번째 센차터치 앱 Notes 를 완료하겠습니다.
그리고 계속 하이브리드 모바일 앱을 위해 Corona SDK, HTML5, Sencha Touch, Phone Gap 등 관련 기술을 익히고 싶네요.

꾹~~ 꾹~~ 응원 부탁드려요... ~~~~~~~
반응형