Dynamically add properties to Vue component - vue.js

I am loading data from the database which drives what type of component I display
An AJAX call goes off and returns me some data (this can be restructured if needed)
{
component_type: 'list-item',
props: {
name: 'test',
value: 'some value'
}
}
This is accessible on my parent object a variable called component
Within the template of my parent object I have the following
<component :is="component.component_type" ></component>
This works fine and loads the component as expected.
Next I want to add the properties from my data object into this tag too
<component :is="component.component_type" {{ component.props }} ></component>
This doesn't work and rejects writing a tag with {{ in it. I presume this is an error thrown by the browser rather than Vue, although I'm unsure.
For reference I want the output to actually look like:
<component :is="component.component_type" name='test' value='some value' ></component>
How can I go about passing in these properties? Ideally I'd like these to be tied to data / props of the parent as I'm showing so that they can easily be changed in database and the UI will change accordingly.
At worst I will generate it all on server side, but I'd rather do it via ajax as I'm currently trying to do.

In case anyone is wondering how to do this using Vue 2 you can just pass an object to v-bind:
<template>
<component :is="componentType" v-bind="props"></component>
</template>
<script>
export default {
data() {
return {
componentType: 'my-component',
props: {
foo: 'foofoo',
bar: 'barbar'
}
}
}
}
</script>

Following this thread, I see two options to do this.
one is to pass a single prop which is an object in itself, and pass all the relevant key values in it which can be used by the child component, something like following:
<component :is="component. component_type"
:options="component.props"
</component>
Other solution mentioned is to have a directive, where you pass the object and it will set the attributes which are keys in that object to corresponding values, you can see this in work here.
Vue.directive('props', {
priority: 3000,
bind() {
// set the last component child as the current
let comp = this.vm.$children[this.vm.$children.length - 1];
let values = null;
if(this._scope && this._scope.$eval) {
values = this._scope.$eval(this.expression);
} else {
values = this.vm.$eval(this.expression);
}
if(typeof values !== 'object' || values instanceof Array) {
values = { data: values };
}
// apply properties to component data
for(let key in values) {
if(values.hasOwnProperty(key)) {
let hkey = this.hyphenate(key);
let val = values[key];
if(typeof val === 'string') {
comp.$options.el.setAttribute(hkey, values[key]);
} else {
comp.$options.el.setAttribute(':' + hkey, values[key]);
}
}
}
console.log(comp.$options.el.outerHTML);
comp._initState();
},
/*
* Hyphenate a camelCase string.
*/
hyphenate(str) {
let hyphenateRE = /([a-z\d])([A-Z])/g;
return str.replace(hyphenateRE, '$1-$2').toLowerCase();
}
});
and use it like this:
<div class="app">
<component is="component.component_type" v-props="component.props"></component>
</div>

As far as I know, there is no rest props (spread props) syntax in Vue.js template.
A possible solution is render functions. You have full power of JavaScript when using render functions, so you can do something like this:
render (h) {
return h('foo', {
props: {
...yourData
}
})
}
I created a simple example here: http://codepen.io/CodinCat/pen/bgoVrw?editors=1010
both component type (like foo) and props can be dynamic (but you still need to declare all the possible fields of prop (like a, b and c) in your child components)
There is another solution is JSX.
Use the JSX babel plugin: https://github.com/vuejs/babel-plugin-transform-vue-jsx
then you can use the spread props syntax:
return <foo {...{yourData}}></foo>

Related

How to avoid rerendering all child components which are created by v-for directive

