CanJS - Chat

canjs

<script type="text/mustache" id="app-template">
    <chat></chat>
</script>

<div id="my-app"></div>
<script src='http://agile-everglades-1937.herokuapp.com/socket.io/socket.io.js'></script>
var myServerUrl = "http://agile-everglades-1937.herokuapp.com";

// define our message model
var Message = can.Model({
    findAll : 'GET ' + myServerUrl + '/messages',
    create : function(attrs) {
        $.post(myServerUrl + '/messages', attrs)
        //keep '{Message} created' from firing twice
        return $.Deferred()
    }
},{});

can.Component.extend({
    tag: 'chat',
    template: '<ul id="messages">' +
                '{{#each messages}}' +
                '<li>{{body}}</li>' +
                '{{/each}}' +
              '</ul>' +
              '<form id="create-message" action="" can-submit="submitMessage">' +
                  '<input type="text" id="body" placeholder="type message here..." can-value="newMessage" />' +
              '</form>',
    scope: {    
        Message: Message,
        messages: new Message.List({}),
        submitMessage: function(el, ev, ev2){
            ev2.preventDefault();
            new Message({body: this.attr("newMessage")}).save();
            this.attr("newMessage", "");
        }
    },
    events: {
        '{Message} created': function(construct, ev, message){
            this.scope.attr('messages').push(message);
        }
    }
});

var frag = can.view('app-template', {});
$('#my-app').html(frag);

// connect using Socket.io
var socket = io.connect(myServerUrl)
// listen for 'message-created' event to create a Message instance
// Note: the 'created' event IS triggered on this model instance. 
socket.on('message-created', function(message){
    new Message(message).created();
})

In the chat component's scope, we will use the Message model to save new messages and observe changes to the Model. The new Message.List({}) is a shortcut to perform the findAll operation on a can.Model and return a can.List.

There’s one more helper used in the template: can-value. This automatically two-way binds the value of an input field to an observable property on the scope of the component (in this case, newMessage).

When submitMessage is called, a new Message is created with new Message(). Since can-value was declared on the input element, newMessage will always be the current text in the input field. The body of the message is fetched from the Component's newMessage attribute when a user submits the form.

To save the new message to the server, call save():

submitMessage: function(scope, el, ev){
    ev.preventDefault();
    new Message({body: this.attr("newMessage")}).save();
    this.attr("newMessage", "");
}

Finally, when a new Message is created, the messages list must be updated.

events: {
    '{Message} created': function(construct, ev, message){
        this.scope.attr('messages').push(message);
    }
}

There are two ways that messages are added: from the current user, or from another user. We use socket.io to update the Message model with messages from other users in real time. Binding to the created event for all messages allows us to create a single entry point that pushes new messages to the scope, regardless of where those messages are from.

When the chat Component is loaded, messages are loaded from the server using can.Model and new Message.List({}). When a new message is submitted:

  • submitMessage is called via the event handler bound by the can-submit attribute
  • a new Message is created and saved to the server
  • '{Message} created' detects this change and adds the new message to messages
  • The template is automatically updated since messages is an observable can.List

When a message is created on another chat client, socket.io will notify this client by triggering the message-created event, wich will render the new message in the page by adding it to the Message model:

var socket = io.connect(myServerUrl);
socket.on('message-created', function(message){
    new Message(message).created();
});

To keep the created event from firing twice, we modify the create function in the model. If there was simply a return statement, Model would create and fire a create event, which socket is already doing. By returning a Deferred, we prevent firing of one of these events.

var Message = can.Model({
    findAll : 'GET ' + myServerUrl + '/messages',
    create : function(attrs) {
        $.post(myServerUrl + '/messages', attrs);
        //keep '{Message} created' from firing twice
        return $.Deferred();
    }
},{});
// HTML:
<ul id='messages'></ul>
<form id='create-message' action=''>
    <input type="text" id="body" placeholder="type message here ..."/>
</form>    

<script id="messageEJS" type="text/ejs">
<% this.forEach(function(message){ %>
    <li><%= message.body %></li>
<% }) %>
</script>

<script src='http://agile-everglades-1937.herokuapp.com/socket.io/socket.io.js'></script>

// JavaScript:
// define our message model
var Message = can.Model({
    findAll : 'GET ' + myServerUrl + '/messages',
    // create a message, but prevent the 'created' event from firing
    create : function(attrs) {
        $.post(myServerUrl + '/messages', attrs)
        return $.Deferred()
    }
},{});

// connect using Socket.io
var socket = io.connect(myServerUrl)

// listen for 'message-created' event to create a Message instance
// Note: the 'created' event IS triggered on this model instance. 
socket.on('message-created', function(message){
    new Message(message).created()
})

// initially find all messages from the server and append them to the DOM
Message.findAll({},function(messages){
  $("#messages").html( can.view('messageEJS', messages) )
  scrollToBottom()

})

// listen for the 'created' event on the Model 
// append the message to the DOM using a template
Message.bind("created", function(ev, message){
    var messagesCont = $("#messages"),
        shouldScroll = ( messagesCont.scrollTop() + messagesCont[0].clientHeight 
        === messagesCont[0].scrollHeight )

    messagesCont.append( can.view('messageEJS', [message]) )

    if (shouldScroll) {
        scrollToBottom()
    }
})

// listen for the form to submit 
// create a message and post the results to the server
$("#create-message").bind("submit",function(ev){
    ev.preventDefault()
    new Message({body: $("#body").val()}).save()
    $("#body").val('');
})

var scrollToBottom = function() {
     $("#messages").scrollTop($("#messages")[0].scrollHeight);   
}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License