Real time updates on the Notes home page

The first thing we'll do with Socket.IO is change the Notes home page to automatically update the list of notes as notes are edited or deleted. It means a little bit of work in routes/index.js and views/index.ejs, and a lot of work in the Notes model.

Where the Notes model so far has been a passive repository of documents, it now needs to emit events to any interested parties. This is the listener pattern, and, in theory, there will be code that is interested in knowing when notes are created, edited, or destroyed. At the moment, we'll use that knowledge to update the Notes home page, but there are many potential other uses of that knowledge.

The Notes model as an EventEmitter class

The EventEmitter is the class that implements listener support. Let's create a new module, models/notes-events.js, containing the following:

'use strict';

const EventEmitter = require('events');
class NotesEmitter extends EventEmitter {}
module.exports = new NotesEmitter();

module.exports.noteCreated = function(note) {
    module.exports.emit('notecreated', note);
};
module.exports.noteUpdate = function(note) {
    module.exports.emit('noteupdate', note);
};
module.exports.noteDestroy = function(data) {
    module.exports.emit('notedestroy', data);
};

This module maintains for us the listeners to notes-related events. Because we have multiple Notes models, it is useful to handle listener maintenance in a common module.

This module exports an EventEmitter object, meaning that the module exports two functions: .on and .emit. We then add additional methods to emit specific events related to the notes life cycle.

Let's now update models/notes-sequelize.js to use notes-events to emit events. Similar changes can be made in the other Notes models. Add the following code to it:

exports.events = require('./notes-events');

This technique adds the content of one module to the exports of another module. Not every export from a module needs to be implemented by that module. A Node.js module is simply an object, module.exports, that is returned by the require statement. The contents of that module object can come from a variety of sources. In this case, the events export will come from the notes-events module.

In the create function, add the following:

exports.create = function(key, title, body) {
    return exports.connectDB()
    .then(SQNote => { 
        return SQNote.create({
            notekey: key,
            title: title,
            body: body
        });
        })
    .then(newnote => {
        exports.events.noteCreated({
            key: newnote.key,
            title: newnote.title,
            body: newnote.body
        });
        return newnote;
    });
};

This is the pre-existing create function but with a second step to emit the notecreated event. We'll make similar changes throughout this module, adding an additional step to emit events.

Remember that the exports.events object came from require('./notes-events'). That object exported a set of functions, one of which is noteCreated. This is how we'll use that module.

In this case, we add a clause to the promise to emit an event for every note that is created. The Promise still returns the SQNote object that was created. We simply add this side effect of emitting an event.

Then, make a similar change for updating a note:

exports.update = function(key, title, body) {
    return exports.connectDB()
    .then(SQNote => { 
        return SQNote.find({ where: { notekey: key } })
    })
    .then(note => { 
        if (!note) {
            throw new Error("No note found for key " + key);
        } else {
            return note.updateAttributes({
                title: title,
                body: body
            });
        }
     })
    .then(newnote => {
        exports.events.noteUpdate({
            key,
            title: newnote.title,
            body: newnote.body
        });
        return newnote;
    });
};

Here, we send a notification that a note was edited.

Finally, the destroy method:

exports.destroy = function(key) {
    return exports.connectDB()
    .then(SQNote => {
        return SQNote.find({ where: { notekey: key } })
    })
    .then(note => note.destroy())
    .then(() => exports.events.noteDestroy({ key }));
};

This sends a notification that a note was destroyed.

We only changed the notes-sequelize.js module. The other notes models will require similar changes should you want to use one of them to store notes.

Real-time changes in the Notes home page

The changes in the Notes model are only the first step. Making the events visible to our users means the controller and view portions of the application must consume those events.

Let's start making changes to routes/index.js:

router.get('/', function(req, res, next) {
    getKeyTitlesList().then(notelist => {
        var user = req.user ? req.user : undefined;
        res.render('index', {
            title: 'Notes',
            notelist: notelist,
            user: user,
            breadcrumbs: [{ href: '/', text: 'Home' }]
        });
    })
    .catch(err => { error('home page '+ err); next(err); });
});

We need to reuse part of the code that had been in this router function. Let's now add this function:

module.exports = router;

var getKeyTitlesList = function() {
    return notes.keylist()
    .then(keylist => {
        var keyPromises = keylist.map(key => {
            return notes.read(key).then(note => {
                return { key: note.key, title: note.title };
            });
        });
        return Promise.all(keyPromises);
    });
};

