Building Enterprise HTML5 Application

Step 4) Adding The Events

Probably one of the most important application code design is its event model and structure. There are 3 type of events in an HTML5 applications.

  1. HTML Element events: Those are the traditional, click, mouseover, touch, and any standard HTML Element events.
  2. Data Events: Those are all the events that a data layer can expose. With brite, those are exposed via brite.onDataChange and brite.onResult type of events.
  3. Application Events: Those are any custom events that Views or any application logic can trigger, consume, or listen to using standard DOM event way to bind and trigger. By convention, application events will use the UPPER_CASE type of notation. Also brite best practice recommends to split action application events, such as DO_SELECT_PROJECT, from status change application events, such as "PROJECT_SELECTION_CHANGE." See below to understand the why and the how.

In this step of the tutorial, we will implement the first view events of the applications to show how things should be wired together. We will add the events necessary for project navigation and task deletion. All the other events can be seen in the final application).

Tutorial 04 - Source | Demo

Project selection (application events)

As mentioned above, brite.js best practice recommends to split application events into action events, starting with "DO_...." and status change events. Action events are triggered by a view to request an action to be processed (usually by a parent view), for example, a project selection change, and status events are to notify all views that a state has changed, for example, a new project got selected. By convention, all applications events are UPPER_CASE formatted.

Such an architecture might sounds a little bit overwhelming at first, especially for developers foreign to MVC style programing, however, its benefit is significant, as it allows components to be completely integrated without having to know too much about each other. This model comes very handy when doing responsive applications, where some views might or might be present depending on the form factor.

With brite eventing support, implementing such a flow is trivial. Just need to add view event binding (with the jQuery binding notattions) to the ProjectListNav and MainView and make sure to correctly seggregate the action event logic from the status change ones.

Here is the flow of events for the project selection process.

  1. On <li data-entity="Project" ..> click ProjectListNav triggers an application action Event DO_PROJECT_CHANGE with the corresponding projectId
  2. On DO_PROJECT_CHANGE MainView process the project change, and trigger application change event PROJECT_SELECTION_CHANGE with the corresponding projectId
  3. On PROJECT_SELECTION_CHANGE ProjectListNav updates its <li data-entity="Project" ..> to show the right selection.
  4. On PROJECT_SELECTION_CHANGE MainView create a new ProjectView, discard the old one, and show the new one

Item 1) and 3) will be implemented in the ProjectListNav view as follow.

js/ProjectListNav.js

(function() {
  
  brite.registerView("ProjectListNav",{
    
    create: function(){
      //...same as step 3...
    },
    
    postDisplay: function(){
      //...same as step 3...
    }, 
    
    // Bind DOM events at the view level with key = "event_types; selector"
    events: {
      // 1) On LI click, trigger the application action event DO_SELECTION     
      "click; li[data-entity='Project']": function(event){
        var $li = $(event.currentTarget);
        // get the project id from the "data-entity-id" attribute
        var projectIdClicked = $li.attr("data-entity-id");
        // trigger the action event
        this.$el.trigger("DO_SELECT_PROJECT",{projectId:projectIdClicked});
      }
    },
    
    // bind DOM events at the Document level same key format as .events
    // Note: those event will be automatically namespaced with the view id
    //       and cleaned up on view destroy
    docEvents: {
      // 3) on PROJECT_SELECTION_CHANGE select the appropriate LI
      "PROJECT_SELECTION_CHANGE": function(event,extra){
        showProjectSelected.call(this,extra.project.id);
      }
    }
  });
  
  
  function showProjectSelected(projectId){
    var view = this;
  
    // deselect any eventual selection
    view.$el.find("li.sel").removeClass("sel");
    
    // get the new selectedLi and update it class
    var $selectedLi = view.$el.find("li[data-entity-id='" + projectId + "']");
    $selectedLi.addClass("sel");
  } 
    
  //...same as step 3...  
})(); 

Note Here we bind the application status change event PROJECT SELECTION CHANGE to the "document" HTML element, as the event will probably be triggered from a parent element or might be different elements. So, by binding at the "document" level, ProjectListNav does not need to know what view or component will trigger such event. It is usually a good practice to bind application status events at the document level at it offers better flexibility at little or no architecture or performance impact. Also note that any "docEvents" will be cleaned if/when the view is discarded (when a $element.bRemove() or $element.bEmpty())

Items 2) and 4) are implemented in the MainView as follow

js/MainView.js

