본문 바로가기

데일리 공부 기록

hands on vue3 - 동적 component를 사용하기 위한 기술들 연습

728x90

[목표]

: 동적으로 변하는 component들을 사용하기 위해 필요한 스킬들을 연습해보자

 

1. component는 동적으로 변하기 때문에 custom event(자식에서 넘어온 emit 이벤트)들을 어떻게 처리할 것인가.

2. component가 A에서 작성하다가 B로 넘어왔을 경우 A에 대한 데이터를 어떻게 유지할 것인가.

3. 추가) form에서 submit를 할 경우 어떻게 reload가 안되게 할 것인가.

 

먼저 소스와 요구사항들을 확인하고 직접 구현한 뒤에 비교해보자

 

요구사항 

1. 버튼을 hover할 경우만 색상이 진하게 바뀌도록 하라

2. 클릭한 버튼만 보라색으로 바뀌게 하라

3. 두 개의 버튼을 base-card 컴포넌트로 묶어라

4. Add Resource에서 추가했을 때 Stored Resource에서 보이도록 하라

5. Add Resource에서 추가했을 때 reload가 되지 않게 하라

6. Add Resource에서 추가했을 때 자동으로 Stored Resource로 이동하라

 

힌트 

1) @submit.prevent="메소드명"

2) provide, inject

3) newDate().toISOString() <- 고유값으로 사용하기 좋음 v-for문에서 id값으로 사용해도 됨

 


아래는 수정 전 소스다

[App.vue]

<template>
  <the-header/>
  <div>
    <the-resources>

    </the-resources>
  </div>
</template>

<script>
import TheHeader from './components/layout/TheHeader.vue'
import TheResources from './components/layout/TheResources.vue'

export default {
  components: { 
    TheHeader,
    TheResources,
  },
  methods:{

  },
}
</script>

<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');

* {
  box-sizing: border-box;
}

html {
  font-family: 'Roboto', sans-serif;
}

body {
  margin: 0;
}
</style>

[TheResources.vue]

<template>
	<ul>
		<base-button @click="setComp('StoredResource')" :mode="setComponent=='storedResButtonMode'">
			Stored Resources
		</base-button>
		<base-button @click="setComp('AddResource')" :mode="setComponent=='addResButtonMode'">
			Add Resource
		</base-button>
		<component 
			:is="setComponent"
			@addResc="addResc"
			>

		</component>
	</ul>
</template>

<script>
import StoredResource from '../learning-resource/StoredResource.vue';
import AddResource from '../learning-resource/AddResource.vue';

export default {
	components: {
		StoredResource,
		AddResource,
	},
	provide(){
		return{
			resources : this.resources,
		}
	},
	data(){
		return{
			setComponent:'',
			resources:[
				{
					id: '1',
					title: 'Google',
					description: 'Learning something by Google!',
					link: 'https://google.com'
				},
				{
					id: '2',
					title: 'Naver',
					description: 'Have you ever had experience with Naver?',
					link: 'https://www.naver.com'
				},
				{
					id: '3',
					title: 'Daum',
					description: 'I am from Hell.',
					link: 'https://www.daum.net'
				},
			]
		};
	},
	methods: {
		setComp(val){
			this.setComponent = val; 
		},
		rmvRsc(id){
      console.log("From App.vue rmvRec is Running...");
      console.log(`emitVal: ${id}`);
      this.resources = this.resources.filter((element, index, array) => {
        return element.id != id;
      });
      console.log(JSON.stringify(this.resources));
    },
		addResc(obj){
			obj.id = this.resources[this.resources.length -1]+1;
			this.resources.push(obj);
			console.table(obj);
		}
	},
	computed:{
		storedResButtonMode(){
			return this.setComponent == 'StoredResource' ? null : 'flat';
		},
		addResButtonMode(){
			return this.setComponent == 'AddResource' ? null : 'flat';
		},
	},
}
</script>

<style>

</style>

[LearningResource.vue]

<template>
	<base-card>
		<li>
			<div>
				<header>
					<h2>{{res.title}}</h2>
					<p>{{res.description}}</p>
					<base-button :mode="'flat'">
						Delete
					</base-button>
				</header>
			</div>
			<a :href="link">View Resource</a>
		</li>
	</base-card>
</template>

