Skip to content

hash-bang/obsvr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Obsvr

Swiss-army knife for observation of objects (Angular compatible).

Features:

  • Watch an object hierarchically by path
  • Fully-featured event system - emit(), on(), one(), once(), off() are all supported
  • Self-destructing watchers - remove the watcher completely when no other hooks are present (works well with once / one event handlers to only capture one change then stop watching)
  • Ability to ignore initial variable states (see the ignoreInitial option)
  • Blazingly fast - by default (change by setting method) the data object is mutated into setters/getters where each data change is detected in a function and recorded. No deep object traversal is performed - state changes detection is done exclusively via callbacks. For more optimizations see the scanKeyChange property.
  • Configurable path seperator - integrate with globbing libraries or regular expressions easier by changing dotted notation (e.g. foo.bar.baz) to something else

NOTE: This project is primarily designed for integration into Angular but works perfectly well within Node as per the test suite.

For a more complex example of this libraries capabilies see the demo directory for a fully working Angular integration.

Examples

General use within an Angular component

angular
	.module('app')
	.component('myComponent', {
		controller: function($observe) {

			// Assign observe to run on each Angular update cycle
			this.doCheck = $watch.cycle;

			// Watch something
			this.myObj = {foo: 'Foo!', bar: 'Bar!', baz: 'Baz!'};

			$observe(this, 'myObj')
				.on('change', _=> console.log('Object changed in some way'))
				.on('key', (key, newVal, oldVal) => console.log('Key', key, 'Changed', oldVal, '=>', newVal))
				// Other events: keyDelete, keyAdd

		},
	});

The examples below all assume that you are using $observe within a similar structure, the Component wrapping is omitted for brevity.

React to a change ONCE when everything has been set

The following uses several techniques to only fire an observer once:

$observe(this, ['path1', 'path2', 'path3'])
	.once(function() {
		// We only run this function if path1, path2 and path3 are not undefined!
		// Since this is also inside a 'once' this observer will also self-destruct
	})

Whats happening here:

  1. $observe is being passed multiple paths (similar to a $scope.$watchGroup in Angular)
  2. Since no parameters are passed, default parameters are used including {ignoreInitial: 'any', selfDestruct: true}
  3. As ignoreInitial = 'any' we ignore the initial undefined states of the paths until all are set
  4. As a once() hook is being used and selfDestruct = true we detect that no further hooks are waiting and destroy the watcher automatically

Installation within Angular

  1. Add angular-obsvr as a module in your main angular.module() call.
  2. Include the service somewhere in your project by either loading the dist/angular-obsvr.js file or rolling into your minifier / webpack / concat process of choice.
  3. Add $observe as a dependency to any controller you wish to use it in.

Migration

The $observe() call is compatible with Angular's $watch(), $watchGroup() and $watchCollection() with a few small changes.

A $observe() can mix-and-match several approaches support is possible for hybrid observers such as combining deep, multi-collection watching where we can ignore the initial unset state.

$scope.$watch(path | func, callback)

You can use any of the following patterns:

  • $observe(this, path, callback)
  • $observe(this, path).on('change', callback)

$scope.$watch(path | func, callback, true)

Deep watching can be accomplished with any of the following:

  • $observe(this, path, callback, true)
  • $observe(this, path, {deep: true}).on('change', callback)
  • $observe.deep(this, path, callback)
  • $observe.deep(this, path).on('change', callback)

$scope.$watchGroup(paths..., callback)

Path can already be an array in a $observe call so any of the usage patterns available with a regular $observe() call will all work.

$scope.$watchCollection(path | func, callback)

Watching a collection with $observe() is essentially just specifying that the depth = 2 (watch only the immediate array indexes AND the keys of the sub-object).

Any of the following patterns should work:

  • $observe(this, path, callback, 2)
  • $observe(this, path, {deep: 2}).on('change', callback)
  • $observe.deep(this, path, callback, 2)
  • $observe.deep(this, path, 2).on('change', callback)

API

The below API represents the developer-facing functionality. For a full list of functions, methods and variables please read the source code JSDoc comments instead.

General notes:

  • By default $observe() will ignore the initial undefined values (ignoreInitial=any) which differs from Angular's default behaviour of always triggering a $scope.$watch callback at least once. If you want this behaviour pass {ignoreIntial: 'never'} as a parameter.

$observe(scope, path, [callback], [config | depth])

The main Observer worker. Calling this function factory with a scope and a path will register an observer worker against it. Any changes will then fire events.

controller: function($observe) {
	$observe(this, 'myAmazingObject')
		.on('change', _=> console.log('Object changed in some way'))

	// Tell Angular to run all checks each digest cycle
	$ctrl.$doCheck = $observe.checkAll;
},

