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

Search, List, Details

  • Edit on GitHub

This advanced guide walks through building a Search, List, Details flow with lazy-loaded routes.

The final widget looks like:

See the Pen Search / List / Details - Final by Bitovi (@bitovi) on CodePen.

The following sections are broken down into the following parts:

  • The problem — A description of what the section is trying to accomplish.
  • What you need to know — Information about CanJS that is useful for solving the problem.
  • How to verify it works - How to make sure the solution works (if it’s not obvious).
  • The solution — The solution to the problem.

Setup

The problem

In this section, we will fork this CodePen that contains some starting code that we will modify to have a Search, List, Details flow with lazy-loaded routes.

What you need to know

This CodePen:

  • Loads all of CanJS’s packages. Each package is available as a named export. For example can-stache-element is available as import { StacheElement } from "can";.
  • Creates a basic <character-search-app> element.
  • Includes a <mock-url> element for interacting with the route of the CodePen page.

The solution

START THIS TUTORIAL BY CLONING THE FOLLOWING CodePen:

Click the EDIT ON CODEPEN button. The CodePen will open in a new window. In that new window, click FORK.

See the Pen Search / List / Details - Setup by Bitovi (@bitovi) on CodePen.

Configure routing

The problem

In this section, we will:

  • Import can-route.
  • Define a routeData property whose value is route.data.
  • Set up "pretty" routing rules.
  • Initialize can-route.

We want to support the following URL patterns:

  • #!
  • #!search
  • #!list/rick
  • #!details/rick/1

What you need to know

  • Use default to create element properties that default to objects:
class MyElement extends StacheElement {
  static props = {
    dueDate: {
      get default() {
        return new Date();
      }
    }
  };
}
  • route.register( "{abc}" ); will create a URL matching rule.
// default route - if hash is empty, default `viewModel.abc` to `"xyz"`
route.register( "", { abc: "xyz" });

// match routes like `#!xyz` - sets `viewModel.abc` to `xyz`
route.register( "{abc}" );

// match routes like `#!xyz/uvw` - sets `viewModel.abc` to `xyz`, `viewModel.def` to `uvw`
route.register( "{abc}/{def}" );
  • route.start() will initialize can-route.

How to verify it works

You can access the the app’s properties using document.querySelector("character-search-app") from the console.

You should be able to update the element properties and see the URL update. Also, updating the URL should update the properties on the element.

The solution

Update the JavaScript tab to:

import { StacheElement, route } from "//unpkg.com/can@pre/ecosystem.mjs";

class CharacterSearchApp extends StacheElement {
  static view = `
    <div class="header">
      <img src="https://image.ibb.co/nzProU/rick_morty.png" width="400" height="151">
    </div>
  `;

  static props = {
    routeData: {
      get default() {
        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return route.data;
      }
    }
  };
}

customElements.define("character-search-app", CharacterSearchApp);

Lazy load elements

The problem

In this section, we will load the code for each route when that route is displayed. This technique prevents loading code for routes a user may never visit.

The elements we will use for each route are available as ES Modules on unpkg:

  • //unpkg.com/character-search-components@6/character-search.mjs
  • //unpkg.com/character-search-components@6/character-list.mjs
  • //unpkg.com/character-search-components@6/character-details.mjs

What you need to know

  • Use get to create virtual properties that will be re-evaluated when an observable property they depend on changes:
class Person extends ObservableObject {
  static props = {
    first: {
      default: "Kevin"
    },
    last: {
      default: "McCallister"
    }
  };
  // The name property will update whenever `first` or `last` changes
  get name() {
    return this.first + " " + this.last;
  }
}

// make sure to put `name` in the view so that bindings are set up correctly
static view = `{{ name }}`;
  • Call the import() keyword as a function to dynamically import a module.

How to verify it works

Changing the routeData.page property will cause the code for the new route to be loaded in the devtools Network Tab.

The solution

Update the JavaScript tab to:

import { StacheElement, route } from "//unpkg.com/can@pre/ecosystem.mjs";

class CharacterSearchApp extends StacheElement {
  static view = `
    <div class="header">
      <img src="https://image.ibb.co/nzProU/rick_morty.png" width="400" height="151">
    </div>

    {{ this.routeComponent }}
  `;

  static props = {
    routeData: {
      get default() {
        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return route.data;
      }
    },

    get routeComponent() {
      const componentURL =
        "//unpkg.com/character-search-components@6/character-" +
        this.routeData.page +
        ".mjs";

      return import(componentURL).then(module => {});
    }
  };
}
customElements.define("character-search-app", CharacterSearchApp);

