How to bind dynamic props to dynamic components in VueJS 2 - javascript

I'd like to know how I can iterate a list of component names (which come from an AJAX call to an API server) and render them as components, and pass relevant properties to each component (i.e. bind their properties dynamically).
So far I have managed to iterate a JSON list of items that represent components, and successfully render these components. What I'd like to do now is bind the properties for each component using v-bind.
In the example below, the item-one component would receive the image property with the item1.jpg value; and the item-two component wouldn't receive any properties.
<template>
<div v-for="item in items">
<component :is="Object.keys(item)[0]" :v-bind="???"></component>
</div>
</template>
<script>
import ItemOne from '../components/item-one'
import ItemTwo from '../components/item-two'
export default {
components: {
ItemOne,
ItemTwo
},
asyncData () {
return {
items: [
{ 'item-one': { 'image': 'item1.jpg' } },
{ 'item-two': { } }
]
}
}
}
</script>
I tried using :v-bind="Object.values(Object.keys(item)[0])" but I get the attribute v-bind="[object Object]" in the rendered element.

First, you need to get rid of the colon before v-bind. The colon is a short-hand for v-bind when prefixed to an attribute. But, when passing an object of key pairs to bind, you simply use the v-bind directive.
Second, you are not correctly referencing the properties of each item. You can reference them like this:
v-bind="item[Object.keys(item)[0]]"
You wouldn't need to use Object.keys if you changed the structure of your items property a bit:
items: [{
type: 'item-one',
props: { 'image': 'item1.jpg' },
}, {
type: 'item-two',
}]
This way, by explicitly labeling the component type and properties beforehand, your template would be much easier to understand:
<div v-for="item in items">
<component :is="item.type" v-bind="item.props"></component>
</div>

Related

trigger function in child.vue after clicking button in parent.vue

I'm working with BootstrapVue.
I have a method in my parent.vue where I pass a value (this.propsIndex) to my child.vue.
Now I want to use this value each time it will be clicked in a method of my child.vue - but how can I trigger my function and make it working?
Thank You very much!
If it's possible I want to avoid using watch
my parent.vue
<template>
<div v-for="(id, index) in inputs" :key="index">
<b-button #click="deleteViaIndex(index)">Delete</b-button>
<child :indexProps="indexProps" />
</div>
<div>
<b-button #click="addInput()">Add Input</b-button>
</div>
</template>
<script>
methods: {
deleteViaIndex(index) {
this.propsIndex= index;
},
addInput() {
this.inputs.push({})
},
},
data() {
return {
inputs: [{}],
propsIndex: '',
}
}
</script>
my child.vue (script)
props: ["propsIndex"],
methods: {
deleteViaParentIndex() {
//HERE I WANT TO USE IT EVERY TIME IT WILL BE CLICKED IN MY PARENT.VUE
//BUT FOR NOW IT'S NOT DOING ANYTHING WHEN I CONSOLE.LOG(this.propsIndex)
}
}
Aside from the naming mismatch mentioned by Marcin, you can access a child component from a parent component using a ref in the template:
<child ref="childrenComponents" :props-index="propsIndex" />
Since you have multiple of the child components inside a v-for, this makes the childrenComponents in $refs an array of components. To call deleteViaParentIndex on all of them, you need to iterate through them:
deleteViaIndex(index) {
this.propsIndex = index;
this.$refs.childrenComponents.forEach(child => child.deleteViaParentIndex());
}
There's one more optimization you can make.
Since you're using propsIndex only to pass an index that the child component uses, and you already have the index as a param in deleteViaIndex, you can simply pass that to the child as a param during the deleteViaParentIndex function call, thus removing the need for the propsIndex data altogether:
in parent.vue:
deleteViaIndex(index) {
this.$refs.childrenComponents.forEach(child => child.deleteViaParentIndex(index));
}
in child.vue:
deleteViaParentIndex(index) {
// operations using index
}
Looks like a naming mismatch. In a child component, you have a prop propsIndex, yet in a parent template you are passing indexProps.
When passing props, you have to remember, that prop name is always in the first part, and the value you are passing should go next. https://v2.vuejs.org/v2/guide/components-props.html#Passing-Static-or-Dynamic-Props
Furthermore, since HTML attribute names are case-insensitive, you should pass a prop this way:
<child :props-index="indexProps" />

