DoneJS StealJS jQuery++ FuncUnit DocumentJS
4.3.0
5.0.0 3.13.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Technology Overview
      • Reading the Docs (API Guide)
      • Experimental ES Module Usage
    • recipes
      • Canvas Clock (Simple)
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • Text Editor (Medium)
      • Tinder Carousel (Medium)
      • TodoMVC with StealJS
      • Video Player (Simple)
    • topics
      • Debugging
      • Forms
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Chat Guide

  • Edit on GitHub

This guide will walk you through building a real-time chat application with CanJS’s Core libraries. It takes about 30 minutes to complete.

Setup

The easiest way to get started is to clone the following JS Bin by clicking the JS Bin button on the top left:

JS Bin on jsbin.com

The JS Bin loads Bootstrap for its styles and socket.io for a socket library. It will be connecting to a RESTful and real-time service layer at https://chat.donejs.com/api/messages.

The JS Bin also loads can.js, which is a script that includes all of CanJS core under a single global can namespace.

Generally speaking, you should not use the global can script, but instead you should import things directly with a module loader like StealJS, WebPack or Browserify. In a real app, your code will look like:

import DefineMap from 'can-define/map/map';
import DefineList from 'can-define/list/list';

var Message = DefineMap.extend({ ... });
Message.List = DefineList.extend({ ... });

Not:

var Message = can.DefineMap.extend({ ... });
Message.List = can.DefineList.extend({ ... });

Read Setting Up CanJS for instructions on how to set up CanJS in a real app. Check out the DoneJS version of this guide.

Hello World

In this section, we will:

  • Show a big “Chat Home” title within a Bootstrap container.
  • Make it so when “Chat Home” is clicked, an exclamation mark (“!”) is added to the end of the title.

In your JS Bin, update the HTML tab to:

  • Use the <chat-app> element we will define in the JS tab.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Chat Guide 3.0 - Hello World">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<chat-app></chat-app>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@^4.0.0-pre.5/dist/global/can.all.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.8/socket.io.js"></script>
</body>

</html>

Update the JavaScript tab to:

  • Define an application component (chat-app) by extending can-component. Its definition includes:
    • A tag that is the name of the custom element being defined.
    • A can-stache view that contains the contents of the chat-app element. This view:
      • Inserts a message value within a responsive Bootstrap container using {{expression}}.
      • Listen for click events and call addExcitement with on:event.
    • A can-define/map/map ViewModel definition. This definition includes:
      • A message property that is a string value initialized to "Chat Home".
      • An addExcitement method that adds "!" to the end of the message property.
can.Component.extend({
    tag: "chat-app",
    view: `
        <div class="container">
          <div class="row">
            <div class="col-sm-8 col-sm-offset-2">
              <h1 class="page-header text-center" on:click="addExcitement()">
                {{message}}
              </h1>
            </div>
          </div>
        </div>`,
    ViewModel: {
        message: {
            type: "string",
            default: "Chat Home"
        },
        addExcitement(){
            this.message = this.message + "!";
        }
    }
});

When complete, you should see a large “Chat Home” title in the Output panel. Click on it and things will get really exciting!

This step sets up the essential basics of a CanJS application — a can-component custom element with a can-stache view and can-define/map/map ViewModel.

The properties and methods the view uses are defined in the ViewModel type. We defined a message and an addExcitement method.

