Chat Guide
This guide will walk you through building a real-time chat application with CanJS’s Core libraries. It takes about 30 minutes to complete.
The Goal
We are building the following functionality:
See the Pen CanJS 6.0 Chat - Final by Bitovi (@bitovi) on CodePen.
Setup
The easiest way to get started is to Fork the following CodePen by clicking the Edit On CodePen button on the top right:
See the Pen CanJS 6.0 Chat - Start by Bitovi (@bitovi) on CodePen.
The CodePen is configured to load 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 CodePen also importing core.mjs, which exports all of CanJS's Core APIs as named exports. The following is how you can import different APIs into your app:
import { ObservableObject, StacheElement } from "//unpkg.com/can@6/core.mjs";
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 CodePen, update the HTML panel to:
- Use the
<chat-app>element we will define in theJSpanel.
<chat-app></chat-app>
Update the JavaScript panel to:
- Define an application class (
chat-app) by extending can-stache-element class. Its definition includes:- A can-stache
viewthat contains the contents of thechat-appelement. This view:- Inserts a
messagevalue within a responsive Bootstrap container using {{expression}}. - Listen for
clickevents and calladdExcitementwith on:event.
- Inserts a
- A can-observable-object
propsdefinition. This definition includes:- A
messageproperty that is a string value initialized to"Chat Home".
- A
- An
addExcitementmethod that adds"!"to the end of themessageproperty.- Register the customElement.
- A can-stache
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class ChatApp extends StacheElement {
static view = `
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<h1 class="page-header text-center" on:click="this.addExcitement()">
{{ this.message }}
</h1>
</div>
</div>
</div>
`;
static props = {
// Properties
message: {
type: String,
default: "Chat Home"
}
};
addExcitement() {
this.message = this.message + "!";
}
};
customElements.define("chat-app", ChatApp);
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-stache-element custom element with a can-stache view and can-observable-object props.
The properties and methods the view uses are defined in the props and on the class. 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 for(of)
- branching with if or {{#is(expressions)}}
Key take-away: You define method and property behaviors. The methods can be called by can-stache
views. TheStacheElementproperties can be observed by can-stacheviews.
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 panel to:
- Update the
chat-appviewto:- Check if the
StacheElement’srouteData.pageproperty 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
pagewill be set on route.data to either"home"or"chat".
- Check if the
- Update the
chat-apppropsto:- Setup a connection between the route.data state and the
propsrouteDataproperty by:- Defining a
routeDataproperty with a default value that returns the route.data. - Create a pretty routing rule so if the URL looks like
"#!chat", thepageproperty of route.data will be set tochatwith register. If there is nothing in the hash,pagewill be set to"home". - Initialize the url’s values on route.data and set up the two-way connection with start.
- Defining a
- Setup a connection between the route.data state and the
import { route, StacheElement } from "//unpkg.com/can@6/core.mjs";
class ChatApp extends StacheElement {
static view = `
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{# eq(this.routeData.page, 'home') }}
<h1 class="page-header text-center" on:click="this.addExcitement()">
{{ this.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>
`;
static props = {
// Properties
message: {
type: String,
default: "Chat Home"
},
routeData: {
get default() {
route.register("{page}",{page: "home"});
route.start();
return route.data;
}
}
};
addExcitement() {
this.message = this.message + "!";
}
};
customElements.define("chat-app", ChatApp);
When complete, you should be able to toggle between the two pages. If you type:
window.location.hash
in CodePen’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 values accessible to the application props. When
those properties change, different content is shown.
We connected the application props to the routing system by having the
props's routeData property return can-route.data.
We initialized that connection between can-route.data and the url with can-route.start.
This makes it so if the routeData.page property changes, the browser’s URL will change. If the browser’s URL changes, the routeData.page property changes.
Key take-away: can-route two-way binds changes in the browser’s URL to the application
propsand vice versa. Use changes in the applicationpropsto 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 panel to:
- Define a
<chat-messages>custom element with can-stache-element. It'sviewwill contain the content of the chat messages page. - Update
<chat-app>’sviewto create a<chat-messages>element.
import { route, StacheElement, type } from "//unpkg.com/can@6/core.mjs";
class ChatMessages extends StacheElement {
static view = `
<h1 class="page-header text-center">
Chat Messages
</h1>
<h5><a href="{{ routeUrl(page='home') }}">Home</a></h5>
`;
};
customElements.define("chat-messages", ChatMessages);
class ChatApp extends StacheElement {
static view = `
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{# eq(this.routeData.page, 'home') }}
<h1 class="page-header text-center" on:click="this.addExcitement()">
{{ this.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>
`;
static props = {
// Properties
message: {
type: String,
default: "Chat Home"
},
routeData: {
get default() {
route.register("{page}",{page: "home"});
route.start();
return route.data;
}
}
};
addExcitement() {
this.message = this.message + "!";
}
};
customElements.define("chat-app", ChatApp);
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-stache-element. By default, their view only
has access to the data in the props. You can use event and data bindings
like key:from and key:bind to pass data
between custom elements.
Key take-away: can-stache-element 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 panel to:
- Define a
Messagetype with can-observable-object. - Define a
MessageListtype that containsMessageitems. - Connect the
MessageandMessage.Listtype to the RESTful messages service athttps://chat.donejs.com/api/messagesusing can-realtime-rest-model. - Update the
<chat-messages>’sviewto:- 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>’spropsto:
import {
ObservableArray,
ObservableObject,
realtimeRestModel,
route,
StacheElement,
type,
} from "//unpkg.com/can@6/core.mjs";
class Message extends ObservableObject {
static props = {
id: type.maybeConvert(Number),
name: type.maybeConvert(String),
body: type.maybeConvert(String),
created_at: type.maybeConvert(Date)
};
}
class MessageList extends ObservableArray {
static props = {};
static items = Message;
}
const MessageConnection = realtimeRestModel({
url: {
resource: 'https://chat.donejs.com/api/messages',
contentType: 'application/x-www-form-urlencoded'
},
ObjectType: Message,
ArrayType: MessageList
});
class ChatMessages extends StacheElement {
static view = `
<h1 class="page-header text-center">
Chat Messages
</h1>
<h5><a href="{{ routeUrl(page='home') }}">Home</a></h5>
{{# if(this.messagesPromise.isPending) }}
<div class="list-group-item list-group-item-info">
<h4 class="list-group-item-heading">Loading…</h4>
</div>
{{/ if }}
{{# if(this.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">{{ this.messagesPromise.reason }}</p>
</div>
{{/ if }}
{{# if(this.messagesPromise.isResolved) }}
{{# for(message of this.messagesPromise.value) }}
<div class="list-group-item">
<h4 class="list-group3--item-heading">{{ message.name }}</h4>
<p class="list-group-item-text">{{ message.body }}</p>
</div>
{{ else }}
<div class="list-group-item">
<h4 class="list-group-item-heading">No messages</h4>
</div>
{{/ for }}
{{/ if }}
`;
static props = {
// Properties
messagesPromise: {
get default() {
return Message.getList({});
}
}
};
}
customElements.define("chat-messages", ChatMessages);
class ChatApp extends StacheElement {
static view = `
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{# eq(this.routeData.page, 'home') }}
<h1 class="page-header text-center" on:click="this.addExcitement()">
{{ this.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>
`;
static props = {
// Properties
message: {
type: String,
default: "Chat Home"
},
routeData: {
get default() {
route.register("{page}",{page: "home"});
route.start();
return route.data;
}
}
};
addExcitement() {
this.message = this.message + "!";
}
}
customElements.define("chat-app", ChatApp);
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 can-realtime-rest-model mixin 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
nameandbody. - When the form is submitted, call
sendon theChatMessageswith on:event. - Connect the first
<input>’svalueto theChatMessages’snameproperty with key:bind. - Connect the second
<input>’svalueto theChatMessages’sbodyproperty with key:bind.
Update the <chat-messages> props to:
- Define a
nameandbodyproperty onChatMessages. - Define a
sendmethod onChatMessagesthat creates a newMessageand sends it to the server.
import {
ObservableArray,
ObservableObject,
realtimeRestModel,
route,
StacheElement,
type,
} from "//unpkg.com/can@6/core.mjs";
class Message extends ObservableObject {
static props = {
id: Number,
name: String,
body: String,
created_at: type.maybeConvert(Date)
};
}
class MessageList extends ObservableArray {
static props = {};
static items = Message
}
const MessageConnection = realtimeRestModel({
url: {
resource: 'https://chat.donejs.com/api/messages',
contentType: 'application/x-www-form-urlencoded'
},
ObjectType: Message,
ArrayType: MessageList
});
class ChatMessages extends StacheElement {
static view = `
<h1 class="page-header text-center">
Chat Messages
</h1>
<h5><a href="{{ routeUrl(page='home') }}">Home</a></h5>
{{# if(this.messagesPromise.isPending) }}
<div class="list-group-item list-group-item-info">
<h4 class="list-group-item-heading">Loading…</h4>
</div>
{{/ if }}
{{# if(this.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">{{ this.messagesPromise.reason }}</p>
</div>
{{/ if }}
{{# if(this.messagesPromise.isResolved) }}
{{# for(message of this.messagesPromise.value) }}
<div class="list-group-item">
<h4 class="list-group3--item-heading">{{ message.name }}</h4>
<p class="list-group-item-text">{{ message.body }}</p>
</div>
{{ else }}
<div class="list-group-item">
<h4 class="list-group-item-heading">No messages</h4>
</div>
{{/ for }}
{{/ if }}
<form class="row" on:submit="this.send(scope.event)">
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Your name"
value:bind="this.name"/>
</div>
<div class="col-sm-6">
<input type="text" class="form-control" placeholder="Your message"
value:bind="this.body"/>
</div>
<div class="col-sm-3">
<input type="submit" class="btn btn-primary btn-block" value="Send"/>
</div>
</form>
`;
static props = {
// Properties
messagesPromise: {
get default() {
return Message.getList({});
}
},
name: String,
body: String
};
send(event) {
event.preventDefault();
new Message({
name: this.name,
body: this.body
}).save().then(() => {
this.body = "";
});
}
}
customElements.define("chat-messages", ChatMessages);
class ChatApp extends StacheElement {
static view = `
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{# eq(this.routeData.page, 'home') }}
<h1 class="page-header text-center" on:click="this.addExcitement()">
{{ this.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>
`;
static props = {
// Properties
message: {
type: String,
default: "Chat Home"
},
routeData: {
get default() {
route.register("{page}",{page: "home"});
route.start();
return route.data;
}
}
};
addExcitement() {
this.message = this.message + "!";
}
}
customElements.define("chat-app", ChatApp);
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-realtime-rest-model 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 panel to:
- Create a https://socket.io/ connection (
socket). - Listen for when messages are created, updated, and destroyed, and call the corresponding real-time methods.
import {
ObservableArray,
ObservableObject,
realtimeRestModel,
route,
StacheElement,
type,
} from "//unpkg.com/can@6/core.mjs";
class Message extends ObservableObject {
static props = {
id: Number,
name: String,
body: String,
created_at: Date
};
static propertyDefaults = DeepObservable;
}
class MessageList extends ObservableArray {
static props = {};
static items = Message;
}
const MessageConnection = realtimeRestModel({
url: {
resource: 'https://chat.donejs.com/api/messages',
contentType: 'application/x-www-form-urlencoded'
},
ObjectType: Message,
ArrayType: MessageList
});
const socket = io('https://chat.donejs.com');
socket.on('messages created', function(message){
MessageConnection.createInstance(message);
});
socket.on('messages updated', function(message){
MessageConnection.updateInstance(message);
});
socket.on('messages removed', function(message){
MessageConnection.destroyInstance(message);
});
class ChatMessages extends StacheElement {
static view = `
<h1 class="page-header text-center">
Chat Messages
</h1>
<h5><a href="{{ routeUrl(page='home') }}">Home</a></h5>
{{# if(this.messagesPromise.isPending) }}
<div class="list-group-item list-group-item-info">
<h4 class="list-group-item-heading">Loading…</h4>
</div>
{{/ if }}
{{# if(this.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">{{ this.messagesPromise.reason }}</p>
</div>
{{/ if }}
{{# if(this.messagesPromise.isResolved) }}
{{# for(message of this.messagesPromise.value) }}
<div class="list-group-item">
<h4 class="list-group3--item-heading">{{ message.name }}</h4>
<p class="list-group-item-text">{{ message.body }}</p>
</div>
{{ else }}
<div class="list-group-item">
<h4 class="list-group-item-heading">No messages</h4>
</div>
{{/ for }}
{{/ if }}
<form class="row" on:submit="this.send(scope.event)">
<div class="col-sm-3">
<input type="text" class="form-control" placeholder="Your name"
value:bind="this.name"/>
</div>
<div class="col-sm-6">
<input type="text" class="form-control" placeholder="Your message"
value:bind="this.body"/>
</div>
<div class="col-sm-3">
<input type="submit" class="btn btn-primary btn-block" value="Send"/>
</div>
</form>
`;
static props = {
// Properties
messagesPromise: {
get default() {
return Message.getList({});
}
},
name: type.maybeConvert(String),
body: type.maybeConvert(String)
};
send(event) {
event.preventDefault();
new Message({
name: this.name,
body: this.body
}).save().then(() => {
this.body = "";
});
}
}
customElements.define("chat-messages", ChatMessages);
class ChatApp extends StacheElement {
static view = `
<div class="container">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
{{# eq(this.routeData.page, 'home') }}
<h1 class="page-header text-center" on:click="this.addExcitement()">
{{ this.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>
`;
static props = {
// Properties
message: {
type: String,
default: "Chat Home"
},
routeData: {
get default() {
route.register("{page}",{page: "home"});
route.start();
return route.data;
}
}
};
addExcitement() {
this.message = this.message + "!";
}
}
customElements.define("chat-app", ChatApp);
When complete, you can open up the same CodePen in another window, create a message, and it will appear in the first CodePen’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 CodePen:
See the Pen CanJS 6.0 Chat - Final by Bitovi (@bitovi) on CodePen.