How to change value of component property in a code in Vue.js? - vue.js

I have an array of generic child components in my parent component:
<component v-for="item in items" :key="item.id" :is="componentName">
I can get a child via this.$refs, but I can't set a new value for a prop :is like:
this.$refs[id][0].is = 'MyNewComponentName'
How can I set a value of component instance property in a code?

First define your prop structure like
{
...item, // to use your current variables
componentName: 'MyExistingComponentName'
}
Receive the prop and bind it to a data variable, so something like
data: function() {
returns {
items: this.propItem
}
}
Make the required adjustment in your tag
<component v-for="item in items" :key="item.id" :is="item.componentName">
Now you got 2 options, you can either change the item.componentName by referencing this.items in a method, finding the index and changing it or you could get the parent to change the value of the prop using a custom event using $.event(event-name, 'MyNewComponent`). Both methods are fine, it really depends on your requirements.
Refer to https://v2.vuejs.org/v2/guide/components-custom-events.html
You could also read stackoverflow questions on mutating prop values.

Related

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.

VueJs - What's The Correct Way to Create a Child Component With Input Fields

I'm trying to use vuejs to display a list of instances of a child component.
The child component has input fields that a user will fill in.
The parent will retrieve the array of data to fill in the child components (If any exists), but since they're input fields the child component will be making changes to the value (Which generates errors in the console, as child components aren't supposed to change values passed from the parent).
I could just be lazy and just do everything at the parent level (i.e. use a v-for over the retrieved array and construct the list of elements and inputs directly in the parent and not use a child component at all), but I understand that it's not really the vuejs way.
I'm not very familiar with child components, but I think if it was just static data I could just declare props in the child component, and fill it from the parent.
However what I kind to need to do is fill the child component from the parent, but also allow changes from within the child component.
Could someone please describe the correct way to achieve this?
Thanks
You can use inputs on child components. The pattern is like this (edit it's the same pattern for an array of strings or an array of objects that each have a string property as shown here):
data: function() {
return {
objects: [ { someString: '' }, { someString: '' } ]
}
}
<the-child-component v-for="(object, i) in objects" :key="i"
v-model="object.someString"
></the-child-component>
Then, in the child component:
<template>
<div>
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</div>
</template>
export default {
name: 'the-child-component',
props: ['value'],
}
See it described here: https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

VueJS: Altering prop directly vs. this.[prop] in v-if

I built a vue component that get's a number value via prop from the outside laravel blade, like this:
<my-custom-template :mynumber="{{$numbervalue}}" :list:{{$alist}}></my-custom-template>
inside the template I have a v-for list and the prop:
props:{
list:Array,
mynumber: Number,
[..]
}
and
<template>
<ul>
<li v-for="item in list">{{item}}<span v-if="item.id == mynumber">active</span></li>
</ul>
</template>
Whenever the ID of the item is the same as the value mynumber, I want the "active" tag/span to be displayed.
Now in this template I also have a method that sends an axios request and on success it alters the value of the prop "mynumber", so the list should rerender:
axios.post('/api/someurl', this.obj)
.then(res => {
this.mynumber= res.data[something]; // returns a new number from the db.
})
.catch(error => { [..]
};
Issue: If I use this.mynumber in the list's v-if condition, the "active" tag is never being shown. If I use directly == mynumber then it works, but I cannot alter it with the axios response.
How should I approach this correctly?
How can I alter the initial prop, with the new value from the axios call?
First, you shouldn't be modifying props directly, as mentioned in this prior Stack Overflow post.
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "propRoomSelected"
Second, as seen in the Vue documentation for conditionals, you do not use this within templates, the this is inferred.
Now, to get to the meat of your question, how to look at either a prop or a new value when rendering. Here's how I'd do it.
<template>
<ul>
<li v-for="item in list">{{item}}<span v-if="isCurrent(item.id)">active</span></li>
</ul>
</template>
<script>
export default {
props: ['list', 'myNumber'],
data() {
return {
myNewNumber: undefined
}
},
methods: {
isCurrent(itemId) {
return itemId == (myNewNumber || myNumber)
}
}
}
</script>
Edit:
Note that there is a difference between
return itemId == (myNewNumber || myNumber)
and
return (itemId == myNewNumber) || (itemId == myNumber)
The first one "short circuits" a comparison against myNumber once myNewNumber becomes anything "truthy". Read more here.
Don't mutate props directly. Use this.$emit (Docs: https://v2.vuejs.org/v2/guide/components-custom-events.html) instead to change the myNumber in the parent. myNumber will then automatically update in the child component.

VueJS - toggle background image of list item on hover

I'm relatively new to Vue and I'm wondering what's wrong with my component that my isHover variable (prop?) isn't working to change the background on mouseover.
<template>
<div class="list-wrap" v-if="gridItems">
<div
class="list-itme"
v-for="(item, index) in gridItems"
:key="index"
#click.stop="setCurrentLocation(location)"
>
<a
#mouseover="mouseOver(index)"
#mouseleave="mouseLeave(index)"
:style="{
background: isHover
? `url(${item.location_image.thumbnails.large.url})`
: `url(${item.location_image.thumbnails.largeHover.url})`
}"
>
{{ item.location_name }}
{{ isHover }}
</a>
</div>
</div>
</template>
<script>
export default {
name: "GridItems",
computed: mapState(["filters", "GridItems"]),
methods: {
mouseOver(index) {
this.item[index].isHover = true;
},
mouseLeave(index) {
this.item[index].isHover = false;
}
},
data() {
return {
isHover: false
};
}
};
</script>
background: isHover
? `url(${item.location_image.thumbnails.large.url})`
: `url(${item.location_image.thumbnails.largeHover.url})`
The isHover above references the data property of the component.
Your mouseOver() and mouseLeave() methods are assigning a property also called isHover on this.item[index]. These are two completely different properties. Where are you getting this.item from? I don't see any props or it being declared as a data attribute.
Edit
You could have a isHover property on the gridItem. Therefore instead of passing index as an argument into the mouse event methods you can actually pass item. Then just set item.isHover = true. On the style binding you can just check against item.isHover.
Which means you don't need the "other" isHover data property on the component.
There are a few things to consider in your code, the isHover variable which is being used to change the background of your elements is a data property, but in your mouseOver and mouseLeave you are trying to change the isHover property from an element on an array called item which is not declared in the code you posted. Another thing to notice is that it is not necessary to return anything on your mouseOver and mouseLeave methods.
As I understand, the expected behavior of your code is to change the background color of the item you are hovering with your cursor. A couple of suggestions, you should use class binding instead of adding inline styles to your template elements, also you could pass the item instead of the index on your mouseover and mouseleave handlers. Another thing to mention is that I would only recommend doing this if for some reason you need the isHover property on your item for something else, otherwise you should just use CSS :hover to achieve this. I made a small demo so you can take a look on what you can do to make your code work: codepen
Edit
To change the image when hovering over an item you should be using the isHover property of that particular item instead of the component's data property isHover which you are currently using to try to change the image url. I updated my codepen.

How to encapsulate / wrap a VueJS component?

Hi everybody, please pardon my english :-)
I have a Vue component that can take dynamic slots (the names of the slots will depend on a props).
I use it on several places and some of the slots are always present.
To avoid redundancy, I'm looking for a way to create a component that "wrap" the final component to allow me to define only the additionals slots.
If there is an "obvious" way to achieve it, I may have missed it :-)
Code example
Without a "wrap component"
<b-table
show-empty
small
hover
[...some others and always present props...]
:items="aDataVarThatWillChangeBasedOnTheContext"
[...some others and uniq props...]
>
<template slot="same-1">
A slot that will always be present with the same content (for example, a checkbox in the first column)
</template>
<template slot="same-2">
A slot that will always be present with the same content (for example, some action buttons in the last column)
</template>
[...some others and always present slots...]
<template slot="not-the-same">
A slot that is only used in this context (for example, a duration based on a row timestamp and a timestamp picked by the user)
</template>
[...some others and uniq slots...]
</b-table>
With a "wrap component"
<my-b-table
:items="aDataVarThatWillChangeBasedOnTheContext"
>
<template slot="not-the-same">
A slot that is only used in this context (for example, a duration based on a row timestamp and a timestamp picked by the user)
</template>
</my-b-table>
Note: The dynamic slot name is not predictible.
If I suddenly need a "foo" column, I should be able to pass a "foo" slot (and a "HEAD_foo" slot, in my case)
Some researches
I read here that:
They’re (the functionnal components) also very useful as wrapper components. For example, when you need to:
Programmatically choose one of several other components to delegate to
Manipulate children, props, or data before passing them on to a child component
And "Manipulate children, props, or data before passing them on to a child component" seems to be exactly what I need.
I looked on render function but a lot of things seems to be not implemented, like the v-model, and I have difficulties to figure out how to pass dynamic slots...
Thank you in advance for your(s) answer(s) !
up: At the 07.03.2018 I still dont have any idea about how to solve this case
Found the answer that was somehow unclear to me that month ago.
("Dynamic" means here "not explicitely declared by the component, but gived by the parent")
Wrapper component
Props and scoped slots can be gived dynamically by the options object of createElement function.
"Simple" Slots can be gived dynamically by the childs array of createElement function.
Wrapped component
Props can't be dynamic unless the component is functional.
Slots can always be retrieved dynamically.
Scoped slots can be retrieved only if the component isn't functional.
Conclusion
It's not possible to have dynamics props and scoped slots at the same time...
But it's possible to declare all the needed props and then to use a "non-functionnal" component as wrapper and as wrapped.
How to
Retrieve from non-functional component
var component = Vue.component('component-name', {
props: ['name', 'of', 'the', 'props'],
// [...]
aMethod: function () {
this._props // all the declared props
this.$slots // all the slots
this.$scopedSlots // all the scoped slots
}
});
Retrieve from functional component
var component = Vue.component('component-name', {
functional: true,
render: function (createElement, context) {
context.props // all the props
context.children // all the slots as an array
context.slots() // all the slots as an object
}
});
Give to child component
var component = Vue.component('component-name', {
render: function (createElement) {
return createElement(
childComponent,
{
props: propsToGive,
scopedSlots: scopedSlotsToGive
},
[
// non-scoped slots to give
createElement('slot-tag-name', {slot: 'slot-name'})
]
);
}
});
References
https://v2.vuejs.org/v2/guide/render-function.html
https://v2.vuejs.org/v2/guide/render-function.html#createElement-Arguments
https://v2.vuejs.org/v2/guide/render-function.html#Functional-Components
Sandbox
https://jsfiddle.net/5umk7p52/
Just make a regular component out of your customized <b-table>.
You'll need to define an items prop for your component to pass as the items for the <b-table> component.
And, to define a slot for your component, you'll need to use the <slot> tag, specifying the name using the name attribute.
If you'd like to make one of the slots from the <b-table> component accessible in the <my-b-table> component, simply pass a <slot> tag as the content of the slot in your custom component.
It would look something like this:
Vue.component('my-b-table', {
template: `
<b-table
show-empty
small
hover
:items="items"
>
<template slot="same-1">
Content to pass to the b-table's slot
</template>
<slot name="not-the-same">
A slot that is only used in this context
</slot>
<template slot="last_edit">
<slot name="last_edit">
A slot to pass content to the b-table component's slot
</slot>
</template>
</b-table>
`,
props: { items: Array },
});