Relationship between props and data (vue)

In
https://codesandbox.io/s/v9pp6
the ChromePage component passes a prop to InventorySectionC:
<inventory-section-component :itemSectionProps="getItemSection">
</inventory-section-component>
InventorySectionC:
<template>
<div class="inventory-section-component">
<draggable v-model="itemSectionProps.itemSectionCategory">
<transition-group>
<div
v-for="category in itemSectionProps.itemSectionCategory"
:key="category.itemSectionCategoryId"
>
<!-- <p>{{ category.itemSectionCategoryName }}</p> -->
<inventory-section-group-component :itemSectionGroupData="category">
</inventory-section-group-component>
</div>
</transition-group>
</draggable>
</div>
</template>
<script>
import InventorySectionGroupComponent from "./InventorySectionGroupC";
import draggable from "vuedraggable";
export default {
name: "InventorySectionComponent",
components: {
InventorySectionGroupComponent,
draggable,
// GridLayout: VueGridLayout.GridLayout,
// GridItem: VueGridLayout.GridItem,
},
props: {
itemSectionProps: {
type: Object,
},
},
data() {
let itemSectionData = itemSectionProps;
return {
itemSectionData
};
},
};
</script>
<style scoped>
</style>
gives a warning at line:
<draggable v-model="itemSectionProps.itemSectionCategory">
:
Unexpected mutation of "itemSectionProps" prop. (vue/no-mutating-props)eslint
Why (how?) is itemSectionProps mutable?
Can a binding be created between props and data (all draggable samples use a data object:
https://sortablejs.github.io/Vue.Draggable/#/nested-example
https://github.com/SortableJS/Vue.Draggable/blob/master/example/components/nested-example.vue
)?
The idea is to have auto updating, nested, draggable components.
The code as is "works" but there are warnings/errs:
data() can't seem to see props:
And one more thing, which comes "first"? Data or props? can't seem to figure it out from the docs:
https://v2.vuejs.org/v2/guide/instance.html
Setting the props to a predefined value:
props: {
itemSectionProps: {
type: Object,
default: { itemSectionCategory: '' }
},
},
gives:
Type of the default value for 'itemSectionProps' prop must be a function. (vue/require-valid-default-prop).
I'm not sure why vue expects props to return a function.
After adding a default() onto props, props are empty when passed on to components:
https://codesandbox.io/s/sjm0x
(this grew too long for a comment, but probably already answers what you need)
itemSectionProps:
Your props are defined as:
props: {
itemSectionProps: {
type: Object,
},
},
You reference a prop of that object in your template
<draggable v-model="itemSectionProps.itemSectionCategory">
Vue cannot assume itemSectionProps.itemSectionCategory will exist in the future.
You should give it a default (see Vue docs) to create the expected values in that object.
props: {
itemSectionProps: {
type: Object,
default() {
return { itemSectionCategory: '' };
}
},
},
Do this for all the props you use on itemSectionProps.
data() can't seem to see props:
You can write this.itemSectionProps instead of only itemSectionProps.
But itemSectionProps is already defined in props. You can just remove itemSectionProps from data.
If you need to change that value, use a copy and promote changes with this.$emit.
You are probably calling the props without using this. on your data method.
You can as well define your variable itemSectionData as below:
data(){
return {
itemSectionData: Object.assign({}, this.itemSectionProps)
}
}
Object.assign()
The Object.assign() method copies all enumerable own properties from one or more source objects to a target object. It returns the target object. See more details here
Then use the newly defined variable itemSectionData within your component. Like:
<draggable v-model="itemSectionData.itemSectionCategory">
If you want to update the prop's values, simply emit an event from your child component and capture it on the parent as below:
methods:{
updatePropValues(){
this.$emit('updateProp', this.yourNewValues);
}
}
On the parent component handle the event as:
<inventory-section-component #updateProp="setNewValues" :itemSectionProps="getItemSection">
</inventory-section-component>
methods:{
setNewValues(newValues){
this.itemSections = newValues;
}
}
Check it out in action here