brite.registerView("MainView",{emptyParent:true},{
  
  create: ...,
  
  postDisplay: ...,
  
  events: {
    
    // 2) on DO_PROJECT_CHANGE trigger PROJECTION_SELECTION_CHANGE
    // Note: It is often more efficient for the action event handler to fetch the data
    //       and trigger the corresponding status event with the full data. This is 
    //       why we are fetching project before triggering the PROJECT_SELECTION_CHANGE.
    "DO_SELECT_PROJECT": function(event,extra){
      var view = this;
      main.projectDao.get(extra.projectId).done(function(project){
        view.$el.trigger("PROJECT_SELECTION_CHANGE",{project:project});
      });
    }
  },
  
  docEvents: {
    
    // 4) On PROJECT_SELECTION_CHANGE replace the ProjectView
    "PROJECT_SELECTION_CHANGE": function(event,extra){
      var view = this;
      
      // NOTE: Call the brite jQuery .bEmpty() to make sure that brite destroy
      //       properly any views below this element 
      $projectViewPanel = view.$el.find(".MainView-projectViewPanel").bEmpty();
      // display the new projectView
      brite.display("ProjectView",$projectViewPanel,{project:extra.project});
    }
  }
    
});

Note As mentioned in the code above, it is often efficient to have the application action event handlers (i.e. the logic bound to the DO_.... events) to do the data fetching as they can trigger the corresponding status events with the full data (avoiding all event listeners to have to fetch the data again).

Now, the great benefit of this architecture, is that any component or logic can trigger or listen to these application events, and as long as the event contracts are respected the right behavior will happen. For example, we need to update MainView.js to show the first Project at startup time, and for this, it is as simple as triggering DO_SELECT_PROJECT with the appropriate projectId when the ProjectListNav has been display. For this, just updating the MainView.postDisplay as follow will do the job.

js/MainView.js

brite.registerView("MainView",{emptyParent:true},{
  
  create: ...,
  
  // Called after the view is displayed to the user
  postDisplay: function(){
    var view = this;
    
    // Create and display the ProjectListNav view and add it to the .MainView-content
    brite.display("ProjectListNav", view.$el.find(".MainView-left")).done(function(){
      // When the ProjectListNav is displayed, we trigger the DO_SELECT_PROJECT with 
      // the hardcoded (for now) project ID.        
      view.$el.trigger("DO_SELECT_PROJECT",{projectId:"001"});
    });
    
  },
  
  events: ...
});

And finally, as we have optimized the PROJECT_SELECTION_CHANGE event to have the full Project object, we need to make sure the ProjectView view can either display with a projectId or the full project object which will not require a data fetching. For this, the updated code would like like this:

  
brite.registerView("ProjectView",{
  
  create: function(data){
    var view = this;
    
    // if the project is given, then, just render it. 
    if (data.project){
      view.project = data.project;
      return render("tmpl-ProjectView",{project:view.project});
    }
    // otherwise, we fetch it and return the appropriate promise.
    else{
      return main.projectDao.get(data.projectId).pipe(function(project){
        view.project = project;
        return render("tmpl-ProjectView",{project:project});
      });   
    }
  }
  
  ...

Task deletion (Data/DAO events)

Now, that we have a good understand of the application events, another type of useful event type is the "data events," in brite, bound via the Dao Binding APIs.

For example, here are the steps to wire the task delete action.

  1. On click from a td .icon-trash, get the enclosing data-entity HTMLElement to get the reference (using convenient $.fn.bEntity() brite jQuery extension)
  2. Delete the task by using the taskDao
  3. On dataChange event on entity type Task, just refresh the list (could be more sophisticated if needed)

js/ProjectView.js

(function() {
  
  brite.registerView("ProjectView",{
    
    create: ..., 
    
    events: {
      
      // 1) on click of a td .icon-trash of this view
      "click; td .icon-trash": function(event){
        // get the enclosing data-entity="Task" html element to get its id
        var entityRef = $(event.currentTarget).bEntity("Task");
        
        // 2) Delete the task by using the taskDao 
        main.taskDao.delete(entityRef.id);
      } 
    },
    
    daoEvents: {
      // 3) on the dao event dataChange on Task, refresh the task table
      "dataChange; Task": refreshTable
    }
  });
  
  // --------- Private Methods --------- //
  function refreshTable(){
    var view = this;
    
    return main.taskDao.list({match:{projectId:view.project.id}}).done(function(taskList){
      var taskTableHtml = render("tmpl-ProjectView-taskList",{tasks:taskList});
      view.$sectionContent.html(taskTableHtml);     
    });
  }
  // --------- /Private Methods --------- // 
  
})();  
Step 3 - Adding The Daos Step 5 - Styling and Finishes

Ask, learn, share about brite.js

Go to brite.js G+ community