How can I reassign and repass props data with keeping its reactivity? - vue.js

<template>
<p>
<input type="text" v-model="appInput">
{{ appInput }}
</p>
<ParentComponent :appInput='appInput' :appObject='appObject'/>
</template>
<script setup>
import ParentComponent from './components/ParentComponent.vue'
import { ref } from 'vue'
const appInput = ref('')
const appObject = ref({
text1: 'text1',
text2: 'text2'
})
</script>
<!-- ------------------------------------- -->
<template>
<!-- {{ appInput }} -->
{{ props.appInput }}
<!-- {{ appObject }} -->
{{ props.appObject.text1 }}
<ChildComponentVue :appInput='appInput'/>
</template>
<script setup>
import { defineProps } from 'vue';
import ChildComponentVue from './ChildComponent.vue';
const props = defineProps(['appInput', 'appObject'])
// const appInput = props.appInput
// const appObject = props.appObject
</script>
Using Vue 3, I want to reactively render text from App.vue on ParentComponent.vue and ChildComponent.vue by passing data from App to Parent, and Parent to child successively.
But when I reassign props data on ParentComponent for use it conveniently, it lose reactivity.
I tried repack it with ref() on ParentComponent, but as Vue3 Official Document says, it doesn't work.
But it is too dirty to use passed data without reassign in template (like {{ props.appObject.text1 }}) and too hard to repass not entire but just some part of passed data.
and here are my questions
Are there some ways to use and repass passed data more concisely without losing its reactivity?
What is the convension on Vue3 to deal with this kind of problem happen. Just using Vuex?

Related

Vue 3 why is the parent updated when child data changes?

With this parent...
<template>
<h2>Parent</h2>
{{ parent.a }}
{{ parent.b }}
<ChildComponent :data="parent" />
</template>
<script setup>
import { reactive } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const parent = reactive({ a: 1, b: 2 })
</script>
And this child...
<template>
<h2>Child component</h2>
<p>{{ child.a }}</p>
<p>{{ child.b }}</p>
<input type="text" v-model="child.b" />
</template>
<script setup>
import { reactive } from 'vue'
const props = defineProps(['data'])
const child = reactive(props.data)
child.a = 'why do i update the parent?'
</script>
Why is the data on the parent being updated here? I thought that with binding of the 'data' prop being one-way, I would need an emit to send the data back to the parent? Instead any changes to the child object in the child component is updating the parent object in the parent.
In the documentation it says
When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array's nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.
But from my example, a and b aren't nested?
Further reading and I've found that it's the use of reactive on the child that is the issue. It is creating a reactive copy of the original (a reference), so any updates to the copy were affecting both. I needed to use ref instead:
<template>
<div>
<h2>Child component</h2>
<p>{{ a }}</p>
<p>{{ b }}</p>
<input type="text" v-model="b" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps(['data'])
const a = ref(props.data.a)
const b = ref(props.data.b)
</script>
The reason is that JS treats an object like a reference. A more accurate term is call-by-sharing.
So when you modify a props object inside a child component, you actually modify the same object of the parent component regardless you are using reactive or not
Let's consider this code snippet:
<template>
<h2>Child component</h2>
<p>{{ child.a }}</p>
<p>{{ child.b }}</p>
<input type="text" v-model="child.b" />
</template>
<script setup>
import { reactive } from 'vue'
const props = defineProps(['data'])
const child = props.data // <--- No need to use reactive here
child.a = 'why do i update the parent?'
</script>
The data of the parent component will be still updated along with the child component regardless of the use of reactive. So, to avoid side effects, Vue recommended never mutating props directly. You should use event emitter instead
The reason why child component is updating parent values is the two way data binding. You can solve this problem by creating a copy of an object inside a child component and then use the copied version.

How do Template Refs work inside a constant?

