Tide your asynchronic code up with angular-rope!
Build user actions around tasks and chains instead of a mess of nested promises.
So something like this:
var service = {
somethingAsync: function(_data) {
var prom = api.create(_data);
if(_data.special) {
prom = prom.then(somethingElse);
}
return prom
}
}
function consumer() {
dialogs.confirm.then(function(_ok) {
if(_ok) {
service.somethingAsync(data).then(function(_obj) {
return dialogs.userInput();
}).then(function() {
return service.doSomethingElse();
});
}
})
}
Looks like this:
var service = {
willSomethingAsync: rope.task(function(_data) {
rope.next(api.create(_data))
.nexfIf(_data.special)
.next(somethingElse);
}
};
function consumer() {
rope.next(dialogs.confirm)
.nextIf()
.next(service.willSomethingAsync)
.next(dialogs.userInput)
.next(service.doSomethingElse, service);
}
- We will leave the Issues open as a discussion forum only.
- We do not guarantee a response from us in the Issues.
- We are no longer accepting pull requests.
Optional Use bower to retrieve package
bower install angular-rope --save
Include angular module
angular.module('platanus.rope')
The general idea is to define tasks and then chain them to produce different action sequences.
next
is used to chain tasks, promises, functions and values. To start a new chain call rope.next
.
rope.next('hello') // values
.next(someservice.willDoSomething) // tasks
.next($timeout(something)) // promises
.next(function(_value) { // functions
return _value + 1;
});
Steps in a chain are executed sequentially.
Try to use tasks whenever posible, tasks are properly isolated and will prevent unwanted code to be run before is needed.
You define tasks by using task factories, tasks factories are created using the rope.task
method with a task handler. Tasks factories will generate tasks bound to calling context and arguments. Tasks can be passed to every chain operation, the task handler will only be called when the task is executed and it will be given the attributes and context used to invoke the factory.
If access to the previous task/promise value is required, then the handler must return a function that will inmediatelly be called with the value as unique argument and in the same context as the handler.
Its recommended to use a common prefix (like 'will') for tasks to differentiated them from methods.
var service = {
otherMethod: function() {
return true;
},
willAppend: rope.task(function(_someArg) {
return function(_last) { // if a function is returned, it is called inmediatelly with last value.
if(this.otherMethod()) { // reference to this is maintained
return _last + ' ' + _someArg;
}
}
})
};
rope.seed('hello')
.next(service.willAppend('world'))
.next(function(_val) { console.log(_val); }) // will output 'hello world'
If an error occurs inside one of the tasks, then following tasks are skipped until error is handled.
rope.next(something) // this is executed
.next(function() { throw 'my bad'; })
.next(somethingElse) // this is not executed
.next(iaraiara); // this is not executed either
handle
is used to catch errors that occur in tasks higher in the chain, handle
can also handle nested chains. Handle follows the same rules than a $q promise rejection handler.
rope.next(something) // this is executed
.next(function() { throw 'my bad'; })
.next(iaraiara) // this is not executed
.handle(function(error) {
console.log(error); // logs 'my bad'
rope.next(childTask); // this is executed
})
.next(lastTask); // this is also executed
always
tasks executed even if a error has ocurred and hast been handled. Always follows the same rules than a $q promise finally handler.
rope.next(function() { throw 'my bad'; })
.always(willAlwaysRun); // this is executed
rope.next(function() { console.log('somthing harmless'); })
.always(willAlwaysRun); // this is also executed
If a chain is created inside a task or function of another chain, then the parent chain will wait the child chain to finish before moving forward.
rope.next(aTasks) // this is executed before child tasks
.next(function(_value) {
rope.next(someChildTask);
})
.next(someParentTask); // this is executed after someChildTask
If an error occurs inside a child chain and is not handled, then it will bubble.
rope.next(aTasks) // this is executed before child tasks
.next(function(_value) {
rope.next(function() { throw 'ball' });
rope.next(someChildTask2); // This is executed because is another chain
})
.handle(function(error) { console.log(error); }); // this will output 'ball'
If more than one child chain is created inside a task, then parent will wait for all chains to complete before moving forward (this uses $q.all
).
rope.next(aTasks) // this is executed before child tasks
.next(function(_value) {
// first child chain
rope.next(child1)
.next(child2);
// second child chain
rope.next(child3);
})
.next(someParentTask); // this is executed after child2 and child3.
seed
can be used if you need to force a value to be considered a value (maybe it has a then method but is not a promise)
rope.seed('hello')
.next(function(value) { console.log(value); }); // this will output 'hello'
rope also provides some flow control chainable methods:
nextIf
, orNextIf
and orNext
can be used to replace conditional control flow structures without leaving the chain or using nested functions.
var retries = 3;
rope.next(confirmDialog)
.nextIf()
// the following will execute if confirmDialog task resolves to true.
.next(willConfirmAction)
.orNextIf(function() { retries == 0; })
// the following will execute if confirmDialog returns false and retries == 0.
.next(function() { retries--; })
.next(willRestart)
.orNext()
// the following will execute in case none of the previous conditions hold.
.next(willRollback)
.end()
.always(willCleanUp) // this will executed always
nextCase
, orNextCase
and orNext
can be used to replace switches without leaving the chain or using nested functions.
rope.next(selectDialog)
.nextCase('launch') // If selectDialog resolves to 'launch'
.next(willLaunch)
.orNextCase('abort') // If selectDialog resolves to 'abort'
.next(willAbort)
.orNext() // In any other case
.next(invalidAction)
.end()
.always(willCleanUp) // this will executed always
exit
can be used to break a chain execution, no matter how deep in conditional blocks it is.
rope.next(selectDialog)
.nextCase('launch') // If selectDialog resolves to 'launch'
.next(willLaunch)
.exit() // nothing else will be executed after this (not even always blocks)
.end()
.always(willCleanUp) // wont get executed.
wait
can be used to introduce a delay in the chain
rope.next(doSomething)
.wait(2000) // wait 2 seconds before executing next task.
.next(doSomethingDelayed);
call
and apply
can be used to execute a method on the last returned value given the method's name. They differ in that apply
will take an array of arguments an pass it as the method arguments and call
will pass each individual argument directly to the method (like javascript's call and apply).
rope.seed({ method: function(_log) { console.log(_log); } })
.call('method', 'im a teapot')
.apply('method', [ 'im a flying toaster' ]);
TODO: loadParentStatus
, loadParentStack
, nextUnless
, orNextUnless
, get
, set
, push
, pop
, `forkEach.