GYUMIN DEV LOG

이번 포스팅에서는 구현된 ListView에 CRUD와 search기능을 추가하도록하겠습니다.


CRUD는 각각 add, edit, delete 버튼을 이용해 구현되며 Dialog인 Modal을 띄워서 구현하겠습니다.

Modal을 구현하기 위해서 BaseModal 이름으로 컴포넌트를 추가생성하였으며, 


BaseModal은 addDialog, EditDialog, DeleteDialog 3개의 Modal을 띄울 때 재사용하는 컴포넌트입니다.


이와 같이 Vue.js는 컴포넌트를 재사용 할 수 있는것이 장점입니다.


search기능은 input form에 입력을 하면 바로바로 입력된 글자가 subject에 포함되어있는 데이터만 보여지게 구현하였습니다.

v-model을 이용해서 data 속성에 있는 search 값을 맵핑해놓으면 search값이 변할 때마다 자동으로 UI가 갱신되게 됩니다.



<template> 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
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<template>
  <v-app id="list-sample">
    <v-flex sm6 offset-sm3>
      <v-text-field label="Search Data..."
                    v-model="search">
      </v-text-field>
      <v-list two-line
              v-for="(listItem, index) in calData"
              :key="index">
        <v-list-tile>
          <v-list-tile-content>
            <v-list-tile-title class="text--primary">
              {{ listItem.subject }}
            </v-list-tile-title>
            <v-list-tile-sub-title>
              {{ listItem.desc }}
            </v-list-tile-sub-title>
          </v-list-tile-content>
 
          <v-list-tile-action>
            <v-icon ripple
                    @click="onEdit(index)">
              edit
            </v-icon>
            <v-icon ripple
                    @click="onRemove(index)">
              delete
            </v-icon>
          </v-list-tile-action>
        </v-list-tile>
        <v-divider></v-divider>
      </v-list>
      <br/>
      <v-pagination
        v-model="curPageNum"
        :length="numOfPages">
      </v-pagination>
    </v-flex>
 
    <v-layout class="create-data-btn">
      <v-btn fab
             dark
             color="indigo"
             @click="onAdd">
        <v-icon dark>
          add
        </v-icon>
      </v-btn>
    </v-layout>
 
    <v-dialog v-model="addDialog"
              persistent
              max-width="500px"
              @keydown.esc="closeDialog">
      <base-modal>
        <div slot="title"
             class="headline">Add Data
        </div>
        <v-container slot="content"
                     grid-list-md>
          <v-layout wrap>
            <v-flex xs12>
              <v-text-field label="Subject"
                            v-model="newData.subject"
                            required>
              </v-text-field>
            </v-flex>
            <v-flex xs12>
              <v-text-field
                            label="Desc"
                            v-model="newData.desc"
                            @keydown.enter="submit"
                            required>
              </v-text-field>
            </v-flex>
          </v-layout>
        </v-container>
        <v-btn slot="negative-button"
               color="blue darken-1"
               flat
               @click="closeDialog">
          Close
        </v-btn>
        <v-btn slot="positive-button"
               color="blue darken-1"
               flat
               @click="submit">
          Save
        </v-btn>
      </base-modal>
    </v-dialog>
 
    <v-dialog v-model="editDialog"
              persistent
              max-width="500px"
              @keydown.esc="closeDialog">
      <base-modal>
        <div slot="title"
             class="headline">Edit Data
        </div>
        <v-container slot="content"
                     grid-list-md>
          <v-layout wrap>
            <v-flex xs12>
              <v-text-field
                label="Subject"
                v-model="selectedData.subject"
                required>
              </v-text-field>
            </v-flex>
            <v-flex xs12>
              <v-text-field
                label="Desc"
                v-model="selectedData.desc"
                required
                @keydown.enter="editData">
              </v-text-field>
            </v-flex>
          </v-layout>
        </v-container>
        <v-btn slot="negative-button"
               color="blue darken-1"
               flat
               @click="closeDialog">
          Close
        </v-btn>
        <v-btn slot="positive-button"
               color="blue darken-1"
               flat
               @click="editData">
          Save
        </v-btn>
      </base-modal>
    </v-dialog>
 
    <v-dialog v-model="removeDialog"
              max-width="500px"
              @keydown.esc="removeDialog=false"
              @keydown.enter="removeData"
              persistent>
      <base-modal>
        <div slot="title"
             class="headline">Remove Data
        </div>
        <v-btn slot="negative-button"
               color="blue darken-1"
               flat
               @click="closeDialog">
          Cancel
        </v-btn>
        <v-btn slot="positive-button"
               color="blue darken-1"
               flat
               @click="removeData">
          Confirm
        </v-btn>
      </base-modal>
    </v-dialog>
  </v-app>
</template>
cs



template 전체 코드입니다. 

add, edit, delete Icon에는 각각 onAdd, onEdit, onRemove 함수가 호출되도록 맵핑되어있습니다.



Modal 기능에 대해서는 추가된 BaseModal을 재사용해 각각의 modal을 화면에 보여주도록 하고 있습니다.


BaseModal.vue Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
  <v-card>
    <v-card-title>
      <slot name="title"></slot>
    </v-card-title>
    <v-card-text>
      <slot name="content"></slot>
    </v-card-text>
    <v-card-actions>
      <v-spacer></v-spacer>
      <slot name="negative-button"></slot>
      <slot name="positive-button"></slot>
    </v-card-actions>
  </v-card>
</template>
cs


vue.js의 slot 속성을 이용해서 각각의 slot을 만들어 놓고 ListSample에서는 slot의 가상 dom을 삽입하는 방식으로 구현하였습니다.