Display elements

The problem

Now that the code is loaded for each route, we can create an instance of the loaded element and display it in the view.

What you need to know

  • import() returns a promise.
  • Promises can be used directly in the view.
class MyElement extends StacheElement {
  static view = `
    {{# if(this.aPromise.isPending) }}
      The code is still loading
    {{/ if }}

    {{# if(this.aPromise.isRejected) }}
      There was an error loading the code
    {{/ if }}

    {{# if(this.aPromise.isResolved) }}
      The code is loaded: {{ this.aPromise.value }} -> Hello
    {{/ if }}
  `;
  static props = {
    aPromise: {
      get default() {
        return new Promise((resolve) => {
          resolve("Hello");
        });
      }
    }
  };
}
  • import() resolves with a module object - module.default is the element constructor.
  • Elements can be instantiated programmatically using new ElementConstructor().

How to verify it works

You can check the devtools Elements Panel for the correct element on each page:

  • #!search -> <character-search-page>
  • #!list -> <character-list-page>
  • #!details -> <character-details-page>

The solution

Update the JavaScript tab to:

import { StacheElement, route } from "//unpkg.com/can@pre/ecosystem.mjs";

class CharacterSearchApp extends StacheElement {
  static view = `
    <div class="header">
      <img src="https://image.ibb.co/nzProU/rick_morty.png" width="400" height="151">
    </div>

    {{# if(this.routeComponent.isPending) }}
      Loading…
    {{/ if }}

    {{# if(this.routeComponent.isResolved) }}
      {{ this.routeComponent.value }}
    {{/ if }}
  `;

  static props = {
    routeData: {
      get default() {
        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return route.data;
      }
    },

    get routeComponent() {
      const componentURL =
        "//unpkg.com/character-search-components@6/character-" +
        this.routeData.page +
        ".mjs";

      return import(componentURL).then(module => {
        const ElementConstructor = module.default;

        return new ElementConstructor();
      });
    }
  };
}

customElements.define("character-search-app", CharacterSearchApp);

Pass data to elements

The problem

After the last step, the correct element is displayed for each route, but the elements do not work correctly. To make these work, we will pass properties from the character-search-app element into each element.

What you need to know

  • The bindings lifecycle method can be used to create bindings between an element’s properties and parent observables.
  • can-value can be used to programmatically create observables that are bound to another object.
const elementInstance = new ElementConstructor().bindings({
  givenName: value.from(this, "name.first"),
  familyName: value.bind(this, "name.last"),
  fullName: value.to(this, "name.full")
});
  • The element for all three pages need a query property. The <character-details-page> also needs an id property.

How to verify it works

The app should be fully functional:

  • Typing in the <input> and clicking the Search button should take you to the list page with a list of matching characters.
  • Clicking a character on the list page should take you to the details page for that character.
  • Clicking the < Characters button should take you back to the list page.
  • Clicking the < Search button should take you back to the search page with the query still populated in the <input>.

The solution

Update the JavaScript tab to:

import { StacheElement, route, value } from "//unpkg.com/can@pre/ecosystem.mjs";

class CharacterSearchApp extends StacheElement {
  static view = `
    <div class="header">
      <img src="https://image.ibb.co/nzProU/rick_morty.png" width="300" height="113">
    </div>

    {{# if(this.routeComponent.isPending) }}
      Loading...
    {{/ if }}

    {{# if(this.routeComponent.isResolved) }}
      {{ this.routeComponent.value }}
    {{/ if }}
  `;

  static props = {
    routeData: {
      get default() {
        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return route.data;
      }
    },

    get routeComponentData() {
      switch (this.routeData.page) {
        case "search":
        case "list":
          return {
            query: value.from(this.routeData, "query")
          };

        case "details":
          return {
            query: value.from(this.routeData, "query"),
            id: value.from(this.routeData, "characterId")
          };
      }
    },

    get routeComponent() {
      const componentURL =
        "//unpkg.com/character-search-components@6/character-" +
        this.routeData.page +
        ".mjs";

      return import(componentURL).then(module => {
        const ElementConstructor = module.default;

        return new ElementConstructor().bindings(this.routeComponentData);
      });
    }
  };
}

customElements.define("character-search-app", CharacterSearchApp);

Result

When complete, you should have a working Search, List, Details flow like the following CodePen:

See the Pen Search / List / Details - Final 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