Skip to content

Commit ccb6504

Browse files
authored
Merge pull request #66 from grapoza/add-nodes
Adds creation of child nodes
2 parents 696b617 + 4dc382e commit ccb6504

File tree

12 files changed

+598
-382
lines changed

12 files changed

+598
-382
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ yarn-error.log*
2020
*.njsproj
2121
*.sln
2222
*.sw*
23+
24+
scratchpad.md

docs/demo/addRemove.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default [
2+
{
3+
id: 'rootNode',
4+
label: 'Root Node'
5+
}
6+
];

docs/demo/addRemove.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
layout: demo
3+
---
4+
5+
## Basic Treeview Checkbox Demo
6+
7+
This page demonstrates adding and removing nodes. [See the data used](./addRemove.js). The callback for adding nodes is set in `modelDefaults` so any newly created node will use the same creation method automatically.
8+
9+
{% raw %}
10+
<div id="app">
11+
<tree id="customtree" :model="model" :model-defaults="modelDefaults" ref="tree"></tree>
12+
</div>
13+
{% endraw %}
14+
15+
<script type='module'>
16+
import arModel from './addRemove.js';
17+
18+
new Vue({
19+
components: {
20+
tree: window['vue-tree']
21+
},
22+
data() {
23+
return {
24+
childCounter = 0;
25+
model: arModel,
26+
modelDefaults: {
27+
addChildCallback: this.addChildCallback
28+
}
29+
};
30+
},
31+
methods: {
32+
addChildCallback (parentModel) {
33+
this.childCounter++;
34+
return Promise.resolve({ id: `child-node${this.childCounter}`, label: `Added Child ${this.childCounter}`, deletable: true });
35+
}
36+
}
37+
}).$mount('#app');
38+
</script>

docs/demo/demos.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Two basic demos are currently available. These will be expanded as time allows,
55
Note that demos probably (definitely) won't work in IE due to the way the component is loaded.
66

77
- [Basic Checkbox Usage](basic.html)
8-
- [Basic Radio Button Usage](radioBasic.html)
8+
- [Basic Radio Button Usage](radioBasic.html)
9+
- [Adding and Removing Nodes](addRemove.html)

docs/index.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ Features include:
1313
- Expandable nodes
1414
- Checkboxes
1515
- Radio buttons
16+
- Addition and removal of nodes
1617

1718
Planned:
1819

