Vue js sync parent and child data with Vue-Router - vue.js

In my vuejs application i am using vue-router for routing. Everything is working fine except this one.
In my parent i have list view having a link as below on the left side .
<div class="col-md-5">
<ul>
....
<a v-link="{ name: 'task-detail', params: { taskId: task.id }}">{{ task.title }}</a>
</ul>
</div>
<div class="col-md-7">
<router-view></router-view>
</div>
When i click it, my nested route gets activated and i display detail view on the right side.
Now my problem is
In my parent view i can toggle if the task is completed or not.
I have a label showing if the task is completed or not in child view.
<label class="label label-success pull-right" v-show="task.is_completed">Completed</label>
How do i reflect the status change on my child view when i do it in parent view.
Do i need to refresh the page ? or is there simpler solution.
In simpler terms
When i toggle completion status on parent view my label should change on child view.

So vue wants you to implement a 'data down/actions up' pattern. So if your data is loaded in the parent (eg as a collection) and again loaded - separately in your child (eg as a model instance) - you have two sets of what starts as the same data. But when you change one, its independent of the other. So, you either need to communicate between the two (which can be fiddly), use a central data store, where both routes access the same instance of data (possibly using Vuex, if you're inclined), or if you have all the data you need in the collection, simply pass it downstream with, for example:
<router-view :task="selectedTask" /> where the selected task is driven by the $route.params.id in your URL (as a computed property in the parent, likely)
for example if your task list was called 'tasks' in the parent, your computed property might look like this (in ES6 JS):
selectedTask () {
return this.tasks.find(t => t.id === this.$route.params.id)
}
This data-down is the easiest to implement, but if you find you're doing this a lot, suggest using a central data store.

Does it work if you try this in your child view<label class="label label-success pull-right" v-show="$parent.task.is_completed">Completed</label>

Related

How do I find out whether a Vue 3 component has any children?

Vue 3 removed the $children property and butchered $slots. However I can't find another solution for this scenario I'm having:
I have a component Checkbox. This component renders a checkbox. It also has a default slot. Depending on whether there was anything in that default slot I need to show/hide an element and apply some classes to the Checkbox' root element for styling and animation purposes of elements around it. I don't need access to the actual children, only the information whether there are any.
In Vue 2 I could write something like this:
if (this.$slots.default) {
// do something
}
The same thing in Vue 3 always is true, because it's a function now. However, calling that function returns nonsense:
console.log(this.$slots.default())
// Returns:
// [ Vnode ]
It doesn't matter whether I put something into the default slot or not, the end result is always the same, only with some elements buried in the Vnode tree having changed.
How am I supposed to find out whether there are any children in Vue 3?
For reference, the component template I used before:
<transition name="slide-down">
<div class="children" v-if="$slots.default">
<slot></slot>
</div>
</transition>
The above and below does not work in Vue 3 (the div always is being rendered):
<transition name="slide-down">
<div class="children" v-if="$slots.default()">
<slot></slot>
</div>
</transition>

Confusion of Properties, Data, and Computed Values when Working with Components (especially nested)

I am really confused how to pass down properties from a parent and update them in children then emit those changes all the way back up to the parent. By reference it looks to work but from everything I read that is not the correct way to do it.
I have a parent component which "toggles" a create form. A new instance of the artifact is created and passed into a "create" component which is used in other places. The create component has some common properties but, through a slot, custom properties can be added at the parent level.
Within the ArtifactCreate component, it passes a "clone" of that prop to both the form and the custom properties.
I am trying to understand how to take the "prop" value, work with it internally, and then bubble up the final result to the ArtifactCreate component which in-turn bubbles that up to the parent.
The "child" components (ArtifactCreateForm and DatasetProperties) do not have any additional methods and are directly updating the prop reference passed in. DatasetProperties can be used in other places as well.
In most cases, I have a "parent" that I want to handle the main interaction but need to pass some model down to one or more components, which in turn may pass that in its children.
The structure looks close to this:
Parent Component (somewhat like the controller handling main actions)
|
|--- View Component (takes in 'artifact' prop and passes it down)
|
|--- Child Component
|
|--- GrandChild 1..x
|-----------------|--- GrandChild (through slot)
I am really confused on on the relationship between properties and data and/or computed values and how to correctly work with that data in the grandchildren. I am fine with the concept of pass data down and emit events up and I seem to grasp how to do that with primitive properties but how can I do this with an object that has many properties?
My questions are:
What is the correct way to pass props and handle them inside the components (as a copy), then bubble that back up to the parent?
When bubbling the event from lower components is the only way to repeat it until it reaches the parent?
I am using "objects" as the prop/data not individual values. The reference gets updated (which is a copy) then bubbled to the parent (as I want). Is this the right way?
(FYI, coming from Java to Vue so this is a whole new world to me).
Parent Component
This is my entry into the create component. It manages the instance (in this case a new one) and passes it down to the ArtifactCreate component which internally clones it. The updated copy is bubbled back to here where it is saved.
<template>
<div>
<artifact-header/>
<v-container fluid>
<!-- List -->
<reference-list
:paths="paths"
#open="load"/>
<!-- Create Artifact -->
<artifact-create
:advanced="true"
:artifact="newArtifact"
:title="newTitle"
#save="save">
<template #default="{ model }">
<dataset-properties :artifact="model"/>
</template>
</artifact-create>
</v-container>
</div>
</template>
Supported by:
computed: {
newArtifact: {
get(): Dataset {
return newDataset();
}
}
},
methods: {
save(item: Dataset){
this.$store.dispatch("saveDataset", item);
},
ArtifactCreate Component (child / container component)
<app-dialog
:title="title"
:visible="visible"
#action="save"
#close="close">
<v-tabs v-if="advanced"
v-model="tab"
grow>
<v-tab>Basic</v-tab>
<v-tab>Advanced</v-tab>
<!-- Basic Properties -->
<v-tab-item :key="'Basic'">
<artifact-create-form :artifact="internal"/>
</v-tab-item>
<!-- Advanced Properties -->
<v-tab-item :key="'Advanced'">
<slot :model="internal"/>
</v-tab-item>
</v-tabs>
<!-- Basic Properties ONLY -->
<artifact-create-form v-else
:artifact="internal"/>
</app-dialog>
And it is supported with:
export default Vue.extend({
name: "ArtifactCreate",
props:{
artifact: {
type: Object as Prop<IArtifact>,
default: {}
},
},
computed:{
internal: {
get(): IArtifact {
return clone(this.artifact);
},
}
},
methods: {
save(item) {
this.$emit('save', this.internal);
this.visible = false;
},
},
});
ArtifactCreateForm Component (child in ArtifactCreate)
<template>
<v-form>
<v-text-field
v-model="artifact.name"
label="Name*"
required>
</v-text-field>
</v-form>
</template>
Supported by:
export default Vue.extend({
name: "ArtifactCreateForm",
props:{
artifact: {
type: Object as Prop<IArtifact>,
default: {}
},
},
});
DatasetProperties Component (child in ArtifactCreate / registered in parent through slot)
<template>
<v-form>
<v-text-field
v-model="artifact.source"
label="Source">
</v-text-field>
<v-text-field
v-model="artifact.primaryKey"
label="Primary Key">
</v-text-field>
</v-form>
</template>
Supported by:
export default Vue.extend({
name: 'DatasetProperties',
props:{
artifact: {
type: Object as Prop<Dataset>,
default: {}
}
},
})
I have Vuex in place and "could" use that but it seems like overkill for creating a new object? But similar to how to work with data still trying to get where/when it is appropriate.
Complex model and form handling is not straight forward and there are very few good examples how to handle complex forms with custom component v-model implementations especially if you want v-model on objects. I'll attempt to give my recommendations below.
What is the correct way to pass props and handle them inside the components (as a copy), then bubble that back up to the parent?
It depends, if like in your example at the parent level you want to pass an initial object value through a prop and happy to wait until a final event occurs e.g. :artifact="newArtifact" #save="save", then that is the way to go. In other words, to me your Parent Component looks good.
However once you start working on child and lower components where you start working with the actual model properties via v-model it gets a bit more complex. See below.
When bubbling the event from lower components is the only way to repeat it until it reaches the parent?
As you pointed out in the original question, the "Vue way" way is -> data down and events up. In fact that is even how v-model work, remember v-model="model" is basically short for :value="model" #input="model = $event". But this gets a little messy when the model is an object and as mentioned above, there are few good examples on how to handle custom component v-model with complex objects as models.
See 3 below.
I am using "objects" as the prop/data not individual values. The reference gets updated (which is a copy) then bubbled to the parent (as I want). Is this the right way?
Using data updates by reference is not considered best practice for the reasons pointed out in #bviala answer. It also breaks the data down events up approach. Ideally, your child components ArtifactCreateForm and DatasetProperties should implement v-model functionality. As mentioned, implementing v-model in your custom component especially on objects is not as clean as you would hope. But you can implement helper functions to make it a bit easier.
For example, implementing v-model on your ArtifactCreateForm component, it would look like this:
<template>
<v-form>
<v-text-field
:value="value.name"
#input="update(m => (m.name = $event))
label="Name*"
required>
</v-text-field>
<v-text-field
:value="value.description"
#input="update(m => (m.description = $event))
label="Description"
required>
</v-text-field>
</v-form>
</template>
export default Vue.extend({
name: 'ArtifactCreateForm',
props:{
value: {
type: Object as Prop<IArtifact>
}
},
methods: {
update(cb: (m: IArtifact) => void) {
const model = clone(this.value);
cb(model);
this.$emit("input", model);
}
}
})
And you would use this as:
<app-dialog
:title="title"
:visible="visible"
#action="save"
#close="close">
<artifact-create-form v-model="internal"/>
</app-dialog>
For reference the following blog post helped me a lot on how to implement v-model with objects on custom components: https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
What is the correct way to pass props and handle them inside the components (as a copy), then bubble that back up to the parent?
As a copy, like you did in ArtifactCreate : Clone the prop, mutate it however you want, send it to the parent with an event if needed.
But if your child component doesn't need to have its own internal value, you shouldn't clone the prop, but rather just emit events and let the parent handle the mutation. It would look like this in the child:
<v-text-field
:value="artifact.source"
label="Source"
#input="$emit('updateSource', $event)"
>
What's weird in your example is that your child component clones data that the parent just created. Couldn't the child be the one in charge of calling newDataset()?
When bubbling the event from lower components is the only way to repeat it until it reaches the parent?
Yes, the only proper way (see next answer)
I am using "objects" as the prop/data not individual values. The reference gets updated (which is a copy) then bubbled to the parent (as I want). Is this the right way?
What you did in ArtifactCreateForm DatasetProperties works but is considered an anti-pattern. The reason being that the data lives in the parent component and is mutated by the child, with the source of the mutation being unknown by the parent. It can lead to maintenance issues if your component hierarchy becomes complex. The correct way is to send events.
That's why in my opinion, you should split your components mindfully: Do you really have reusability potential for ArtifactCreateForm and DatasetProperties ? If not, you can make your life easier by sticking to a single ArtifactCreate component.

How can I update data object whenever changes happens inside v-for of child

How can I update data object of parent whenever changes happen inside v-for. I have a child component that I use inside parent component.
ParentComponent.vue
<template>
....
....
<child-component
v-for="i in count"
ref="childComponent"
:key="i"
currentPage="i" // currentPage doesn't update.
:page="i"
/>
<q-header>
{{currentPage}} // always displays default value:1
</q-header>
</template>
<script>
data () {
return {
pageCount: 10,
currentPage: 1,
}
},
How can I update currentPage of data object whenever i changes inside v-for. I have tried using watch without much luck. I don't have access to child component and can't modify it.
Much appreciated!
There is some slight confusion with how v-for is working on the child-component here. Writing currentPage="i" as a property (which should actually be v-bind:currentPage in order for the i to be interpreted as JS) will simply declare the attribute on each child-component
How can I update currentPage of data object whenever i changes inside v-for
i doesn't "change" in the traditional context of running a for loop inside of a normal JavaScript application. In Vue, your rendering logic and application logic are separate, and rightly so, because running logic as part of the rendering doesn't really make sense.
For example, let's look at how your app will render the child-component:
<!-- Vue output -->
<child-component ... currentPage="1" />
<child-component ... currentPage="2" />
<child-component ... currentPage="3" />
So let's look at separating the rendering logic from the application logic.
I realise you don't have access to child-component, but based on the context I will assume it is some kind of tabbing functionality (based on you trying to set a value for the "current page" - feel free to be more specific and I can update my answer).
We need to bridge that gap between the rendering logic and the application logic and we can do that by using events:
<child-component
v-for="i in count"
:ref="`childComponent-${i}`" // ref should be unique so add the i as part of it
:key="i"
:page="i"
v-on:click="currentPage = i" // when the user clicks this child component, the current page will be updated
/>
You may have to utilise a different event other than click but I hope this gets you closer to what you are trying to achieve. For the value of currentPage to update there has to be some kind of user input, so just find out which event makes the most sense. Maybe the child-component library you are using has custom events that are more appropriate.
you should look into Custom Events.
https://v2.vuejs.org/v2/guide/components-custom-events.html
Idea is, that whenever there is some update of your desire in child component, you can execute this.$emit(“change”), which will throw an event.
On parent side you can catch this event by #change=“myMethod” as one of the attributes.
methods: {
myMethod() {
console.log("Testing")
}
}
<child-component
v-for="i in count"
ref="childComponent"
:key="i"
currentPage="i"
:page="i"
#change=“myMethod”
/>
Let me know if that helped.

Watching a dynamically rendered field in Laravel Nova Vue component

In Laravel Nova, action modals are rendered in Vue by retrieving a list of fields to display through a dynamic component. I have replaced the action modal with own custom component, but am struggling to achieve the effect I want without also extending the entire set of components for rendering form fields.
I have my CustomResourceIndex.vue, containing a conditionally loaded (via v-if) ActionModal.vue, in which the form fields are rendered like so:
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:resource-name="resourceName"
:field="field"
/>
</div>
where the actual form field component is chosen based on the field.component value.
Those form fields (which I ideally do not want to have to extend and edit) are rendered like so:
<template>
<default-field :field="field" :errors="errors">
<template slot="field">
<input
class="w-full form-control form-input form-input-bordered"
:id="field.attribute"
:dusk="field.attribute"
v-model="value"
v-bind="extraAttributes"
:disabled="isReadonly"
/>
</template>
</default-field>
</template>
I would like to watch the value of specific fields and run methods when they change. Unfortunately due to a lack of ref attribute on the input elements or access to the value that the form element is bound to, I'm not sure how I can accomplish that from within ActionModal.vue.
I am hoping that because I have access to the ids still, there is some potential way for me to emulate this behavior.
Many resources I've found on my own have told me that anything with an ID is accessible via this.$refs but that does not seem to be true. I can only see elements that have an explicitly declared ref attribute in this.$refs, so I am not sure if I've misunderstood something there.
I would recommend looking into VueJS watch property.
You can listen to function calls, value changes etc.
watch: {
'field.component': function(newVal, oldVal) {
console.log('value changed from ' + oldVal + ' to ' + newVal);
},
},
Are those components triggering events? Try looking into the events tab of the Vue DevTools to see if some events are triggered from the default-field component when you update the value.
My guess is that you could write something like:
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:resource-name="resourceName"
:field="field"
#input="doSomething($event)"
/>
</div>
The $event value being the new value of the field.
Hit me on the comments if you have more info on the behavior of the default form fields (Are their complete code accessible somewhere?).

How to emit data to DIRECT PARENT only in Vue 2.0?

I have a reusable child component SizeSelector and it has been used under 2 components served for different purposes.
In SizeSelector
onSizeSelectionChanged() {
if (this.isMultiSelectable === false) {
this.selectedVariants = [this.selectedVariant];
}
this.$emit('update', this.selectedVariants);
}
Under component A:
<product-size-form :product="product" #update="selectedVariant = $event[0]"></product-size-form>
Under component B:
<product-size-form
:product="product"
:is-multi-selectable="true"
#update="selectedVariants = $event; log($event, 'modal')">
</product-size-form>
My problem is when I click and change the selectedVariants in B it emited to A. How to fix it? I seems the event got emitted globally
Update
It sounds crazy and it is.
This one is not working
<div class="product-form">
<product-size-form
:product="product"
:is-multi-selectable="true"
#update="selectedVariants = $event; log($event, 'modal')">
</product-size-form>
</div>
And this one works completely fine
<div class="product-form-wow-this-is-working-wtf">
<product-size-form
:product="product"
:is-multi-selectable="true"
#update="selectedVariants = $event; log($event, 'modal')">
</product-size-form>
</div>
The way I see it you have 2 options:
1)Just change the names of your events, call one "update" (not particularly descriptive), and the other "update2" or anything except "update".
2)The other thing you can do is work react style i.e. instead of emitting an event pass in a prop with a callback function that does something in the parent.
Like it was pointed out this should not be happeing events are not supposed to be emitted globally.