ng-contexts v1.1.7
:evergreen_tree: ng-contexts
Intuitive state management for AngularJS applications
Summary
Contexts
define the selected state of your application.
Quick highlights:
- :sparkles: Transparent management of context-based states of interdependent
Services
and related components. - :smile: Non-invasive implementation. A "convention over configuration" approach that allows you to choose how to integrate and when to use.
- :rocket: Fast, efficient and lazy - based on native PubSub, minimizing the complexity and size of the
$digest
cycle (check out this post on$broadcast
) - :cloud: Light-weight - less than 350 lines of code and under 3KB minified!
Problem
A good description of the problem of intuitively and comprehensively managing selected state in Angular 1.x is here.
Usage
Initial Service Configuration
ng-contexts
enables you to define a "state tree" -- a hierarchy of related contextualized data objects that synchronize
with your already-existant Service
s and components, non-invasively.
To include a Service
in your tree, simply establish the following properties on a service:
this.name
(required) a unique name to identify the service (often lowercase version of service)this.model
(optional) pseudo-constructor function that's refreshed on updates to your Service entitiesthis.rels
(optional) collection of immediate child entities, order independentthis.all
(optional) function that will retrieve all potential Service entities that can be selectedthis.current
(optional) function to determine the current selected entity
And then inject Contexts
and register the service at the end of your definition:
Contexts.register(this)
That's it, that's the minimum configuration needed. You'll likely want to establish this.all()
and this.current()
functions only once in your application if you're creating a canonical method to fetch and select entities. If so, these functions can be defined in a shared service and inherited in your individual services.
Utilizing Exposed Functions
Once your tree is configured and you want to begin storing and working with selected state, these functions will be useful. They may be called directly in the relevant service or via injecting the service (or the Contexts
service) into your controllers.
this.select()
- Selects an entity for the context. If the entity's data is different from the existing selected data, this will publish to the tree and clear any nodes below it.- data (
Object
) required param overwrites/adds to the selected entity any properties included on the object. - force (
Boolean
) param triggers a publish even if the data is not different.
- data (
this.use()
- Subscribes to a function and will execute a callback when the function's value changes. Similar to Angular's$scope.$watch
in creating a subscription to a data entity.- name (
Function
) required param for the function to use to determine whether data has changed. - andThen (
Function
) parameter which is the callback to be executed. - defer (
Boolean
) param prevents the callback from executing when theuse()
is first called. - returns a subscription function object with a
.stop()
that can be invoked to cancel the subscription. (Note that.stop()
is simply an alias of the function object itself.)
- name (
this.modify()
- Update selected data for a context without subscribing or publishing. Avoids triggering updates to the tree.- updates (
Object
) required param holds the data to overwrite/add to the selected entity - publish (
Boolean
) param to request a publish of the entity to the tree.
- updates (
this.exists()
- Returns a simple boolean value if there is a selected entity for the context. Note: Uses theuuid
property to avoid false positives when functions have been added viamodel()
but no entity is selected.this.get()
- Returns the selected entity.- name (
String
) required param to idenitfy the entity.
- name (
this.getOr()
- Returns the selected entity, or an alternative value ifexists()
is false.- name (
String
) required param to idenitfy the entity. - none (any) parameter for what to return if
exists()
is false.
- name (
this.selected()
- Returns the selected data entity for the service. Doesn't need a parameter. Shorthand forContexts.get([context.name])
this.clear()
- Explicitly clear allselect()
data anduse()
subscriptions for the service.this.clearSubscriptions()
- Explicitly clearuse()
subscriptions for the service while leaving selected contexts and data in place.
Example
Below is a simple use case and implementation. We're managing a dynamic selected state for solar sales software, where our relevant services are: Users, Sites, Contacts, and Quotes. Each user can have multiple sites and contacts, and each site can have multiple quotes.
We establish a contexts tree like this:
User
|
+-----------------------------+
| |
v v
Site Contact
|
|
v
Quote
We configure our Services to establish this tree.
/* First, inject `Contexts` service */
module.service('User', function(Contexts) {
var self = this
this.name = 'user' // name to use as primary lookup and to establish relations
this.rels = ['site', 'contact'] // define the tree of services that have an immediate relationship
this.model = function(user) { // model logic for a single `User` entity
user.firstName = function() {
return user.givenName + ' ' + user.familyName
}
return user
}
/*
* User defined generator method to fetch all potential entities to select.
* Typically something using `$http` or `$resource` with cache.
* multiple users are considered here because
* more than one user may use the application
* in a single window session (asynchronous re-authentication)
*/
this.all = function() {
return [
{id: 1, name: 'bob'},
{id: 2, name: 'donna'}
]
}
/*
* User defined method to determine the "current" user.
* Can be via a url, a token, anything!
* Here we're lazy and if there isn't already a selected entity, we're
* simply returning the first element in the array.
*/
this.current = function() {
return self.all().then(function(users) {
return Contexts.getOr('user', users[0])
})
}
/*
* Required registration as the final statement of your `Service`.
* registers your Service with the global "tree" of contexts
*/
Contexts.register(this)
})
We would now also define Site
, Contact
and Quote
services that resemble User
. Each of course is free to have its own implementation and functionality. Let's just look at Site
:
module.service('Site', function(Contexts) {
var self = this
this.name = 'site'
this.rels = ['quote']
this.model = function(site) {
site.label = function() {
return site.street_number + ' ' + site.street_name + ', ' + site.city + ', ' + site.state
}
return site
}
this.all = function() {
/* Define the method to get all selectable entities for Site */
return [
{id: 1, street_number: '123', street_name: 'Magic Way', city: 'San Francisco', state: 'CA' },
{id: 2, street_number: '456', street_name: 'JavaS Way', city: 'San Francisco', state: 'CA' }
]
}
this.current = function() {
/* Define the method to identify the currenly selected entity */
return self.all().then(function(sites) {
return Contexts.getOr('site', sites[0])
})
}
Contexts.register(this)
})
Once our Services
are defined and wired together, any components or directives that inherit their contexts will be synchronized accordingly whenever anything related to the context is published or updated.
For instance, any select()
called to update User
will clear data and re-delegate to Site
and Contact
, and will also clear Quote
beause Quote
is related to Site
which is related to User
. Every controller, directive or component dependent on these contexts will also receive the udpates.
module.directive('currentQuote', function(Contexts, Quote, $log) {
return {
restrict: 'EA',
template: '<h1>Selected Quote</h1><p>{{ quote | json }}</p>',
controller: function(scope) {
/* Define a callback to be triggered whenever a new `User`, `Site`, or `Quote` is selected */
Quote.use('current', function(quote) {
$log.info('New quote selected', quote)
scope.quote = quote
})
scope.selectQuote(quote) {
/*
* Handle a user selection of a new quote in the UI.
* Will also publish the new entity and trigger the callback definied in the `use()` above
*/
Quote.select(quote)
}
scope.updateQuoteCost(newCost) {
/*
* A sample controller function to update a property on an instance without publishing.
* Handle user update to a property via the UI.
*/
var cost = newCost
Quote.modify({cost})
}
}
}
})
Installation
npm install ng-contexts
ES5
var Contexts = require('ng-contexts')
ES6
import Contexts from 'ng-contexts'
Be sure to require angular
first so that it's accessible to ng-contexts
:
import angular
import Contexts from 'ng-contexts'
Then add it to your own module:
angular.module('myModule', ['ng-contexts'])
If you aren't using a package tool like webpack
or browserify
, fall back to the traditional method:
Full
<script type="text/javascript" src="/node_modules/ng-contexts/ng-contexts.js"></script>
Minified
<script type="text/javascript" src="/node_modules/ng-contexts/ng-contexts.min.js"></script>