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
TL;DR
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?
Parent
Parent Html template
<template name="parent">
{{#each child in getChildren}}
{{> childTemplate (childArgs child)}}
{{/each}}
</template>
Parent Javascript
Reactive variable
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);
}
}
}
})
Child
Child html template
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>
Child javascript (events)
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);
},
Problem
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.
Text Example:
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.
Good: The getChildren() function is called when deleting the child in the ReactiveDict and I have the correct data in the variable (children in the ReactiveDict).
Good: If I have 3 children and I delete one, the parent template renders only 2 children.
Bad: Yet the child's onRendered() is never called (neither is the child's onCreated() function). Which means the data displayed for the child template is wrong.
Picture example
I am adding pictures to help understand:
Correct html
The 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 data
I 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?
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
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.
Edits from the original code
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.
Html template
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>
Javascript
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;
}
});
Complete Code
Here is the complete solution.
Parent
Html template
<template name="parent">
{{#each childId in getChildrenIds}}
{{#let child=(getChild childId)}}
{{> childTemplate (childArgs child)}}
{{/let}}
{{/each}}
</template>
Javascript
// 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);
}
}
}
});
Child
Html template
<template name="child">
<div>{{value}}</div>
<button class="add_row" type="button">add</button>
<button class="delete_row" type="button">delete</button>
</template>
Javascript
Template.child.events({
'click .add_row'(event, templateInstance) {
templateInstance.data.onAddClick();
},
'click .delete_row'(event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id);
}
});
Caveats
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.
Correct way of solving this issue
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).
Examples if you need to deal with arrays and #each blocks
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>"
}
]
Related
I am stuck at the task of traversing a tree and replacing the nodes if they match the key. This is necessary as I load children lazy via REST, insert them into the parent node and then replace the parent node (The node I clicked onto) with the parent with loaded children.
The treeData interface is:
export interface TreeData {
keyNum:number,
icon:string,
name:string,
length:number,
type: string,
links: string,
children?: TreeData[]
}
_nodes = Temp array that replaces the current nodes
nodes = All elements
event = Current parent
test = Temporary array for pushing items when doing it recursively
In a first attempt I created this function:
let _nodes = nodes.map(node => {
if (node.keyNum=== event.keyNum) {
node = event;
}
return node;
})
setNodes(_nodes)
Unfortunately this doesn't do this recursively. Only when I open up the root item. In a next attempt I created this function:
const test = [] as TreeData[]
traverseTree(event,nodes, test)
dispatch(setNodes(test))
const traverseTree = (event: TreeData, currentNodes: TreeData[], temp: TreeData[])=>{
currentNodes.forEach(node=>{
if (node.keyNum === event.keyNum) {
node = event;
temp.push(node)
}
else{
if(node.children && node.children.length>0){
return node.children.forEach(nodeInLoop=>{
return traverseTree(event,[nodeInLoop], temp)
})
}
}
})
}
I can finally traverse the tree but the problem is that when I click element1
-root
- element 1
- element 2
this gets transformed to:
- element 1
- subelement 1
- subelement 2
- ...
So when I click a child everything above is replaced. How can I preserve also the parent when clicking element 1. So that the expected output is
- root
- element 1
- subelement 1
- subelement 2
- ...
- element 2
I would not use a third parameter like in your recursive code. Instead make the function return the new tree.
It is indeed a problem that in your attempt the only thing that happens to temp is that nodes are pushed unto it. But you'll also need to repopulate children arrays in the hierarchy. You can better use map for that, and spread syntax to clone nodes with newly created children arrays.
Your recursive attempt could be corrected like this:
const traverseTree = (event: TreeData, nodes: TreeData[]): TreeData[] =>
nodes.map(node =>
node.keyNum === event.keyNum ? event
: !node.children?.length ? node
: { ...node, children: traverseTree(event, node.children) };
);
Call like:
dispatch(setNodes(traverseTree(event,nodes)))
Currently I'm working on a react project, but I'm seeing some unexpected behavior when sorting an array of stateful child components.
If I have a parent component
export function Parent(){
const [children, setChildren] = useState([
{name:'Orange',value:2},
{name:'Apple',value:1},
{name:'Melon',value:3}
])
var count = 0
function handleSort() {
var newChildren=[...children]
newChildren.sort((a,b)=>{return a.value-b.value})
setChildren(newChildren)
}
return (
<div>
<button onClick={handleSort}>Sort</button>
{children.map((child) => {
count++
return(<ChildComp key={count} details={child}/>)
})}
</div>
)
}
And a child component
function ChildComp(props){
const[intCount,setIntCount] = useState(0)
function handleCount(){
setIntCount(intCount+1)
}
return (
<div>
<p>{props.details.name}</p>
<button onClick={handleCount}>{intCount}</button>
</div>
)
}
When the page first renders everything looks great, three divs render with a button showing the number of times it was clicked and the prop name as it was declared in the array. I've noticed that when I sort, it sorts the props being passed to the child components which then rerender, but the intCount state of the child component stays tied to the original location and is not sorted. is there any way to keep the state coupled with the array element through the sort while still maintaining state data at the child level, or is the only way to accomplish this to raise the state up to the parent component and pass a callback or dispatch to the child to update it?
The count is not is not sorted. It just got updated when you sorted.
Keys help React identify which items have changed, are added, or are
removed. Keys should be given to the elements inside the array to give
the elements a stable identity
Every time you sort, key stay the same, as you use count.
Try using value as key
export function Parent(){
// ....
return (
<div>
<button onClick={handleSort}>Sort</button>
{children.map(child => {
return <ChildComp key={child.value} details={child}/> // key is important
})}
</div>
)
}
More info: https://reactjs.org/docs/lists-and-keys.html#keys
I have a model in which I'm initializing an array on ajax success after the model is mounted
var self = this;
$.getJSON("somejson.json",
function (data) {
var list = [];
list = data.list.map(function (item) {
return { id: item.id, text: item.text };
});
self.selectableItems = list;
});
I have a click method on each of these items which removes the item from selectableItems
select: function (item) {
this.selectableItems.pop(item);
},
selectableItems renders correctly initially, but when I mutate the array, the dom isn't updating. Although the actual array is being modified correctly.
I verified this by having a computed property that returns the count of selectableItems. This count is updated correctly when the item is removed, but the dom still shows the item.
I also noticed that when I hard code the value of selectableItems in the ajax, everything works as expected!
self.selectableItems = [{ id: 1, text: "adsad"}];
I'm aware of the caveats of array mutation in vue. But I feel I'm missing something basic here, as I have just started exploring Vue.
Can someone point out on what I'm missing?
Array.pop() removes the last item from the array, it does not take any argument. It only removes the last item any argument you pass it.
That the reason your computed property showing the array count works as last item is being removed but not the item you want.
Use Array.splice()instead.
pass the index to your click method like this:
<ul>
<li v-for="(item, index) in selectableItems" #click="select(index)>{{item}}</li>
</ul>
script
select: function (index) {
this.selectableItems.splice(index, 1);
},
See this gist for the complete picture.
Basically I will have this form:
When you click the plus, another row should appear with a drop down for day and a time field.
I can create the code to add inputs to the form, however I'm having trouble with the individual components (selectTimeInput is a row) actually updating their values.
The onChange in the MultipleDayTimeInput is receiving the correct data, it is just the display that isn't updating. I extremely new to react so I don't know what is causing the display to not update....
I think it is because the SelectTimeInput render function isn't being called because the passed in props aren't being updated, but I'm not sure of the correct way to achieve that.
Thinking about it, does the setState need to be called in the onChange of the MultipleDayTimeInput and the input that changed needs to be removed from the this.state.inputs and readded in order to force the render to fire... this seems a little clunky to me...
When you update the display value of the inputs in state, you need to use this.setState to change the state data and cause a re-render with the new data. Using input.key = value is not the correct way.
Using State Correctly
There are three things you should know about
setState().
Do Not Modify State Directly
For example, this will not re-render a
component:
// Wrong
this.state.comment = 'Hello';
Instead, use setState():
// Correct
this.setState({comment: 'Hello'});
The only place where you
can assign this.state is the constructor.
read more from Facebook directly here
I would actually suggest a little bit of a restructure of your code though. It's not really encouraged to have components as part of your state values. I would suggest having your different inputs as data objects in your this.state.inputs, and loop through the data and build each of the displays that way in your render method. Like this:
suppose you have one input in your this.state.inputs (and suppose your inputs is an object for key access):
inputs = {
1: {
selectedTime: 0:00,
selectedValue: 2
}
}
in your render, do something like this:
render() {
let inputs = Object.keys(this.state.inputs).map((key) => {
let input = this.state.inputs[key]
return (<SelectTimeInput
key={key}
name={'option_' + key}
placeholder={this.props.placeholder}
options={this.props.options}
onChange={this.onChange.bind(this, key)}
timeValue={input.selectedTime}
selectValue={input.selectedValue}
/>)
)}
return (
<div>
<button className="button" onClick={this.onAddClick}><i className="fa fa-plus" /></button>
{ inputs }
</div>
);
}
Notice how we're binding the key on the onChange, so that we know which input to update. now, in your onChange function, you just set the correct input's value with setState:
onChange(event, key) {
this.setState({
inputs: Immutable.fromJS(this.state.inputs).setIn([`${key}`, 'selectedTime'], event.target.value).toJS()
// or
inputs: Object.assign(this.state.inputs, Object.assign(this.state.inputs[key], { timeValue: event.target.value }))
})
}
this isn't tested, but basically this Immutable statement is going to make a copy of this.state.inputs and set the selectedTime value inside of the object that matches the key, to the event.target.value. State is updated now, a re-render is triggered, and when you loop through the inputs again in the render, you'll use the new time value as the timeValue to your component.
again, with the Object.assign edit, it isn't tested, but learn more [here]. 2 Basically this statement is merging a new timeValue value in with the this.state.inputs[key] object, and then merging that new object in with the entire this.state.inputs object.
does this make sense?
I modified the onChange in the MultipleDayTimeInput:
onChange(event) {
const comparisonKey = event.target.name.substring(event.target.name.length - 1);
const input = this.getInputState(comparisonKey);
input.selected = event.target.value;
input.display = this.renderTimeInput(input);
let spliceIndex = -1;
for (let i = 0; i < this.state.inputs.length; i++) {
const matches = inputFilter(comparisonKey)(this.state.inputs[i]);
if (matches) {
spliceIndex = i;
break;
}
}
if (spliceIndex < 0) {
throw 'error updating inputs';
}
this.setState({
inputs: [...this.state.inputs].splice(spliceIndex, 1, input)
});
}
The key points are:
// re render the input
input.display = this.renderTimeInput(input);
// set the state by copying the inputs and interchanging the old input with the new input....
this.setState({
inputs: [...this.state.inputs].splice(spliceIndex, 1, input)
});
Having thought about it though, input is an object reference to the input in the this.state.inputs so actually [...this.states.inputs] would have been enough??
new to Reactjs, so I'm trying to understand what concepts I'm missing.
I have an array of values, say [1,1,1,2,2,2,3,3,3] that I would like to process it as: "1...2...3...", but I have no idea how to keep track of what the previous value that was rendered.
My first thought was to set a state value to keep track what was processed. If the new value is the same as the previous, I can replace the text. But I can't set the state since it's in the rendering function:
Warning: setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
So how can I keep track of the value being rendered, or is there a better way to do this?
Example of what I tried:
var stuff = [1,1,1,2,2,2,3,3,3];
var foo = React.createClass({
display: function(k) {
return <line myStuff={k}>
},
render: function() {
return (
{Object.keys(stuff).sort().map(this.display)}
)
}
});
var line = React.createClass({
getInitialState: function() {
return {
currentValue: 0
};
},
checkRepeat: function(value) {
if (this.state.currentValue == value) {
return '.'
} else {
this.setState({currentValue: value});
return value;
}
},
render: function() {
<p>{this.checkRepeat(this.props.myStuff)}</p>
}
});
Thank you!
What your code currently does:
inside <Foo> component, Object.keys(stuff).sort() turns your array [1,1,1,2,2,2,3,3,3] into a different array, containing the keys, so result = ["0", "1", "2", "3", "4", "5", "6", "7", "8"]
each of these results is then used to create a new <Line> component, passing the "0", "1" etc as a prop to the component.
The intention of the code inside the <Line> component is probably to check if the number passed is unique (same a number from <Line> component before). If so, render the number, otherwise, render "."
Main issue with this code is that you are trying inside a child (<Line>), to compare its prop with a prop of a sibling child.
The best place to put this logic of comparing numbers, is inside the parent (<Foo>).
The child (<Line>) really should have simple and isolated logic: just render whatever prop the parent sends.
To fix:
Change the render() function inside <Foo>to:
render: function() {
// copy the stuff array to a new array,
// where we replace all repeated values with a "." symbol
var stuffCopy = stuff.map((item,i) =>
{ return (i==0 || item != arr[i-1]) ? item.toString() : "." });
return (
{stuffCopy.map(this.display)}
)
}
Then you can simplify your <Line> component to:
var line = React.createClass({
// this component no longer needs state
// it can simply render the prop
render: function() {
<p>{this.props.myStuff}</p>
}
});
PS: "Previous" can have two very different meanings:
"the value of the exact same variable at a previous moment in time": looking at one component, where the state or prop had value A, and then a new prop or state value of B is sent to the one component.
"the value of the same variable, in another instance, rendered at the same time": if you render 9 components at the same time, previous value could mean, the value of the sibling component, rendered at the same moment, but appearing before this component in the list.
Your code suggests your question is about meaning nr. 2. (and my answer is focused on that meaning).