Chat Guide
This guide walks through building 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 JSBin by clicking the JS Bin button on the top left:
The JSBin 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 http://chat.donejs.com/api/messages.
The JSBin 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 and instead should import things directly with a module loader like StealJS, WebPack or Browserify. In a real app your code will look like:
var DefineMap = require("can-define/map/map");
var DefineList = require("can-define/list/list");
var Todo = DefineMap.extend({ ... });
Todo.List = DefineList.extend({ ... });
Not:
var Todo = can.DefineMap.extend({ ... });
Todo.List = can.DefineList.extend({ ... });
Read Setting up CanJS on how to setup CanJS in a real app. Checkout the DoneJS version of this guide.
Hello World
In this section, we will:
- Show a big "Chat Home" within a bootstrap container.
- Make it when "Chat Home" is clicked, a "!" is added to the end of the title.
Update the HTML tab to:
- To create a
<script>tag containing the content of thechat-templatetemplate. - Have the content insert a
messagevalue within a responsive Bootstrap container using {{expression}}. - Listen for
clickevents and calladdExcitementwith (event).
<!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>
<script type='text/stache' id='chat-template'>
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<h1 class="page-header text-center" ($click)="addExcitement()">
{{message}}
</h1>
</div>
</div>
</div>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.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 view-model (
AppVM) type by extending can-define/map/map. Its definition includes:- A
messageproperty that is a string value initialized to"Chat Home". - An
addExcitementmethod that adds"!"to the end of themessageproperty.
- A
- Create an instance of the
AppVMtype (appVM). - Compile a can-stache template renderer function from the contents of the
<script>tag. - Render that template with
appVMas a source of data into a document fragment. - Insert the document fragment into the
<body>tag.
var AppVM = can.DefineMap.extend({
message: {
type: "string",
value: "Chat Home"
},
addExcitement: function(){
this.message = this.message + "!";
}
});
var appVM = new AppVM();
var template = can.stache.from('chat-template');
var frag = template(appVM);
document.body.appendChild(frag);
When complete, you should see a large "Chat Home" 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-stache template rendered with an observable application view model instance.
The properties and methods the template uses are defined in the AppVM
type. The AppVM type extends can-define/map/map. We
defined a message and an addExcitement method.
We then created an instance of the appVM with the new operator. This created
an object with a message property and addExcitement method. For example, adding:
console.log(appVM.message)
appVM.addExcitement();
console.log(appVM.message)
Will print out "Chat Home" and then "Chat Home!".
DefineMap instances are observable. This is why when message changes,
the template updates automatically.
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 types like
AppVMwith method and property behaviors. Instances of those types are observable by can-stache templates.
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 HTML tab to:
- Check if the
appVM'spageproperty is 'home'. If it is, render the home page's content. If it's not, it will render the chat messages page's content (using {{else}}). - Use {{routeUrl hashes}} to create the right link urls so that
pagewill be set onappVMto either"home"or"chat".
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Chat Guide 3.0 - Routing">
<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>
<script type='text/stache' id='chat-template'>
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{#eq page 'home'}}
<h1 class="page-header text-center" ($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>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.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:
- Add a
pageproperty that will be updated when the browser's URL changes. - Prevent the
messageproperty from becoming part of the URL changes. - Connect changes in the url to changes in the
appVMwith data. - Create a pretty routing rule so if the url looks like
"#!chat", thepageproperty ofappVMwill be set tochatwith can-route. If there is nothing in the hash,pagewill be set to"home". - Initialize the url's values on
appVMand setup the two way connection with ready.
var AppVM = can.DefineMap.extend({
page: "string",
message: {
type: "string",
value: "Chat Home",
serialize: false
},
addExcitement: function(){
this.message = this.message + "!";
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{page}",{page: "home"});
can.route.ready();
var template = can.stache.from('chat-template');
var frag = template(appVM);
document.body.appendChild(frag);
When complete, you should be able to toggle between the two pages. If you type:
window.location.hash
in JSBin's console tab after clicking a new page, you will be able to see the hash change between !# and #!chat.
This step sets up a 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.ready.
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 connects changes in the browser's url to changes in 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-message>element that contains the behavior of the chat messages page.
Update the HTML tab to:
- Use the
<chat-messages/>element. - Create a template for the
<chat-messages/>element that contains the content of the chat messages page.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Chat Guide 3.0 - Chat Messages">
<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>
<script type='text/stache' id='chat-template'>
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{#eq page 'home'}}
<h1 class="page-header text-center" ($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>
</script>
<script type='text/stache' id='chat-messages-template'>
<h1 class="page-header text-center">
Chat Messages
</h1>
<h5><a href="{{routeUrl page='home'}}">Home</a></h5>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.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 a view model for the custom element (
ChatMessagesVM). - Using can-component define a custom element that will render its
viewtemplate with an instance of itsViewModel.
var ChatMessagesVM = can.DefineMap.extend({
});
can.Component.extend({
tag: "chat-messages",
ViewModel: ChatMessagesVM,
view: can.stache.from("chat-messages-template")
});
var AppVM = can.DefineMap.extend({
route: "string",
message: {
type: "string",
value: "Chat Home",
serialize: false
},
addExcitement: function(){
this.message = this.message + "!";
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{page}",{page: "home"});
can.route.ready();
var template = can.stache.from('chat-template');
var frag = template(appVM);
document.body.appendChild(frag);
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 {(selected-files)}="selectedFiles"/>
<app-directory {(selected-files)}="selectedFiles"/>
<app-files {(selected-files)}="selectedFiles"/>
<app-file-details {(selected-files)}="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 within
the element with an instance of their ViewModel. By default their view only
has access to that data in the ViewModel. You can use event and data bindings like {to-child} and {(two-way)} to pass data
between custom elements.
Key take away: can-component makes custom elements. Break down your application into many bit-sized custom elements.
List Messages
In this section, we will:
- Display messages from http://chat.donejs.com/api/messages
- Show a "Loading..." message while the messages are loading.
- Show an error if those messages fail to load.
Update the HTML tab 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".
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Chat Guide 3.0 - List Messages">
<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>
<script type='text/stache' id='chat-template'>
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{#eq page 'home'}}
<h1 class="page-header text-center" ($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>
</script>
<script type='text/stache' id='chat-messages-template'>
<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}}
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.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 a
Messagetype with can-define/map/map. - Define a
Message.Listtype that containsMessageitems. - Connect the
MessageandMessage.Listtype to the restful messages service athttp://chat.donejs.com/api/messagesusing can-connect/can/super-map/super-map. - Create a
messagesPromiseproperty onChatMessagesVMthat's value 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: 'http://chat.donejs.com/api/messages',
contentType: 'application/x-www-form-urlencoded'
},
Map: Message,
List: Message.List,
name: 'message'
});
var ChatMessagesVM = can.DefineMap.extend({
messagesPromise: {
value: function(){
return Message.getList({});
}
}
});
can.Component.extend({
tag: "chat-messages",
ViewModel: ChatMessagesVM,
view: can.stache.from("chat-messages-template")
});
var AppVM = can.DefineMap.extend({
page: "string",
message: {
type: "string",
value: "Chat Home",
serialize: false
},
addExcitement: function(){
this.message = this.message + "!";
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{page}",{page: "home"});
can.route.ready();
var template = can.stache.from('chat-template');
var frag = template(appVM);
document.body.appendChild(frag);
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 http://chat.donejs.com/api/messages. This
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 to connect to backend data.
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 HTML tab to:
- Create a form to enter a message's
nameandbody. - When the form is submitted, call
sendon theChatMessagesVMwith (event). - Connect the first
<input>'svalueto theChatMessagesVM'snameproperty with {(two-way)}. - Connect the second
<input>'svalueto theChatMessagesVM'sbodyproperty with {(two-way)}.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Chat Guide 3.0 - Create Messages">
<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>
<script type='text/stache' id='chat-template'>
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{#eq page 'home'}}
<h1 class="page-header text-center" ($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>
</script>
<script type='text/stache' id='chat-messages-template'>
<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" ($submit)="send(%event)">
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Your name"
{($value)}="name"/>
</div>
<div class="col-sm-6">
<input type="text" class="form-control" placeholder="Your message"
{($value)}="body"/>
</div>
<div class="col-sm-3">
<input type="submit" class="btn btn-primary btn-block" value="Send"/>
</div>
</form>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can/dist/global/can.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.8/socket.io.js"></script>
</body>
</html>
Update the JS tab to:
- Define a
nameandbodyproperty onChatMessagesVM. - Define a
sendmethod onChatMessagesVMthat creates a newMessageand saves 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: 'http://chat.donejs.com/api/messages',
contentType: 'application/x-www-form-urlencoded'
},
Map: Message,
List: Message.List,
name: 'message'
});
var ChatMessagesVM = can.DefineMap.extend({
messagesPromise: {
value: 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-messages",
ViewModel: ChatMessagesVM,
view: can.stache.from("chat-messages-template")
});
var AppVM = can.DefineMap.extend({
page: "string",
message: {
type: "string",
value: "Chat Home",
serialize: false
},
addExcitement: function(){
this.message = this.message + "!";
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{page}",{page: "home"});
can.route.ready();
var template = can.stache.from('chat-template');
var frag = template(appVM);
document.body.appendChild(frag);
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 can-connect/real-time/real-time behavior. The
can-connect/real-time/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 http://socket.io/ connection (
socket). - Listen to when messages are created, updated, and destroyed and and call the corresponding can-connect/real-time/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: 'http://chat.donejs.com/api/messages',
contentType: 'application/x-www-form-urlencoded'
},
Map: Message,
List: Message.List,
name: 'message'
});
var socket = io('http://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);
});
var ChatMessagesVM = can.DefineMap.extend({
messagesPromise: {
value: 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-messages",
ViewModel: ChatMessagesVM,
view: can.stache.from("chat-messages-template")
});
var AppVM = can.DefineMap.extend({
page: "string",
message: {
type: "string",
value: "Chat Home",
serialize: false
},
addExcitement: function(){
this.message = this.message + "!";
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{page}",{page: "home"});
can.route.ready();
var template = can.stache.from('chat-template');
var frag = template(appVM);
document.body.appendChild(frag);
When complete, you can open up the same JSBin in another window, create a message, and it will appear in the first JSBin's messages list.
This step connects to a WebSocket API
that pushes messages when Messages are created, updated or destroyed. By calling the
can-connect/real-time/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 JSBin: