Custom UI - quandis/qbo3-Documentation GitHub Wiki

Javascript

QBO provides an extensive Javascript library (rendered with a call to Theme.ashx/Javascript) that integrates QBO API calls into an AJAX-driven user interface. The core javascript library leverages Mootools in addition to jQuery, primarily because of a nice class inheritance model and behaviors.

Guidelines

When coding javascript for QBO, adhere to the following:

  • Use behaviors instead of lots of window.addEvent('domready', ...) code.
  • Create small, modular script files instead of a single large script file and enter these into Javascript.config
  • Use PascalCase for class names, and camelCase for method names and variables
  • Alias global variables as local variables to enhance minification

Behaviors

QBO leverages Mootool's Behaviors to simplify page load javascript code. The Behavior API handles some key infrastructure considerations for use, including:

  • A formal cleanup API to help ensure good memory management
  • Ensuring loading code is only called once per element
  • An event arbitration infrastructure to allow different DOM elements and javascript classes to communicate by raising events.

Examples of behaviors included in QBO include:

  • qbo.Paginate.js: handles pagination of data grids (tables)
  • qbo.OrderBy.js: handles setting sort criteria of data grids
  • qbo.ObjectBind.js: handles creating qbo.*Object from div tags (instead of class="panel")

In many cases, the behavior API can be used to help abstract functionality between behavior filters we write. Take qbo3.ContactObject and qbo.Paginate as an example: when the user clicks on page 2 (something the Paginate filter recognizes), we need to have qbo3.ContactObject respond to that click and request page 2 from the server. Using the behavior API:

Behavior.addGlobalFilter('Paginate', function (el, api) {
    ...
    // Monitor for record start changes.
    el.addEvent('click:relay(a.page)', function (e, el) {
        api.fireEvent('paginate', { RecordStart: el.get('start') });
    });
});

and in the behavior that loads qbo.*Object:

Behavior.addGlobalFilter('ObjectBind', function (el, api) {
    ...
    // Monitor for pagination events
    api.addEvent('paginate', function (data) {
        alert('Got a paginate request!');
        qboObject.refresh(data);
    });
    ...
});

Any objects participating in the behavior will have the behavior API events raised to them, and can listen appropriately.

This approach is key to long-term maintainability of complicated forms.

Applying Behaviors to Dynamic Content

If DOM content is created dynamically (either by an AJAX call or via javascript), one should apply behaviors with:

qbo3.behavior.apply(element);

For example, creating a new data control with javascript could be done with:

myDate = new Element('input', {
  'type': 'text',
  'name': 'myDate',
  'data-behavior': 'Date',
  'data-date-options': {some options here}
}).inject(someLocation);
qbo3.behavior.apply(myDate);

ObjectBind Behavior

The ObjectBind behavior is used to bind an HTML element to some dynamically generated content. It is the most commonly used method for fetching data via AJAX from QBO.

Example: Render a Decision Search panel

<div id="search" class="tab-pane" data-behavior="ObjectBind" data-objectbind-options="{{ 'class': 'qbo3.DecisionObject', 'method': 'Search', 'render': false, 'listen': ['search'], 'cacheKey': 'Decision.Home.Search' }}">.</div>

Example: Render a Contact Select panel

<div id="select" class="span12" data-behavior="ObjectBind" data-objectbind-options="{{ 'class': 'qbo3.ContactObject', 'method': 'Select', 'remember': false, 'data': {{'ID': '{ContactID}' }} }}">.</div>

Example: Render a Smart Worklist Dashboard

<div data-behavior="ObjectBind" data-objectbind-options="{{ 'class': 'qbo3.SmartWorklistMemberObject', 'cacheKey': 'SmartWorklistHome-Current', 'method': 'Dashboard', 'listen': ['refreshAll'], 'data': {{'Dimension': 'SmartWorklistID', 'Transform': 'SmartWorklistMember.Dashboard.xslt', 'SortBy': 'SmartWorklist'}} }}">.</div>

In the examples above:

  • The Decision Search panel will call Decision/Decision.ashx/Search
  • The Contact Select panel will call Contact/Contact.ashx/Select?ID={some ContactID}
  • The Smart Worklist Dashboard panel will call Decision/SmartWorklistMember.ashx/Dashboard?Dimension=SmartWorklistID&Transform=SmartWorklistMember.Dashboard.xslt&SortBy=SmartWorklist

The key ObjectBind options include:

Option Required (default) Description
class Yes Name of the qbo3.AbstractObject-derived class to use to render the data.
method No Name of the method or operation to execute when rendering the data.
data No JSON parameters to pass to the server when making the AJAX call.
render No (true) If true. invoke the method signature immediately. If false, the panel will 'wait' for some other javascript event to cause the panel to make the AJAX call.
listen No An array of events to listen for on the behavior api; when any of these events are raised, the panel will make an AJAX call.
remember No (true) If true, cache the panel's content in local storage, so when the user revisits the page, the content is fetched from disk instead of over the wire.
cacheKey No Name of the key to store the panel's content as.
maxCacheDuration No Maximum length content may remain in cache; once this time is exceeded, the cached content will be ignored and refreshed from the server.

