본문 바로가기

Dev.Project/Todolist_project

[TodoMVC] Step 2. To do list Add Item / list 추가하기

Step 2. To do list에 Item 추가하기

두번째 단계. 이제 본격적인 코딩에 들어간다.

list에 Item 을 생성해서 input tag에 입력한 값을 넣어주자.

list를 추가하는 것과 추가한 list를 포함해, 전에 포함되어있던 list까지 보여줘야 하므로,

다른 Step 보다 많은 작업량과 코드량을 요구한다.


1단계 . template.js 파일을 작성한다.

1-1. 우선 list에 추가될 html 코드 조각을 template화 하자.

template.js code>

1
2
3
4
5
6
7
8
9
10
11
function Template(){
    console.log('template created');
    this.defaultTemplate =
        '<li data-id="{{id}}" class="{{completed}}">' +
            '<div class="view">' +
               '<input class="toggle" type="checkbox" {{checked}}>' +
               '<label>{{title}}</label>' +
               '<button class="destroy"></button>' +
            '</div>' +
        '</li>';
}
cs


이전 Step에서 생성해둔 생성자 함수에 defaultTemplate이라는 프로퍼티로 html 코드 조각을 추가하자.

ul태그 밑으로 list 형식으로 들어가게 되니 li 태그를 작성해준다. ( index.html 코드를 참고하자! )

그리고 HTML5 Custom Data Attribute를 이용하여 id 값을 보다 쉽게 지정하자.

>> HTML5 Custom Data Attribute에 대한 포스팅 >>

우리가 값을 입력될, label tag와

해당 list를 체크하고 해제할 수 있는 input tag와

추후 삭제를 위한 button tag를 삽입해준다.


그리고 이 세 가지 태그를 div tag로 감싸주자.

해당 클래스 명은 Step 0 단계에서 TodoMVC 공식 홈페이지로부터 받은 css를 적용해야하기 때문에 그대로 따르도록 하자.

{{ }}를 통해 변경될 수 있는 사항들을 표시해둔다.

이렇게 템플릿 코드 작성이 끝났다.



1-2. 이제, 어딘가로부터 넘겨받을 data를 템플릿에 삽입할 메소드를 만들어주자.

template.js code>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Template.prototype.insert = function(data){
    console.log('Template.insert method execute!');
    var view = '';
    for(var i = 0; i < data.length; i++){
        var template = this.defaultTemplate;
        var completed = '';
        var checked = '';
 
        if(data[i].completed){ //data[i].completed's default value = false
            completed = 'completed';
            checked = 'checked';
        }
 
        template = template.replace('{{id}}', data[i].id);
        template = template.replace('{{title}}', data[i].title);
        template = template.replace('{{completed}}', completed);
        template = template.replace('{{checked}}', checked);
 
        view = view + template;
    }
    return view;
};
cs


Template에 prototype으로 insert란 메소드를 추가한다.

저 data는 여러 Object를 담고 있는 배열로 다음과 같은 구조를 갖고 있을 것이다.

1
2
3
4
5
6
7
8
//data: from Storage.findAll -> Model.read -> Controller.showAll -> View.render[showEntries]
//      example
//      data[] : {
//              id :
//              title :
//              completed :
//              checked :
//             }, ...
cs


from을 통해 Template에서 data란 인자를 어떻게 전달받는지 나타내었다.

그리고 data : {  } 를 통해 data라는 Object가 어떤 구조로 구성되어 있는지 나타내었다.


다시 위 template.js 코드로 돌아가서,

for 문을 통해, data에 접근하고 있다.

그리고 replace 메소드를 통해 {{ }} 인자들을, data로부터 얻은 값들로 교체해준다.

이렇게 되면 data값으로 치환된 완전한 하나의 템플릿을 생성할 수 있게 된다.

cf ) return 을 해주기 위해, for문 밖에 view라는 임시 변수를 설정하고, for문 밖에서 template 변수를 받아, 더해 리턴해준다.

추가적으로 String으로 Casting 해주는 역할도 포함되어 있다.



2단계 . view.js 파일을 작성한다.

2-1 .우선 template로부터 전달받는 view를 추가시킬 ul tag과 값을 입력할 input tag를 select하자.

view.js code>

1
2
3
4
5
6
7
function View(template){
    console.log('view created!');
    this.template = template;
 
    this.$todoList = document.getElementById('todo-list'); //ul tag
    this.$newTodo = document.getElementById('new-todo'); //input tag
}
cs


생성자 함수에 프로퍼티로 각 태그들을 select하여 추가한다.



2-2. View에서 핵심 역할을 하게 될 두 메소드 중 하나인 bind를 생성하자.

