Custom UI - quandis/qbo3-Documentation GitHub Wiki
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.
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
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 creatingqbo.*Object
fromdiv
tags (instead ofclass="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.
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);
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.
<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>
<div id="select" class="span12" data-behavior="ObjectBind" data-objectbind-options="{{ 'class': 'qbo3.ContactObject', 'method': 'Select', 'remember': false, 'data': {{'ID': '{ContactID}' }} }}">.</div>
<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.
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')
QBO APIs can be called from other UI frameworks to enable customers to create custom user interfaces.
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.
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.
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();
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);
}
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>
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>
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.
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);
}
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;
}
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);
};
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});