Vuex pass mutable object as prop, call mutation with the passed object as argument

Let's say I have a Vuex State which contains an array of objects which I want to mutate in the component. I iterate over the array and spawn a Component for each Object which takes the Object as a prop. Inside the component I call Vuex mutations with the Object passed down as a direct argument. For example:
Parent
<template>
<ItemComponent
v-for="(item, index) in items"
:key="index"
:index="index"
:item="item"
/>
</template>
<script>
import ItemComponent from 'ItemComponent.vue';
export default {
components: {
ItemComponent
},
computed: {
items() {
return this.$store.getters.items;
}
}
};
</script>
Child:
<template>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true
}
},
methods: {
changeItemProp() {
this.$store.dispatch('changeItemXValue', {this.item, 'newValue'});
}
}
};
</script>
Store:
// ...
mutations: {
changeItemXValue(state, { item, value }) {
item.x = value
}
}
It works, yes. But I'm pretty sure this is an Antipattern, right?
// ...
mutations: {
changeItemXValue(state, { item, value }) {
state.items.find((i) => i === item).x = value
}
}
This doesn't seem much better.
My question: Is this the way to go if I want to encapsulate my mutable data in a subcomponent, or is this considered an anti-pattern? In which case I would like to know what the proper way is to handle this case. I initially prepared vuex exactly to handle this case but I'm not sure if this is quite the right way. Thank you.
Well, there are multiple ways of doing this, but I'll explain a recommended way of doing.
What you're doing right now is that, you're fetching items from store, passing them to a child component (By iteration) then mutating following mutation,
// ...
mutations: {
changeItemXValue(state, { item, value }) {
state.items.find((i) => i === item).x = value
}
}
First of all, you don't need to mutate this item directly, instead you should create an action that finds the item for you and then mutate it using item id or index.
So what you need to do is that, create an action, when changing value of item, dispatch that action, and pass item id or even item to that action. Inside that action, find the item you want to mutate and then commit mutation, inside your mutation, simply write
state.items[itemIndex] = newValue

referring to a v-for element from inside a getter of computed function

In Vue, I have a v-for loop that renders an input according to an object I have.
The input is with a v-model to a computed function:
<template v-for="product in products>"
{{ product.Name }}: <input type="text" v-model="productAmount">
</template>
and accordingly, the computed function with get and setter:
computed: {
productAmount: {
get () {
this.product.DefaultAmount
},
set (newAmount) {
// doing something
}
}
}
The problem is that this.product in get() is undefined.
Is there a way to refer to a dynamic variable of v-for inside the getter of the computed function?
If you want to use a computed property you should create a child component and pass the product as a property (<Procuct v-for="p in products" :product="p"/>).
Alternatively if it is not an expensive operation you could easily replace the computed with a method instead though and use that:
...
methods: {
getAmountForProduct(product) {
return product.DefaultAmount
}
}

Can you force Vue.js to reload/re-render?

