I'm new to the World of Vue.js and I have to build a recursive Component renderer that turns JSON into rendered Vue components.
So far the recursive rendering works just fine, except for the props I'm passing to the createElement function (code below ;) ) is not available as props, but inside the $vnode.data object.
Any ideas what I'm missing?
The mock JSON I'm using looks like this:
{
"_id": "aslwi2",
"template": "NavigationBar",
"data": {},
"substructure": [
{
"_id": "assd2",
"template": "NavigationBarListItem",
"data": {
"title": "NavList Item 1"
}
},
{
"_id": "a2323uk",
"template": "NavigationBarListItem",
"data": {
"title": "NavList Item 2"
}
}
]
}
My Recursive Rendering Component:
const createComponent = function(node ,h){
if(!node || !node.template){
return;
}
let children = [];
if(node.substructure instanceof Array){
node.substructure.map( async childNode => {
const childComponent = createComponent(childNode, h);
children.push(childComponent);
});
}
return h(node.template, {xdata : clone(node.data)}, children.length > 0 ? children : null );
};
export default Vue.component('Structure', {
render: function(createElement){
if(!this.structure) return;
const component = createComponent(this.structure, createElement, registry);
console.log(component);
return component;
},
props: {
structure: {
type: Object
}
}
})
And my dynamically instantiated components:
<!-- Component: NavBar -->
<template>
<div class="nav-bar">
<slot></slot>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object
}
}
}
</script>
<!-- Component: NavBarListItem -->
<template>
<div class="nav-bar-item">
{{title}}
</div>
</template>
<script>
export default {
data () {
return {
title : "default"
}
},
created() {
console.log('list item: ', this)
}
}
</script>
Inside the log of the created method in my list item component we find the passed props within the $vnode...
The problem is here:
return h(node.template, {xdata : clone(node.data)}, ...
I don't know what xdata is but it isn't something you should be passing to h. The options are documented at https://v2.vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth.
To pass a props you'd use props.
return h(node.template, {props : clone(node.data)}, ...
While I'm struggling a little to follow the intent behind your code it looks like you may also have a problem with NavBarListItem not defining a title prop. You can't pass a title unless it's defined as a prop. Defining it in data isn't the same thing.
Related
I have an array of objects, which i need to pass to child component via v-for to process in. Child component must tell the parent component that its data was changed; I tried to do it this way:
Parent component:
<template>
<row
v-for="(calc, index) in calculators"
:key="index">
<child-component v-model:calc="calculators[index]"></child-component>
</row>
<button #click="addCalculator"
</template>
<script>
....
data() {
return {
calculators: [
{
"name":"some name",
..
},
{
"name":"some name 2",
..
}
]
}
},
methods: {
addCalculator() {
this.calculators.push(someDefaultCalculatorObject)
}
}
</script>
Child component:
<template>
<input v-model="calc.name"/>
</template>
<script>
...
props: {
calc:Object
},
setup(props) {
const calc = ref(props.calc)
return calc
},
watch: {
calc: {
handler(val) {
this.$emit('update:calc', val)
},
deep: true
}
}
</script>
But when i'm trying to edit value of input of one object, every object in array updating to this value. How can i bind objects in array inside v-for?
This is the pared down code for the main component. I have reactive data elements setup here.
<template>
<div class="scheduler">
<recursive-render :items="data.probeGroupedData"/>
</div>
</template>
<script>
import { ref } from "vue";
import RecursiveRender from "./RecursiveRender.vue";
export default {
name: "Scheduler",
components: {
RecursiveRender
},
setup() {
const data = ref({
probeGroupedData: [],
probeScriptInfo: [],
probeScriptInfoHashById: {},
});
return {
data,
};
},
methods: {
probeSort: function() {
var result = []
//I do various things to sort the data and fill up result and then:
this.data.probeGroupedData = result;
console.log("\nresult:" + JSON.stringify(result, null, ' '));
}
}
},
mounted() {
//I do various things to request the data here, and call this.probeSort
},
};
</script>
The component writes data just fine if I paste the data directly into it on setup.
probeGroupedData: [{
"id": "_1",
"label": "Renton, WA",
"keyName": "id",
"cssClass": "top",
"children": [
{
"label": "Motorola",
"id": "_0",
"cssClass": "manufacturer",
"children": [
{
"label": "Atrix",
"id": "_1",
"cssClass": "family",
"children": [
{
"label": "HD",
"id": "_2",
"cssClass": "model",
"children": [
{
"isLeaf": true,
"id": 1,
"cssClass": "device"
}
]
}
]
}
]
}]
But it isn't updating it when it is being written by probeSort. Do I need to have a watcher or something know that it has changed?
This is the entire code for RecursiveRender
<template>
<div>
<div v-for="item in data.items" :class="cssClass" :key="item.id">
<div v-if="item.isLeaf" :id="item.id">
{{item.label}}
</div>
<div v-if="item.children">
{{item.label}}
<recursive-render :items="item.children" />
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import RecursiveRender from "./RecursiveRender.vue";
export default {
name: 'RecursiveRender',
components: {
RecursiveRender
},
setup() {
const data = ref({
items: [],
});
return {
data
};
},
props: {
items: {
type: Array,
default: () => []
}
},
mounted() {
//this is where the data is received
this.data.items = this.items;
}
};
</script>
I think the issue may be due to use of ref instead of reactive
ref is for values like boolean, string, and number. It will not update on deep changes. If you have nested data like array or object, reactive might be a better option since it can look for deep data changes.
I also noticed you're not using data.value. this.data.probeGroupedData = result; is likely storing the data in the wrong place. You could try this.data.value.probeGroupedData = result; which may still have issues with reactivity. you could also do this.data.value = {...this.data.value, probeGroupedData : result} which will trigger the reactivity without worrying about the depth issue.
Thanks for the clues that Daniel gave me. The error is passing the value on mount. Just having the prop is enough, it doesn't have to be passed back to data.
<template>
<div>
<div v-for="item in items" :class="cssClass" :key="item.id">
<div v-if="item.isLeaf" :id="item.id">
{{item.label}}
</div>
<div v-if="item.children">
{{item.label}}
<recursive-render :items="item.children" />
</div>
</div>
</div>
</template>
<script>
import RecursiveRender from "./RecursiveRender.vue";
export default {
name: 'RecursiveRender',
components: {
RecursiveRender
},
props: {
items: {
type: Array,
default: () => []
}
},
};
</script>
With more experience, I've learned you need to use provide / inject.
In the parent component, the data you want to have reactive needs to be provided:
//add provide to the import:
import { provide, ref } from "vue";
//in setup() you need to pass this to provide:
setup() {
const boardData = ref ({ <your data here> });
provide('boardData', boardData);
return {
boardData,
<and anything else>
}
}
In the child component you need to add inject to the import:
import { ref, inject } from "vue";
In setup you inject it using the same name as you used to provide it:
setup() {
const boardData = inject('boardData');
return {
boardData,
//and whatever else you need for the component
}
}
This is being used now for a backgammon game that has child pips, and it works well. For layout reasons the pips need to be in four groups of six, so I used a child component for them so I didn't have to have complex repeated loops in the main code. The pips now behave as they are all on rendered in the parent, and changes that the pips do to the reactive data are rendered properly in other sets of pips.
Currently I'm defining several component template(s) within a single vue component. I've defined some as strings, however it makes it somewhat incomprehensible if it gets more complex. So instead I'm making it return a separate component as a template. However I'm not sure how to pass data to the component.
This is a sample of the current approach for one of my component templates within my vue component. It's returning the template as a string and renders the html.
progessTemplate: function () {
return {
template: ('progessTemplate', {
template: `
<div id="myProgress" class="pbar">
<div id="myBar" :class="barColor" :style="{'width': width}">
<div id="label" class="barlabel" v-html=width ></div>
</div>
</div>`,
data: function () {
return { data: {} };
},
computed: {
width: function () {
if (this.data.SLA <= 20) {
this.data.SLA += 20;
}
return this.data.SLA + '%';
},
barColor: function(){
if(this.data.SLA > 60 && this.data.SLA <= 80){
return 'bar progressWarning';
}else if(this.data.SLA > 80){
return 'bar progressUrgent';
}
}
}
})
}
},
I'd like to avoid this approach and call a separate file instead.
I import the component into my vue file
import QueryDetailTemplate from '../../ej2/queryDetailTemplate';
and within my main vue file I have this function 'QueryDetailTemplate':
export default{
data(){
return{
data: [...],
QueryDetailTemplate: function(){
return {
template: QueryDetailTemplate,
props:{
test: 'Hello World',
},
};
},//end of QueryDetailTemplate
}//end of data
...
}
In my QueryDetailTemplate.vue this is my code:
<template>
<div>
Heyy {{test}} //undefined
</div>
</template>
<script>
export default{
props: ['test'],
created(){
console.log(this.test); //shows undefined
}
}
</script>
It renders the 'Heyy' that's hardcoded but it doesn't get the 'test' prop.
Appreciate any pointers
I'm not quite sure what you're trying to achieve but...you should be specifying the component as a component like so:
export default{
components: {
QueryDetailTemplate
},
data(){
return{
data: [...],
}
}
OR if you want to import it asynchronously:
import Vue from 'vue'
export default {
methods: {
import() {
Vue.component(componentName, () => import(`./${componentName}.vue`));
}
}
}
And then you can render it in main:
<query-detail-template
test='Hello World'>
</query-detail-template>
I am trying to send this.TC from typing.js to ending-page.js which are sibling components. Emits and event hubs not working. But emit from typing.js to parent works as I want. (There will be only one more call in this app, so i don't want use Vuex if it isnt necessary for this - i want to do it with simple emits ) Here's my code:
Parent:
<template>
<div id = "app">
<typing v-if = "DynamicComponent === 'typing'" />
<ending_page v-else-if = "DynamicComponent === 'ending_page'" />
</div>
</template>
<script>
/* Importing siblings components to parent component */
import typing from './components/typing/index.vue'
import ending_page from './components/ending-page/index.vue'
export default {
name: 'app',
components: {
typing,
ending_page
},
data() {
return {
DynamicComponent: "typing",
};
},
methods: {
updateDynamicComponent: function(evt, data){
this.DynamicComponent = evt;
},
},
};
</script>
typing.js:
import { eventBus } from "../../main";
export default {
name: 'app',
components: {
},
data() {
return {
/* Text what is in input. If you write this.input = "sometext" input text will change (It just works from JS to HTML and from HTML to JS) */
input: "",
/* Object of TypingCore.js */
TC: "somedata",
/* Timer obejct */
timer: null,
is_started: false,
style_preferences: null,
};
},
ICallThisFunctionWhenIWantToEmitSomething: function(evt) {
/* Sending data to ending_page component */
this.$root.$emit('eventname', 'somedata');
/* Calling parent to ChangeDynamicComponent && sending TC.data what will be given to ending_page (I think it looks better with one syntax here) */
this.$emit('myEvent', 'ending_page', this.TC.data);
}
},
};
ending-page.js:
import { eventBus } from "../../main";
export default {
name: 'ending-page',
components: {},
data () {
return {
data: "nothing",
}
},
computed: {
},
props: {
},
methods: {
},
/* I know arrow functions etc but i was trying everyting */
created: function () {
this.$root.$on('eventname', function (data) {
console.log(data)
this.title = data
this.$nextTick()
})
}
}
It is an example of how to share data between siblings components.
Children components emits events to parent. Parent components send data to children.
So, the parent has the property title shared between the children. When typing emits
the input event the directive v-modelcapture it an set the value on parent.
Ref:
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
https://benjaminlistwon.com/blog/data-flow-in-vue-and-vuex/
Vue.component('typing', {
props: {
value: ''
},
template: '<button #click="emit">Click to change</button>',
methods: {
emit() {
this.$emit('input', `changed on ${Date.now()}`);
}
}
});
Vue.component('ending-page', {
props: {
title: ''
},
template: '<div>{{ title }}</div>',
});
var app = new Vue({
el: '#app',
data() {
return {
title: 'unchanged',
};
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<typing v-model="title"></typing>
<ending-page :title="title"></ending-page>
</div>
One can try communication using vuex,
the data you want to share make it on this.$store.state or if recalling for functions use mutation(sync functions) and actions(async functions)
https://vuex.vuejs.org/
I like what Jeffrey Way suggested once, just create a global events object (which accidentally can be another Vue instance) and then use that as an event bus for any global communication.
window.eventBus = new Vue();
// in components that emit:
eventBus.$emit('event', data);
// in components that listen
eventBus.$on('event');
I have a simple application which need to render 2 components dynamically.
Component A - needs to have onClick event.
Component B - needs to have onChange event.
How is it possible to dynamically attach different events to component A/B?
<template>
<component v-bind:is="currentView">
</component>
</template>
<script>
import A from '../components/a.vue'
import B from '../components/b.vue'
export default {
data: function () {
return {
currentView: A
}
},
components: { A, B }
}
</script>
Here is a solution for a little more complicated and realistic use case. In this use case you have to render multiple different components using v-for.
The parent component passes an array of components to create-components. create-components will use v-for on this array, and display all those components with the correct event.
I'm using a custom directive custom-events to achieve this behavior.
parent:
<template>
<div class="parent">
<create-components :components="components"></create-components>
</div>
</template>
<script>
import CreateComponents from '#/components/CreateComponents'
import ComponentA from '#/components/ComponentA'
import ComponentB from '#/components/ComponentB'
export default {
name: 'parent',
data() {
return {
components: [
{
is: ComponentA,
events: {
"change":this.componentA_onChange.bind(this)
}
},
{
is: ComponentB,
events: {
"click":this.componentB_onClick.bind(this)
}
}
]
}
},
methods: {
componentA_onChange() {
alert('componentA_onChange');
},
componentB_onClick() {
alert('componentB_onClick');
}
},
components: { CreateComponents }
};
</script>
create-components:
<template>
<div class="create-components">
<div v-for="(component, componentIndex) in components">
<component v-bind:is="component.is" v-custom-events="component.events"></component>
</div>
</div>
</template>
<script>
export default {
name: 'create-components',
props: {
components: {
type: Array
}
},
directives: {
CustomEvents: {
bind: function (el, binding, vnode) {
let allEvents = binding.value;
if(typeof allEvents !== "undefined"){
let allEventsName = Object.keys(binding.value);
allEventsName.forEach(function(event) {
vnode.componentInstance.$on(event, (eventData) => {
allEvents[event](eventData);
});
});
}
},
unbind: function (el, binding, vnode) {
vnode.componentInstance.$off();
}
}
}
}
</script>
You don't have to dynamically add them.
<component v-bind:is="currentView" #click="onClick" #change="onChange">
If you want to be careful you can bail in the handler of the currentView is not correct.
methods: {
onClick(){
if (this.currentView != A) return
// handle click
},
onChange(){
if (this.currentView != B) return
// handle change
}
}