프로그래밍/Vue.js
[Vue.js] 간단한 todo app 구현
dev109
2018. 2. 22. 16:23
반응형
프로젝트 폴더 구조
기능
입력폼에 데이터 입력 후 엔터 혹은 add 버튼 클릭 시 데이터 추가
탭 [todo, finish] 클릭 시 해당 목록 출력
todo 목록에서 완료버튼 클릭 시 finish로 이동
finish 목록에서 reset버튼 클릭 시 todo로 이동
vue-cli 설치(전역)
sudo npm install vue-cli
'todo-app' 폴더 생성 후 webpack-simple 템플릿으로 프로젝트 생성
mkdir todo-app
cd todo-app
vue init webpack-simple
전부 enter(기본값)
npm install
npm run dev
프로젝트 생성 후 폴더 구조
todo-app/style.css(생성 후 작성)
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | body, ul { margin: 0; padding: 0; } ul { list-style: none; } img { width: 100%; } .container { margin: 0 15px 0 15px; } header { border-bottom: 1px #ccc solid; padding: 10px 0 10px 0; text-align: center; } input[type=text] { display: block; box-sizing: border-box; width: 100%; margin: 15px 0 15px 0; padding: 10px 15px; font-size: 14px; line-height: 1.5; border: 1px solid #cccccc; } .content { border: 1px solid #ccc; } ul.tabs { display: flex; } .tabs li { display: inline-block; width: 50%; padding: 15px; text-align: center; box-sizing: border-box; border-bottom: 1px solid #ccc; background-color: #eee; color: #999; } .tabs li.active { /* background-color: #2ac1bc; */ background-color: #045FB4; color: #fff; } .list li { box-sizing: border-box; display: block; padding: 15px; border-bottom: 1px solid #ccc; position: relative; } .list li:last-child { border-bottom: none; } .list li .number{ margin-right: 15px; color: #ccc; } .list li .date{ position: absolute; right: 50px; top: 15px; margin-right: 15px; color: #ccc; } .list li .btn-remove{ position: absolute; right: 0px; top: 15px; margin-right: 15px; } form { position: relative; } .btn-reset, .btn-remove { border-radius: 15%; background-color: #ccc; color: white; border: none; padding: 2px 5px; } .btn-reset { position: absolute; top: 12px; right: 10px; } .btn-reset::before, .btn-remove::before { content: 'add' } #search-result li { display: flex; margin-bottom: 15px; } #search-result img { width: 30%; height: 30%; } .todoBtn { float: right; margin: 2px; } | cs |
todo-app/index.html 수정
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>todo-app</title> <link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"> </head> <body> <div id="app"></div> <script src="/dist/build.js"></script> </body> </html> | cs |
src/components/FormComponent.vue
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 27 28 29 | <template> <form v-on:enter.prevent="addTodo"> <input type="text" v-model="inputValue" /> <button class="btn-reset" v-on:click.prevent="addTodo"></button> <!-- v-on:click => click 이벤트 리슨 (.prevent 속성을 추가하면 이벤트 전파를 중단함) --> </form> </template> <script> export default { props: ['value'], //부모(App.vue)로부터 받아옴 data() { return { inputValue: this.value } }, methods: { addTodo() { //$emit => 부모(App.vue)에게 @submit이라는 이름으로 이벤트 전달 //this.inputValue 인자로 같이 전달 this.$emit('@submit', this.inputValue) //데이터 추가 후 form을 비우기 위해 inputValue값을 비워줌 this.inputValue = '' } } } </script> | cs |
src/components/TabComponent.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <template> <ul class="tabs"> <li v-for="tab in tabs" v-bind:class="{active: tab === selectedTab}" v-on:click="onClickTabs(tab)"> <!-- v-bind:class => 선택한 탭에 active class를 지정 --> {{tab}} </li> </ul> </template> <script> export default { props: ['tabs', 'selectedTab'], //부모(App.vue)로부터 받아옴 => tabs(todo, finish), selectedTab : 현재 선택한 탭 methods: { onClickTabs(tab) { //$emit => 부모(App.vue)에게 @change 이름으로 이벤트 전달 //tab(선택한 탭) 인자로 같이 전달 this.$emit('@change', tab) } } } </script> | cs |
src/components/ListComponent.vue
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 27 28 29 30 31 32 33 34 35 | <template> <div> <ul class="list"> <li v-for="(item, index) in data"> <span class="glyphicon glyphicon-asterisk"></span> {{item.todo}} <span class="todoBtn label label-primary" v-if="selectedTab === 'todo'" v-on:click="finishBtnClick(index)"> <span class="glyphicon glyphicon-ok"></span> </span> <span class="todoBtn label label-danger" v-if="selectedTab === 'finish'" v-on:click="resetBtnClick(index)">reset</span> </li> </ul> </div> </template> <script> export default { props: ['data', 'selectedTab'], methods: { finishBtnClick(item) { this.$emit('@finish', item) }, resetBtnClick(item) { this.$emit('@reset', item) } } } </script> | cs |
src/models/TodoModel.js
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 27 28 29 30 31 32 33 34 35 36 37 | export default { data: [ {todo: '할 일 0', state: true}, {todo: '할 일 1', state: true}, {todo: '할 일 2', state: false}, {todo: '할 일 3', state: true}, {todo: '할 일 4', state: false} ], list(tab) { return new Promise(res => { if(tab === 'todo') res(this.data.filter(item => item.state === true)) if(tab === 'finish') res(this.data.filter(item => item.state === false)) }) }, add(todo = '') { todo = todo.trim() if (!todo) return const state = true this.data.push({todo, state}) }, finish(index) { this.data.filter(item => item.state === true)[index].state = false }, reset(index) { this.data.filter(item => item.state === false)[index].state = true }, remove(todo) { this.data = this.data.filter(item => item.todo !== todo) } } | cs |
src/App.vue
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | <template> <div> <header> <h2 calss="container">Todo</h2> </header> <div calss="container"> <add-form v-bind:value="query" v-on:@submit="onInputTodo"></add-form> <!-- FormComponent.vue에서 inputValue에 넣어준 value를 v-bind가 App.vue의 query와 바인딩해줌 --> <!-- v-on:@submit => FormComponent.vue에서 $emit으로 전달한 @submit이라는 이벤트를 받아와서 onInputTodo 메서드와 연결 --> <tab v-bind:tabs="tabs" v-bind:selected-tab="selectedTab" v-on:@change="onClickTab"></tab> </div> <div> <list v-bind:selected-tab="selectedTab" v-bind:data="todoList" v-on:@finish="onClickFinish" v-on:@reset="onClickReset"></list> </div> </div> </template> <script> //FormComponent 불러옴 import FormComponent from './components/FormComponent.vue' //TabComponent 불러옴 import TabComponent from './components/TabComponent.vue' //ListComponent 불러옴 import ListComponent from './components/ListComponent.vue' //Model 불러옴 import TodoModel from './models/TodoModel.js' export default { name: 'app', data () { return { query: '', tabs: ['todo', 'finish'], selectedTab: '', todoList: [] } }, created() { //vue 인스턴스가 생성된 후에 실행됨 this.selectedTab = this.tabs[0] //todo 탭 선택 this.search() //todo list 출력 }, components: { //사용할 컴포넌트 등록 'add-form': FormComponent, 'tab': TabComponent, 'list': ListComponent }, methods: { search() { //list 검색 TodoModel.list(this.selectedTab).then(data => { this.todoList = data }) }, onClickTab(tab) { //tab 선택 this.selectedTab = tab this.search() }, onClickFinish(item) { //todo 완료 TodoModel.finish(item) this.search() }, onClickReset(item) { //완료된 todo 리셋 TodoModel.reset(item) this.search() }, onInputTodo(query) { //todo 입력 TodoModel.add(query) this.selectedTab = this.tabs[0] this.search() } } } </script> <style> </style> | cs |
반응형