See the Events section for what events can be listened for.

This returns an Observable.

Callback is optional, if provided it will be automatically bound with Observable.on('change', CALLBACK).

Config is an optional object of options to configure $observe's behaviour. If config is a number it will be assumed that {deep: CONFIG} was specified.

Option Type Default Description
deep true OR Number 1 The maximum depth to iterate when watching a target. If the value is true all levels are examined
hookWarnings Boolean true When enabled extra checks are preformed for common hook name misspellings
ignoreInitial String "any" Ignore initial values (the first time a target is set). Values are: "never" / false - always trigger the change event even if any / all of the initial values are undefined, "any" - ignore the initial change detection if any of the values watched are undefined, "all" - ignore initial change detection if all of the values are undefined
ignoreKeys Array [/^\$/] An array of RegExps to check keys against before we inject them (when method='setters')
method String setters The method used to watch the object. Can be one of: dirty - use dirty checking, very slow but accurate, setters - inject getters/setters to every primitive very fast but cannot property detect key additions / deletions (see scanKeyChange for this)
root true OR String true If a string is specified all paths used in event emitters are made relative to the one specified, if true the relative path is calculated from the provided paths only if a single path was specified (this replicates the default behaviour of Angular)
scanKeyChange Boolean true Scan for key additions / deletions. There is a major performance boost to disabling this but it renders $observe() unable to detect any changes to existing objects. It is highly recommended you enable this if you have a relatively static data set or your data set is being brought from a remote database with all the keys mapped (Angular-Resource for example)
selfDestruct Boolean true Whether the object should call Observer.destruct() when all the hooks listed in selfDestructHooks are empty. Set this to false if you intend to dynamically attach hooks to the Observer object at a later date
selfDestructHooks Array ['change', 'key', 'path'] What hooks to watch if selfDestruct=true
seperator String . The path definition seperator. Change this to forward slashes or something if you want to integrate with a globbing library
setEqualIsChange Boolean false Indicates that setting a value to its current value counts as a change condition. e.g. setting a boolean which is already true to true will mark that path as modified

$observe.deep(scope, path, [callback], [config | depth])

A convenience function to call $observe with {deep: true} set as a config parameter.

$observe.checkAll()

An alias function for $observeProvider.checkAll().

Observable.check()

Runs a check on the observe target and fires any events.

You probably don't need to interact with this method directly. Instead use $ctrl.$doCheck = $observe.checkAll to tell Angular to run all checks on each digest cycle.

Observable.checkAll()

An alias function for $observeProvider.checkAll().

Observable.get([path])

Fetch the object being observed or a path within it.

Observable.destroy()

Destroy the observer and deregister it with $observeProvider so it no longer receives updates.

Observable.isModified([path])

If called with no path this function returns an array of all modified paths within the object. If given a specific path to examine this function returns a boolean indicating if that path has been modified.

Observable.traverse([callback], [path])

Run a callback on every item within the current object.

$observeProvider

The overseeing observer system (Angular only). This also allows management of any registered observer processes.

$observeProvider.checkAll()

Run check() on all registered $observe objects. This is also accessible as $observe.checkAll().

Events

The following events can be attached to any Observable instance via on, one / once and can be removed with off.

Event Parameters Description
change (newValue) Emitted if any part of the observable target changes
destroy (method) Emitted when the observer is destroyed method can be 'manual' or 'selfDestruct'
key (key, newValue) Emitted if the observable target is an object and any of the top level only key values change
path (path, newValue) Emitted if any deeply nested paths change within the observable
postChange (newValue) Emitted after all other keys have finished before the next injection stage
postInject (newValue) Emitted after the object has been 'sealed' again before the next check cycle
finally () Emitted after all other hooks have been called
initial (newValue) Emitted on the first change detection. Not if ignoreInitial is set this can fire multiple times as the Observer will assume any undefined value is still an initial value

TODO

  • Object.defineProperty optimisations
  • Emitters that can only fire once
  • Config parameter - $observe(scope, path, config)
  • Callbacks without events - $observe([scope], path, callback)
  • Deep watching via config.deep = true
  • Depth specifier via $observe(scope, path, [callback], depth)
  • Depth specifier via config.depth = Boolean | Number
  • Paths can be arrays (for group support)
  • Ignore paths array - Observer.ignore(path...)
  • Relative paths (defaults to true if only one path is being watched)
  • Old values provided to emitters
  • Observer destruction
  • Observer auto-destruction when no hooks remain
  • Observer pausing
  • Ignore initial undefined
  • Integration with this.$changes
  • Observer.set(PATH, value)
  • Observer.merge(object)
  • Path globbing
  • Multiple watchers on the same object but different paths

About

Observe mutations to a scalar, object or array in Angular

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published