There is a list of child component
<question-list-item
v-for="(item, index) in questionListParsed"
:key="item.id"
:both-question="item"
:class-id="classId"
:subject-id="subjectId"
:index="index+1"
/>
and the questionListParsed is a getter in vuex.
/**************************************************************************
* getters
**************************************************************************/
get questionListParsed(): QuestionListItemRes[] {
const { questionList, showingOriginalQuestion } = this
const questionListParsed = questionList.map((e) => {
const recommendQuestion = e.recommendedQuestions[0]
const recommendQuestionIds = showingOriginalQuestion[e.questionNumber]
let arr = []
if (recommendQuestionIds) {
arr = recommendQuestionIds.filter((item) => {
return !this.removedRecommendQuestionIds.includes(item)
})
}
return {
recommendQuestion: {
...recommendQuestion,
stem: recommendQuestion.question,
knowledges: splitMultiKnowledge(recommendQuestion.knowledge),
questionSourceList: recommendQuestion.sources,
categoryId: recommendQuestion.categoryId,
},
originalQuestion: {
...e,
id: e.questionNumber,
stem: e.question,
difficulty: e.complexity,
knowledges: splitMultiKnowledge(e.knowledge),
},
id: recommendQuestion.id,
questionSimilarId: e.questionNumber,
mistakeAnswerId: e.id,
targetExerciseId: e.targetExerciseId,
status: recommendQuestion.status,
}
})
return questionListParsed
}
and the questionListParsed is mainly depends on the state questionList whitch is the originnal data from server side. Now i change questionList by the following way
#Mutation
updateQuestionListByIndex(data: UpdateParams): void {
if (data.value) {
const temp = [...this.questionList]
temp[data.index] = data.value
this.questionList = temp
}
}
and commit the mutation inside an Action like these
this.context.commit('updateQuestionListByIndex', {
index: targetIndex,
value: originQuestion[0],
})
I just want to change one item in the array questionList and then questionListParsed changed.
The expectation is that only one component updated but all of the child component updated(use console.log('updated') in its updated hocks).
How to do that?
The reason why all components are updated is because you use computed property (Vuex getters are Vue computed properties).
Whenever anything in questionList is changed, questionListParsed is recomputed and because you are using map and generating new objects, the result is a new array with completely new objects --> every child in list is updated
I would not consider it a problem because in reality only the DOM elements of the changed item are updated (that is the beauty of virtual DOM). If you do see some performance problem, the way around it is to stop using computed/getters and instead do the transformation only once when data is loaded and continue to work only with questionListParsed
You don't need to prevent the child components from rerendering, Vue does that for you. By providing a unique key to each list element :key="item.id" you give Vue a hint about the item, so Vue can identify and reuse the already rendered parts.
See https://v2.vuejs.org/v2/api/#key for more information.

Add focus to first input on load and next input after success using vue

I am creating a fill in the blank using vue. I have three components, a parent component where I build the question, an input component where I validate and the text component. I have stripped out a lot of the code to try and keep the question relevant, anything commented out is unsuccessful. Hope I am not out in left field on this one but have never attempted this so thought I would try here.
First issue:
Some of the questions have two inputs and I want to auto provide focus to the first input, (using a custom directive ) I am able to gain focus on the last created input. I am not really sure how to access first child I guess. Works well with single input questions though. I have tried doing so by using $refs and $nextTick(()) but no luck.
Second issue:
Once one input gets isCorrect I want to auto focus to the next available non correct element. I figured I would have be able to access the child input from the parent component and have been trying using this link but I have been unsuccessful.
Any help or insights is much appreciated. thanks
What it looks like for visual
What I am after
Parent Component
import ivInput from "../inline-components/iv-input.vue";
import ivText from "../inline-components/iv-text.vue";
<component
:is="buildQuestion(index)"
ref="ivWrap"
v-for="(item, index) in questionParts"
:key="index">
</component>
export default() {
components:{
ivInput,
ivText
},
mounted(){
// console.log(this.$refs.ivWrap)
// console.log(this.$refs.iv)
},
methods: {
buildQuestion: function (index) {
if(this.questionParts[index].includes('*_*')){
return ivInput
}else{
return ivText
}
},
//focus: function (){
// this.$refs.iv.focus()
// console.log(this.$refs.iv)
// }
}
}
Input Component
<div :class="'iv-input-wrap'">
<input
ref="iv"
v-focus
type="text"
v-model="userInput"
:class="[{'is-correct':isCorrect, 'is-error':isError}, 'iv-input']"
:disabled="isCorrect">
</div>
export default{
// directives:{
// directive definition
// inserted: function (el) {
// el.focus()
// },
}
computed{
isCorrect: function () {
if(this.isType == true && this.isMatch == true){
// this.$refs.iv.focus()
// this.$nextTick(() => this.$refs.iv.focus)
return true
}
}
}
}

vuejs handsontable official and calling handsontable method

