Why are my nested components not rendering? - vuejs2

Using the render function provided by VueJS (https://v2.vuejs.org/v2/guide/render-function.html) the following works:
render(h, context) {
let children = [];
children.push(h("span", "test nested span"));
return h(
"span",
children
)
}
// results in <span><span>test nested span</span></span>
But it does not work if I attempt to pass configuration data to the parent:
render(h, context) {
let children = [];
children.push(h("span", "test nested span"));
return h(
'span', {
domProps: {
innerHTML: "parent"
},
style: "...",
class: "..."
},
children
)
}
// results in <span>parent</span>
What am I doing wrong?
Update
So I think what's happening here is the usage of innerHTML which is overriding the child elements because they are technically part of the innerHTML. But my goal is still the same. I want to be able to use innerHTML because it allows raw html and I also want to render the children after it.

You are probably passing a function render instead of the array render. It would be better if you changed let render = [] to let children = []

Related

document.getElementById in Computed Property

I have a component with v-if which is true/visible in some point of time.
Inside a computed property I would like to get the height of this component, but somehow getElementById does not work inside the computed property, even though it works as a method.
computedProp: function() {
let element = document.getElementById("prop-id")
let style = window.getComputedStyle(element)
return style.getPropertyValue("height")
},
element is undefined.
The DOM elements with their different changes are not reactive, so they will not trigger the computed property, but you could take advantage of MutationObserver
to watch any change in the observed element :
data: () => ({
computedPropHeight: 0,
observer: null,
}),
mounted() {
let element = document.getElementById("prop-id")
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation) {
let style = window.getComputedStyle(element)
this.computedProp = style.getPropertyValue("height")
}
});
});
this.observer.observe(element, {attributes: true});
},
beforeDestroy(){
this.observer.disconnect()
}

Do Vue.js render functions allow return of an array of VNodes?

I am working on extending a Vue.js frontend application. I am currently inspecting a render function within a functional component. After looking over the docs, I had the current understanding that the render function within the functional component will return a single VNode created with CreateElement aka h.
My confusion came when I saw a VNode being returned as an element in an array. I could not find any reference to this syntax in the docs. Does anyone have any insight?
export default {
name: 'OfferModule',
functional: true,
props: {
data: Object,
placementInt: Number,
len: Number
},
render (h, ctx) {
let adunitTheme = []
const isDev = str => (process.env.dev ? str : '')
const i = parseInt(ctx.props.placementInt)
const isDevice = ctx.props.data.Component === 'Device'
const Component = isDevice ? Device : Adunit
/* device helper classes */
const adunitWrapper = ctx.props.data.Decorate?.CodeName === 'AdunitWrapper'
if (!isDevice /* is Adunit */) {
const offerTypeInt = ctx.props.data.OfferType
adunitTheme = [
'adunit-themes',
`adunit-themes--${type}`.toLowerCase(),
`adunit-themes--${theme}`.toLowerCase(),
`adunit-themes--${type}-${theme}`.toLowerCase(),
]
}
const renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate =
ctx.props.data.Decorate?.Position === 'AboveAdunit' || false
const renderOfferModuleWithoutDisplayAdContainers =
ctx.props.data.Decorate?.RemoveAds /* for adunits */ ||
ctx.props.data.DeviceData?.RemoveAds /* for devices */ ||
false
const getStyle = (className) => {
try {
return ctx.parent.$style[className]
} catch (error) {
console.log('$test', 'invalid style not found on parent selector')
}
}
const PrimaryOfferModule = (aboveAdunitSlot = {}) =>
h(Component, {
props: {
data: ctx.props.data,
itemIndex: i,
adunitTheme: adunitTheme.join('.')
},
attrs: {
class: [
...adunitTheme,
getStyle('product')
]
.join(' ')
.trim()
},
scopedSlots: {
...aboveAdunitSlot
}
})
if (renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate) {
return [
PrimaryOfferModule({
aboveAdunit (props) {
return h({
data () {
return ctx.props.data.Decorate
},
template: ctx.props.data.Decorate?.Template.replace(
'v-show="false"',
''
)
})
}
})
]
} else if (renderOfferModuleWithoutDisplayAdContainers) {
return [PrimaryOfferModule()]
} else {
const withAd = i > 0 && i % 1 === 0
const adWrap = (placement, position, className) => {
return h(
'div',
{
class: 'm4d-wrap-sticky'
},
[
h(Advertisement, {
props: {
placement,
position: String(position)
},
class: getStyle(className)
})
]
)
}
return [
withAd && adWrap('inline-sticky', i, 'inlineAd'),
h('div', {
class: 'm4d-wrap-sticky-adjacent'
}),
h(
'div',
{
attrs: {
id: `inline-device--${String(i)}`
},
class: 'inline-device'
},
isDev(`inline-device id#: inline-device--${String(i)}`)
),
withAd &&
i !== ctx.props.len - 1 &&
h(EcomAdvertisement, {
props: {
placement: 'inline-static',
position: String(i)
},
class: getStyle('inlineStaticAd')
}),
PrimaryOfferModule()
]
}
}
}
It turns out that returning an array of VNodes actually predates the scopedSlots update.
I couldn't find it documented anywhere in the docs either, but via this comment on a Vue GitHub issue by a member of the Vue.js core team (which predates the scopedSlots commit by ~1 year), render() can return an Array of VNodes, which Vue will take and render in order. However, this only works in one, singular case: functional components.
Trying to return an array of VNodes with greater than 1 element in a normal (non-functional, stateful) component results in an error:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.
But doing this in a functional component, as your example does, works just fine:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
functional: true, // <--- This is the key
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
If you're interested in the why, another member of the Vue core team explained this limitation further down in the thread.
It basically boils down to assumptions made by the Vue patching and diffing algorithm, with the main one being that "each child component is represented in its parent virtual DOM by a single VNode", which is untrue if multiple root nodes are allowed.
The increase in complexity to allow this would require large changes to that algorithm which is at the very core of Vue. This is a big deal, since this algorithm must not only be good at what it does, but also very, very performant.
Functional components don't need to conform to this restriction, because "they are not represented with a VNode in the parent, since they don't have an instance and don't manage their own virtual DOM"– they're stateless, which makes the restriction unnecessary.
It should be noted, however, that this is possible on non-functional components in Vue 3, as the algorithm in question was reworked to allow it.
It seems this was implemented in:
https://github.com/vuejs/vue/commit/c7c13c2a156269d29fd9c9f8f6a3e53a2f2cac3d
This was a result of an issue raised in 2018 (https://github.com/vuejs/vue/issues/8056) , because this.$scopedSlots.default() returned both a VNode or an array of VNodes depending on the content.
The main argument was that this is inconsistent with how regular slots behave in render functions, and means any render function component rendering scoped slots as children needs to type check the result of invoking the slot to decide if it needs to be wrapped in an array
So Evan comments on the issue thread here, explaining that this.$scopedSlots.default would always return Arrays beginning v2.6 to allow for consistency, but to avoid breaking changes for how $scopedSlots was being used, the update would also allow return of an Array of a single VNode from render functions as well.

How to pass an array of objects to child component in VueJS 2.x

I am trying to send an array containing arrays which in turn contains objects to one component from another, but the content from the array seems to be empty in the child component.
I have tried sending the data as a String using JSON.Stringify() and also as an array
My parent component:
data: function(){
return{
myLineItems : []
}
},
created(){
this.CreateLineItems();
},
methods:{
CreateLineItems(){
let myArrayData = [[{"title":"Title1","value":2768.88}],[{"title":"Title2","value":9}],[{"title":"Title3","value":53.61},{"title":"Title4","value":888.77},{"title":"Title5","value":1206.11},{"title":"Title6","value":162.5}]]
this.myLineItems = myArrayData;
}
}
My parent component's template:
/*
template: `<div><InvoiceChart v-bind:lineItems="myLineItems"></InvoiceChart></div>`
My child component:
const ChildComponent= {
props: {
lineItems: {
type: Array
}
},
mounted() {
console.log(this.lineItems);
}
};
The parent component is created as so (inside a method of our main component):
var ComponentClass = Vue.extend(InvoiceDetails);
var instance = new ComponentClass({
propsData: { invoiceid: invoiceId }
});
instance.$mount();
var elem = this.$refs['details-' + invoiceId];
elem[0].innerHTML = "";
elem[0].appendChild(instance.$el);
If I try to do a console.log(this) inside the childcomponent, I can see the correct array data exist on the lineItems property..but i can't seem to access it.
I have just started using VueJS so I haven't quite gotten a hang of the dataflow here yet, though I've tried reading the documentation as well as similar cases here on stackoverflow to no avail.
Expected result: using this.lineItems should be a populated array of my values sent from the parent.
Actual results: this.lineItems is an empty Array
Edit: The problem seemed to be related to how I created my parent component:
var ComponentClass = Vue.extend(InvoiceDetails);
var instance = new ComponentClass({
propsData: { invoiceid: invoiceId }
});
instance.$mount();
var elem = this.$refs['details-' + invoiceId];
elem[0].innerHTML = "";
elem[0].appendChild(instance.$el);
Changing this to a regular custom vue component fixed the issue
Code - https://codesandbox.io/s/znl2yy478p
You can print your object through function JSON.stringify() - in this case all functions will be omitted and only values will be printed.
Everything looks good in your code.
The issue is the property is not correctly getting passed down, and the default property is being used.
Update the way you instantiate the top level component.
Try as below =>
const ChildComponent= {
props: {
lineItems: {
type: Array
}
},
mounted() {
console.log(this.lineItems);
}
};

Vuejs deep nested computed properties

I'm not really understanding where to put function() { return {} } and where not to when it comes to deeply nesting computed properties.
By the way, this is in a component!
computed: {
styles: function() {
return {
slider: function() {
return {
height: {
cache: false,
get: function() {
return 'auto';
}
},
width: {
cache: false,
get: function() {
return $('#slideshow').width();
}
}
}
}
}
}
},
This is returning undefined. When I get rid of the function() { return {} } inside of the slider index, it returns an object when I do styles.slider.width instead of the get() return. It just shows an object with cache and get as indexes..
Thanks for any help!
The reason I'm asking is because I have multiple nested components that involve styling from the parent. Slider, tabs, carousels, etc. So I wanted to organize them like this.
I believe you mean to return a computed object, but not actually structure the computation in a nested manner?
What the others have said regarding the 'computed' hook not having syntax for nesting is correct, you will likely need to structure it differently.
This may work for you: I generate many objects in a similar fashion.
computed: {
computedStyles(){
var style = {slider:{}}
style.slider.height = 'auto'
style.slider.width = this.computedSlideshowWidth
return style
},
computedSlideshowWidth(){
return $('#slideshow').width()
}
As per 2020 and Vue 2.6.12 this is completelly possible. I believe this has been possible since v.2 but cannot confirm.
Here is the working example:
this.computed = {
// One level deep nested,
// get these at `iscomplete.experience`
// or `iscomplete.volume`
iscomplete: function() {
return {
experience: this.$data.experience !== null,
volume: this.$data.volume > 100,
// etc like this
};
},
// More levels deep nested.
// Get these at `istemp.value.v1 and `istemp.value.v2`
istemp: function() {
return {
value1: {
v1: this.$data.experience,
v2: 'constant'
}
}
}
};
As a result you will be able to access your deep nested computed in your template as e.g. follows <span v-text="iscomplete.experience"></span> that will output <span>true</span> for the first example computed above.
Note that:
Since Vue v.2 cache key is deprecated;
Vue would not execute functions assigned to a computed object nested keys;
You cannot have computed for non-Vue-reactive things which in your case is e.g. $('#slideshow').width(). This means they are not going to be re-computed on their content change in this case (which is a computed's sole purpose). Hence these should be taken away from computed key.
Other than that I find nested computeds to be quite helpful sometimes to keep things in better order.

set AfterLabelTextTpl after form render

I have a form with dynamic fields. In the afterrender event of the form I want to set the afterLabelTextTpl property. I can set this property but I can't see it change in my form. How can I achieve this?
Snippet:
listeners: {
beforerender: function () {
var fields = me.getForm().getFields();
Ext.each(fields.items, function (f, idx) {
f.afterLabelTextTpl = requiredTpl;
console.log(f.afterLabelTextTpl);
}); //eo Ext.each
}
}
Edit:
I was looking for the beforerender method
Try
f.labelEl.dom.innerHTML = "LABEL:<span style='color:red;font-weight:bold' data-qtip='Required'>*</span>";
You can not use this property after the component is already rendered.
The initRenderTpl (which makes use of the label templates) method is run only if the component is not yet rendered. Once its rendered it will not run again.
You will need to update the DOM directly.
I would recomend something like this in your form:
setRequired: function(field, index) {
field.afterLabelTextTpl = requiredTpl;
},
initComponent: function(arguments) {
var me = this;
this.on('beforeadd', function(me, field){
var fields;
if (field.isXType('fieldset')) {
fields = field.query('field');
Ext.each(fields, me.setRequired);
} else {
me.setRequired(field);
}
});
// rest of logic
me.callParent(arguments);
},