Search code examples
javascriptrestauthenticationbackbone.jsmongoose

Permanently reference a piece of data from another in Backbone js


I'm using Backbone to create a site where users would interact with content (bookmark, star, etc) and I want a way to store the content a user has interacted with in the user data. Simple enough, but I'm looking for info on the best approach here.

I have built a prototype without user accounts (data is loaded into localStorage), so the content itself stores the user interaction right now (in the metadata of that content), but to open this up to more than one user at a time, I want to change it so that once a user saves a piece of content, the ID (or something permanent) of that content gets saved into the user account data instead.

I'm currently using the localStorage adapter and I load the content in on page load, but with Backbone, the ID of the models changes each time. Once I move to a REST db with user accounts, I'm not sure how to save the piece of content the user has interacted with in their account details object and be able to reliably use that in the UI.

Any help pointing me in the right direction would be great. How are other people solving this? Custom uid? An id from the db?

Thanks,

Paul


Solution

  • Update: in a private conversation with the OP, he explained that he required multiple instances of the site in various devices to display consistent user data for logged in user(i.e. user preferences should be reflected across all devices). To maintain a fresh link between the client and the server, the OP will be using socket.io.

    Overview of a solution

    There are essentially three types of data that will be passed around in this scenario. Site content data which will be displayed as individual views. User profile data that servers to identify the user; basically a unique user id (UID). And finally, data reflecting conent that was bookmarked (starred) by the user.

    The challenge is to maintain the user selection data fresh in all concurrent sessions for a given user. The idea I'll describe below will explain how to establish a communication link between all three data sources so that the dynamic data, the user selections, can be persisted and refresh immediately. To do this we will use Backbone's event aggregator, and the encapsulation provided by Backbone views and models.

    Databse structure

    Reflecting the three groups of data, the database should be designed with three tables: A User Profile table, a Content table, and a User Content lookup table. A schema should look like this,

                         User Profile                         Content
           ---------------------------------------   --------------------------
           | UID | Username | First | Last | ... |   | ID | Title| Date | ... |
           ---------------------------------------   --------------------------
    
                                    User Content Lookup 
                                       ------------
                                       | UID | ID | 
                                       ------------
    

    Front-end design

    We'll have to set up three types of Backbone objects to handle the data round-robin. A user view to instantiate our user prefrences object. Views for each of the content items to handle event delegation and item rendering. And a Backbone collection object that will act as a controller, with logic to persist and fetch data. To make everyone talk, we'll install an event aggregator with local scope that all these view can subscribe and publish to.

    The user and content views

    The user view is a vanilla Backbone view. Graphically it offers nothing more than log-in/log-out, account profile, etc. links. It's also a repository for user profile data, which it stores it its model. What we care about is that it 1. has the user's unique ID, and 2. will instantiate the controller object with that UID.

    // We set up the user model to initialize the 
    // controller when the data has been fetched
    var UserModel = Backbone.Model.extend({
      initialize: function(options) {
        this.controller= options.controller; // A reference to the controller is passed
                                             // in before we instantiate the model
        _.bindAll(this, "initializeController"); // Ensures proper context
        this.listenTo(this, "add", this.initializeController);
      },
    
      initializeController: function () {
        this.controller.fetch({ id: this.get('UID') });
      },
    }); 
    
    // We fetch the user data, which, when it comes in, initialized the
    // user's ID
    var usrModel = new UserModel({ controller: appController });
    usrModel.fetch();
    

    Each content row has its own view. This allows for easy event management when a user interacts with that content item. Each view will send its own ID to the controller along with an event describing the interaction. Conversely, the controller can send the view events alerting it if the user in a separate concurrent session selected or unselected the view.

    // We wire the star event
    initialize: function() {
      _.bindAll(this, "checkItem", "uncheckItem", "onClickStar");
      // Listen to items that have changed in a concurrent session
      this.listenTo(App.vent, "new:item:selected", this.checkItem);
      this.listenTo(App.vent, "new:item:unselected", this.uncheckItem);
    },
    
    events: {
      "click .star": "onClickStar"
    },
    
    onClickStar: function () {
      // we check if the star has been checked or unchecked
      // Here you can roll your own, depending on your implementation (e.g. checkbox)
      if (checked)
       App.vent.trigger("item:selected", this.model.get('ID'));
      else
       App.vent.trigger("item:unselected", this.model.get('ID'));
    },
    
    checkItem: function (id) {
      if (this.model.get('ID') == id)
        // find the check item and check it (without using click)
    },
    
    uncheckItem: function (id) {
      if (this.model.get('ID') == id)
        // find the check item and uncheck it (without using click)
    }
    

    The key is to send the ID on the trigger, so the controller, listening for the trigger knows what to add/remove.

    The Controller

    The controller itself is the brains of the operation. Here is were we subscribe to content item changes, and propagate those changes on the back end, where we listen to changes on the backend and publish them to the views. Here, as well, is where we set up the socket.io bindings that interact with the content events.

    // Let's define the controller collection object
    // The controller has to defined AND instantitated BEFORE 
    // the user model object, so that it can pass a reference to
    // itself to the user model
    var Controller = Backbone.Collection.extend({
      initialize: function () {
        _.bindAll(this, "socketIni");
        this.socketIni();
      },
    
      // We override fetch to pass in the id we need for the fetch route
      fetch: function(options) {
        this.url = "/user/" + options.id + "/content";
        return Backbone.Collection.prototype.fetch.call(this, options);
      },
    
      socketIni: function() {
        var socket = io();
        this.listenTo(App.vent, "item:selected", function (id) {
          // When a check is added, socket emits the id and event
          socket.emit('item added', id);
        });
        this.listenTo(App.vent, "item:unselected", function (id) {
          // When a check is removed, socket emits the id and event
          socket.emit('item removed', id);
        });
    
        // Set up socket to let the content views when something changes in the db
        socket.on('new item selected', function(id) {
          App.vent.trigger('new:item:selected', id);
        });
        socket.on('new item unselected', function(id) {
          App.vent.trigger('new:item:unselected', id);
        });
      },
    });
    
    // We instantiate the controller that we passed in to usrModel
    // REMEMBER: This must be done BEFORE you construct userModel.
    vet appController = new Controller();
    

    The code above may not be perfect, but it demonstrates the basic idea of how to keep a host of views fresh in multiple concurrent sessions.