This is the same as earlier, just in its own function. It generates an array of items containing the key and title for all existing notes:

module.exports.socketio = function(io) {
    var emitNoteTitles = () => {
        getKeyTitlesList().then(notelist => {
            io.of('/home').emit('notetitles', { notelist });
        });
    };
    notes.events.on('notecreated', emitNoteTitles);
    notes.events.on('noteupdate',  emitNoteTitles);
    notes.events.on('notedestroy', emitNoteTitles);
};

This is where app.js injects the io object into the home page routing module.

Now, we get to some actual Socket.IO code. Remember that this function is called from app.js, and it is given the io object we created there.

Note

The io.of method defines what Socket.IO calls a namespace. Namespaces limit the scope of messages sent through Socket.IO. The default namespace is /, and namespaces look like pathnames; in that, they're a series of slash-separated names. An event emitted into a namespace is delivered to any socket listening to that namespace.

The code in this case is fairly straightforward. It listens to the events we just implemented, notecreated, noteupdate, and notedestroy. For each of these events, it emits an event, notetitles, containing the list of note keys and titles.

That's it!

As notes are created, updated, and destroyed, we ensure that the home page will be refreshed to match. The home page template, views/index.ejs, will require code to receive that event and rewrite the page to match.

Changing the home page template

Socket.IO runs on both client and server, with the two communicating back and forth over the HTTP connection. This requires loading the client JavaScript library into the client browser. Each page of the Notes application in which we seek to implement Socket.IO services must load the client library and have custom client code for our application.

In views/index.ejs, add the following code:

<% include footer %>

<script src="/socket.io/socket.io.js"></script>
<script>
$(document).ready(function () {
  var socket = io('/home');
  socket.on('notetitles', function(data) {
    var notelist = data.notelist;
    $('#notetitles').empty();
    for (var i = 0; i < notelist.length; i++) {
      notedata = notelist[i];
      $('#notetitles')
      .append('<a class="btn btn-lg btn-block btn-default" href="/notes/view?key='+
                notedata.key +'">'+ notedata.title +'</a>');
    }
  });
});
</script>

The first line is where we load the Socket.IO client library. You'll notice that we never set up any Express route to handle the /socket.io URL. Instead, the Socket.IO library did that for us.

Because we've already loaded jQuery (to support Bootstrap), we can easily ensure that this code is executed once the page is fully loaded using $(document).ready.

This code first connects a socket object to the /home namespace. That namespace is being used for events related to the Notes homepage. We then listen for the notetitles events, for which some jQuery DOM manipulation erases the current list of notes and renders a new list on the screen.

That's it. Our code in routes/index.js listened to various events from the Notes model, and, in response, sent a notetitles event to the browser. The browser code takes that list of note information and redraws the screen.

Running Notes with real-time home page updates

We now have enough implemented to run the application and see some real-time action.

As you did earlier, start the user information microservice in one window:

$ cd chap09/users
$ npm start
> [email protected] start /Users/david/chap08/users
> DEBUG=users:* PORT=3333 SEQUELIZE_CONNECT=sequelize-sqlite.yaml node user-server

  users:server User-Auth-Service listening at http://127.0.0.1:3333 +0ms

Then, in another window, start the Notes application:

$ cd chap09/notes
$ npm start
> [email protected] start /Users/david/chap09/notes
> SEQUELIZE_CONNECT=models/sequelize-sqlite.yaml NOTES_MODEL=models/notes-sequelize USERS_MODEL=models/users-rest USER_SERVICE_URL=http://localhost:3333 node ./app

  notes:server Listening on port 3000 +0ms

Then, in a browser window, go to http://localhost:3000 and log in to the Notes application. To see the real-time effects, open multiple browser windows. If you can use Notes from multiple computers, then do that as well.

In one browser window, start creating and deleting notes, while leaving the other browser windows viewing the home page. Create a note, and it should show up immediately on the home page in the other browser windows. Delete a note and it should disappear immediately as well.

Real-time action while viewing notes

It's cool how we can now see real-time changes in a part of the Notes application. Let's turn to the /notes/view page to see what we can do. What comes to mind is this functionality:

  • Update the note if someone else edits it
  • Redirect the viewer to the homepage if someone else deletes the note
  • Allow users to leave comments on the note

