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)))
Related
I have a tree structure {name, [children]} (JS object). The user can arbitrarily select any nodes in order to partially duplicate the structure (into an array of JS objects, since loose leaf nodes could be selected). e.g. the user could select a leaf node but not its parent/grandparent, then the leaf node will just be in the flat array in the children of the highest parent.
Example:
original (JS Object) selected
A✓ A
| \ \ / \
B✓ C D✓ B D
/ \ => / \
E✓ F E G
|
G✓
original selected(Array of JS objects)
A B, D
| \ \ / \
B✓ C D✓ E G
/ \ =>
E✓ F
|
G✓
My first thoughts were: for each node on click, assume we maintain a selected array:
if this node is in selected => deselect (fairly straight forward):
a1. if the node is a "top-level" element => remove it from the array, and push its children to the array.
a2. if the node is a (grand)child of a top-level element => remove the node from its parent's children, and reattach the node's children to its parent.
if this node is not in selected => select (weird things happen):
a. Initialize a tempNode with an empty children, for we don't know its children yet
b. if this node has (grand)children => for each (grand)child, if the (grand)child is in selected => remove the (grand)child in selected => add this (grand)child to the tempNode's children (should be recursive for multiple levels)
c1. if this node's (grand)parent is in selected already => attach the tempNode to the (grand)parent (should be recursive for multiple levels)
c2. if this node's (grand)parent is not in selected, push it to the array.
I'm mainly stuck on step 2b and 2c.
//for adding node into selected
const addChildInSelected = (child) => {
var tempNode = {
name: child.name,
children: []
}
var clonedState = clone(selected)
var childInTree = findChildInTree(child.id, props.data)
//if the child has children, check if they are in selected yet.
if (childInTree.children.length > 0) {
//Problem 1: I should run this recursively for all the grandchildren,
//but I struggle to find the base case. But recursion might not be the best solution
for (var i = 0; i < childInTree.children.length; i++) {
//Problem 2: findChildInSelected/removeChildInSelect are all recursive calls as well,
//due to the tree structure. very worried about stack overflow during this step...
var grandChildInSelected = findChildInSelected(childInTree.children[i].id)
if (grandChildInSelected !== null) {
clonedState = removeChildInSelected(clonedState)
tempNode.children.push(findChildInTree(childInTree.children[i]))
}
}
}
//Problem 3. I realized I needed to check for each node in the `selected` again.
//another potential performance hurdle
}
Now that I rethink about this issue, each node only cares about itself, its immediate parent/grandparent, and all of its children/grandchildren as a flat array. That might be a good perspective to look into this problem. Maybe a helper function that finds the parent/grandparent or all children would be a good start. (Plus I realized that in several functions I forgot to check grandparent-child...)
But since I'm operating with multiple tree structures, the function below just doesn't seem ideal...
//just a quick example
const findDescendantsInSelected = (node, descendants=[]) => {
//recursive call within recursive function which could overflow if the tree is big
if (findNodeInSelected(node) !== null) {
descendents.push(node)
}
if (node.children.length > 0) {
for (var i = 0; i < entry.children.length; i++) {
result = findDescendantsInSelected(node.children[i], descendants);
}
return result
}
return descendents
}
All in all, maintaining almost 3 separate trees (selected state, original data, and clone on each step since I'm using react) and tracking the parents/children in each of them, across different functions made my head hurt. Any insight on how to simplify the problem would be greatly appreciated!
A simple and pure recursive function that uses a depth-first traversal should suffice. It returns the list of selected trees you want (known as a forest). If a node is selected, it will collect all the selected descendant trees and return a new node that has these as children. If the node is not selected, it just joins the lists from its children and returns that list.
function selectedForest(node) {
const trees = node.children.flatMap(selectedForest);
return isSelected(node) ? [ {...node, children: trees} ] : trees;
}
Demo:
const demo = { name: "A", children: [
{ name: "B", children: [
{ name: "E", children: [] },
{ name: "F", children: [
{ name: "G", children: [] },
] },
] },
{ name: "C", children: [] },
{ name: "D", children: [] },
] };
let selection;
function isSelected({name}) {
return selection.has(name);
}
function selectedForest(node) {
const trees = node.children.flatMap(selectedForest);
return isSelected(node) ? [ {...node, children: trees} ] : trees;
}
selection = new Set("ABDEG");
console.log(JSON.stringify(selectedForest(demo), null, 2));
selection = new Set("BDEG");
console.log(JSON.stringify(selectedForest(demo), null, 2));
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>"
}
]
I'm trying to enable a user to click nodes in a force layout and reduce the data set to just those nodes and their associated links.
I've gotten as far as this; http://jsfiddle.net/hiwilson1/4gemo0xe/1/
The section starting at 179;
d3.select("#filterResults")
.on("click", function() {
//feed the indexes of the selected nodes into array
var nodeArray = []
d3.selectAll("circle[fill=\"Red\"]")
.each(function(d, i) {
nodeArray.push(d3.select(this).attr("node-index"))
})
//step2: loop through nodes array and return only those nodes that share
//index with nodeArray into newNodes
var newNodes = []
nodes.forEach(function(nde, idx, list) {
nodeArray.forEach(function(nde2, idx2, list2) {
if (idx == nde2) {
newNodes.push(nde)
}
})
})
//step3: loop through links array and return only those links that
//have source or target as index of nodes in nodeArray
var newLinks = []
links.forEach(function(link, lidx, llist) {
nodeArray.forEach(function(nde, idx, list) {
if (link.source == nde || link.target == nde) {
newLinks.push(link)
}
})
})
alert(newLinks.length)
})
is where I'm stuck, specifically step 3. I can't seem to get at the links from within the onclick function, the alert returns zero where it should return however many links are associated with the selected nodes.
Ultimately I want to identify nodes and links associated with the selected and then update the data set to reflect this new data set, if that's even possible! Only one way to find out..
Once force is started, source and target values got replaced with corresponding node objects. So instead of comparing link sources with node index, compare it with the node.
if (link.source == nodes[nde] || link.target == nodes[nde]) {
newLinks.push(link)
}
Or
if (link.source.index == nde || link.target.index ==nde) {
newLinks.push(link)
}
Updated JSFiddle
Note: the values of the source and target attributes may be initially
specified as indexes into the nodes array; these will be replaced by
references after the call to start.
This is the sample structure of a node.
{
fixed: 0,
index: 12,
label: "I am an information label",
px: 287.64956227452404,
py: 83.71383910494417,
weight: 7,
x: 287.7214898272057,
y: 83.59069881862021
}
d3.select(".node").data() will return you the data associated with that node, where node is the class name.
You can refer more about d3 force layout from here.
I'm using a tree filter plugin created by one of the members at Sencha. Here is his fiddle for the plugin: http://jsfiddle.net/slemmon/fSJwF/2/
As you can see in the fiddle when a typeAhead result is a parent node, all of its children are expanded. So I want to highlight all matching results because I often get numerous parent nodes that contain the matching typeAhead search term and they have hundreds of children, making it hard to find the matching parent node(s).
copy pasting a snippet from the fiddle since SO is throwing "links to jsfiddle must be accompanied by code" error...
filter: function (value, property, re) {
var me = this
, tree = me.tree
, matches = [] // array of nodes matching the search criteria
, root = tree.getRootNode() // root node of the tree
, property = property || 'text' // property is optional - will be set to the 'text' propert of the treeStore record by default
, re = re || new RegExp(value, "ig") // the regExp could be modified to allow for case-sensitive, starts with, etc.
, visibleNodes = [] // array of nodes matching the search criteria + each parent non-leaf node up to root
, viewNode;
if (Ext.isEmpty(value)) { // if the search field is empty
me.clearFilter();
return;
}
tree.expandAll(); // expand all nodes for the the following iterative routines
// iterate over all nodes in the tree in order to evalute them against the search criteria
root.cascadeBy(function (node) {
if (node.get(property).match(re)) { // if the node matches the search criteria and is a leaf (could be modified to searh non-leaf nodes)
matches.push(node) // add the node to the matches array
}
});
if (me.allowParentFolders === false) { // if me.allowParentFolders is false (default) then remove any non-leaf nodes from the regex match
Ext.each(matches, function (match) {
if (!match.isLeaf()) { Ext.Array.remove(matches, match); }
});
}
Ext.each(matches, function (item, i, arr) { // loop through all matching leaf nodes
root.cascadeBy(function (node) { // find each parent node containing the node from the matches array
if (node.contains(item) == true) {
visibleNodes.push(node) // if it's an ancestor of the evaluated node add it to the visibleNodes array
}
});
if (me.allowParentFolders === true && !item.isLeaf()) { // if me.allowParentFolders is true and the item is a non-leaf item
item.cascadeBy(function (node) { // iterate over its children and set them as visible
visibleNodes.push(node)
});
}
visibleNodes.push(item) // also add the evaluated node itself to the visibleNodes array
});
root.cascadeBy(function (node) { // finally loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional
viewNode.setVisibilityMode(Ext.Element.DISPLAY); // set the visibility mode of the dom node to display (vs offsets)
viewNode.setVisible(Ext.Array.contains(visibleNodes, node));
}
});
}
Thanks.
One of the methods to solve that problem is to append additional class to matched elements and style it the way you want.
In the source below, I add class 'matched' to every matched item. Fiddle link is http://jsfiddle.net/0o0wtr7j/
Ext.define('TreeFilter', {
extend: 'Ext.AbstractPlugin'
, alias: 'plugin.treefilter'
, collapseOnClear: true // collapse all nodes when clearing/resetting the filter
, allowParentFolders: false // allow nodes not designated as 'leaf' (and their child items) to be matched by the filter
, init: function (tree) {
var me = this;
me.tree = tree;
tree.filter = Ext.Function.bind(me.filter, me);
tree.clearFilter = Ext.Function.bind(me.clearFilter, me);
}
, filter: function (value, property, re) {
var me = this
, tree = me.tree
, matches = [] // array of nodes matching the search criteria
, root = tree.getRootNode() // root node of the tree
, property = property || 'text' // property is optional - will be set to the 'text' propert of the treeStore record by default
, re = re || new RegExp(value, "ig") // the regExp could be modified to allow for case-sensitive, starts with, etc.
, visibleNodes = [] // array of nodes matching the search criteria + each parent non-leaf node up to root
, matchedClass = 'matched'
, viewNode;
if (Ext.isEmpty(value)) { // if the search field is empty
me.clearFilter();
return;
}
tree.expandAll(); // expand all nodes for the the following iterative routines
// iterate over all nodes in the tree in order to evalute them against the search criteria
root.cascadeBy(function (node) {
if (node.get(property).match(re)) { // if the node matches the search criteria and is a leaf (could be modified to searh non-leaf nodes)
node.set('cls', matchedClass);
matches.push(node) // add the node to the matches array
} else {
node.set('cls', '');
}
});
if (me.allowParentFolders === false) { // if me.allowParentFolders is false (default) then remove any non-leaf nodes from the regex match
Ext.each(matches, function (match) {
if (!match.isLeaf()) { Ext.Array.remove(matches, match); }
});
}
Ext.each(matches, function (item, i, arr) { // loop through all matching leaf nodes
root.cascadeBy(function (node) { // find each parent node containing the node from the matches array
if (node.contains(item) == true) {
visibleNodes.push(node) // if it's an ancestor of the evaluated node add it to the visibleNodes array
}
});
if (me.allowParentFolders === true && !item.isLeaf()) { // if me.allowParentFolders is true and the item is a non-leaf item
item.cascadeBy(function (node) { // iterate over its children and set them as visible
visibleNodes.push(node)
});
}
visibleNodes.push(item) // also add the evaluated node itself to the visibleNodes array
});
root.cascadeBy(function (node) { // finally loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional
viewNode.setVisibilityMode(Ext.Element.DISPLAY); // set the visibility mode of the dom node to display (vs offsets)
viewNode.setVisible(Ext.Array.contains(visibleNodes, node));
}
});
}
, clearFilter: function () {
var me = this
, tree = this.tree
, root = tree.getRootNode();
if (me.collapseOnClear) { tree.collapseAll(); } // collapse the tree nodes
root.cascadeBy(function (node) { // final loop to hide/show each node
node.set('cls', '');
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional and show all nodes
viewNode.show();
}
});
}});
I'm using the latest version of ember-cli, ember-data, ember-localstorage-adapter, and ember.
I have a Node object which has a parent and children. Since I had issues with creating multiple relationships with the same type of object, I decided to store the parentID in a string, and the childIDs in an array of strings. However, when I create a new Node and try to add the new Node's to the parents array of IDs, the ID ends up being added to the correct parent, but also other parents.
level 1 0
/ \
level 2 1 2
| |
level 3 3 4
In a structure like this, 0, 1, and 2 all have correct child and parent IDs. However, after adding 3 and 4, node 1 and node 2's childIDs are [3, 4], instead of [3], [4] respectively.
The Array attribute:
var ArrayTransform = DS.Transform.extend({
serialize: function(value) {
if (!value) {
return [];
}
return value;
},
deserialize: function(value) {
if (!value) {
return [];
}
return value;
}
});
The insertNode code:
insert: function(elem) {
var i,
_store = elem.node.store,
newNodeJSON = elem.node.serialize();
newNodeJSON.childIds = [];
newNodeJSON.level = getNextLevel();
_store.filter('node', function(node) {
return node.get('level') === newnodeJSON.level-1;
}).then(function(prevLevelNodes) {
// if no other nodes yet
if (prevLevelNodes.toArray().length === 0) {
makeNewNode(_store, newNodeJSON, elem.node);
}
// else, generates however many nodes that are in the previous level
else {
prevLevelNodes.toArray().forEach(function(node, idx) {
newNodeJSON.parentId = node.get('id');
makeNewNode(_store, newNodeJSON, elem.node);
});
}
});
}
var makeNewNode = function(_store, newNodeJSON, node) {
console.log(newNodeJSON.parentId); // returns correct value
var newNode = _store.createRecord('node', newNodeJSON);
newNode.save();
var newNodeId = newNode.get('id');
if (newNode.get('parentId')) {
_store.find('node', newNode.get('parentId')).then(function(n) {
var cids = n.get('childIds');
console.log(newNodeId); // returns expected value
console.log(cids); // **DOESN'T RETURN AN EMPTY ARRAY**: returns array with [3,4]
cids.push(newNodeId);
console.log(n.get('childIds')); // returns array with [3,4]
n.save();
});
}
To top this off, this error happens 90% of the time, but 10% of the time it performs as expected. This seems to suggest that there's some sort of race condition, but I'm not sure where that would even be. Some places that I feel like might be causing issues: the ember-cli compilation, passing the entire _store in when making a new node, ember-data being weird, ember-localstorage-adapter being funky... no clue.
For anyone else who may have this problem in the future: the problem lies in two things.
In ArrayTransform, typically I am returning the value sans modification.
In my insert code, I'm passing the same JSON that I defined at the top of the function to makeNewNode.
This JSON contains a reference to a single childIds array; therefore, each new node that gets created uses this same reference for its childIds. Although this doesn't quite explain why the cids array wasn't empty before the push executed (perhaps this is some sort of compiler oddity or console printing lag), it explains why these both Level 3 children were in both Level 2 parents' childIds array.
tl;dr: pass by value vs pass by reference error