view.js code>
1
2
3
4
5
6
7
8
9
10
11
12
View.prototype.bind = function(event, handler){
    var self = this;
    //event: newTodo
    //handler: controller.addItem
    if(event === 'newTodo'){
        console.log('View.bind.newTodo execute!');
        var temp = self.$newTodo;
        temp.addEventListener('change'function(){
            handler(self.$newTodo.value); //addItem(self.$newTodo.value)
        });
    }
};
cs


bind 메소드에서는 발생하는 event에 따라 다른 함수를 실행시키게 된다.

bind의 역할은 이벤트를 추가하고, 그 이벤트 핸들러를 실행시키는 역할을 하게 된다.

Controller로부터 넘겨온 정보를 받고 그대로 수행만 하는 것이다.

Controller에서는 view에게 역할을 전달하기만 하고, 실질적인 이벤트 수행은 View에서 한다.

하지만 View에서의 이벤트 수행은 데이터를 다루는 수행이 아니고, 해당하는 값을 인자로 넘겨준다.

그럼 Controller에서는 넘겨받은 값을 Model 에게 전달하여 직접적인 데이터와의 연산을 위임한다.



2-3. View에서 핵심 역할을 하게 되는 나머지 메소드인 render를 생성하자.

view.js code>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
View.prototype.render = function(viewCmd, data){
    var self = this;
    var viewCommands = {
        //data로 넘어오는 값이 storage에 있는 모든 data이다. 모든 데이터를 출력하는 역할을 하는 메소드.
        showEntries : function(){
            console.log('View.render.showEntries execute!');
            self._addItem(parameter);
        },
        //입력을 마치고 나면 input tag안을 비워주는 역할을 하는 메소드.
        clearNewTodo : function(){
            console.log('View.render.clearNewTodo execute!');
            self.$newTodo.value = '';
        }
    };
    viewCommands[viewCmd]();
};
cs


render 메소드에는 앞으로 수 많은 함수들이 추가될 것이다.

그리고 그 함수들은 인자로 넘겨받은 viewCmd를 통해서 실행된다.

render 메소드는 어떠한 함수를 실행시킬 것인가에 대한 값( viewCmd )과, data를 인자로 넘겨받는다.

이 data역시 Storage로부터 넘겨 받는 객체들을 담고 있는 배열 data이다.

showEntries라는 이름의 함수는 View에 private으로 설정되어 있는 ( 실제로 private 하지 않다! ) addItem 메소드를 실행시킨다.

clearNewTodo라는 이름의 함수는 input tag를 비워주는 역할을 한다.




2-4. View에서 실질적인 역할을 수행하는 private 메소드를 생성하자. -> _addItem

view.js code>
1
2
3
View.prototype._addItem = function(id){
    this.$todoList.innerHTML = this.template.insert(id);
};
cs


View에서 수행되는 프린팅 작업이다.

innerHTML메소드를 이용하여 template.js를 통해 data가 삽입된 html 코드 조각을 삽입한다.




3단계 . controller.js 파일을 작성한다.

3-1. 우선 생성자 함수에서 view의 메소드를 실행한다.

controller.js code>

1
2
3
4
5
6
7
8
9
10
11
function Controller(model, view){
    console.log('controller created!');
    this.model = model;
    this.view = view;
    var self = this;
    //bind를 통해 레코드 변경을 자동적으로 view에 반영한다.
    this.view.bind('newTodo'function(title){
        self.addItem(title);
    });
    this.showAll();//initializing!
}
cs


초기에 생성되자마자 모든 list를 보여주기 위해 controller prototype에 있는 showAll 메소드를 호출한다.

생성자 함수에는 각종 bind를 통해 view와 communication하게 된다.

이렇게 하게 되면 controller에서 view를 통제하고, model을 통제하는데 수월하다.

이벤트가 발생하게 되면 발생한 이벤트에 해당하는 function이 호출되고,

view에 있는 handler라는 인자를 통해 controller prototype에 저장되어 있는 메소드가 호출된다.



3-2. showAll 메소드를 추가한다.

controller.js code>

1
2
3
4
5
6
7
Controller.prototype.showAll = function(){
    console.log('Controller.showAll method execute!');
    var self = this;
    this.model.read(function(data){
        self.view.render('showEntries', data);
    });
};
cs


Controller의 prototype에 있는 메소드들은 모두 model에 data 조작을 위임하는 메소드들이다.

또한, callback 구조를 통해서 변경된 data에 대한 정보를 view에게 전달하여 rendering 하도록 한다.



3-3. addItem 메소드를 추가한다.

controller.js code>

1
2
3
4
5
6
7
8
9
10
11
Controller.prototype.addItem = function(title){
    console.log('Controller.addItem method execute!');
    var self = this;
    if(title.trim() === ''){
        return;
    }
    self.model.create(title, function(){//값을 저장할 object를 생성한다.
        self.view.render('clearNewTodo', title);//input tag를 비워준다.
    });
    this.showAll();
};
cs


