Set of utils for building web app's using RequireJS, Knockout and jQuery.
- Usage / Getting Started
- Containers and Regions
- Routing
- Libs & Abstractions
- KnockoutJS and Binding
- Observer Patterns
- Requests and Stored XHR
- Store - localStorage
- IndexedDB
- Validation
- Dates
Riggr can be easily installed via bower install riggr
. The bower package
contains all core files and dependencies. The core utilizes a small set of the
dependencies' methods, so utilizing different versions can be done by simply
changing the path in your RequireJS config.
The core idea behind Riggr is to establish a simple application structure by creating
a core app
definition and component controllers and views which are associated
with routes.
This is achieved by setting basic properties in the main.js
file, then using
the rigg
method to build the application. For example:
Given the following structure:
js/
require-config.js
main.js
controllers/
app.js
pages/
one.js
two.js
views/
app.html
pages/
one.html
two.html
index.html
The js/main.js
would be similar to the following:
require(['require-config'], function () {
require([
'riggr',
'controllers/app',
], function (rigg, app) {
// Set app title
app.title = 'Example';
// Set view transition
app.transition = 150;
// Set paths
app.paths = {
controllers: 'controllers',
views: '../views'
};
// Initialize app
rigg(app);
});
});
Then, in the js/controllers/app.js
file:
define([
'knockout'
], function (ko) {
var app = {
// Route table
routes: {
'/': 'pages/one',
'/two': 'pages/two',
// Example with parameter
'/two/:param': 'pages/two'
}
};
return app;
});
The app
file can contain other methods and properties used throughout the application.
There are two containers required in the DOM for loading routes. The main app
looks for a container element in index.html
with id="appContainer"
.
In the app.html
view (once initialized) all additional views are loaded into
an element with id="viewContainer"
.
From here, routing guides all other initialization operations within the application.
Routes defined in app
are monitored by the router for a match. When matched/called the
controllers respond to their assigned routes with the following methods:
define([], function () {
var myController = {
pageTitle: 'Foo',
init: function() {
// Do something once when the app is loaded
// Good for sub / pub
},
before: function (fn) {
// Check for condition to allow route to be loaded or not...
// then fire the fn with boolean to continue or block
fn(true);
},
load: function () {
// Do something on route/view load
// Any params in the URL are passed as arguments
},
unload: function () {
// Do something when the route/view is unloaded
}
};
return myController;
});
Views should match the pathing of their associated controller and
automatically load on route match (and if applicable, after passing of the before
handler).
To help maintain clean abstractions, a libs
object in the controller allows for
specifying lib files utilized by controllers.
To utilize, in main.js
specify an app libs path:
// Set paths
app.paths = {
controllers: 'controllers',
views: '../views',
libs: 'libs'
};
In the above, the libs
directory is at the same directory level as the
controllers
root.
In the controller where the lib is utilized, simply specify a libs
object:
libs: {
myUtil: 'my-util'
}
Above my-util
is pathed as libs/my-util.js
. The my-util
should utilize the
define()
wrapper as used with other RequireJS files.
Calling the lib's methods or properties inside the controller is then done via:
someNativeMethod: function () {
this.libs.myUtil.someUtilMethod();
}
When a controller is loaded, Knockout's applyBindings
method is fired to create
a composite controller/viewmodel.
define([
'knockout'
], function (ko) {
var foo = {
bar: ko.observable();
// ...additional properties/methods...
};
return foo;
});
The above controller/viewmodel has Knockout binding applied on load/init and the
corresponding view will respond to the ko
objects.
<span data-bind="text: bar"></span>
The {riggr-path}/observer.js
file provides basic pub/sub functionality. Controllers
using this must explicitly pass it in using define
, then can use the following
methods:
var mysub = observer.subscribe('example', function () {
// Topic `example` triggered
});
observer.publish('example', [OPTIONAL_ARGUMENTS]);
observer.unsubscribe(mysub);
The {riggr-path}/request.js
file provides AJAX/XHR request management. Controllers
using this must explicitly pass it in using define
, then can call the request
methods and attach to the returned jQuery XHR object.
Requests use jQuery's $.ajax()
method but allow for storing and managing common
requests.
var req = request.send({
url: '/some/endpoint',
type: 'POST',
data: { foo: 'bar' }
});
req.done(function() { .... });
req.fail(function() { .... });
// Create a stored request
request.create('myReq', {
url: '/some/endpoint/',
type: 'GET'
});
// Calling a stored request
request.send('myReq')
.done(function () {
// ...
})
.fail(function () {
// ...
});
// Removing a stored request
request.remove('myReq');
The request
object allows for the overriding of default/pre-defined properties
of the stored request with run-time properties:
// Create request
request.create('myReq', {
url: '/some/endpoint/'
});
// Calling stored request with parameter overrides
request.send('myReq', { type: 'POST', data: { name: 'Foo' }})
.done(function () {
// ...
})
.fail(function () {
// ...
});
The request
object allows for creating dynamic URL parameters through the
url_params
property:
// Create request
request.create('myReq', {
url: '/some/endpoint/{id}/',
type: 'GET'
});
// Call request and replace {id} param in URL
request.send('myReq', { url_params: { id: '383729282' }})
.done(function () {
// ...
})
.fail(function () {
// ...
});
The {riggr-path}/store.js
file provides localStorage management. Controllers
using this must explicitly pass it in using define
, then can access the methods
provided:
When setting, the store.set
method will determine data type and parse the data
for storage
// Set a string value
store.set('myStoreString', 'foo');
// Set an object or array (data is stringified for storage)
store.set('myStoreObj', { foo: 'bar' });
When retrieving (getting) an item, the store.get
method will parse the data to
return the original type:
// Returns the origin object from the set example above
store.get('myStoreObj');
Using store.remove
will clear an item from localStorage:
// Removes the item and it's data
store.remove('myStoreObj');
The {riggr-path}/indexed.js
file provides IndexedDB management. Controllers
using this must explicitly pass it in using define
, then can access the methods
provided:
The first thing needed when working with IndexedDB is a datastore. Creating this can simply be done with:
indexed('myDB').create();
The insert()
method can be used to add a single object or array of objects:
indexed('myDB').insert({
name: 'John Doe'
email: 'jdoe@email.com'
}, function (err, data) {
if (err) {
console.log('Nope.');
} else {
console.log(data);
}
});
The above would insert the single record and (on success) return the new record as 0
index of an array.
Records automatically receive an _id
property as their UID, so the output would be:
{
'_id': 928376488383,
'name': 'John Doe',
'email': 'jdoe@email.com'
}
To insert multiple records, simply supply an array:
indexed('myDB').insert([
{
name: 'John Doe',
email: 'jdoe@email.com'
}, {
name: 'Jane Smith',
email: 'jsmith@email.com'
}
], function (err, data) {
// Handle response...
});
The above would insert the records and return an array of the records.
The find()
method can return all, or matching, records from the data store.
indexed('myDB').find(function (err, data) {
if (err) {
console.log('Nope.');
} else {
console.log(data);
}
});
The above would return all results from the datastore.
To query specific records the find()
method supports object-based queries:
indexed('myDB').find({
_id: 28972387982
}, function (err, data) {
if (err) {
console.log('Nope.');
} else {
console.log(data);
}
});
The above would return the record matching the _id
.
Additionally, comparison queries can be made as objects with $gt
(greater than), $lt
(less than), $gte
(greater than or equal), $lte
(less than or equal), $like
, and $ne
(not equal). For example:
indexed('myDB').find({
someNumber: { $gt : 25 }
}, function (err, data) {
// Handle response...
});
The above would return all records where the property someNumber
is greater than 25
.
The update()
method allows for updating individual records, only matching or the entire datastore. It uses the same querying pattern as the find()
method. For example:
indexed('myDB').update({
_id: 893897389789
}, {
name: 'New Name'
}, function (err, data) {
// Handle response...
});
The above would update the record with matching _id
to set the name
property to New Name
.
By leaving of the first (query) object argument, all records in the datastore can be updated.
The delete()
method allows for removing individual records, only matching, or the entire datastore. Again, this uses the same querying pattern as find()
. For example:
indexed('myDB').delete({
_id: 838973897879
}, function (err, data) {
// Handle response...
});
The above would delete the record matching the _id
.
By leaving off the first (query) object argument, all records in the datastore will be deleted.
The drop()
method allows for completely removing the datastore:
indexed('myDB').drop();
The above would remove the datastore from IndexedDB storage.
The {riggr-path}/validation.js
file provides RegEx and Type validation. Controllers
using this must explicitly pass it in using define
, then can access the methods
provided:
The validation
lib has toString.call()
based type checking:
validation.typeOf('something');
The above would return string
, other types returned are undefined
, null
,
number
, boolean
, array
, and object
.
The validation
lib has a built-in set of RegEx's which can be overwritten or
added to. For example, a test could be added via:
validation.add('ssn', /^([0-9]{3}[-]*[0-9]{2}[-]*[0-9]{4})+$/);
The above would add the regex and then be usable throughout the application.
To check a value or values an object with test: value
formatting can be passed
to the test()
method, for example:
validation.test({
email: 'jon@email.com'
});
Additionally, this can be done across multiple objects:
validation.test({
string: 'Jon Doe',
email: 'jon@email.com',
ssn: '123-45-6789'
});
Since the above contains all passing values the return of this test would be true
.
In the case of a failure the original object is returned with only the failing tests included. For example:
validation.test({
string: 'Jon Doe',
email: 'jonemailcom', // Not going to pass
ssn: '123-45-6789'
});
The above would return:
{
email: 'jonemailcom'
}
Since timestamps and date formats are common across almost all front-end applications
riggr includes a {riggr-path}/date.js
utility which allows simple formating
of timestamps:
// Example: August 1, 2014 6:22:34 PM
date.format(new Date(), 'MM d, yyyy h:ii:ss AA');
Beyond custom formatting, the util provides pattern presets:
presets: {
'default': 'D M dd yyyy HH:ii:ss',
shortDate: 'm/d/yy',
mediumDate: 'M d, yyyy',
longDate: 'MM d, yyyy',
fullDate: 'DD, MM d, yyyy',
shortTime: 'h:ii AA',
mediumTime: 'h:ii:ss AA',
longTime: 'h:ii:ss AA e',
isoDate: 'yyyy-mm-dd',
isoTime: 'HH:ii:ss',
isoDateTime: 'yyyy-mm-dd\'A\'HH:ii:ss',
isoUtcDateTime: 'UTC:yyyy-mm-dd\'A\'HH:ii:ss\'e\''
}
Riggr is released under the MIT license and as such is completely free to use, modify and redistribute.