I'm a beginner, this is probably more of a javascript problem than vue but anyway:
there a plugin for spreadsheet named handsontable and in the normal use you make the table by doing this
hot = new Handsontable(container, {option})
and then you can use the method like hot.loadData() etc..
To use handsontable with vuejs, there a wrapper we can find here https://github.com/handsontable/vue-handsontable-official. With the wrapper you make a table like this :
<template>
<div id="hot-preview">
<HotTable :root="root" :settings="hotSettings"></HotTable>
</div>
</template>
<script>
import HotTable from 'vue-handsontable-official';
import Vue from 'vue';
export default {
data: function() {
return {
root: 'test-hot',
hotSettings: {
data: [['sample', 'data']],
colHeaders: true
}
};
},
components: {
HotTable
}
mounted () {
localforage.config({
driver: localforage.INDEXEDDB,
name: 'matchlist-database'
})
localforage.getItem('DB').then(function (value) {
console.log('then i fetch the DB: ' + JSON.stringify(value))
if (value !== 'null') {
console.log('dB contain something')
**root**.loadData(value)
}
</script>
So it work fine when i give an array but to load the data from a DB you must call the handsontable method hot.loadData(data).
i cannot find how to call this method in vuejs i always get the error
TypeError: root.loadData is not a function
i tried with all i could think of instead of root ex: HotTable.loadData(value)
but to no avail
Can someone point me out how i would call handsontable methods from the vuejs wrapper. Or point me out what kind of reading i should do to understand my mistake. Thank a lot
There are two problems here, not bad ones :)
1st problem:
If you want to refer to your data inside Vue's methods/computed properties/watchers/lifecycle events, you should use the this keyword. If you have data: function() { return { root: "root-value" }} and you would like to console.log that "root-value" string, you should write console.log(this.root) inside your mounted handler.
If you had something like:
data: function() {
return {
hot = new Handsontable(container, {option})
....
};
You could call hot.loadData() like so:
mounted() {
this.hot.loadData();
...
}
So this refers to the Vue instance which exposes your data properties.
2nd problem:
If I understand the component wrapper correctly, you are supposed to pass data to it as props, not call any Handsontable methods directly.
<HotTable :root="root" :settings="hotSettings"></HotTable>
This means that Vue passes whatever you have as root in your data to the HotTable component. It also passes whatever you have as settings in your data. In the example, HotTable receives these:
root: 'test-hot',
hotSettings: {
data: [['sample', 'data']],
colHeaders: true
}
Now if you want to change/update/modify/add data that should be passed to the HotTable component, you should update your data in the Vue instance. You should do something like this.hotSettings = something new and this.root = something else and the HotTable component would receive those.
To understand what's really happnening with the HotTable, read all of the component documentation. Really. You will save lots of time if you read through the documentation. It all makes sense after that!
https://v2.vuejs.org/v2/guide/components.html

Can't copy props to model data and render it in Vue 2

I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you

Binding method result to v-model with Vue.js

How do you bind a method result to a v-model with Vue.js?
example :
<someTag v-model="method_name(data_attribute)"></someTag>
I can't make it work for some reason.
Thank you.
Years later, with more experience, I found out that is it easier to bind :value instead of using v-model. Then you can handle the update by catching #change.
Edit (per request):
<input :value="myValue" #change="updateMyValue">
...
methods: {
updateMyValue (event) {
myValue = event.target.value.trim() // Formatting example
}
}
And in a child component:
// ChildComponent.vue
<template>
<button
v-for="i in [1,2,3]">
#click="$emit('change', i) />
</template>
// ParentComponent.vue
<template>
<child-component #change="updateMyValue" />
</template>
<script>
import ChildComponent from './child-component'
export default {
components: {
ChildComponent
},
data () {
return {
myvalue: 0
}
},
methods: {
updateMyValue (newValue) {
this.myvalue = newValue
}
}
}
</script>
v-model expressions must have a get and set function. For most variables this is pretty straight forward but you can also use a computed property to define them yourself like so:
data:function(){
return { value: 5 }
},
computed: {
doubleValue: {
get(){
//this function will determine what is displayed in the input
return this.value*2;
},
set(newVal){
//this function will run whenever the input changes
this.value = newVal/2;
}
}
}
Then you can use <input v-model="doubleValue"></input>
if you just want the tag to display a method result, use <tag>{{method_name(data_attribute)}}</tag>
Agree with the :value and #change combination greenymaster.
Even when we split the computed property in get/set, which is help, it seems very complicated to make it work if you require a parameter when you call for get().
My example is a medium sized dynamic object list, that populates a complex list of inputs, so:
I can't put a watch easily on a child element, unless I watch the entire parent list with deep, but it would require more complex function to determine which of the innter props and/or lists changed and do what fromthere
I can't use directly a method with v-model, since, it works for providing a 'get(param)' method (so to speak), but it does not have a 'set()' one
And the splitting of a computed property, have the same problem but inverse, having a 'set()' but not a 'get(param)'