<script>
import BaseButton from '../UI/BaseButton.vue';
import BaseCard from '../UI/BaseCard.vue';

export default {
  components: { BaseCard, BaseButton },
	props: ['res'],
	methods:{
		rmvRsc(){
			console.log("rmvRec is Running...");
			this.$emit('rmvRsc', this.res.id);
		}
	},
}
</script>

<style scoped>
li {
  margin: auto;
  max-width: 40rem;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

h3 {
  font-size: 1.25rem;
  margin: 0.5rem 0;
}

p {
  margin: 0.5rem 0;
}

a {
  text-decoration: none;
  color: #ce5c00;
}

a:hover,
a:active {
  color: #c89300;
}
/* button{
	background-color:#640032;
	color:white;
	border-radius: 12px;
	box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.26);
} */
</style>

[AddResource.vue]

<template>
	<base-card>
		<form class="control">
			<div>
				<label for="title">Title</label>
				<input type="text" v-model="addRes.title">
			</div>
			<div>
				<label for="description">description</label>
				<textarea name="" id="" cols="30" rows="10" v-model="addRes.description"></textarea>
			</div>
			<div>
				<label for="link"> link</label>
				<input type="text" v-model="addRes.link">
			</div>
			<div>
				<base-button @click="addRsc" type="submit">
					Add Resource
				</base-button>
			</div>
		</form>
	</base-card>
</template>

<script>
import BaseButton from '../UI/BaseButton.vue'
export default {
  components: { BaseButton },
	data(){
		return{
			addRes: {
				id: '',
				title: '',
				description: '',
				link: '',
			},
		};
	},
	methods:{
		addRsc(event){
			event.preventDefault();
			this.$emit('addResc', this.addRes);
		}
	}

}
</script>
 
<style scoped>
label {
  font-weight: bold;
  display: block;
  margin-bottom: 0.5rem;
}

input,
textarea {
  display: block;
  width: 100%;
  font: inherit;
  padding: 0.15rem;
  border: 1px solid #ccc;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: #3a0061;
  background-color: #f7ebff;
}

.form-control {
  margin: 1rem 0;
}
div{
	margin-top:10px;
}
</style>

[StoredResource.vue]

<template>
		<learning-resource
      v-for="res in resources"
      :key="res.id"
      :res="res"
      @rmvRsc="rmvRsc"
    >
		</learning-resource>
</template>

<script>
import LearningResource from './LearningResource.vue'

export default {
	emits: ['rmvRsc'],
  inject: ['resources'],
	components:{
		LearningResource,
	},
	methods:{
		rmvRsc(val){
			this.$emit('rmvRsc', val);
		}
	},
}
</script>

<style scoped>
ul {
  list-style: none;
  margin: 0;
  padding: 0;
  margin: auto;
  max-width: 40rem;
}

</style>

[TheHeader.vue]

<template>
	<header>
		<h1>Resources</h1> 
	</header>
</template>

<script>
export default {

}
</script>

<style scoped>
header{
	width: 100%;
	height: 5rem;

	justify-content: center;
	display:flex;

	background-color:#640032;
	
}
header h1{
	color: white;
}
</style>

 

[BaseCard.vue]

<template>
	<div>
		<slot></slot>
	</div>
</template>

<script>
export default {

}
</script>

<style scoped>
div{
	max-width:40rem;
	margin:2rem auto;
	padding:36px;
	border-radius: 12px;
	box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.26);
}
</style>

[BaseButton.vue]

<template>
	<button :class="mode">
		<slot></slot>
	</button>
</template>

<script>
export default {
	props:['type', 'mode'],
	mouted(){
	}
}
</script>

<style scoped>
button {
  padding: 0.75rem 1.5rem;
  font-family: inherit;
  background-color: #3a0061;
  border: 1px solid #3a0061;
  color: white;
  cursor: pointer;
}

button:hover,
button:active {
  background-color: #270041;
  border-color: #270041;
}

.flat {
  background-color: transparent;
  color: #3a0061;
  border: none;
}

.flat:hover,
.flat:active {
  background-color: #edd2ff;
}
</style>

[main.js]

import {createApp} from 'vue';
import App from './App.vue';
import BaseButton from './components/UI/BaseButton.vue';
import BaseCard from './components/UI/BaseCard.vue';

const app = createApp(App);
app.component('BaseButton', BaseButton);
app.component('BaseCard', BaseCard);
app.mount("#app");