Question about Template Refs in vue
I'm taking a look at the vue documentation about "refs" and in the part where it explains about ref inside a v-for it gives the following example:
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
I can understand its use in
const itemRefs = ref([])
but I couldn't understand why the ref is also applied in
const list = ref([
/* ... */
])
In a sandbox it is possible to remove the ref from the "list constant" without harming the function, so what would be the real application inside this constant?
<script setup>
import { ref, onMounted } from 'vue'
// const with ref
const list = ref([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// const without ref
const list = ([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
Using the ref turns list into reactive variable. Whenever new item is appended or it is otherwise mutated, the other functions or template parts watching it get updated. In your example without ref, when you append a new item to the list, it won't be automatically rendered in the template.
I can understand your confusion and I assume you probably come from vue2 world. I can't recommend enough reading vue3 docs about reactivity.
These are two different types of refs:
itemRefs is an array of template ref. It's a reference to an html element / vue component from your component template. It's associated to the ref attribute on the element.
list is a "regular" ref. It adds reactivity over the array of values. Without it, Vue won't react to the value changes and won't update the rendered component.

How to declare local property from composition API in Vue 3?

In Vue 2 I would do this:
<script>
export default {
props: ['initialCounter'],
data() {
return { counter: this.initialCounter }
}
}
</script>
In Vue 3 I tried this:
<script setup>
import { ref } from 'vue';
defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
</script>
This obviously doesn't work because props is undefined.
How can I bind one-way properties to a local variable in Vue 3?
It seems the result of defineProps is not assigned as a variable. check Vue3 official doc on defineProps. Not really sure what is the use case of ref() here but toRef API can be used as well.
import { ref } from 'vue';
const props = defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
Retrieving a read-only value and assigning it to another one is a bad practice if your component is not a form but for example styled input. You have an answer to your question, but I want you to point out that the better way to change props value is to emit update:modalValue of parent v-model passed to child component.
And this is how you can use it:
<template>
<div>
<label>{{ label }}</label>
<input v-bind="$attrs" :placeholder="label" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
<span v-for="item of errors">{{ item.value }}</span>
</div>
</template>
<script setup>
defineProps({ label: String, modelValue: String, errors: Array })
defineEmits(['update:modelValue'])
</script>
v-bind="$attrs" point where passed attributes need to be assigned. Like type="email" attribute/property of a DOM element.
And parent an email field:
<BaseInput type="email" v-model="formData.email" :label="$t('sign.email')" :errors="formErrors.email" #focusout="validate('email')"/>
In this approach, formdata.email in parent will be dynamically updated.

How to use v-model using props in v-for in Vue 3

I want to use v-model using props in v-for. I used v-model with props before using computed variables, but don't know how to do in v-for.
This is my code:
<script setup>
const fieldValue = computed({
get() {
return props.modelValue;
},
set(value) {
emits('update:modelValue', value);
},
});
</script>
<template>
<Mentionable
v-for="(field, index) in props.data.placeholder"
:key="index"
:keys="['#']"
:items="props.data.items"
>
<textarea
v-model="fieldValue"
:disabled="!props.data.loaded"
:placeholder="field.placeholder"
:maxlength="props.maxLength"
:required="props.required"
/>
</Mentionable>
</template>
Note: Mentionable is the component from vue-mention library.
Use model-value and update:model-value event instend of v-model.
If the component does not provide model-value and update:model-value event, there must be prop and event you can do the same.
A example, AInput from Ant Design Vue provides value prop and change event.
<script setup name="Demo">
const {ref} from 'vue'
const arrayInputs = ref(['1', 'test', '3'])
function change(i, event) {
arrayInputs.value[i] = event.target.value
}
</script>
<template>
<div class="vue-component">
<h2>ant-design-vue</h2>
<template v-for="(item, index) in arrayInputs" :key="index">
<!-- the index is important -->
<AInput :value="item" #change="event => change(index, event)" />
</template>
<div>{{ arrayInputs }}</div>
</div>
</template>

vue3 js component :is not changing component

I have a component which gathers data from an API. The data brought back from the API is an array with details in. One of the values in the array is the type of component which should be rendered, all other data is passed through to the component.
I'm trying to render the correct component based of the value brought back from the database, but it is sadly not working.
I'm new to Vue but had it working in vue2 but would like it to work in Vue 3 using the composition API.
this is my component code which I want to replace:
<component :is="question.type" :propdata="question" />
When viewed within the browser this is what is actually displayed, but doesn't use the SelectInput component:
<selectinput :propdata="question"></selectinput>
SelectInput is a component with my directory, and works as intended if I hard code the :is value, like below:
<component :is="SelectInput" propdata="question" />
my full component which calls the component component and swaps components:
<template>
<div class="item-group section-wrap">
<div v-bind:key="question.guid" class='component-wrap'>
<div class="component-container">
<!-- working -->
<component :is="SelectInput" :propData="question" />
<!-- not working -->
<component v-bind:is="question.type" :propData="question" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, toRefs } from 'vue';
import SelectInput from "./SelectInput";
import TextareaInput from "./TextareaInput";
const props = defineProps({
question: Object,
});
const { question } = toRefs(props);
</script>
In case that your Components filenames are equal to the type specifier on your question Object, then you could dynamically import them to save some code lines.
This would also result in better scalability since you don't have to touch this component anymore in case you create more types.
<template>
<div class="item-group section-wrap">
<div v-bind:key="question.guid" class='component-wrap'>
<div class="component-container">
<!-- working -->
<component v-bind:is="getComponent(question.type)" :propData="question" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, toRefs } from 'vue';
const props = defineProps({
question: Object,
});
// GIVEN THE question.types are equal to the fileNames of the components to render:
const getComponent = (name) => import(`./${name}.vue`);
const { question } = toRefs(props);
</script>
Figured out that because I'm using script setup the components aren't named so the component component doesn't know which component to render.
so, i created an object with the components and a reference to the component:
const components = {
'SelectInput': SelectInput,
'TextareaInput': TextareaInput
};
and another function which takes in the component i want to show and links it to the actual component:
const component_type = (type) => components[type];
then in the template i call the function and the correct component is rendered:
<component v-bind:is="component_type(question.type)" :propData="question" />
complete fixed code:
<template>
<div class="item-group section-wrap">
<div v-bind:key="question.guid" class='component-wrap'>
<div class="component-container">
<!-- working -->
<component v-bind:is="component_type(question.type)" :propData="question" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, toRefs } from 'vue';
import SelectInput from "./SelectInput";
import TextareaInput from "./TextareaInput";
const props = defineProps({
question: Object,
});
const components = {
'SelectInput': SelectInput,
'TextareaInput': TextareaInput
};
const component_type = (type) => components[type];
const { question } = toRefs(props);
</script>
Not too sure if this is the correct way of doing this but it now renders the correct component.