Just a quick question.
Can you force Vue.js to reload/recalculate everything? If so, how?
Try this magic spell:
vm.$forceUpdate();
//or in file components
this.$forceUpdate();
No need to create any hanging vars :)
Update: I found this solution when I only started working with VueJS. However further exploration proved this approach as a crutch. As far as I recall, in a while I got rid of it simply putting all the properties that failed to refresh automatically (mostly nested ones) into computed properties.
More info here: https://v2.vuejs.org/v2/guide/computed.html
This seems like a pretty clean solution from matthiasg on this issue:
you can also use :key="someVariableUnderYourControl" and change the key when you want to component to be completely rebuilt
For my use case, I was feeding a Vuex getter into a component as a prop. Somehow Vuex would fetch the data but the reactivity wouldn't reliably kick in to rerender the component. In my case, setting the component key to some attribute on the prop guaranteed a refresh when the getters (and the attribute) finally resolved.
Please read this
http://michaelnthiessen.com/force-re-render/
The horrible way: reloading the entire page The terrible way:
using the v-if hack The better way: using Vue’s built-in
forceUpdate method The best way: key-changing on your
component
<template>
<component-to-re-render :key="componentKey" />
</template>
<script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
</script>
I also use watch: in some situations.
Try to use this.$router.go(0); to manually reload the current page.
Why?
...do you need to force an update?
Perhaps you are not exploring Vue at its best:
To have Vue automatically react to value changes, the objects must be initially declared in data. Or, if not, they must be added using Vue.set().
See comments in the demo below. Or open the same demo in a JSFiddle here.
new Vue({
el: '#app',
data: {
person: {
name: 'Edson'
}
},
methods: {
changeName() {
// because name is declared in data, whenever it
// changes, Vue automatically updates
this.person.name = 'Arantes';
},
changeNickname() {
// because nickname is NOT declared in data, when it
// changes, Vue will NOT automatically update
this.person.nickname = 'Pele';
// although if anything else updates, this change will be seen
},
changeNicknameProperly() {
// when some property is NOT INITIALLY declared in data, the correct way
// to add it is using Vue.set or this.$set
Vue.set(this.person, 'address', '123th avenue.');
// subsequent changes can be done directly now and it will auto update
this.person.address = '345th avenue.';
}
}
})
/* CSS just for the demo, it is not necessary at all! */
span:nth-of-type(1),button:nth-of-type(1) { color: blue; }
span:nth-of-type(2),button:nth-of-type(2) { color: red; }
span:nth-of-type(3),button:nth-of-type(3) { color: green; }
span { font-family: monospace }
<script src="https://unpkg.com/vue"></script>
<div id="app">
<span>person.name: {{ person.name }}</span><br>
<span>person.nickname: {{ person.nickname }}</span><br>
<span>person.address: {{ person.address }}</span><br>
<br>
<button #click="changeName">this.person.name = 'Arantes'; (will auto update because `name` was in `data`)</button><br>
<button #click="changeNickname">this.person.nickname = 'Pele'; (will NOT auto update because `nickname` was not in `data`)</button><br>
<button #click="changeNicknameProperly">Vue.set(this.person, 'address', '99th st.'); (WILL auto update even though `address` was not in `data`)</button>
<br>
<br>
For more info, read the comments in the code. Or check the docs on <b>Reactivity</b> (link below).
</div>
To master this part of Vue, check the Official Docs on Reactivity - Change Detection Caveats. It is a must read!
Use vm.$set('varName', value).
Look for details into Change_Detection_Caveats
Sure .. you can simply use the key attribute to force re-render (recreation) at any time.
<mycomponent :key="somevalueunderyourcontrol"></mycomponent>
See https://jsfiddle.net/mgoetzke/epqy1xgf/ for an example
It was also discussed here: https://github.com/vuejs/Discussion/issues/356#issuecomment-336060875
<my-component :key="uniqueKey" />
along with it use this.$set(obj,'obj_key',value)
and update uniqueKey for every update in object (obj) value
for every update this.uniqueKey++
it worked for me this way
So there's two way you can do this,
You can use $forceUpdate() inside your method handler i.e
<your-component #click="reRender()"></your-component>
<script>
export default {
methods: {
reRender(){
this.$forceUpdate()
}
}
}
</script>
You can give a :key attribute to your component and increment when want to rerender
<your-component :key="index" #click="reRender()"></your-component>
<script>
export default {
data() {
return {
index: 1
}
},
methods: {
reRender(){
this.index++
}
}
}
</script>
In order to reload/re-render/refresh component, stop the long codings. There is a Vue.JS way of doing that.
Just use :key attribute.
For example:
<my-component :key="unique" />
I am using that one in BS Vue Table Slot. Telling that I will do something for this component so make it unique.
Using v-if directive
<div v-if="trulyvalue">
<component-here />
</div>
So simply by changing the value of trulyvalue from false to true will cause the component between the div to rerender again
Dec, 2021 Update:
You can force-reload components by adding :key="$route.fullPath".
For Child Component:
<Child :key="$route.fullPath" />
For router-view tag:
<router-view :key="$route.fullPath" />
However, :key="$route.fullPath" only can force-reload the components of the different route but not the components of the same route. To be able to force-reload the components of the same route as well, we need to add "value" with an array to :key="$route.fullPath" and change "value". So it becomes :key="[$route.fullPath, value]" and we need to change "value".
*We can assign Array to :key=.
<template>
<Child
:key="[$route.fullPath, value]" // Can assign "Array" to ":key="
#childReload="reload" // Call #click="$emit('childReload')" in
/> // Child Component to increment the value.
</template>
OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR
<template>
<router-view
:key="[$route.fullPath, value]" // Can assign "Array" to ":key="
#routerViewReload="reload" // Call #click="$emit('routerViewReload')"
/> // in Child Component to increment the value.
</template>
<script>
export default {
name: "Parent", components: { Child, },
data() {
return {
value: 0,
};
},
methods: {
reload() {
this.value++;
}
}
}
</script>
However, to keep using both "$route.fullPath" and "value" causes some error sometimes so only when some event like Click happens, we use both "$route.fullPath" and "value". Except when some event like Click happens, we always need to use only "$route.fullPath".
This is the final code:
<template>
<Child
:key="state ? $route.fullPath : [$route.fullPath, value]"
#childReload="reload" // Call #click="$emit('childReload')" in
/> // Child Component to increment the value.
</template>
OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR OR
<template>
<router-view
:key="state ? $route.fullPath : [$route.fullPath, value]"
#routerViewReload="reload" // Call #click="$emit('routerViewReload')" in
/> // Child Component to increment the value.
</template>
<script>
export default {
name: "Parent", components: { Child, },
data() {
return {
state: true,
value: 0,
};
},
methods: {
reload() {
this.state = false;
this.value++;
this.$nextTick(() => this.state = true);
}
}
}
</script>
Unfortunately, there are no simple ways to force-reload components properly in Vue. That's the problem of Vue for now.
This has worked for me.
created() {
EventBus.$on('refresh-stores-list', () => {
this.$forceUpdate();
});
},
The other component fires the refresh-stores-list event will cause the current component to rerender
<router-view :key="$route.params.slug" />
Just use key with your any params its auto reload children..
I found a way. It's a bit hacky but works.
vm.$set("x",0);
vm.$delete("x");
Where vm is your view-model object, and x is a non-existent variable.
Vue.js will complain about this in the console log but it does trigger a refresh for all data. Tested with version 1.0.26.
Worked for me
data () {
return {
userInfo: null,
offers: null
}
},
watch: {
'$route'() {
this.userInfo = null
this.offers = null
this.loadUserInfo()
this.getUserOffers()
}
}
The approach of adding :key to the vue-router lib's router-view component cause's fickers for me, so I went vue-router's 'in-component guard' to intercept updates and refresh the entire page accordingly when there's an update of the path on the same route (as $router.go, $router.push, $router.replace weren't any help). The only caveat with this is that we're for a second breaking the singe-page app behavior, by refreshing the page.
beforeRouteUpdate(to, from, next) {
if (to.path !== from.path) {
window.location = to.path;
}
},
Except page reload method(flickering), none of them works for me (:key didn't worked).
and I found this method from old vue.js forum which is works for me:
https://github.com/vuejs/Discussion/issues/356
<template>
<div v-if="show">
<button #click="rerender">re-render</button>
</div>
</template>
<script>
export default {
data(){
return {show:true}
},
methods:{
rerender(){
this.show = false
this.$nextTick(() => {
this.show = true
console.log('re-render start')
this.$nextTick(() => {
console.log('re-render end')
})
})
}
}
}
</script>
Add this code:
this.$forceUpdate()
For anyone still looking around, there's a package for this now.
https://github.com/gabrielmbmb/vuex-multi-tab-state
All I had to do was install it and add it to my plugins in main.ts (as it shows on that page) and it did exactly what I wanted.
If your URL changes as well when if the component is loaded you can just use it in the :key attribute. This works especially well if you use it on the router-view tag directly. And this commes with the added benedit of the key being a value that is actually tied to the content of the page instead of just some random number.
<router-view :key="this.$route.path"></router-view>
If you are using router-view or Vue Router, you can directly use the key feature
<router-view :key="$route.path"></router-view>
This will tell the router view to re-render the page every time the path is changed.

Categories

Resources