The templates are a dialect of mustache and handlebars syntax. The mustache syntax allows a very terse writing style for the most common patterns within templates:

  • inserting data with {{expression}}
  • looping with {{#each(expression)}}
  • branching with {{#if(expression)}} or {{#is(expressions)}}

Key take-away: You define ViewModel method and property behaviors. The ViewModel methods can be called by can-stache views. The ViewModel properties can be observed by can-stache views.

Route between two pages

In this section we will:

  • Create a home page and chat messages page that the user can navigate between with links and the browser’s back and forward button.

Update the JavaScript tab to:

  • Update the chat-app component's view to:
    • Check if the ViewModel’s page property is "home". If it is, render the home page’s content. If it’s not, it will render the chat messages page’s content with the {{else}} helper.
    • Use {{routeUrl(hashes)}} to create the right link urls so that page will be set on appVM to either "home" or "chat".
  • Update the chat-app component's ViewModel to:
    • Setup a connection between the ViewModel and the route state in the ViewModel's init by:
      • Create a pretty routing rule so if the url looks like "#!chat", the page property of appVM will be set to chat with register. If there is nothing in the hash, page will be set to "home".
      • Connect changes in the url to changes in the <chat-app>'s ViewModel with data.
      • Initialize the url’s values on the ViewModel and set up the two-way connection with start.
    • Include a page property that will be updated when the browser’s URL changes.
    • Prevent the message property from becoming part of the URL changes by using serialize: false.
can.Component.extend({
    tag: "chat-app",
    view: `
        <div class="container">
          <div class="row">
            <div class="col-sm-8 col-sm-offset-2">
              {{#eq(page, 'home')}}
                  <h1 class="page-header text-center" on:click="addExcitement()">
                    {{message}}
                  </h1>
                  <a href="{{routeUrl(page='chat')}}"
                     class="btn btn-primary btn-block btn-lg">
                       Start chat
                  </a>
              {{else}}
                 <h1 class="page-header text-center">
                    Chat Messages
                 </h1>
                 <h5><a href="{{routeUrl(page='home')}}">Home</a></h5>
              {{/eq}}
            </div>
          </div>
        </div>`,
    ViewModel: {
        init(){
            can.route.register("{page}",{page: "home"});
            can.route.data = this;
            can.route.start();
        },
        page: "string",
        message: {
            type: "string",
            default: "Chat Home",
            serialize: false
        },
        addExcitement(){
            this.message = this.message + "!";
        }
    }
});

When complete, you should be able to toggle between the two pages. If you type:

window.location.hash

in JS Bin’s console tab after clicking a new page, you will be able to see the hash change between !# and #!chat.

This step sets up basic routing between different “pages” in an application. CanJS’s routing is based on the properties in the application view model. When those properties change, different content is shown.

We connected the application view model to the routing system with can-route.data and initialized that connection with can-route.start.

This makes it so if the page property changes, the browser’s url will change. If the browser’s url changes, the page property changes.

Key take-away: can-route two-way binds changes in the browser’s url to the application view model and vice versa. Use changes in the application view model to control which content is shown.

Chat Messages Component

In this section, we will:

  • Define and use a custom <chat-messages> element that contains the behavior of the chat messages page.

Update the JavaScript tab to:

  • Define a <chat-messages> custom element with can-component. It's view will contain the content of the chat messages page.
  • Update <chat-app>'s view to create a <chat-messages> element.
can.Component.extend({
    tag: "chat-messages",
    view: `
        <h1 class="page-header text-center">
           Chat Messages
        </h1>
        <h5><a href="{{routeUrl(page='home')}}">Home</a></h5>`
});

can.Component.extend({
    tag: "chat-app",
    view: `
        <div class="container">
          <div class="row">
            <div class="col-sm-8 col-sm-offset-2">
              {{#eq(page, 'home')}}
                  <h1 class="page-header text-center" on:click="addExcitement()">
                    {{message}}
                  </h1>
                  <a href="{{routeUrl(page='chat')}}"
                     class="btn btn-primary btn-block btn-lg">
                       Start chat
                  </a>
              {{else}}
                 <chat-messages/>
              {{/eq}}
            </div>
          </div>
        </div>`,
    ViewModel: {
        init(){
            can.route.register("{page}",{page: "home"});
            can.route.data = this;
            can.route.start();
        },
        page: "string",
        message: {
            type: "string",
            default: "Chat Home",
            serialize: false
        },
        addExcitement(){
            this.message = this.message + "!";
        }
    }
});

When complete, you should see the same behavior as the previous step. You should be able to click back and forth between the two different pages.

This step creates the <chat-messages> custom element. Custom elements are used to represent some grouping of related (and typically visual) functionality such as:

  • Widgets like <my-slider> or <acme-navigation>.
  • Pages like <chat-login> or <chat-messages>.

Custom elements are the macroscopic building blocks of an application. They are the orchestration pieces used to assemble the application into a whole.

For example, an application’s template might assemble many custom elements to work together like:

{{#if(session)}}
  <app-toolbar selectedFiles:bind="selectedFiles"/>
  <app-directory selectedFiles:bind="selectedFiles"/>
  <app-files selectedFiles:bind="selectedFiles"/>
  <app-file-details selectedFiles:bind="selectedFiles"/>
{{else}}
  <app-login/>
{{/if}}

Breaking down an application into many isolated and potentially reusable components is a critical piece of CanJS software architecture.

Custom elements are defined with can-component. Components render their view with a ViewModel instance. By default, their view only has access to the data in the ViewModel. You can use event and data bindings like toChild:from and twoWay:bind to pass data between custom elements.

Key take-away: can-component makes custom elements. Break down your application into many bite-sized custom elements.

List Messages

In this section, we will:

  • Display messages from https://chat.donejs.com/api/messages when messagesPromise.isResolved.
  • Show a “Loading…” message while the messages are loading (messagesPromise.isPending).
  • Show an error if those messages fail to load (messagesPromise.isRejected).

Update the JavaScript tab to:

  • Define a Message type with can-define/map/map.
  • Define a Message.List type that contains Message items.
  • Connect the Message and Message.List type to the RESTful messages service at https://chat.donejs.com/api/messages using can-connect/can/super-map/super-map.
  • Update the <chat-messages>'s view to:
    • Check if the messages are in the process of loading and show a loading indicator.
    • Check if the messages failed to load and display the reason for the failure.
    • If messages successfully loaded, list each message’s name and body. If there are no messages, write out “No messages”.
  • Update the <chat-messages>'s ViewModel to:
    • Define a messagesPromise property on ChatMessagesVM that’s default is initialized to a Promise that represents the loading of all messages using getList.
var Message = can.DefineMap.extend({
    id: "number",
    name: "string",
    body: "string",
    created_at: "date"
});

Message.List = can.DefineList.extend({
    "#": Message
});

Message.connection = can.connect.superMap({
    url: {
        resource: 'https://chat.donejs.com/api/messages',
        contentType: 'application/x-www-form-urlencoded'
    },
    Map: Message,
    List: Message.List,
    name: 'message'
});

can.Component.extend({
    tag: "chat-messages",
    view: `
        <h1 class="page-header text-center">
           Chat Messages
        </h1>
        <h5><a href="{{routeUrl(page='home')}}">Home</a></h5>

        {{#if(messagesPromise.isPending)}}
          <div class="list-group-item list-group-item-info">
            <h4 class="list-group-item-heading">Loading…</h4>
          </div>
        {{/if}}
        {{#if(messagesPromise.isRejected)}}
          <div class="list-group-item list-group-item-danger">
            <h4 class="list-group3--item-heading">Error</h4>
            <p class="list-group-item-text">{{messagesPromise.reason}}</p>
          </div>
        {{/if}}
        {{#if(messagesPromise.isResolved)}}
          {{#each(messagesPromise.value)}}
            <div class="list-group-item">
              <h4 class="list-group3--item-heading">{{name}}</h4>
              <p class="list-group-item-text">{{body}}</p>
            </div>
          {{else}}
            <div class="list-group-item">
              <h4 class="list-group-item-heading">No messages</h4>
            </div>
          {{/each}}
        {{/if}}`,
    ViewModel: {
        messagesPromise: {
            default: function(){
                return Message.getList({});
            }
        }
    }
});

can.Component.extend({
    tag: "chat-app",
    view: `
        <div class="container">
          <div class="row">
            <div class="col-sm-8 col-sm-offset-2">
              {{#eq(page, 'home')}}
                  <h1 class="page-header text-center" on:click="addExcitement()">
                    {{message}}
                  </h1>
                  <a href="{{routeUrl(page='chat')}}"
                     class="btn btn-primary btn-block btn-lg">
                       Start chat
                  </a>
              {{else}}
                 <chat-messages/>
              {{/eq}}
            </div>
          </div>
        </div>`,
    ViewModel: {
        init(){
            can.route.register("{page}",{page: "home"});
            can.route.data = this;
            can.route.start();
        },
        page: "string",
        message: {
            type: "string",
            default: "Chat Home",
            serialize: false
        },
        addExcitement(){
            this.message = this.message + "!";
        }
    }
});

When complete, you should see a list of messages in the chat messages page.

This step creates a Message model by first creating the Message type and then connecting it to a messages service at https://chat.donejs.com/api/messages.

Explanation

The super-map module adds methods to the Message type that let you:

  • Get a list of messages:

    Message.getList({}).then(function(messages){})
    
  • Get a single message:

    Message.get({id: 5}).then(function(message){})
    
  • Create a message on the server:

    message = new Message({name: "You", body: "Hello World"})
    message.save()
    
  • Update a message on the server:

    message.body = "Welcome Earth!";
    message.save();
    
  • Delete message on the server:

    message.destroy();
    

There are also methods to let you know when a message isNew, isSaving, and isDestroying.

With the message model created, it’s used to load and list messages on the server.

Key take-away: Create a model for your data’s schema and use it to communicate with a backend server.

Create Messages

In this section, we will:

  • Add the ability to create messages on the server and have them added to the list of messages.

Update the <chat-messages> view to:

  • Create a form to enter a message’s name and body.
  • When the form is submitted, call send on the ChatMessagesVM with on:event.
  • Connect the first <input>’s value to the ChatMessagesVM’s name property with twoWay:bind.
  • Connect the second <input>’s value to the ChatMessagesVM’s body property with twoWay:bind.

Update the <chat-messages> ViewModel to:

  • Define a name and body property on ChatMessagesVM.
  • Define a send method on ChatMessagesVM that creates a new Message and sends it to the server.
var Message = can.DefineMap.extend({
    id: "number",
    name: "string",
    body: "string",
    created_at: "date"
});

Message.List = can.DefineList.extend({
    "#": Message
});

Message.connection = can.connect.superMap({
    url: {
        resource: 'https://chat.donejs.com/api/messages',
        contentType: 'application/x-www-form-urlencoded'
    },
    Map: Message,
    List: Message.List,
    name: 'message'
});

can.Component.extend({
    tag: "chat-messages",
    view: `
        <h1 class="page-header text-center">
           Chat Messages
        </h1>
        <h5><a href="{{routeUrl(page='home')}}">Home</a></h5>

        {{#if(messagesPromise.isPending)}}
          <div class="list-group-item list-group-item-info">
            <h4 class="list-group-item-heading">Loading…</h4>
          </div>
        {{/if}}
        {{#if(messagesPromise.isRejected)}}
          <div class="list-group-item list-group-item-danger">
            <h4 class="list-group3--item-heading">Error</h4>
            <p class="list-group-item-text">{{messagesPromise.reason}}</p>
          </div>
        {{/if}}
        {{#if(messagesPromise.isResolved)}}
          {{#each(messagesPromise.value)}}
            <div class="list-group-item">
              <h4 class="list-group3--item-heading">{{name}}</h4>
              <p class="list-group-item-text">{{body}}</p>
            </div>
          {{else}}
            <div class="list-group-item">
              <h4 class="list-group-item-heading">No messages</h4>
            </div>
          {{/each}}
        {{/if}}

        <form class="row" on:submit="send(scope.event)">
            <div class="col-sm-3">
              <input type="text" class="form-control" placeholder="Your name"
                     value:bind="name"/>
            </div>
            <div class="col-sm-6">
              <input type="text" class="form-control" placeholder="Your message"
                     value:bind="body"/>
            </div>
            <div class="col-sm-3">
              <input type="submit" class="btn btn-primary btn-block" value="Send"/>
            </div>
        </form>`,
    ViewModel: {
        messagesPromise: {
            default: function(){
                return Message.getList({});
            }
        },
        name: "string",
        body: "string",
        send: function(event) {
            event.preventDefault();

            new Message({
                name: this.name,
                body: this.body
            }).save().then(function(){
                this.body = "";
            }.bind(this));
        }
    }
});

can.Component.extend({
    tag: "chat-app",
    view: `
        <div class="container">
          <div class="row">
            <div class="col-sm-8 col-sm-offset-2">
              {{#eq(page, 'home')}}
                  <h1 class="page-header text-center" on:click="addExcitement()">
                    {{message}}
                  </h1>
                  <a href="{{routeUrl(page='chat')}}"
                     class="btn btn-primary btn-block btn-lg">
                       Start chat
                  </a>
              {{else}}
                 <chat-messages/>
              {{/eq}}
            </div>
          </div>
        </div>`,
    ViewModel: {
        init(){
            can.route.register("{page}",{page: "home"});
            can.route.data = this;
            can.route.start();
        },
        page: "string",
        message: {
            type: "string",
            default: "Chat Home",
            serialize: false
        },
        addExcitement(){
            this.message = this.message + "!";
        }
    }
});

When complete, you will be able to create messages and have them appear in the list.

This step sets up a form to create a Message on the server. Notice that the new Message automatically appears in the list of messages. This is because can-connect/can/super-map/super-map adds the real-time behavior. The real-time behavior automatically inserts newly created messages into lists that they belong within. This is one of CanJS’s best features — automatic list management.

Key take-away: CanJS will add, remove, and update lists for you automatically.

Real Time

In this section, we will:

  • Listen to messages created by other users and add them to the list of messages.

Update the JavaScript tab to:

  • Create a https://socket.io/ connection (socket).
  • Listen for when messages are created, updated, and destroyed, and call the corresponding real-time methods.
var Message = can.DefineMap.extend({
    id: "number",
    name: "string",
    body: "string",
    created_at: "date"
});

Message.List = can.DefineList.extend({
    "#": Message
});

Message.connection = can.connect.superMap({
    url: {
        resource: 'https://chat.donejs.com/api/messages',
        contentType: 'application/x-www-form-urlencoded'
    },
    Map: Message,
    List: Message.List,
    name: 'message'
});

var socket = io('https://chat.donejs.com');

socket.on('messages created', function(message){
    Message.connection.createInstance(message);
});
socket.on('messages updated', function(message){
    Message.connection.updateInstance(message);
});
socket.on('messages removed', function(message){
    Message.connection.destroyInstance(message);
});

can.Component.extend({
    tag: "chat-messages",
    view: `
        <h1 class="page-header text-center">
           Chat Messages
        </h1>
        <h5><a href="{{routeUrl(page='home')}}">Home</a></h5>

        {{#if(messagesPromise.isPending)}}
          <div class="list-group-item list-group-item-info">
            <h4 class="list-group-item-heading">Loading…</h4>
          </div>
        {{/if}}
        {{#if(messagesPromise.isRejected)}}
          <div class="list-group-item list-group-item-danger">
            <h4 class="list-group3--item-heading">Error</h4>
            <p class="list-group-item-text">{{messagesPromise.reason}}</p>
          </div>
        {{/if}}
        {{#if(messagesPromise.isResolved)}}
          {{#each(messagesPromise.value)}}
            <div class="list-group-item">
              <h4 class="list-group3--item-heading">{{name}}</h4>
              <p class="list-group-item-text">{{body}}</p>
            </div>
          {{else}}
            <div class="list-group-item">
              <h4 class="list-group-item-heading">No messages</h4>
            </div>
          {{/each}}
        {{/if}}

        <form class="row" on:submit="send(scope.event)">
            <div class="col-sm-3">
              <input type="text" class="form-control" placeholder="Your name"
                     value:bind="name"/>
            </div>
            <div class="col-sm-6">
              <input type="text" class="form-control" placeholder="Your message"
                     value:bind="body"/>
            </div>
            <div class="col-sm-3">
              <input type="submit" class="btn btn-primary btn-block" value="Send"/>
            </div>
        </form>`,
    ViewModel: {
        messagesPromise: {
            default: function(){
                return Message.getList({});
            }
        },
        name: "string",
        body: "string",
        send: function(event) {
            event.preventDefault();

            new Message({
                name: this.name,
                body: this.body
            }).save().then(function(){
                this.body = "";
            }.bind(this));
        }
    }
});

can.Component.extend({
    tag: "chat-app",
    view: `
        <div class="container">
          <div class="row">
            <div class="col-sm-8 col-sm-offset-2">
              {{#eq(page, 'home')}}
                  <h1 class="page-header text-center" on:click="addExcitement()">
                    {{message}}
                  </h1>
                  <a href="{{routeUrl(page='chat')}}"
                     class="btn btn-primary btn-block btn-lg">
                       Start chat
                  </a>
              {{else}}
                 <chat-messages/>
              {{/eq}}
            </div>
          </div>
        </div>`,
    ViewModel: {
        init(){
            can.route.register("{page}",{page: "home"});
            can.route.data = this;
            can.route.start();
        },
        page: "string",
        message: {
            type: "string",
            default: "Chat Home",
            serialize: false
        },
        addExcitement: function(){
            this.message = this.message + "!";
        }
    }
});

When complete, you can open up the same JS Bin in another window, create a message, and it will appear in the first JS Bin’s messages list.

This step connects to a WebSocket API that pushes messages when Messages are created, updated or destroyed. By calling the real-time methods when these events happen, CanJS will automatically update the messages list.

Key take-away: CanJS will add, remove, and update lists for you automatically. It’s awesome!

Result

When finished, you should see something like the following JS Bin:

JS Bin on jsbin.com

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

On this page

Get help

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