DoneJS StealJS jQuery++ FuncUnit DocumentJS
5.33.3
6.0.0 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
      • 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-component is available as import { Component } from "can".
  • Creates a basic <character-search-app> component.
  • Includes a <mock-url> component 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:

  • Create an observable key-value object.
  • Cross-bind the observable with the URL.
  • Set up "pretty" routing rules.

We want to support the following URL patterns:

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

What you need to know

  • Use default to create default values for properties on the ViewModel.
ViewModel: {
    dueDate: {
        default() {
            return new Date();
        }
    }
}
  • Use new observe.Object({ /* ... */ }) to create an observable Object.
  • Set route.data to the object you want cross-bound to the URL.
  • 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 viewModel for the app using document.querySelector("character-search-app").viewModel from the console.

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

The solution

Update the JavaScript tab to:

import { Component, observe, route } from "//unpkg.com/can@5/ecosystem.mjs";

Component.extend({
  tag: "character-search-app",

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

  ViewModel: {
    routeData: {
      default() {
        const observableRouteData = new observe.Object();
        route.data = observableRouteData;

        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return observableRouteData;
      }
    }
  }
});

Lazy load components

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 components we will use for each route are available as ES Modules on unpkg:

  • //unpkg.com/character-search-components@5/character-search.mjs
  • //unpkg.com/character-search-components@5/character-list.mjs
  • //unpkg.com/character-search-components@5/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:
ViewModel: {
    first: {
        default() {
            return "Kevin";
        }
    },
    last: {
        default() {
            return "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
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 { Component, observe, route } from "//unpkg.com/can@5/ecosystem.mjs";

Component.extend({
  tag: "character-search-app",

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

    {{ routeComponent }}
  `,

  ViewModel: {
    routeData: {
      default() {
        const observableRouteData = new observe.Object();
        route.data = observableRouteData;

        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return observableRouteData;
      }
    },

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

      return import(componentURL).then((module) => {
      });
    }
  }
});

Display components

The problem

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

What you need to know

  • import() returns a promise.
  • Promises can be used directly in the view.
ViewModel: {
    get aPromise() {
        return new Promise((resolve) => {
            resolve("Hello");
        });
    }
},
view: `
    {{# if(aPromise.isPending) }}
        The code is still loading
    {{/ if }}

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

    {{# if(aPromise.isResolved) }}
        The code is loaded: {{aPromise.value}} -> Hello
    {{/ if }}
`
  • import() resolves with a module object - module.default is the component constructor.
  • Components can be instantiated programmatically using new ComponentConstructor().

How to verify it works

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

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

The solution

Update the JavaScript tab to:

import { Component, observe, route } from "//unpkg.com/can@5/ecosystem.mjs";

Component.extend({
  tag: "character-search-app",

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

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

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

  ViewModel: {
    routeData: {
      default() {
        const observableRouteData = new observe.Object();
        route.data = observableRouteData;

        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return observableRouteData;
      }
    },

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

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

        return new ComponentConstructor({
        });
      });
    }
  }
});

Pass data to components

The problem

After the last step, the correct component is displayed for each route, but the components do not work correctly. To make these work, we will pass properties from the main ViewModel into each component.

What you need to know

  • You can pass a viewModel property when instantiating components to pass values to the component’s viewModel and set up bindings.
  • can-value can be used to programmatically create observables that are bound to another object.
const componentInstance = new ComponentConstructor({
  viewModel: {
    givenName: value.from(this, "name.first"),
    familyName: value.bind(this, "name.last"),
    fullName: value.to(this, "name.full")
  }
});
  • The component 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 { Component, observe, route, value } from "//unpkg.com/can@5/ecosystem.mjs";

Component.extend({
  tag: "character-search-app",

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

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

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

  ViewModel: {
    routeData: {
      default() {
        const observableRouteData = new observe.Object();
        route.data = observableRouteData;

        route.register("", { page: "search" });
        route.register("{page}");
        route.register("{page}/{query}");
        route.register("{page}/{query}/{characterId}");

        route.start();

        return observableRouteData;
      }
    },

    get routeComponentData() {
      const viewModelData = {
        query: value.from(this.routeData, "query")
      };

      if(this.routeData.page === "details") {
        viewModelData.id = value.from(this.routeData, "characterId");
      }

      return viewModelData;
    },

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

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

        return new ComponentConstructor({
          viewModel: this.routeComponentData
        });
      });
    }
  }
});

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 5.33.3.

On this page

Get help

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