그 중 데이터를 추가할 때 불리는 modal입니다.

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
<v-dialog v-model="addDialog"
              persistent
              max-width="500px"
              @keydown.esc="closeDialog">
      <base-modal>
        <div slot="title"
             class="headline">Add Data
        </div>
        <v-container slot="content"
                     grid-list-md>
          <v-layout wrap>
            <v-flex xs12>
              <v-text-field label="Subject"
                            v-model="newData.subject"
                            required>
              </v-text-field>
            </v-flex>
            <v-flex xs12>
              <v-text-field
                            label="Desc"
                            v-model="newData.desc"
                            @keydown.enter="submit"
                            required>
              </v-text-field>
            </v-flex>
          </v-layout>
        </v-container>
        <v-btn slot="negative-button"
               color="blue darken-1"
               flat
               @click="closeDialog">
          Close
        </v-btn>
        <v-btn slot="positive-button"
               color="blue darken-1"
               flat
               @click="submit">
          Save
        </v-btn>
      </base-modal>
    </v-dialog>
cs


BaseModal에서 만든 slot에 UI를 구성하여 넣어주고 있고 각 input Form에 대응하는 data, 버튼 이벤트에 알맞는 함수를 맵핑해주고있습니다.



<script> 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
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<script>
  import BaseModal from './modal/BaseModal'
 
  export default {
    data() {
      return {
        addDialog: false,
        editDialog: false,
        removeDialog: false,
        search: '',
        listData: [],
        searchData: [],
        dataPerPage: 10,
        curPageNum: 1,
        curSelectIndex: 0,
 
        newData: {
          id: Number,
          subject: '',
          desc: ''
        },
        selectedData: {
          id: Number,
          subject: '',
          desc: ''
        }
      }
    },
    created() {
      this.axios.get('http://localhost:8080/server/listTest.json')
        .then((response) => {
          this.listData = response.data;
          console.log(this.listData);
        });
    },
    computed: {
      startOffset() {
        return ((this.curPageNum - 1* this.dataPerPage);
      },
      endOffset() {
        return (this.startOffset + this.dataPerPage);
      },
      numOfPages() {
        return Math.ceil(this.searchData.length / this.dataPerPage);
      },
      calData() {
        this.searchData = this.listData.filter((data) => {
          return data.subject.toLowerCase().includes(this.search.toLowerCase())
        }).slice(0);
 
        return this.searchData.slice(this.startOffset, this.endOffset)
      }
    },
    methods: {
      submit() {
        const submitData = {
          id: this.newData.id,
          subject: this.newData.subject,
          desc: this.newData.desc
        };
        /*
        Ajax 통신 후
         */
        this.addData(submitData);
        this.closeDialog();
        this.clearForm();
      },
      editData() {
        const editData = {
          id: this.selectedData.id,
          subject: this.selectedData.subject,
          desc: this.selectedData.desc
        };
        /*
        Ajax 통신 후
         */
        this.listData.splice(this.curSelectIndex, 1, editData);
        this.closeDialog();
      },
      addData(submitData) {
        this.listData.push(submitData);
        this.curPageNum = this.numOfPages;
      },
      removeData() {
        this.listData.splice(this.curSelectIndex, 1);
        if (this.calData.length === 0) {
          this.prevPage();
        }
        this.closeDialog();
      },
      onAdd() {
        this.addDialog = true;
      },
      onRemove(index) {
        this.curSelectIndex = this.calIndex(index);
 
        this.removeDialog = true;
      },
      onEdit(index) {
        this.curSelectIndex = this.calIndex(index);
        this.selectedData.id = this.listData[this.curSelectIndex].id;
        this.selectedData.subject = this.listData[this.curSelectIndex].subject;
        this.selectedData.desc = this.listData[this.curSelectIndex].desc;
 
        this.editDialog = true;
      },
      calIndex(index) {
        switch (this.curPageNum) {
          case 1:
            return index;
            break;
          default:
            return index + ((this.curPageNum - 1* this.dataPerPage);
            break;
        }
      },
      clearForm() {
        for (let key in this.newData) {
          this.newData[key] = '';
        }
      },
      prevPage() {
        this.curPageNum--;
      },
      closeDialog() {
        this.addDialog = false;
        this.editDialog = false;
        this.removeDialog = false;
      }
    },
    components: {
      'BaseModal': BaseModal
    }
  }
</script>
 
cs



data 속성 중 searchData는 calData()에서 .filter(callback)와 .includes()를 이용해서 search 값이 포함된 subject를 가지고 있는 데이터들을 return 해주고 있습니다. 마찬가지로 나머지 pagination과 관련된 함수들도 searchData를 기준으로 계산하게 변경하였습니다.


Edit과 Delete기능은 현재 포스팅에서는 template 코드에서 index를 받아와 원본 데이터를 수정하고 있습니다. 하지만 실제로 서비스를 하여 서버를 통해 받아오는 경우 데이터의 속성 중 UUID 같은 유니크한 Key를 이용해서 수정이 일어나야합니다. 

index 대신 이벤트가 일어난 list의 item자체를 전달하여 해당 key를 이용해 수정을 하면 되겠습니다.


pagination이 적용되어 있으므로 edit, delete기능을 구현할 때 index 계산이 추가적으로 진행하여 원본 데이터를 수정해야합니다. calIndex(index) 함수를 참고하면됩니다.



완성된 Code, Demo


See the Pen ejbjGQ by Sim Gyu-Min (@sim-gyu-min) on CodePen.




이렇게 간단한 ListView에 pagination, crud, search 기능을 구현하였습니다.


앞으로 사내프로젝트를 진행하면서 Vue.js 기능에 대해 알아가며 포스팅하려 합니다.

댓글로 피드백 부탁드리겠습니다 :)