Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
DoneJS Hack Night - File Navigator Beginner Track

DoneJS Hack Night - File Navigator Beginner Track

This is a DoneJS Hack Night article that covers building a file navigation widget that looks like:

600_457801363

The full widget looks like: http://jsbin.com/sodida/edit?html,js,output

In this track, we will:

  1. Understand the data
  2. Render the root folder and its contents
  3. Render all the files and folders
  4. Make the data observable
  5. Make the folders open and close

Start with this JSBin

1. Understand the data

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

2. Render the root folder and its contents

The problem

Lets render rootEntityData in the page with its immediate children.

What you need to know

  • 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 includes file or folder and hasChildren if the folder has children.
    • The <li> should have πŸ“ <span>{{FILE_NAME}}</span> if a file and πŸ“ <span>{{FOLDER_NAME}}</span> if a folder.

The solution

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 );

3. Render all the files and folders

The Problem

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.

Things to know

  • 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 );

The Solution

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 );

4. Make the data observable

The problem

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.

Things to know

  • 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);
        }
      }]
    });

The solution

Update the JS tab to:

  • Define an Entity type and the type of its properties.
  • Create an instance of the Entity type called rootEntity
  • 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 );

Test it

Try to run the following the console:

rootEntity.name= "Something New";
rootEntity.children.pop();

5. Make the folders open and close

The problem

We want to be able to toggle if a folder is open or closed.

Things to know

  • 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 in can-stache.

  • Use ($EVENT) to listen to an event on an element and call a method in can-stache. For example, the following calls doSomething() when the <div> is clicked.

    <div ($click)="doSomething()"> ... </div>

The solution

Update the JS tab to:

  • Add an isOpen property to Entity.
  • Add a toggleOpen method to Entity.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment