DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Migrating to CanJS 6
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

File Navigator

  • Edit on GitHub

This beginner guide walks you through building a simple file navigation widget. It takes about 25 minutes to complete. It was written with CanJS 6.0.0. Check out the File Navigator for an example that makes AJAX requests for its data and uses can-stache-element.

The final widget looks like:

See the Pen File Navigator simple [Finished] by Bitovi (@bitovi) on CodePen.

Click ROOT/ to see its files and folders.

Note: If you don’t see any files show up, run the CodePen again. This CodePen uses randomly generated files, so it’s possible nothing shows up.

Start this tutorial by cloning the following CodePen:

See the Pen File Navigator simple [Starter] by Bitovi (@bitovi) on CodePen.

This CodePen has initial prototype HTML and CSS which is useful for getting the application to look right.

The following sections are broken down into:

  • Problem - A description of what the section is trying to accomplish.
  • Things to know - Information about CanJS that is useful for solving the problem.
  • Solution - The solution to the problem.
  • Test it (uncommon) - How to make sure the solution works.

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

Render the root folder and its contents

The problem

Let’s render rootEntityData in the page with its immediate children.

What you need to know

  • CanJS uses can-stache-element to render data in a template and keep it updated. Templates can be authored in the element view property like:

    class MyComponent extends StacheElement {
      static view = `TEMPLATE CONTENT`;
    }
    customElements.define("my-component", MyComponent);
    
  • A custom element view is a can-stache template that uses {{key}} magic tags to insert data into the HTML output like:

    class MyComponent extends StacheElement {
      static view = `{{this.message}}`;
      static props = {
        message: "Hello, World"
      };
    }
    customElements.define("my-component", MyComponent);
    
  • Use props to define the custom element data.

  • Use {{# if(value) }} to do if/else branching in can-stache.

  • Use {{# for(of) }} 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:

<file-navigator></file-navigator>

<script type="module">
    import { fixture } from "//unpkg.com/can@pre/core.mjs";

    let entityId = 1;

    const makeEntities = function(parentId, depth) {
        if (depth > 5) {
            return [];
        }

        const entitiesCount = fixture.rand(6);

        const entities = [];

        for (let i = 0; i < entitiesCount; i++) {
            // The id for this entity
            const id = "" + entityId++;

            // If the entity is a folder or file
            const isFolder = Math.random() > 0.3;

            // The children for this folder.
            const children = isFolder ? makeEntities(id, depth + 1) : [];

            const entity = {
                id: id,
                name: (isFolder ? "Folder" : "File") + " " + id,
                parentId: parentId,
                type: isFolder ? "folder" : "file",
                hasChildren: children.length ? true : false
            };
            entities.push(entity);
            if (isFolder) {
                entity.children = children;
            }
        }
        return entities;
    };

    window.rootEntityData = {
        id: "0",
        name: "ROOT/",
        hasChildren: true,
        type: "folder",
        children: makeEntities("0", 0)
    };
</script>

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class FileNavigator extends StacheElement {
  static view = `
    <span>{{this.rootEntity.name}}</span>
    <ul>
      {{# for(child of this.rootEntity.children) }}
        <li class="{{child.type}} {{# if(child.hasChildren) }}hasChildren{{/ if }}">
          {{# eq(child.type, 'file')}}
            📝 <span>{{child.name}}</span>
          {{else}}
            📁 <span>{{child.name}}</span>
          {{/ eq}}
        </li>
      {{/ for }}
    </ul>
  `;

  static props = {
    rootEntity: {
      get default() {
        return rootEntityData;
      }
    }
  };
}

customElements.define("file-navigator", FileNavigator);

Render all the files and folders

The Problem

Now let’s 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 {{>PARTIAL_NAME}} like the following:

    {{>PARTIAL_NAME}}
    
  • You can register an inline named partial within the current template {{<PARTIAL_NAME}} like the following:

    class MyComponent extends StacheElement {
      static view = `{{<partialView}} BLOCK {{/partialView}}`;
    }
    
  • The registered inline named partial can be called recursively like the following:

{{<recursiveView}}
  <div>{{name}} <b>Type:</b> {{#if(nodes.length)}}Branch{{else}}Leaf{{/if}}</div>
  {{# for (node of nodes) }}
    {{>recursiveView}}
  {{/ for }}
{{/recursiveView}}

{{>recursiveView(yayRecursion)}}`

The Solution

Update the JAVSCRIPT tab to:

  • Call to an {{>entities}} partial.
import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class FileNavigator extends StacheElement {
  static view = `
    {{<entities}}
      <span>{{this.name}}</span>
      <ul>
        {{# for(child of this.children) }}
          <li class="{{child.type}} {{# if(child.hasChildren) }}hasChildren{{/ if }}">
            {{# eq(child.type, 'file')}}
              📝 <span>{{child.name}}</span>
            {{else}}
              📁 {{entities(child)}}
            {{/ eq}}
          </li>
        {{/ for }}
      </ul>
    {{/entities}}

    {{entities(this.rootEntity)}}
  `;

  static props = {
    rootEntity: {
      get default() {
        return rootEntityData;
      }
    };
  }
}

customElements.define("file-navigator", FileNavigator);

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

  • can-observable-object allows you to define a type by defining the type’s properties and the properties’ types like:

    import { ObservableObject } from "can";
    
    class Person extends ObservableObject {
      static props = {
        name: String,
        age: Number
      };
    }
    

    This lets you create instances of that type and listen to changes like:

    const 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"
    
  • ObservableObject allows one to specify a property's type as an ObservableArray of typed instances like:

    import { ObservableArray, ObservableObject, type } from "can";
    
    class Person extends ObservableObject {
      static props = {
        name: String,
        age: Number,
        addresses: type.convert(ObservableArray.convertsTo(Address))
      };
    }
    

    However, if Address wasn’t immediately available, you could do the same thing like:

    import { ObservableArray, ObservableObject, type } from "can";
    
    class Person extends ObservableObject {
      static props = {
        name: String,
        age: Number,
        addresses: type.late(() => type.convert(ObservableArray.convertsTo(Address)))
      };
    }
    

The solution

Update the JavaScript 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
import {
  ObservableArray,
  ObservableObject,
  StacheElement,
  type
} from "//unpkg.com/can@6/core.mjs";

class Entity extends ObservableObject {
  static props = {
    id: String,
    name: String,
    parentId: String,
    hasChildren: Boolean,
    type: String,
    children: type.late(() => type.convert(ObservableArray.convertsTo(Entity)))
  };
}

const rootEntity = new Entity(rootEntityData);

class FileNavigator extends StacheElement {
  static view = `
    {{<entities}}
      <span>{{this.name}}</span>
      <ul>
        {{# for(child of this.children) }}
          <li class="{{child.type}} {{# if(child.hasChildren) }}hasChildren{{/ if }}">
            {{# eq(child.type, 'file') }}
              📝 <span>{{child.name}}</span>
            {{else}}
              📁 {{entities(child)}}
            {{/ eq }}
          </li>
        {{/ for }}
      </ul>
    {{/entities}}

    {{entities(this.rootEntity)}}
  `;

  static props = {
    rootEntity: {
      get default() {
        return rootEntity;
      }
    }
  };
}

customElements.define("file-navigator", FileNavigator);

Test it

Run the following the Console tab:

var rootEntity = document.querySelector("file-navigator").rootEntity;
rootEntity.name= "Something New";
rootEntity.children.pop();

You should see the page change automatically.

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

  • ObservableObject can specify a default value and a type:

    import { ObservableObject } from "can";
    
    class Person extends ObservableObject {
      static props = {
        address: Address,
        age: { default: 33, type: Number }
      };
    }
    
  • ObservableObject can also have methods:

    import { ObservableObject } from "can";
    
    class Person extends ObservableObject {
      static props = {
        address: Address,
        age: { default: 33, type: Number }
      };
      birthday() {
        this.age += 1;
      }
    }
    
  • Use {{# if(value) }} to do if/else branching in can-stache.

  • Use on: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 on:click="doSomething()"> ... </div>
    

The solution

Update the JavaScript tab to:

  • Add an isOpen property to Entity.
  • Add a toggleOpen method to Entity.
import {
  ObservableArray,
  ObservableObject,
  StacheElement,
  type
} from "//unpkg.com/can@6/core.mjs";

class Entity extends ObservableObject {
  static props = {
    id: String,
    name: String,
    parentId: String,
    hasChildren: Boolean,
    type: String,
    children: type.late(() => type.convert(ObservableArray.convertsTo(Entity))),
    isOpen: { type: Boolean, default: false }
  };
  toggleOpen() {
    this.isOpen = !this.isOpen;
  }
}

const rootEntity = new Entity(rootEntityData);

class FileNavigator extends StacheElement {
  static view = `
    {{<entities}}
      <span on:click="this.toggleOpen()">{{this.name}}</span>
      {{# if(this.isOpen) }}
        <ul>
          {{# for(child of this.children) }}
            <li class="{{child.type}} {{# if(child.hasChildren) }}hasChildren{{/ if }}">
              {{# eq(child.type, 'file') }}
                📝 <span>{{child.name}}</span>
              {{else}}
                📁 {{entities(child)}}
              {{/ eq }}
            </li>
          {{/ for }}
        </ul>
      {{/ if }}
    {{/entities}}

    {{entities(this.rootEntity)}}
  `,

  static props = {
    rootEntity: {
      get default() {
        return rootEntity;
      }
    }
  };
}

customElements.define("file-navigator", FileNavigator);

Result

When complete, you should have a working file-navigation widget like the following CodePen:

See the Pen File Navigator simple [Finished] by Bitovi (@bitovi) on CodePen.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 6.0.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news