Reputation: 321
I'm new to Meteor/Blaze but that is what my company is using.
I'm struggling to understand how Blaze decides to render what based on ReactiveDict
I create some children templates from a ReactiveDict
array in the parent array. The data is not refreshed in the children templates when the ReactiveDict
changes and I don't understand why.
I probably have misunderstood something about Blaze rendering. Could you help me out?
<template name="parent">
{{#each child in getChildren}}
{{> childTemplate (childArgs child)}}
{{/each}}
</template>
The template renders children templates from a getChildren
helper that just retrieves a ReactiveDict
.
// Child data object
const child = () => ({
id: uuid.v4(),
value: ""
});
// Create a reactive dictionary
Template.parent.onCreated(function() {
this.state = new ReactiveDict();
this.state.setDefault({ children: [child()] });
});
// Return the children from the reactive dictionary
Template.parent.helpers({
getChildren() {
return Template.instance().state.get('children');
}
});
Child template arguments (from parent template)
The parent template gives the child template some data used to set default values and callbacks.
Each is instantiated using a childArgs
function that uses the child's id
to set the correct data and callbacks.
When clicking a add
button, it adds a child to the children
array in the ReactiveDict
.
When clicking a delete
button, it removes the child from the children
array in the ReactiveDict
.
Template.parent.helpers({
// Set the children arguments: default values and callbacks
childArgs(child) {
const instance = Template.instance();
const state = instance.state;
const children = state.get('children');
return {
id: child.id,
// Default values
value: child.value,
// Just adding a child to the reactive variable using callback
onAddClick() {
const newChildren = [...children, child()];
state.set('children', newChildren);
},
// Just deleting the child in the reactive variable in the callback
onDeleteClick(childId) {
const childIndex = children.findIndex(child => child.id === childId);
children.splice(childIndex, 1);
state.set('children', children);
}
}
}
})
The template displays the data from the parent and 2 buttons, add
and delete
.
<template name="child">
<div>{{value}}</div>
<button class="add_row" type="button">add</button>
<button class="delete_row" type="button">delete</button>
</template>
The two functions called here are the callbacks passed as arguments from the parent template.
// The two functions are callbacks passed as parameters to the child template
Template.child.events({
'click .add_row'(event, templateInstance) {
templateInstance.data.onAddClick();
},
'click .delete_row'(event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id);
},
My problem is that when I delete a child (using a callback to set the ReactiveDict
like the onAddClick()
function), my data is not rendered correctly.
I add rows like this.
child 1 | value 1
child 2 | value 2
child 3 | value 3
When I delete the child 2
, I get this:
child 1 | value 1
child 3 | value 2
And I want this:
child 1 | value 1
child 3 | value 3
I'm initialising the child
with the data from childArgs
in the Template.child.onRendered()
function.
getChildren()
function is called when deleting the child
in the ReactiveDict
and I have the correct data in the variable (children
in the ReactiveDict
). onRendered()
is never called (neither is the child's onCreated()
function). Which means the data displayed for the child template is wrong.I am adding pictures to help understand:
Correct htmlThe displayed HTML is correct: I had 3 children, and I deleted the second one. In my HTML, I can see that the two children that are displayed have the correct ID in their divs. Yet the displayed data is wrong.
Stale dataI already deleted the second child in the first picture. The children displayed should be the first and the third. In the console log, my data is correct. Red data is the first. Purple is the third.
Yet we can see that the deleted child's data is displayed (asd
and asdasd
). When deleting a tag, I can see the second child's ID in the log, though it should not exist anymore. The second child ID is in green.
I probably have misunderstood something. Could you help me out?
Upvotes: 4
Views: 297
Reputation: 45
The correct way to fix this is to create your own _id which gets a new unique _id each time the array of objects changes. It is outlined in the Blaze docs here: http://blazejs.org/api/spacebars.html#Reactivity-Model-for-Each
This will only happen when you are dealing with #each blocks with non-cursors, like arrays or arrays of objects.
Cursor-based data together with #each blocks works fine and gets rerendered correctly, like Pages.findOne(id).
Not working
[
{
name: "Fred",
value: 1337
},
{
name: "Olga",
value: 7331
}
]
Working
[
{
name: "Fred",
value: 1337,
_id: "<random generated string>"
},
{
name: "Olga",
value: 7331,
_id: "<random generated string>"
}
]
Upvotes: 0
Reputation: 321
I fixed my problem. But I still don't understand how Blaze chooses to render.
Now, the solution looks a bit like the one given by @Jankapunkt in the first part of his solution, but not exactly. The find
to get the child was working completely fine. But now that I make the template rendering dependent on a reactive helper, it re-renders the template when the id changes (which it did not when it was only dependent on the child itself from the each...in
loop).
In the end, I don't understand what the each...in
loop does and how it uses the data to loop. See Caveats.
To give credits where it's due, I had the idea of implementing that dependency from this post.
I edit the parent template to make the child rendering dependent on its own id. That way, when the child.id
changes, the template re-renders.
I added a dependency on the child.id
to re-render the child template.
<template name="parent">
{{#each childId in getChildrenIds}}
{{#let child=(getChild childId)}}
{{> childTemplate (childArgs child)}}
{{/let}}
{{/each}}
</template>
I have now two helpers. One to return the ids for the each...in
loop, the other to return the child from the id and force the child template re-render.
Template.parent.helpers({
// Return the children ids from the reactive dictionary
getChildrenIds() {
const children = Template.instance().state.get('children');
const childrenIds = children.map(child => child.id);
return childrenIds;
},
// Return the child object from its id
getChild(childId) {
const children = Template.instance().state.get('children');
const child = children.find(child => child.id === childId);
return child;
}
});
Here is the complete solution.
<template name="parent">
{{#each childId in getChildrenIds}}
{{#let child=(getChild childId)}}
{{> childTemplate (childArgs child)}}
{{/let}}
{{/each}}
</template>
// Child data object
const child = () => ({
id: uuid.v4(),
value: ""
});
// Create a reactive dictionary
Template.parent.onCreated(function() {
this.state = new ReactiveDict();
this.state.setDefault({ children: [child()] });
});
Template.parent.helpers({
// Return the children ids from the reactive dictionary
getChildrenIds() {
const children = Template.instance().state.get('children');
const childrenIds = children.map(child => child.id);
return childrenIds;
},
// Return the child object from its id
getChild(childId) {
const children = Template.instance().state.get('children');
const child = children.find(child => child.id === childId);
return child;
},
// Set the children arguments: default values and callbacks
childArgs(child) {
const instance = Template.instance();
const state = instance.state;
const children = state.get('children');
return {
id: child.id,
// Default values
value: child.value,
// Just adding a child to the reactive variable using callback
onAddClick() {
const newChildren = [...children, child()];
state.set('children', newChildren);
},
// Just deleting the child in the reactive variable in the callback
onDeleteClick(childId) {
const childIndex = children.findIndex(child => child.id === childId);
children.splice(childIndex, 1);
state.set('children', children);
}
}
}
});
<template name="child">
<div>{{value}}</div>
<button class="add_row" type="button">add</button>
<button class="delete_row" type="button">delete</button>
</template>
Template.child.events({
'click .add_row'(event, templateInstance) {
templateInstance.data.onAddClick();
},
'click .delete_row'(event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id);
}
});
The solution is working. But my each..in
loop is weird.
When I delete a child, I get the correct IDs when the getChildrenIds()
helper is called.
But the each..in
loops over the original IDs, even those who were deleted and are NOT in the getChildrenIds()
return value. The template is not rendered of course because the getChild(childId)
throws an error (the child is deleted). The display is then correct.
I don't understand that behaviour at all. Anybody knows what is happening here?
If anybody has the definitive answer, I would love to hear it.
Upvotes: 0
Reputation: 8423
I am not sure where to start but there are many errors and I rather like to provide a running solution here with comments.
First the each
function should correctly pass the id instead of the whole child or the find
will result in faults:
<template name="parent">
{{#each child in getChildren}}
{{#with (childArgs child.id)}}
{{> childTemplate this}}
{{/with}}
{{/each}}
</template>
In the helper you can avoid calling too many of the Template.instance()
functions by using lexical scoping:
childArgs (childId) {
const instance = Template.instance()
const children = instance.state.get('children')
const childData = children.find(child => child.id === childId)
const value = {
// Default values
data: childData,
// Just adding a child to the reactive variable using callback
onAddClick () {
const children = instance.state.get('children')
const length = children ? children.length : 1
const newChild = { id: `data ${length}` }
const newChildren = [...children, newChild]
instance.state.set('children', newChildren)
},
// Just deleting the child in the reactive variable in the callback
onDeleteClick (childId) {
const children = instance.state.get('children')
const childIndex = children.findIndex(child => child.id === childId)
children.splice(childIndex, 1)
instance.state.set('children', children)
}
}
return value
}
Then note, that in the event callback 'click .delete_row'
you are using templateInstance.data.id
but this is always undefined
with your current structure. It should be templateInstance.data.data.id
because data
is always defined for all data coming in a Template instance and if you name a property data
then you have to access it via data.data
:
Template.childTemplate.events({
'click .add_row' (event, templateInstance) {
templateInstance.data.onAddClick()
},
'click .delete_row' (event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id)
}
})
Now it also makes sense why your data was weirdly sorted. Take a look at the onDeleteClick
callback:
onDeleteClick (childId) {
// childId is always undefined in your code
const children = instance.state.get('children')
const childIndex = children.findIndex(child => child.id === childId)
// childIndex is always -1 in your code
// next time make a dead switch in such situations:
if (childIndex === -1) {
throw new Error(`Expected child by id ${childId}`)
}
children.splice(childIndex, 1)
// if index is -1 this removes the LAST element
instance.state.set('children', children)
}
So your issue was the splice behavior and passing an unchecked index into splice:
The index at which to start changing the array. If greater than the length of the array, start will be set to the length of the array. If negative, it will begin that many elements from the end of the array (with origin -1, meaning -n is the index of the nth last element and is therefore equivalent to the index of array.length - n). If array.length + start is less than 0, it will begin from index 0.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
Upvotes: 3