1920
- Node selection ([#5](https://github.com/grapoza/vue-tree/issues/5))
2021
- Async loading ([#13](https://github.com/grapoza/vue-tree/issues/13))
21-
- Adding nodes ([#24](https://github.com/grapoza/vue-tree/issues/24))
2222
- Icons ([#22](https://github.com/grapoza/vue-tree/issues/22))
2323
- Searching ([#4](https://github.com/grapoza/vue-tree/issues/4))
2424
- Drag n' Drop ([#6](https://github.com/grapoza/vue-tree/issues/6))
@@ -104,7 +104,7 @@ To see it in action, try out the [demos](demo/demos.html).
104104

105105
## Model Data
106106

107-
The data passed to the treeview's `model` prop should be an array of nodes, where each node has the following structure. The data model passed to the treeview may be updated to include missing properties.
107+
The data passed to the treeview's `model` prop should be an array of nodes, where each node has the following structure. The data model passed to the treeview may be updated by the treeview nodes themselves on creation to include missing properties.
108108

109109
```javascript
110110
{
@@ -139,10 +139,11 @@ The data passed to the treeview's `model` prop should be an array of nodes, wher
139139
state: {
140140
expanded: true,
141141
selected: false
142-
// No input state here; to let complex radio button groupings work, state value is
143-
// bound to a tree-level property. disabled, however, is valid here for radio buttons.
142+
// No input.value here; to let complex radio button groupings work, state value is
143+
// bound to a tree-level property. input.disabled, however, is valid here for radio buttons.
144144
},
145-
children: []
145+
children: [],
146+
addChildCallback: () => Promise.resolve({ id: '1', label: 'label' })
146147
}
147148
```
148149

@@ -168,6 +169,7 @@ The properties below can be specified for each node.
168169
| state.input.disabled | Boolean | True if the node's input field is disabled | `false` | |
169170
| children | Array\<Object\> | The child nodes of this node | `[]` | |
170171
| customizations | Object | A [customizations](#customizing-treeviewnode-markup) object | `{}` | |
172+
| addChildCallback | Function | An async function that resolves to a new node model | `null` | |
171173

172174
\* Selection props are unused; see [#5](https://github.com/grapoza/vue-tree/issues/5).
173175

@@ -208,6 +210,7 @@ If specified, the `modelDefaults` property of the treeview will be merged with n
208210

209211
| Event | Description | Handler Parameters |
210212
|:----------------------------|:--------------------------------------------------------|:-----------------------------------------------------------------------|
213+
| treeViewNodeAdd | Emitted when a node is added | `target` The model of the target (child) node <br/> `parent` The model of the parent node <br/> `event` The original event |
211214
| treeViewNodeClick | Emitted when a node is clicked | `target` The model of the target node <br/> `event` The original event |
212215
| treeViewNodeDblclick | Emitted when a node is double clicked | `target` The model of the target node <br/> `event` The original event |
213216
| treeViewNodeDelete | Emitted when a node is deleted | `target` The model of the target node <br/> `event` The original event |
@@ -233,7 +236,8 @@ The display of the treeview can be customized via CSS using the following classe
233236
| `tree-view-node-self-checkbox` | The checkbox |
234237
| `tree-view-node-self-radio` | The radio button |
235238
| `tree-view-node-self-text` | The text for a non-input node |
236-
| `tree-view-node-self-delete` | The delete button |
239+
| `tree-view-node-self-action` | The action buttons (e.g., add child or delete) |
240+
| `tree-view-node-self-add-child-icon` | The `<i>` element containing the add child icon |
237241
| `tree-view-node-self-delete-icon` | The `<i>` element containing the delete icon |
238242
| `tree-view-node-children` | The list of child nodes |
239243

@@ -257,6 +261,9 @@ A customizations object may have the following properties:
257261
| classes.treeViewNodeSelfCheckbox | String | Classes to add to the checkbox | Add |
258262
| classes.treeViewNodeSelfRadio | String | Classes to add to the radio button | Add |
259263
| classes.treeViewNodeSelfText | String | Classes to add to the text for a non-input node | Add |
264+
| classes.treeViewNodeSelfAction | String | Classes to add to the action buttons | Add |
265+
| classes.treeViewNodeSelfAddChild | String | Classes to add to the add child buttons | Add |
266+
| classes.treeViewNodeSelfAddChildIcon | String | Classes to add to the `<i>` element containing the add child icon | Add |
260267
| classes.treeViewNodeSelfDelete | String | Classes to add to the delete button | Add |
261268
| classes.treeViewNodeSelfDeleteIcon | String | Classes to add to the `<i>` element containing the delete icon | Add |
262269
| classes.treeViewNodeChildren | String | Classes to add to the list of child nodes | Add |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Yet another Vue treeview component.",
44
"author": "Gregg Rapoza <[email protected]>",
55
"license": "MIT",
6-
"version": "0.7.1",
6+
"version": "0.7.2",
77
"browser": "index.js",
88
"repository": {
99
"url": "https://github.com/grapoza/vue-tree",

src/components/TreeView.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
@treeViewNodeCheckboxChange="(t, e)=>$emit('treeViewNodeCheckboxChange', t, e)"
1414
@treeViewNodeRadioChange="(t, e)=>$emit('treeViewNodeRadioChange', t, e)"
1515
@treeViewNodeExpandedChange="(t, e)=>$emit('treeViewNodeExpandedChange', t, e)"
16+
@treeViewNodeAdd="(t, p, e)=>$emit('treeViewNodeAdd', t, p, e)"
1617
@treeViewNodeDelete="(t, e)=>$_treeViewNode_handleChildDeletion(t, e)">
1718
</tree-view-node>
1819
</ul>

src/components/TreeViewNode.vue

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,23 @@
6464
{{ model.label }}
6565
</span>
6666

67+
<!-- Add Child button -->
68+
<button :id="addChildId"
69+
type="button"
70+
v-if="model.addChildCallback"
71+
class="tree-view-node-self-action"
72+
:class="[customClasses.treeViewNodeSelfAction, customClasses.treeViewNodeSelfAddChild]"
73+
@click="$_treeViewNode_onAddChild">
74+
<i class="tree-view-node-self-add-child-icon"
75+
:class="customClasses.treeViewNodeSelfAddChildIcon"></i>
76+
</button>
77+
6778
<!-- Delete button -->
68-
<button :id="nodeId + '-delete'"
79+
<button :id="deleteId"
6980
type="button"
7081
v-if="model.deletable"
71-
class="tree-view-node-self-delete"
72-
:class="customClasses.treeViewNodeSelfDelete"
82+
class="tree-view-node-self-action"
83+
:class="[customClasses.treeViewNodeSelfAction, customClasses.treeViewNodeSelfDelete]"
7384
@click="$_treeViewNode_onDelete">
7485
<i class="tree-view-node-self-delete-icon"
7586
:class="customClasses.treeViewNodeSelfDeleteIcon"></i>
@@ -95,6 +106,7 @@
95106
@treeViewNodeCheckboxChange="(t, e)=>$emit('treeViewNodeCheckboxChange', t, e)"
96107
@treeViewNodeRadioChange="(t, e)=>$emit('treeViewNodeRadioChange', t, e)"
97108
@treeViewNodeExpandedChange="(t, e)=>$emit('treeViewNodeExpandedChange', t, e)"
109+
@treeViewNodeAdd="(t, p, e)=>$emit('treeViewNodeAdd', t, p, e)"
98110
@treeViewNodeDelete="(t, e)=>$_treeViewNode_handleChildDeletion(t, e)">
99111
</TreeViewNode>
100112
</ul>
@@ -154,9 +166,15 @@
154166
this.$_treeViewNode_normalizeNodeData();
155167
},
156168
computed: {
169+
addChildId() {
170+
return this.nodeId ? `${this.nodeId}-add-child` : null;
171+
},
157172
customClasses() {
158173
return Object.assign({}, this.customizations.classes, (this.model.customizations || {}).classes);
159174
},
175+
deleteId() {
176+
return this.nodeId ? `${this.nodeId}-delete` : null;
177+
},
160178
expanderId() {
161179
return this.nodeId ? `${this.nodeId}-exp` : null;
162180
},
@@ -194,6 +212,9 @@
194212
if (typeof this.model.deletable !== 'boolean') {
195213
this.$set(this.model, 'deletable', false);
196214
}
215+
if (typeof this.model.addChildCallback !== 'function') {
216+
this.$set(this.model, 'addChildCallback', null);
217+
}
197218
198219
if (typeof this.model.title !== 'string' || this.model.title.trim().length === 0) {
199220
this.$set(this.model, 'title', null);
@@ -288,6 +309,20 @@
288309
this.$emit('treeViewNodeDblclick', this.model, event);
289310
}
290311
},
312+
/*
313+
* Add a child node to the end of the child nodes list. The child node data is
314+
* supplied by an async callback which is the addChildCallback parameter of this node's model.
315+
*/
316+
async $_treeViewNode_onAddChild(event) {
317+
if (this.model.addChildCallback) {
318+
var childModel = await this.model.addChildCallback(this.model);
319+
320+
if (childModel) {
321+
this.model.children.push(childModel);
322+
this.$emit('treeViewNodeAdd', this.childModel, this.model, event);
323+
}
324+
}
325+
},
291326
$_treeViewNode_onDelete(event) {
292327
this.$emit('treeViewNodeDelete', this.model, event);
293328
},
@@ -370,12 +405,20 @@
370405
margin-left: $itemSpacing;
371406
}
372407
373-
.tree-view-node-self-delete {
408+
.tree-view-node-self-action {
374409
padding: 0;
375410
background: none;
376411
border: none;
377412
height: $baseHeight;
378413
414+
i.tree-view-node-self-add-child-icon {
415+
font-style: normal;
416+
417+
&::before {
418+
content: '+';
419+
}
420+
}
421+
379422
i.tree-view-node-self-delete-icon {
380423
font-style: normal;
381424

tests/data/node-generator.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
/**
2-
* Generates nodes, one per array element that is not an array.
2+
* Generates nodes, one per array element that is not itself an array.
33
* Array elements that are arrays recursively generate child nodes
44
* of the last created node. Additionally, the initial state for radio
55
* buttons will be added to the radioState parameter.
66
*
7-
* The node spec's node string should be in the format:
8-
* `[eE]?[sS]?[d]?[[cCrR]!?]?`
9-
* The presence of e, s, d, or c|r indicate the node is expandable, selectable, deletable and a checkbox or radio buttton
10-
* respectively. If it is capitalized, then the related state should be True. In the case of inputs,
7+
* The node spec's node string should be matched by the regex:
8+
* `[eE]?[sS]?[d]?[a]?[[cCrR]!?]?`
9+
* The presence of e, s, d, a, or c|r indicate the node is expandable, selectable, deletable,
10+
* addable (subnodes can be added), and a checkbox or radio buttton respectively.
11+
* If it is capitalized, then the related state should be True. In the case of inputs,
1112
* the capitalization means the input will be selected. The `!` indicates the input will be disabled.
13+
* A node that allows adds will use the callback function passed to generateNodes.
1214
*
1315
* @param {Array<String, Array>} nodeSpec The node specification array.
1416
* @param {Object} radioState An object in which the state of radio button groups is generated. Essentially an "out".
1517
* @param {String} baseId The base string used in the node IDs.
18+
* @param {Function} addChildCallback A method that returns a Promise that resolves to the node data to add as a subnode.
1619
*/
17-
export function generateNodes(nodeSpec, radioState, baseId = "") {
20+
export function generateNodes(nodeSpec, radioState, baseId = "", addChildCallback = null) {
1821
let nodes = [];
1922
let prevNode = null;
2023

@@ -24,7 +27,7 @@ export function generateNodes(nodeSpec, radioState, baseId = "") {
2427
if (prevNode === null) {
2528
return;
2629
}
27-
prevNode.children = generateNodes(item, radioState, prevNode.id);
30+
prevNode.children = generateNodes(item, radioState, prevNode.id, addChildCallback);
2831
}
2932
else {
3033
let lowerItem = item.toLowerCase();
@@ -45,6 +48,7 @@ export function generateNodes(nodeSpec, radioState, baseId = "") {
4548
expanded: item.includes('E'),
4649
selected: item.includes('S')
4750
},
51+
addChildCallback: item.includes('a') ? addChildCallback : null,
4852
children: []
4953
};
5054

tests/local/basic.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ export default [
7878
state: {
7979
expanded: false,
8080
selected: false
81+
},
82+
addChildCallback: function () {
83+
var entry = prompt("Give it a string.", "");
84+
85+
if (entry) {
86+
return Promise.resolve({ id: entry, label: entry, deletable: true });
87+
}
88+
else {
89+
return Promise.resolve(null);
90+
}
8191
}
8292
}
8393
]

0 commit comments

Comments
 (0)