addItem 메소드 역시 model 에게 data 변경 ( 추가 or 수정 )을 요청하고, callback으로 넘어온 값을 view에게 전달한다.

view.bind 로부터 넘어온 title 값이 비어있는 값인지 확인하는 코드가 추가되었다.

this.showAll( )은 추가한 다음 바로 list를 출력하기 위한 메소드이다.

self.view.render 함수는 아직 수행되지 않았다. model에게 callback function으로 넘어간다!




4단계 . model
.js 파일을 작성한다.

4-1 . create 메소드를 작성한다.

model.js code>

1
2
3
4
5
6
7
8
9
10
11
Model.prototype.create = function(title, callback){
    console.log('Model.create method execute!');
    title = title || '';
    callback = callback || function () {};
 
    var newItem = {
        title : title.trim(),
        completed : false
    };
    this.storage.save(newItem, callback);
};
cs


새로운 객체를 생성한다. 이 객체는 storage로 전달되어, data[ ] 배열에 들어갈 것이다.

todo list에서 완료, 미완료를 나타낼 속성인 completed를 객체에 지정해둔다.

실질적은 localStorage에 data 저장은 storage에서 수행하게 된다.

self.view.render 함수는 아직도 수행되지 않았다. storage에게 callback function으로 넘어간다!



4-2 . read 메소드를 작성한다.

model.js code>

1
2
3
Model.prototype.read = function(callback){
    this.storage.findAll(callback);
};
cs


read를 하기 위해서는 localStorage로부터 모든 data 값을 읽어와야한다.

Storage에 저장되어 있는 findAll 메소드를 실행시켜서 값을 읽어오도록 한다.




5단계 . storage.js 파일을 작성한다.

5-1 . 생성자 함수에서 localStorage를 생성하고, 객체를 저장할 data 배열을 생성한다 .

storage.js code>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Storage(name, callback){
    console.log('storage created!');
    callback = callback || function(){};
 
    this._dbName = name;
    //application 실행 1회 때만 생성한다!
    if(!localStorage[name]){
        var data = {
            todos:[]//배열로 생성하여, index로 접근이 가능하도록 한다.
        };
        localStorage[name= JSON.stringify(data);
    }
}
cs


이번 프로젝트에서는 다른 Database가 아닌 localStorage를 통해 데이터를 저장할 것이다.

localStorage에도 name을 지정해줘서 접근하도록 하자.

localStorage에 data를 저장할 때는 JSON.stringify 메소드를 통해서 저장하도록 하자.



5-2 . findAll 메소드를 작성한다.

storage.js code>

1
2
3
4
5
Storage.prototype.findAll = function(callback){
    console.log('Storage.findAll method execute!');
    callback = callback || function() {};
    callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
cs


인자로 넘겨주기 위해서는 저장하는 것과는 반대로 JSON.parse 메소드를 사용한다.

localStorage에 저장되어있는 값을 parsing하여 넘겨받은 callback 함수에 전달해준다.

이 때는 item 하나를 전달하는 것이 아니라  todos라는 배열을 전달하게 된다.



5-3 . 데이터를 localStorage에 저장하는 save 메소드를 작성한다.

storage.js code>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Storage.prototype.save = function(updateData, callback, id){
    console.log('Storage.save method execute!');
    var data = JSON.parse(localStorage[this._dbName]);
    var todos = data.todos;
 
    callback = callback || function(){};
 
    if(id){
        for(var i = 0; i < todos.length; i++){
            if(todos[i].id === id){
                for(var key in updateData){
                    todo[i][key] = updateData[key];
                }
                break;
            }
        }
        localStorage[this._dbName] = JSON.stringify(data);
        callback.call(this, todos);
    } else {
        updateData.id = new Date().getTime();
 
        todos.push(updateData);
        localStorage[this._dbName] = JSON.stringify(data);
        callback.call(this, [updateData]);
    }
};
cs


if 부분말고 else부분부터 보자.

else 부분이 새로운 값을 추가하는 부분이다.

새로운 객체를 data 배열에 추가해주기 위해서는 기존에 localStorage에 저장되어 있는 data를 JSON.parse 메소드를 통해 parsing 해주고, push 메소드를 통해 저장해준다.

이번 프로젝트에서는 복잡한 id 를 부여하기보다는 Date( ).getTime( )을 통해서 현재 시간에 기반한 id 값을 부여하기로 하자.

여기에서 callback은 input tag에 입력된 값을 저장했으면, input tag에 남아있는 text를 비워주는 view의 함수가 담겨져 있다.

if 부분은 이번 Step 에서 중요하지 않다.

추후 update 기능을 추가하는 Step에서 다룰 예정이다.




-The End-