Child components not rendering when referenced dynamically in composition API - vue.js

I'm converting some components from vue 3's option API to the composition API. In this particular component I have two nested child components:
<script lang="ts" setup>
import ShiftOperation from "#/components/transformation-widgets/ShiftOperation.vue";
import RawJolt from "#/components/transformation-widgets/RawJolt.vue";
console.log([ShiftOperation, RawJolt])
...
From what I understand, if you're using the setup attribute in the script tag then all you have to do is import the component into a variable like I'm doing above and it should be available for the template without having to do anything else, like it's not like the old options api where you had to inject those components into the parent component.
Both components are imported successfully (confirmed by the console log:
When I'm rendering out this parent component I'm using the two child components to render out an array of data where I reference the children dynamically in the template based on information in each block of data that I'm iterating over:
<template>
<div class="renderer-wrapper">
<component
v-for="(block, index) in store.specBlocks"
v-bind:key="index"
:block="block"
:index="index"
:is="determineBlockComponent(block)"
#block-operation-updated="updateBlock"
>
</component>
</div>
</template>
// logic for determining the component to use:
export const determineBlockComponent = (block: JoltOperation) => {
switch (block.renderComponent) {
case 'shift':
return 'ShiftOperation'
default:
return 'RawJolt'
}
}
This worked fine in the options api version of it, but for some reason the components don't actually render. They show up in the elements tab:
But they don't show up in the view. I also added a created lifecycle hook into the child components that just console.log's out saying "created X", but those hooks don't fire.
Business logic wise nothing has changed, it's just been going from option api to composition api, so I'm assuming I'm missing some key detail.
Any ideas?

Your determineBlockComponent function should not return the string but the object of the component. Replace return 'ShiftOperation' with return ShiftOperation

Related

Vue3 Reactivity in script setup for translation

I am adding some DOM elements in the script setup side but I want the messages to change when I change the language. I am using vue-i18n plugin. It's easy to do it in the template section because I can basically use the useI18n().t method but how can I do this in the script setup section. Using the useI18n().t method doesn't ensure reactivity.
Example Code:
$(".time")[0].innerHTML = `
<div>0<span>${useI18n().t("details.hour")}</span></div>
<div>0<span>${useI18n().t("details.minute")}</span></div>
<div>0<span>${useI18n().t("details.second")}</span></div>
`
Manipulating DOM directly inside the script leads to inconsistence in your app, you should drive your component by different reactive data to achieve your goal.
In your current situation try to define a computed property based on the translation then render it inside the template based on its different properties :
<script setup>
const {t} =useI18n()
const time = computed(()=>{
return {
hour:t(""details.hour"),
minute:t(""details.minute"),
second:t(""details.second"),
}
})
</script>
<template>
<div class="time">
<div>0<span>{{time.hour}}</span></div>
<div>0<span>{{time.minute}}</span></div>
<div>0<span>{{time.second}}</span></div>
</div>
</template>

Ref not working on custom component in Vue3

I'm using Vue3 with the composition API. In a form-component I put ref's on each field (child-component).
For some reason the ref's of the custom components are different from ref's for Quasar components.
When I console.log a ref to a custom component I get this in DevTools:
Proxy {__v_skip: true}
(without any properties in Target)
while a ref to a Quasar components gives this :
Proxy {…}
(with all properties of the component in Target)
For this reason I can't use the ref to access properties or methods of these child components.
I have no idea what __v_skip even means.
My custom components are defined with script setup, could that be a reason?
Any idea how to fix this?
UPDATE
If I use defineExpose in the child components for the properties and methods I want to access from outside with a ref, it does work. Not really handy though, since these components have lots of props.
Seem likes currently you cannot access the custom component by ref, if your component is written by Composition API (<script setup>). But you can try the way I mention underneath.
In the Vue 3 doc, there are some lines mentioned this behavior:
An exception here is that components using <script setup> are private
by default: a parent component referencing a child component using
<script setup> won't be able to access anything unless the child
component chooses to expose a public interface using the defineExpose
macro
Read more here: Vue 3 - Ref on Component
That means if you want to access anything from the custom component, your component has to expose that information. I think it's because in Vue 3 you don't need to have root component anymore, so if you define a ref, Vue does not know what the component you want to ref to.
But...
You can try to use yourRef.value.$el, maybe it will help.
Example:
// Parent.vue
<template>
<Child ref="childRef">
</template>
<script setup lang="ts">
// Import things...
const childRef = ref<InstanceType<typeof Child> | null>(null);
onMounted(() => {
console.log(childRef.value.$el);
});
</script>

Get access to the v-slot value inside of the script tag

I am trying to show a loading indicator which is located inside of a component that contains a slot element (lets call this the wrapper component). To do this, I have a function inside the wrapper that sets the state of the indicator based on an input boolean (setSpinnerVisible()). Now, I would like to execute this function from the component that uses this wrapper. To do this, in the parent component I use the v-slot property to get a reference to the function. I would like to be able to call this function inside the mounted() function, or from a function within methods.
However, I am not able to figure out how to do this. The only way I can think of is by passing this v-slot value into a function that is executed on an event like a button press, which works, but I also want to be able to call this method from a function that is not executed by an action in the layout (e.g. in the mounted() function).
This is (a part of) my wrapper component (the function that toggles the spinner is left out for brevity):
<template>
<slot v-bind:setSpinnerVisible="setSpinnerVisible"></slot>
...
<div class="spinner" v-show="spinnerVisible"></div>
</template>
This is (a part of) the component that uses the wrapper:
<Wrapper v-slot="{ setSpinnerVisible }">
...
</Wrapper>
I would like to be able to use the value of setSpinnerVisible inside the mounted function in one way or another, something like this fictional piece of code:
<script>
export default {
mounted() {
this.setSpinnerVisible(true)
}
}
</script>
I am using Vue 2.6.11
There are several approaches you could take.
For example, you could access the parent instance and call the method you need:
this.$parent.setSpinnerVisible()
Alternatively, you could create a gateway component that uses the Wrapper, gets setSpinnerVisible and passes it as a prop to the component that needs it.
You can use dependency injection. Described here: https://v2.vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection
So, in Wrapper.vue
<template>
...
</template>
<script>
export default {
provide () {
return {
setSpinnerVisible: this.setSpinnerVisible
}
}
}
</script>
And in your child component:
<Wrapper>
...
</Wrapper>
<script>
export default {
inject: ['setSpinnerVisible'],
mounted() {
this.setSpinnerVisible(true)
}
}
</script>
The last one would be my recommended approach because it's much neater and is not anti-pattern.

How to force VueJS custom component to re-render itself and all its child component

I have a use case where I have a custom Vue Component that populates its child components dynamically using the JSON. When this top level vue component is created it loads all the children components using the JSON. I have a provision where I can add additional controls on the form when I update the JSON from which it renders itself.
So when I update the JSON on backend using AJAX, I would like re-render everything upon successful post.
Also I came across few articles that say that the regenerating the Form on Custom Vue component should be handled using v-show and/or v-if directive.
This will not fit with my use case.
I looked into forceUpdate API, which is applicable for current component only. It does not affect child components.
So looks like handling forceUpdate on every component is the only way to go in my use case?
Based on the diagram you can see that in the MainFrom component is top level component. Following is the template for the MainForm:
<template>
<div>
<div v-for="(entity, index) of mySchemaChunks >
<FormSection :componentList="entity" />
</div>
</div>
</template>
<script>
export default {
store,
computed: {
mySchemaChunks() {
// returns the chunks from schema (schema is stored in Vuex)
// where each chunks is array (segments of schema)
// Each chunk is a section that has its own list of
// controls.
}
},
methods: {
addNewJsonSchemaNodes() {
// This function will update master Json schema in the backend
// using AJAX which was used to generate the JSON from which
// we generate the MainFrom and all its children
// When App is initialized it prepares the JSON to generate
// MainForm and store that JSON in the Vuex module as schema
// and model object
// I do an AJAX post here which only send me status 200
// This is not enough for me to re-render and generate all
// dynamic component
// May be I should try to get back the JSON after successful
// update of Master JSON in backend ... so that I can update
// my Vuex module schema and model object with new values ...
// And that should do the trick and all components (parent to
// children will be rendered ...????
// Edit: OK.. Now I am getting the data back from AJAX post
// which is now rendering the entire form
}
}
}
</script>
Here is the high level overview of section
<template>
<div>
// Render the list of entity passed to it as property in v-for
// This will add the dynamic Vue components as its children
</div>
</template>
forceUpdate() won't rerender children component UI, but tracked properties will.
In your case. You need to bind the data to component at first. Once ajax response back, you just call Vue.set to update the data bound and it will automatically update the children components

Vue two way prop binding

Below is my current structure (which doesn't work).
Parent component:
<template>
<field-input ref="title" :field.sync="title" />
</template>
<script>
import Field from './input/Field'
export default {
components: {
'field-input': Field
},
data() {
return {
title: {
value: '',
warn: false
}
}
}
}
</script>
Child component:
<template>
<div>
<input type="text" v-model="field.value">
<p v-bind:class="{ 'is-invisible' : !field.warn }">Some text</p>
</div>
</template>
<script>
export default {
props: ['field']
}
</script>
The requirements are:
If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
If the child's <input> is updated (field.value), then the parent's title.value should be updated.
What's the cleanest working solution to achieve this?
Don't bind the child component's <input> to the parent's title.value (like <input type="text" v-model="field.value">). This is a known bad practice, capable of making your app's data flow much harder to understand.
The requirements are:
If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
This is simple, just create a warn prop and pass it from parent to child.
Parent (passing the prop to the child):
<field-input ref="title" :warn="title.warn" />
Child/template (using the prop -- reading, only):
<p v-bind:class="{ 'is-invisible' : !warn }">Some text</p>
Child/JavaScript (declaring the prop and its expected type):
export default {
props: {warn: Boolean}
}
Notice that in the template it is !warn, not !title.warn. Also, you should declare warn as a Boolean prop because if you don't the parent may use a string (e.g. <field-input warn="false" />) which would yield unexpected results (!"false" is actually false, not true).
If the child's <input> is updated (field.value), then the parent's title.value should be updated.
You have a couple of possible options here (like using .sync in a prop), but I'd argue the cleanest solution in this case is to create a value prop and use v-model on the parent.
Parent (binding the prop using v-model):
<field-input ref="title" v-model="title.value" />
Child/template (using the prop as initial value and emitting input events when it changes):
<input type="text" :value="value" #input="$emit('input', $event.target.value)">
Child/JavaScript (declaring the prop and its expected type):
export default {
props: {value: String}
}
Click here for a working DEMO of those two solutions together.
There are several ways of doing it, and some are mentioned in other answers:
Use props on components
Use v-model attribute
Use the sync modifier (for Vue 2.0)
Use v-model arguments (for Vue 3.0)
Use Pinia
Here are some details to the methods that are available:
1.) Use props on components
Props should ideally only be used to pass data down into a component and events should pass data back up. This is the way the system was intended. (Use either v-model or sync modifier as "shorthands")
Props and events are easy to use and are the ideal way to solve most common problems.
Using props for two-way binding is not usually advised but possible, by passing an object or array you can change a property of that object and it will be observed in both child and parent without Vue printing a warning in the console.
Because of how Vue observes changes all properties need to be available on an object or they will not be reactive.
If any properties are added after Vue has finished making them observable 'set' will have to be used.
//Normal usage
Vue.set(aVariable, 'aNewProp', 42);
//This is how to use it in Nuxt
this.$set(this.historyEntry, 'date', new Date());
The object will be reactive for both component and the parent:
I you pass an object/array as a prop, it's two-way syncing automatically - change data in the
child, it is changed in the parent.
If you pass simple values (strings, numbers)
via props, you have to explicitly use the .sync modifier
As quoted from --> https://stackoverflow.com/a/35723888/1087372
2.) Use v-model attribute
The v-model attribute is syntactic sugar that enables easy two-way binding between parent and child. It does the same thing as the sync modifier does only it uses a specific prop and a specific event for the binding
This:
<input v-model="searchText">
is the same as this:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
Where the prop must be value and the event must be input
3.) Use the sync modifier (for Vue 2.0)
The sync modifier is also syntactic sugar and does the same as v-model, just that the prop and event names are set by whatever is being used.
In the parent it can be used as follows:
<text-document v-bind:title.sync="doc.title"></text-document>
From the child an event can be emitted to notify the parent of any changes:
this.$emit('update:title', newTitle)
4.) Use v-model arguments (for Vue 3.0)
In Vue 3.x the sync modifier was removed.
Instead you can use v-model arguments which solve the same problem
<ChildComponent v-model:title="pageTitle" />
<!-- would be shorthand for: -->
<ChildComponent :title="pageTitle" #update:title="pageTitle = $event" />
5.) Use Pinia (or Vuex)
As of now Pinia is the official recommended state manager/data store
Pinia is a store library for Vue, it allows you to share a state across components/pages.
By using the Pinia store it is easier to see the flow of data mutations and they are explicitly defined. By using the vue developer tools it is easy to debug and rollback changes that were made.
This approach needs a bit more boilerplate, but if used throughout a project it becomes a much cleaner way to define how changes are made and from where.
Take a look at their getting started section
**In case of legacy projects** :
If your project already uses Vuex, you can keep on using it.
Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.