Leveraging Backbone Events in Sugar

Sidecar and Backbone.js

The Sidecar user interface framework is built on a variety of browser side technologies.  Being proficient in these underlying technologies is important for any developer writing Sugar 7 user interface code.

They key JavaScript component that pulls Sidecar together is Backbone.js. Backbone provides the underlying structure to Sidecar as well as providing a rich set of base features and APIs that are heavily used throughout the Sugar 7 application.

Sugar 7.6.x uses Backbone 0.9.10.

If you've ever created a Sugar Dashlet or any other user interface component in Sugar 7 then you will familiar with Backbone concepts such as customizing a View or working with a Model. However, many loosely coupled customizations or integrations with Sugar 7 user interface are possible through the use of Backbone Events.

For server side code, recall that you can use Logic Hooks to trap a Module's save events instead of overriding a module's SugarBean.  The same pattern can be applied where instead of overriding a core file, you can listen to essential events and take action as they occur.

We will explore more below.  The code from this example is also available on Sugar Integration Building Blocks project in Github.

Backbone Events

The Backbone.Events module is a lightweight pub-sub pattern that gets mixed in to each Backbone class (Model, View, Collection, Router, etc). This means you can listen to or dispatch custom named events from any Backbone object.

Backbone Events should not be confused with jQuery Events which is an API for working with DOM events. Backbone supports an events hash on Views that can be used to attach event handlers to DOM using jQuery. These are not Backbone Events. This can be confusing since both interfaces include an on() function, allow you to attach an event handler, and other similarities.

The target for jQuery events are DOM elements.  The target for Backbone Events are Backbone objects.

Sidecar classes all extend these base Backbone classes.  So each Sidecar object (Layouts, Views, Fields, Beans, Contexts, etc.) supports Events.

Existing Backbone Event Catalog

All of the existing catalog of Backbone Events are supported and triggered by the Sugar application.

For example, we can listen to built-in Backbone Router events, such as the route event, that are triggered by Sidecar.  Try running the following JavaScript code within your browser's console.

SUGAR.App.router.on('route', function(){console.log(arguments);});

As you click through the Sugar application, each time the router is called you will see routing events appear in your browser console.



Example Sidecar Events

In addition to existing built-in Backbone Events, the Sidecar framework makes use of a variety of unique events.  We will show examples of some events that are particularly useful for customization purposes.

Not all Sidecar Events are documented below.  If you are interested, explore further by downloading the Sidecar Dev Tools and use the Application Stream to monitor many Sidecar Events as they are triggered.

Application (Global) Events

Application events are all triggered on app.events (SUGAR.App.events) object. For example, if you wanted to see all global application events printed in your browser console then you could use the following code.

SUGAR.App.events.on('all', function(){console.log(arguments)});

For example,app:sync:complete

This is a very useful application event to know about. This event is triggered after login and the series of initial synchronization operations is completed.

This is earliest point of the application lifecycle where all metadata, view definitions, Handlebars templates, etc, are available to JavaScript code. So this is a good jumping off point for custom code.

Bean Events

Some events are triggered only on Bean objects.

For example,validation:complete

Validation tasks are run against a Bean prior to saving a Bean object back to server.  If validation passed, then this event will pass TRUE to listeners. If validation failed, then this event will pass FALSE to listeners.

Context Events

The Context object is often used to facilitate communication between different Sidecar components on the page using Events.

For example,button:save_button:click

This event is triggered whenever a Save button is clicked.  The Record View uses this event to run save routines without being tightly coupled to a particular save button.

Adding a new Record View button

Here is a demonstration of how you can use Events as an alternative to overriding Sidecar controllers. This approach is more upgrade and customization friendly than overriding a core application file.

The following steps will create a new custom button on the Accounts module's Record View and wire it to a JavaScript callback function.



Step 1: Insert Button Metadata

We need to add Button metadata to the existing Accounts view using a Sidecar Extension.  This defines the Sidecar Event to be triggered as well as the target object for the event.custom/Extension/modules/Accounts/Ext/clients/base/views/record/custom_button.php

custom_button.php

<?php
/**
* Copyright 2015 SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
*/
//Insert our custom button definition into existing Buttons array before the sidebar toggle button
array_splice($viewdefs['Accounts']['base']['view']['record']['buttons'], -1, 0, array(
array(
'name' => 'custom_button',
'type' => 'button',
'label' => 'My Custom Button',
'css_class' => 'btn-link',
//Set target object for Sidecar Event. 
//By default, button events are triggered on current Context but can optionally set target to 'view' or 'layout'
//'target' => 'context'
'events' => array(
// custom Sidecar Event to trigger on click.  Event name can be anything you want.
'click' => 'button:custom_button:click',
        )
    ),
));


Step 2: Add Event handler JS using JSGroupings

We can add event handling JavaScript into page using a JSGroupings Extension. This is designed to attach a callback function to the appropriate Context object when the application is on the Account module's Record view.custom/Extension/application/Ext/JSGroupings/addCustomButtonJS.php

addCustomButtonJS.php

<?php
/**
* Copyright 2015 SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
*/
//Loop through the groupings to find grouping file you want to append to
foreach ($js_groupings as $key => $groupings)
{
foreach  ($groupings as $file => $target)
    {
//if the target grouping is found
if ($target == 'include/javascript/sugar_grp7.min.js')
        {
//append the custom JavaScript file to existing grouping
$js_groupings[$key]['custom/modules/Accounts/myCustomButton.js'] = 'include/javascript/sugar_grp7.min.js';
        }

break;
    }
}

myCustomButton.js
(function(app){
/**
* Copyright 2015 SugarCRM Inc.  Licensed by SugarCRM under the Apache 2.0 license.
*/
//Run callback when Sidecar metadata is fully initialized
app.events.on('app:sync:complete', function(){

//When a record layout is loaded...
app.router.on('route:record', function(module){
//AND the module is Accounts...
if(module === 'Accounts') {
//AND the 'button:custom_button:click' event occurs on the current Context
app.controller.context.on('button:custom_button:click', function(model){
console.log("Custom Button event triggered on: " + model.get("name"));
//Show an alert on screen
app.alert.show('custom-message-id', {
                        level: 'success',
                        messages: 'It worked!',
                        autoClose: true
                    });
                });
//No off() needed.  See note later in blog post.
            }
        });

    });
})(SUGAR.App);

Step 3: Quick Repair and Rebuild

You then need to run Quick Repair and Rebuild in order to build your new extensions.  You will also need to do a hard refresh of the browser page in order to load the updated JavaScript files.



Sidecar Event Management

As with any single page application, it is important to include memory management considerations when you are coding your solutions. Event handlers can be a common source of memory leaks in JavaScript applications since they can prevent the objects they are attached to from being garbage collected. You do not want to keep old Sidecar Views, Layouts, etc, in memory long after they should have been removed.

Here are a couple tips.For global objects, make sure your event listener is attached only once. Make sure you understand the lifecycle of the objects you are working with.  Do not guess.Events get detached from Sidecar created objects. The Context object and any Layouts, Views, Fields that are created by Sidecar are disposed of whenever a new layout is loaded via Sidecar's built-in controller.  This process detaches any listeners attached on these objects.  This is why it was not necessary for us to remove the Context listener in the example above.If you created it, then you need to clean it up.  If you have JavaScript code that calls app.view.create* then you are responsible for disposing of it properly.  Calling dispose() on your own components when you are finished with them will remove event listeners as well as detach it from the DOM, etc.  Otherwise the view, your event listeners, and anything else that is attached could stay in memory forever.Use Backbone's listenTo() and stopListening() functions where possible.  They are designed to make automatic management of Events easier.