This is a DoneJS Hack Night article that covers building a file navigation widget that looks like:
The full widget looks like: http://jsbin.com/sodida/edit?html,js,output
In this track, we will:
- Understand the data
- Render the root folder and its contents
- Render all the files and folders
- Make the data observable
- Make the folders open and close
There is a randomly generated rootEntityData
variable that contains a nested structure of
folders and files. It looks like:
{
"id": "0",
"name": "ROOT/",
"hasChildren": true,
"type": "folder",
"children": [
{
"id": "1", "name": "File 1",
"parentId": "0",
"type": "file",
"hasChildren": false
},
{
"id": "2", "name": "File 2",
"parentId": "0",
"type": "file",
"hasChildren": false
},
{
"id": "3", "name": "Folder 3",
"parentId": "0",
"type": "folder",
"hasChildren": true,
"children": [
{
"id": "4", "name": "File 4",
"parentId": "3",
"type": "file",
"hasChildren": false
},
{
"id": "5", "name": "File 5",
"parentId": "3",
"type": "file",
"hasChildren": false
},
{
"id": "6", "name": "File 6",
"parentId": "3",
"type": "file",
"hasChildren": false
},
{
"id": "7", "name": "File 7",
"parentId": "3",
"type": "file",
"hasChildren": false
},
{
"id": "8", "name": "Folder 8",
"parentId": "3",
"type": "folder",
"hasChildren": false,
"children": []
}
]
},
{
"id": "9", "name": "File 9",
"parentId": "0",
"type": "file",
"hasChildren": false
}
]
}
Notice that entities have the following properties:
- "id" - a unique id
- name - the name of the file or folder
- type - if this entity a "file" or "folder"
- hasChildren - if this entity has children
- children - An array of the child file and folder entities for this folder
Lets render rootEntityData
in the page with its immediate children.
-
CanJS uses can-stache to render data in a template and keep it live. Templates can be authored in
<script>
tags like:<script type="text/stache" id="app-template"> TEMPLATE CONTENT </script>
A can-stache template uses {{key}} magic tags to insert data into the HTML output like:
<script type="text/stache" id="app-template"> {{something.name}} </script>
-
Load a template from a
<script>
tag with can.stache.from like:var template = can.stache.from(SCRIPT_ID);
-
Render the template with data into a documentFragment like:
var frag = template({ something: {name: "Derek Brunson"} });
-
Insert a fragment into the page with:
document.body.appendChild(frag);
-
Use {{#each value}} to do looping in
can-stache
. -
Use {{#eq value1 value2}} to test equality in
can-stache
. -
{{./key}} only returns the value in the current scope.
-
Write a
<ul>
to contain all the files. Within the<ul>
there should be:- An
<li>
with a className that includesfile
orfolder
andhasChildren
if the folder has children. - The
<li>
should haveπ <span>{{FILE_NAME}}</span>
if a file andπ <span>{{FOLDER_NAME}}</span>
if a folder.
- An
Update the HTML
tab to:
<script type="text/stache" id="entities-template">
<span>{{name}}</span>
<ul>
{{#each ./children}}
<li class="{{type}} {{#if hasChildren}}hasChildren{{/if}}">
{{#eq type 'file'}}
π <span>{{name}}</span>
{{else}}
π <span>{{name}}</span>
{{/eq}}
</li>
{{/each}}
</ul>
</script>
Update the JS
tab to:
var template = can.stache.from("entities-template");
var frag = template( rootEntityData );
document.body.appendChild( frag );
Now lets render all of the files and folders! This means we want to render the files and folders recursively. Every time we find a folder, we need to render its contents.
-
A template can call out to another registered partial template with with {{>PARTIAL_NAME}} like the following:
{{>PARTIAL_NAME}}
-
You can register partial templates with can.stache.registerPartial like the following:
var template = can.stache.from("TEMPLATE_ID"); can.stache.registerPartial( "PARTIAL_NAME", template );
Update the HTML
tab to:
- Call to an
{{>entities}}
partial.
<script type="text/stache" id="entities-template">
<span>{{name}}</span>
<ul>
{{#each ./children}}
<li class="{{type}} {{#if hasChildren}}hasChildren{{/if}}">
{{#eq type 'file'}}
π <span>{{name}}</span>
{{else}}
π {{>entities}}
{{/eq}}
</li>
{{/each}}
</ul>
</script>
Update the JS
tab to:
- Register the
entities-template
as a partial:
var template = can.stache.from("entities-template");
can.stache.registerPartial("entities", template ); // CHANGED
var frag = template(rootEntityData);
document.body.appendChild( frag );
For rich behavior, we need to convert the raw JS data into typed observable data. When we change the data, the UI will automatically change.
-
DefineMap.extend allows you to define a type by defining the type's properties and the properties' types like:
Person = can.DefineMap.extend("Person",{ name: "string", age: "number" })
This lets you create instances of that type and listen to changes like:
var person = new Person({ name: "Justin", age: 34 }); person.on("name", function(ev, newName){ console.log("person name changed to ", newName); }); person.name = "Kevin" //-> logs "entity name changed to Kevin"
-
can.DefineMap
supports an Array shorthand that allows one to specify a can.DefineList of typed instances like:Person = can.DefineMap.extend("Person",{ name: "string", age: "number", addresses: [Address] });
However, if
Address
wasn't immediately available, you could do the same thing like:Person = can.DefineMap.extend("Person",{ name: "string", age: "number", addresses: [{ type: function(rawData){ return new Address(rawData); } }] });
Update the JS
tab to:
- Define an
Entity
type and the type of its properties. - Create an instance of the
Entity
type calledrootEntity
- Use
rootEntity
to render the template
var Entity = can.DefineMap.extend("Entity",{ // ADDED
id: "string",
name: "string",
parentId: "string",
hasChildren: "boolean",
type: "string",
children: [{
type: function(entity){
return new Entity(entity)
}
}]
});
var rootEntity = new Entity(rootEntityData); // ADDED
var template = can.stache.from("entities-template");
can.stache.registerPartial("entities", template );
var frag = template(rootEntity); // CHANGED
document.body.appendChild( frag );
Try to run the following the console
:
rootEntity.name= "Something New";
rootEntity.children.pop();
We want to be able to toggle if a folder is open or closed.
-
can.DefineMap
can specify a default value and a type:var Person = can.DefineMap.extend({ address: Address, age: {value: 33, type: "number"} });
-
can.DefineMap
can also have methods:var Person = can.DefineMap.extend({ address: Address, age: {value: 33, type: "number"}, birthday: function(){ this.age++; } });
-
Use {{#if value}} to do
if/else
branching incan-stache
. -
Use ($EVENT) to listen to an event on an element and call a method in
can-stache
. For example, the following callsdoSomething()
when the<div>
is clicked.<div ($click)="doSomething()"> ... </div>
Update the JS
tab to:
- Add an
isOpen
property toEntity
. - Add a
toggleOpen
method toEntity
.
var Entity = can.DefineMap.extend("Entity",{
id: "string",
name: "string",
parentId: "string",
hasChildren: "boolean",
type: "string",
children: [{
type: function(entity){
return new Entity(entity)
}
}],
isOpen: {type: "boolean", value: false}, // ADDED
toggleOpen: function(){ // ADDED
this.isOpen = !this.isOpen;
}
});
var rootEntity = new Entity(rootEntityData);
var template = can.stache.from("entities-template");
can.stache.registerPartial("entities", template );
var frag = template(rootEntity);
document.body.appendChild( frag );
Update the HTML
tab to:
- Call
toggleOpen()
when clicked. - Only show the children
{{#if isOpen}}
is true.
<script type="text/stache" id="entities-template">
<span ($click)="toggleOpen()">{{name}}</span> <!-- CHANGED -->
{{#if isOpen}} <!-- ADDED -->
<ul>
{{#each ./children}}
<li class="{{type}} {{#if hasChildren}}hasChildren{{/if}}">
{{#eq type 'file'}}
π <span>{{name}}</span>
{{else}}
π {{>entities}}
{{/eq}}
</li>
{{/each}}
</ul>
{{/if}} <!-- ADDED -->
</script>