The ObjectBind behavior does not dictate what is in the data option; this depends on the method/operation being called.

Using jQuery

If you prefer to customize QBO with jQuery, you're welcome to do so in noConflict mode. A typical jQuery operation would use the '$' in this fashion:

$('#results')

In QBO, one must use the 'jQuery' designation instead, like this:

jQuery('#results')

Third Party Frameworks

QBO APIs can be called from other UI frameworks to enable customers to create custom user interfaces.

AngularJS

Configuring CORS requires custom HTTP headers for each call, and results in a great deal of code duplication. This can be avoided by registering a QBO Service with our Angular module.

Here we declare an Angular module called quandisDemo, and register an Angular service called QBO:

angular.module('quandisDemo').factory('qbo', function ($http) {
    return {
        request: function (url, params, success) {
            $http({
                method: 'POST',
                url: url,
                params: params,
                crossDomain: false,
                headers: {
                    'qboAutomation': true
                },
                withCredentials: true,
                responseType: 'json'
            }).success(success);
        }
    };
});

The headers above are required for each CORS request made to QBO, and so we isolate them in order that any future changes can be made in one place.

We also pass in a success() method, as each controller that call this CORS request factory will have specific needs in a success function.

This module will be used in each of the examples below.

Logging In

Given our CORS request factory above, logging in is fairly simple:

angular.module('quandisDemo').controller('LoginController', function (qbo) {
    var login = this;
    login.url = "http://demo.quandis.net/Security/Person.ashx/Select?ID=1&Output=Json";
 
    login.success = function (data) {
        console.log('You have logged in.');
    };
    qbo.request(login.url, login.params, login.success);
});

This login module will prompt a user for a login, and pass the url, username, password, and success function to the CORS request factory.

Each of the examples below will also use this module.

Creating and Editing a Form

Date fields

QBO delivers dates in UTC format, which Angular does not natively display in a Date field. The following filter can be applied to UTC dates in order that the Angular DatePicker will load dates correctly:

angular.module('quandisDemo').filter('myDate', function($filter){
    return function(input){
        var _date = $filter('date')(new Date(input), 'yyyy-MM-dd');
        return _date;
    };
});

When JSON is received as the result of an HTTP request, dates can be filtered in the following manner:

task.formData.TestDate = $filter('myDate')(task.formData.TestDate);

The date can then be displayed in a input field:

<div>Date:
    <input type="date" ng-model="task.formData.TestDate"/>
</div>

When we save an updated form, Date field values should be parsed as such:

task.formData.TestDate = new Date(task.formData.TestDate).toISOString();

EmberJS

Centralize configuring CORS headers:

function qbo(url, success, data) {
    return $.ajax({
        type: 'POST',
        dataType: 'json',
        url: url,
        data: data,
        crossDomain: false,
        username: "[email protected]",
        password: "ChangeToYourPassword",
        headers: {
            'qboAutomation': true
        },
        xhrFields: {
            withCredentials: true
        }
    }).success(success);
}

Logging In

function login() {
    var result = null;
    var url = "http://demo.quandis.net/Security/Person.ashx/Select?ID=1&Output=Json";
    var success = function (json) {
        console.log(json);
        return json;
    };
    return qbo(url, success, null);
}
App.IndexRoute = Ember.Route.extend({
  model: function() {
    return login();
  }
});
<body>
  <script type="text/x-handlebars">
    <h2>Welcome to Ember.js</h2>
    {{outlet}}
  </script>

  <script type="text/x-handlebars" data-template-name="index">
    <ul>
    <li>
     First Name: {{model.FirstName}}
     </li>
     <li>
     Last Name: {{model.LastName}}
     </li>
     <li>
     Email: {{model.Person}}
     </li>
    </ul>
  </script>
</body>

Performing a Matrix Lookup

App.Router.map(function() {
  this.route('matrix');
});
App.MatrixRoute = Ember.Route.extend({
  model: function(){
    return matrixLookup();
  }
});
function matrixLookup() {
    var url = "http://demo.quandis.net/Application/Matrix.ashx/Lookup?Output=Json";
    var data = {
        Client: "Acme Tools",
        MatrixID: 7,
        Measure: "Amount"
    };
    var success = function(json){
        console.log(json);
        return json;
    };
    return qbo(url, success, data);
}
<body>
  <script type="text/x-handlebars">
    <h2>Welcome to Ember.js</h2>
    {{outlet}}
  </script>
  <script type="text/x-handlebars" data-template-name="index">
    <ul>
     {{model.Person}}
    </ul>
    {{#link-to 'matrix' tagName='button'}}
      Get Matrix
    {{/link-to}}
  </script>
  <script type="text/x-handlebars" data-template-name="matrix">
  Test Matrix:
  {{model}}
  </script>
</body>

jQuery

Configuring CORS requires custom headers for each call, and results in a great deal of code duplication. This can be avoided by building a custom wrapper for the jQuery ajax() function. Here we declare a function called qbo() which holds our CORS headers:

function qbo(url, success, data) {
    $.ajax({
        type: 'POST',
        dataType: 'json',
        url: url,
        data: data,
        crossDomain: false,
        username: "[email protected]",
        password: "ChangeToYourPassword",
        headers: {
            'qboAutomation': true
        },
        xhrFields: {
            withCredentials: true
        }
    }).success(success);
}

The headers above are required for each CORS request made to QBO, and so we isolate them in order that any future changes can be made in one place.

We also pass in a success() function, as different calls to the QBO REST API will need to perform different actions on success.

This function will be used in each of the examples below.

Logging In

Given our HTTP Request module defined above, we can define a simple function for logging in:

function login(){
    var url = "http://demo.quandis.net/Security/Person.ashx/Select?ID=1&Output=Json";
    var success = function (json) {
        console.log(json);
        $('#testing').text("Login Status: true");
    };
    qbo(url, success, null);
}

Editing and Saving a Form

Form data can be retrieved with the following function. We need to pass three parameters to our qbo Http Request function:

  • url: our base URL for the request
  • data: our parameters for the method called in the URL
  • success: our custom success method for this function
function retrieveForm() {
    var url = "http://demo.quandis.net/Decision/ImportForm.ashx/Select?Output=Json";
    var data = {ImportFormID: 41};
    var success = function (json) {
        formData = transformResponse(json);
        populateForm(formData);
    };
    qbo(url, success, data);
}

Form data can be saved with a similar function.

function saveForm() {
    var data = getFormData(formData);
    var url = "http://demo.quandis.net/Decision/ImportForm.ashx/Save?Output=Json";
    var success = function (json) {
        formData = transformResponse(json);
        populateForm(formData);
    };
    qbo(url, success, data);
}

In both of the methods above, there are some helper functions being used. populateForm() is a placeholder for the purposes of this demo. It simply places values retrieved from QBO into form fields:

function populateForm(data) {
    $('#form').html(JSON.stringify(data, undefined, 2));
    $('#field').val(data.Available);
    $('#testCheck').attr('checked', data.TestCheck);
    $('#testDate').val(data.TestDate);
}

getFormData() is also a placeholder for the purposes of this demo. It simply pulls the values from form fields.

function getFormData(data) {
    data.Available = $('#field').val();
    data.TestCheck = $('#testCheck').prop('checked');
    return data;
}

transformResponse() is a helper method that normalizes the JSON data received from QBO, so that all data can be used easily. QBO JSON responses include a XmlData.ImportFormXml array. transformResponse() moves items in XmlData.ImportFormXml to the root of the JSON object transformResponse() also performs some type conversion, converting String of value true and false to Boolean values

function transformResponse(data) {
    $.each(data.XmlData.ImportFormXml, function (index, value) {
        if (value === "true") {
            value = true
        };
        if (value === "false") {
            value = false
        };
        data[index] = value;
        console.log(value);
    });
    delete data.XmlData;
    return data;
}

BackboneJS

Configuring CORS Headers

qbo.fetch = function(){
  $.ajax({
        type: 'POST',
        url: qbo.url,
        crossDomain: false,
        username: "[email protected]",
        password: "ChangeToYourPassword",
        headers: {
            'qboAutomation': true
        },
        xhrFields: {
            withCredentials: true
        }
    }).success(qbo.success);
};

Logging In

var QBO = Backbone.Model.extend({});
var qbo = new QBO();
qbo.url = "http://demo.quandis.net/Security/Person.ashx/Select?ID=1&Output=Json";

qbo.success = function(json){
  qbo.set({data: json});
  qboView.render();
};

qbo.fetch = function(){
  $.ajax({
        type: 'POST',
        url: qbo.url,
        crossDomain: false,
        username: "[email protected]",
        password: "ChangeToYourPassword",
        headers: {
            'qboAutomation': true
        },
        xhrFields: {
            withCredentials: true
        }
    }).success(qbo.success);
};

qbo.fetch();

var QBOView = Backbone.View.extend({
  render: function(){
    var data = JSON.stringify(this.model.get('data'), undefined, 2);
    var html = '<div>' + data + '</div>';
    $('#test').html(html);
  }
});
var qboView = new QBOView({model: qbo});
⚠️ **GitHub.com Fallback** ⚠️