기존 강의 내용을 좀 더 기능을 확장하고자 합니다. 여기에 행추가 및 행삭제를 넣어볼까 합니다.
이전 강의를 간략히 설명을 드리면, Vue.js를 활용하여 스텝별로 다른 옵션을 보여주고 선택하는 드롭다운 메뉴를 구현한 코드입니다. 이 코드는 Vue.js의 기본적인 개념인 반응형 데이터, 메소드, 조건부 렌더링(v-show), 이벤트 리스너(v-on:change), 그리고 라이프사이클 훅(mounted)을 활용하고 있습니다. 또한, 각 스텝에서 선택한 옵션에 따라 다음 스텝의 옵션을 동적으로 불러오는 기능을 포함하고 있으며, 이를 위해 Promise 기반의 비동기 처리 방식인 async/await를 사용하여 외부 API에서 데이터를 불러오는 작업을 수행하고 있습니다.
https://aspdotnet.tistory.com/3168
행 추가 및 삭제 기능을 포함하는 코드를 작성하겠습니다. Vue.js에서는 배열을 사용하여 여러 행을 관리할 수 있습니다. 아래 코드는 기존 코드에 행 추가 및 삭제 기능을 포함한 버전입니다: web api 호출은 jquery 를 사용했습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue.js를 이용한 동적 드롭다운</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div id="app">
<div v-for="(row, index) in rows" :key="index">
<select name="step1" v-model="row.selectedBreed" v-on:change="fetchSubBreeds(index)">
<option value="">단계 1 선택</option>
<option v-for="breed in breeds" :value="breed.id">{{ breed.name }}</option>
</select>
<select name="step2" v-model="row.selectedSubBreed" v-on:change="fetchImages(index)">
<option value="">단계 2 선택</option>
<option v-for="subBreed in subBreeds[index]" :value="subBreed">{{ subBreed }}</option>
</select>
<select name="step3" v-model="row.selectedImage">
<option value="">단계 3 선택</option>
<option v-for="image in images[index]" :value="image">{{ image }}</option>
</select>
<button @click="removeRow(index)">행 삭제</button>
<br>
<img v-if="row.selectedImage" :src="row.selectedImage" alt="dog" width="200" height="200">
<br>
</div>
<button @click="addRow">행 추가</button>
</div>
<script>
new Vue({
el: '#app',
data: {
breeds: [],
rows: [{
selectedBreed: 'terrier',
selectedSubBreed: 'american',
selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_366.jpg'
},
{
selectedBreed: 'terrier',
selectedSubBreed: 'american',
selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10164.jpg'
},
{
selectedBreed: 'terrier',
selectedSubBreed: 'american',
selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10164.jpg'
},
{
selectedBreed: 'terrier',
selectedSubBreed: 'american',
selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10328.jpg'
},
{
selectedBreed: 'terrier',
selectedSubBreed: 'american',
selectedImage: 'https://images.dog.ceo/breeds/terrier-american/n02093428_10365.jpg'
}],
subBreeds: [],
images: []
},
methods: {
fetchBreeds() {
return $.ajax({
url: 'https://dog.ceo/api/breeds/list/all',
method: 'GET',
dataType: 'json'
})
.done((data) => {
this.breeds = Object.keys(data.message).map(breed => ({
id: breed,
name: breed
}));
})
.fail((jqXHR, textStatus, errorThrown) => {
console.error("Failed to fetch breeds", errorThrown);
});
},
fetchSubBreeds(index) {
this.images[index] = []; // 이미지 초기화
if (this.rows[index].selectedBreed) {
return $.ajax({
url: `https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/list`,
method: 'GET',
dataType: 'json'
})
.done((data) => {
this.$set(this.subBreeds, index, data.message);
})
.fail((jqXHR, textStatus, errorThrown) => {
console.error(`Failed to fetch sub-breeds for breed ${this.rows[index].selectedBreed}`, errorThrown);
});
} else {
this.rows[index].selectedSubBreed = '';
this.rows[index].selectedImage = '';
}
},
fetchImages(index) {
this.images[index] = [];
if (this.rows[index].selectedBreed) {
let url = this.rows[index].selectedSubBreed ?
`https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/${this.rows[index].selectedSubBreed}/images/random/10` :
`https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/images/random/10`;
return $.ajax({
url: url,
method: 'GET',
dataType: 'json'
})
.done((data) => {
this.$set(this.images, index, data.message);
})
.fail((jqXHR, textStatus, errorThrown) => {
console.error(`Failed to fetch images for breed ${this.rows[index].selectedBreed}`, errorThrown);
});
} else {
this.rows[index].selectedImage = '';
}
},
addRow() {
this.rows.push({
selectedBreed: '',
selectedSubBreed: '',
selectedImage: '',
});
this.subBreeds.push([]);
this.images.push([]);
},
removeRow(index) {
this.$delete(this.rows, index);
this.$delete(this.subBreeds, index);
this.$delete(this.images, index);
}
},
async mounted() {
await this.fetchBreeds();
for (let i = 0; i < this.rows.length; i++) {
await this.fetchSubBreeds(i);
if (this.rows[i].selectedSubBreed) {
await this.fetchImages(i);
}
}
}
});
</script>
</body>
</html>
이 코드에서는 rows 배열에 각 행의 데이터를 저장하고, subBreeds와 images 배열에 각 행별로 sub Breeds 와 이미지를 저장합니다. 행 추가 버튼을 누르면 addRow 메서드가 호출되어 행이 추가되고, 행 삭제 버튼을 누르면 removeRow 메서드가 호출되어 해당 행이 삭제됩니다. 이 때 splice 메서드를 사용하여 배열에서 특정 인덱스의 요소를 제거합니다. 각 드롭다운 메뉴에서 선택한 옵션이 변경되면 해당 행의 데이터를 업데이트하고, 필요한 경우 다음 step 의 옵션을 불러옵니다.
이전 코드와 크게 다른 점은 이제 행추가 기능으로 선택한 값을 아래와 같이 rows: [] 배열로 저장한다는 점입니다.
data: {
breeds: [],
subBreeds: [[]],
images: [[]],
rows: []
}
row 에는 각각 아래 json 형태로 저장됩니다.
{
selectedBreed: 'terrier',
selectedSubBreed: 'border',
selectedImage: 'https://images.dog.ceo/breeds/terrier-border/n02093754_115.jpg',
}
그리고 각 메소드에서 행의 index 를 파라미터로 받아 해당 행의 데이터를 참조하도록 수정하였습니다. this.$set 함수를 사용하여 subBreeds와 images 배열의 특정 인덱스에 값을 설정하였습니다. 이렇게 하면 Vue.js가 배열의 변경을 감지하고 화면을 업데이트할 수 있습니다. 추후 행 삭제를 위해 키값 기준은 index 으로 지정되어 있습니다. (this.$set(this.subBreeds, index, data.message);)
fetchSubBreeds(index) {
this.images[index] = []; // 이미지 초기화
if (this.rows[index].selectedBreed) {
return $.ajax({
url: `https://dog.ceo/api/breed/${this.rows[index].selectedBreed}/list`,
method: 'GET',
dataType: 'json'
})
.done((data) => {
this.$set(this.subBreeds, index, data.message);
})
.fail((jqXHR, textStatus, errorThrown) => {
console.error(`Failed to fetch sub-breeds for breed ${this.rows[index].selectedBreed}`, errorThrown);
});
} else {
this.rows[index].selectedSubBreed = '';
this.rows[index].selectedImage = '';
}
},
vue.js 3 에서는 참고로 아래처럼 this.배열data.$set 으로 작성을 합니다.
this.subBreeds.$set(this.subBreeds, index, data.message);
Vue.js 2에서는 Vue.set 또는 this.$set 을 사용해야 합니다.
this.$set(this.subBreeds, index, data.message);
그리고 초기값으로 선택된 값들을 셋팅하기 위해서는, mounted 라이프사이클 훅에 추가해야 합니다. 이를 위해 fetchSubBreeds() 메소드를 각 행에 대해 호출하도록 수정하겠습니다.
async mounted() {
await this.fetchBreeds();
for (let i = 0; i < this.rows.length; i++) {
await this.fetchSubBreeds(i);
if (this.rows[i].selectedSubBreed) {
await this.fetchImages(i);
}
}
}
행 추가는 addRow() 메소드를 사용하고, 행 삭제는 removeRow(index) 인덱스를 통해 $delete() 함수로 data 배열을 제거합니다. 행 추가의 기본값은 push 함수 내의 json 속성에 기재하면 되며, 2스텝의 subBreads 와 3 스텝의 images 배열은 web api 를 통해 데이터를 받을 예정이므로 지정된 속성이 아닌 [] 으로 초기화 되어 있습니다.
addRow() {
this.rows.push({
selectedBreed: '',
selectedSubBreed: '',
selectedImage: '',
});
this.subBreeds.push([]);
this.images.push([]);
},
removeRow(index) {
this.$delete(this.rows, index);
this.$delete(this.subBreeds, index);
this.$delete(this.images, index);
}
[vue.js] LIST 와 페이징 PAGING 이전, 다음 버튼 (2) | 2024.04.24 |
---|---|
vue.js 2 또는 3 버전에 대한 비교 및 Composition API 알아보기 (0) | 2024.03.11 |
vue.js : watch 활용하여 instance 간 데이터 전달하기 (2) | 2024.02.26 |
vue.js : created 라이프사이클, 어떤 용도일때 사용할까? (1) | 2024.02.26 |
vue.js : data 초기화 과정 - created 와 mounted 그리고 event bus 활용하기 (2) | 2024.02.26 |