아래는 수정된 것만 올린 것임

수정되지 않은 파일 소스는 위와 같다. 

[TheResources.vue]

<template>
	<ul>
		<base-card>
			<base-button @click="setComp('StoredResource')" :mode="storedResButtonMode">
				Stored Resources
			</base-button>
			<base-button @click="setComp('AddResource')" :mode="addResButtonMode">
				Add Resource
			</base-button>
		</base-card>
		<keep-alive>
			<component 
				:is="setComponent"
				>
			</component>
		</keep-alive>
	</ul>
</template>

<script>
import StoredResource from '../learning-resource/StoredResource.vue';
import AddResource from '../learning-resource/AddResource.vue';

export default {
	components: {
		StoredResource,
		AddResource,
	},
	provide(){
		return{
			resources : this.resources,
			addResc : this.addResc,
		}
	},
	data(){
		return{
			setComponent:'',
			resources:[
				{
					id: '1',
					title: 'Google',
					description: 'Learning something by Google!',
					link: 'https://google.com'
				},
				{
					id: '2',
					title: 'Naver',
					description: 'Have you ever had experience with Naver?',
					link: 'https://www.naver.com'
				},
				{
					id: '3',
					title: 'Daum',
					description: 'I am from Hell.',
					link: 'https://www.daum.net'
				},
			]
		};
	},
	methods: {
		setComp(val){
			this.setComponent = val; 
		},
		rmvRsc(id){
      console.log("From App.vue rmvRec is Running...");
      console.log(`emitVal: ${id}`);
      this.resources = this.resources.filter((element, index, array) => {
        return element.id != id;
      });
      console.log(JSON.stringify(this.resources));
    },
		addResc(obj){
			obj.id = this.resources[this.resources.length -1]+1;
			this.resources.push(obj);
			console.table(obj);
			this.setComponent = StoredResource;
		}
	},
	computed:{
		storedResButtonMode(){
			return this.setComponent == 'StoredResource' ? null : 'flat';
		},
		addResButtonMode(){
			return this.setComponent == 'AddResource' ? null : 'flat';
		},
	},
}
</script>

<style>

</style>

 

[AddResources.vue]

<template>
	<base-card>
		<form class="control" @submit.prevent="addRsc">
			<div>
				<label for="title">Title</label>
				<input type="text" v-model="addRes.title">
			</div>
			<div>
				<label for="description">description</label>
				<textarea name="" id="" cols="30" rows="10" v-model="addRes.description"></textarea>
			</div>
			<div>
				<label for="link"> link</label>
				<input type="text" v-model="addRes.link">
			</div>
			<div>
				<!-- 추가하기 클릭했을 때 클릭이벤트 발생 -->
				<base-button type="submit"> 
					Add Resource
				</base-button>
			</div>
		</form>
	</base-card>
</template>

<script>
import BaseButton from '../UI/BaseButton.vue'
export default {
  components: { BaseButton },
	inject: ['addResc'],
	data(){
		return{
			addRes: {
				id: '',
				title: '',
				description: '',
				link: '',
			},
		};
	},
	methods:{
		addRsc(){
			console.log('1');
			this.addResc(this.addRes);
		}
	}

}
</script>
 
<style scoped>
label {
  font-weight: bold;
  display: block;
  margin-bottom: 0.5rem;
}

input,
textarea {
  display: block;
  width: 100%;
  font: inherit;
  padding: 0.15rem;
  border: 1px solid #ccc;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: #3a0061;
  background-color: #f7ebff;
}

.form-control {
  margin: 1rem 0;
}
div{
	margin-top:10px;
}
</style>

 

componet 에서 자식이 보낸 커스텀 이벤트를 받는

대신 provide & inject를 사용함 

    <component
      :is="setComponent"
      >

    </component>
    <component
      :is="setComponent"
      @addResc="addResc"
      >

    </component>

 

form태그 안에서

추가하기 눌렀을 때

버튼, 메소드 안에서 preventDefault까지 해주어야함.

        <!-- 추가하기 클릭했을 때 클릭이벤트 발생 -->
        <base-button @click="addRsc" type="submit">
    addRsc(event){
      event.preventDefault();

vue의 

@submit.prevent를 이용하여

reload를 막음

<form class="control" @submit.prevent="addRsc">