For the first two features, we can rely on the existing events coming from the Notes model. The third feature will require a messaging subsystem, so we'll get to that later in this chapter.

In routes/notes.js, add this to the end of the module:

module.exports.socketio = function(io) {
    var nspView = io.of('/view');
    var forNoteViewClients = function(cb) {
        nspView.clients((err, clients) => {
            clients.forEach(id => {
                cb(nspView.connected[id]);
            });
        });
    };

    notes.events.on('noteupdate',  newnote => {
        forNoteViewClients(socket => {
           socket.emit('noteupdate', newnote);
        });
    });
    notes.events.on('notedestroy', data => {
        forNoteViewClients(socket => {
           socket.emit('notedestroy', data); 
        });
    });
};

Now is the time to uncomment that line of code in app.js because we've now implemented the function we said we'd get to later.

routes.socketio(io);
notes.socketio(io);

First, we will connect to the /view namespace. This means any browser viewing any note in the application will connect to this namespace. Every such browser will receive events about any note being changed, even those notes that are not being viewed. This means that the client code will have to check the key, and only take action if the event refers to the note being displayed.

What we're doing is listening to the noteupdate and notedestroy messages from the Notes model and sending them to the /view Socket.IO namespace. However, the code shown here doesn't use the method documented on the Socket.IO website.

We should be able to write the code as follows:

notes.events.on('noteupdate',  newnote => {
    io.of('/view').emit('noteupdate', newnote);
});

However, with this, there were difficulties initializing communications that meant reloading the page twice.

With this code, we dig into the data that each namespace uses to track the connected clients. We simply loop through those clients and invoke a callback function on each. In this case, the callback emits the message we need to be sent to the browser.

Changing the note view template for real-time action

As we did earlier, to make these events visible to the user, we must add client code to the template; in this case, views/noteview.ejs:

<div class="row"><div class="col-xs-12">
<h3 id="notetitle"><%= note ? note.title : "" %></h3>
<p id="notebody"><%= note ? note.body : "" %></p>
<p>Key: <span id="notekey"><%= notekey %></span></p>
</div></div>

<% if (notekey && user) { %>
    <div class="row">
    <div class="btn-group col-sm-12">
        <a class="btn btn-default" href="/notes/destroy?key=<%= notekey %>" role="button">Delete</a>
        <a class="btn btn-default" href="/notes/edit?key=<%= notekey %>" role="button">Edit</a>
    </div>
    </div>
<% } %>

<% include footer %>

<% if (notekey) { %>
<script src="/socket.io/socket.io.js"></script>
<script>
$(document).ready(function () {
    io('/view').on('noteupdate', function(note) {
        if (note.key === "<%= notekey %>") {
            $('h3#notetitle').empty();
            $('h3#notetitle').text(note.title);
            $('p#notebody').empty();
            $('p#notebody').text(note.body);
        }
    });
    io('/view').on('notedestroy', function(data) {
        if (data.key === "<%= notekey %>") {
            window.location.href = "/";
        }
    });
});
</script>
<% } %>

We connect to the namespace where the messages are sent. As noteupdate or notedestroy messages arrive, we check the key to see whether it matches the key for the note being displayed. There is a technique used here that's important to understand; it mixes JavaScript executed on the server with JavaScript executed in the browser.

For example, remember that the code within the <% .. %> or <%= .. %> delimiters is interpreted by the EJS template engine on the server. Consider the following:

if (note.key === "<%= notekey %>") {
  ..
}

This comparison is between the notekey value in the browser, which arrived inside the message from the server, against the notekey variable on the server. That variable contains the key of the note being displayed. Therefore, in this case, we are able to ensure these code snippets are executed only for the note being shown on the screen.

For the noteupdate event, we take the new note content and display it on the screen. For this to work, we had to add id= attributes to the HTML so we could use jQuery selectors in manipulating the DOM.

For the notedestroy event, we simply redirect the browser window back to the home page. The note being viewed has been deleted, and there's no point for the user to continue looking at a note which no longer exists.

Running Notes with real-time updates while viewing a note

At this point, you can now rerun the Notes application and try this out.

Launch the user authentication server and the Notes application as before. Then, in the browser, open multiple windows on the Notes application. This time, have one viewing the home page, and two viewing a note. In one of those windows, edit the note to make a change, and see the text change on both the homepage and note viewing page.

Then delete the note, and watch it disappear from the homepage, and the browser window which had viewed the note is now on the homepage.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset