Skip to content

Instantly share code, notes, and snippets.

@fracz
Last active June 11, 2016 12:03
Show Gist options
  • Save fracz/71dfb4b3e39e5e893d2e4e2884d94de3 to your computer and use it in GitHub Desktop.
Save fracz/71dfb4b3e39e5e893d2e4e2884d94de3 to your computer and use it in GitHub Desktop.
ReveritbleAction for Angular implementation (allows to undo an operation before the request is sent to the backend).

Angular Revertible Action

Example use:

angular.module('xxx').controller 'TreasureController', ($scope, treasureId, RevertibleAction, Restangular) ->

  Restangular.one('treasure', treasureId).get().then (treasure) ->
    $scope.treasure = treasure

  $scope.deleteTreasure = ->
    treasureBackup = $scope.treasure
    delete $scope.treasure
    RevertibleAction(
      'You have deleted your treasure' # the message to display in the undo notification
      Restangular.one('treasure', treasureId).remove # the callback to execute after confirmation (clicking OK or after a delay)
    )
    # now, the notification appears and waits for reaction
    .then ->
      # executes after successfull callback
      alert 'Your treasure is gone!'
    .catch ->
      # executes if callback failed or the action has been undone
      alert 'Your treasure removal is undone!'
      $scope.treasure = treasureBackup

Dependencies:

<div class="ui-notification revertible-action-toast">
<h3>{{ operationName }}</h3>
<div class="btn-group">
<button class="btn btn-link btn-ok"
ng-click="kill()">
OK
</button>
<button class="btn btn-link btn-undo"
ng-click="revertAction()">
UNDO
</button>
</div>
</div>
.revertible-action-toast {
.btn-group {
float: right;
padding: 3px 5px 0 0;
.btn {
color: white;
text-decoration: none;
&.btn-ok {
.opacity(.8);
}
&.btn-undo {
font-weight: bold;
}
&:hover {
background: fade(white, 10%);
}
&:active {
outline: 0;
}
}
}
}
angular.module('xxx').factory 'RevertibleAction', (Notification, $q) ->
OPERATIONS_TO_UNDO = []
operation = (operationName, callback) ->
defer = $q.defer()
resolved = false
resolution = -> $q.when(callback?()).then(defer.resolve, defer.reject) if not resolved
OPERATIONS_TO_UNDO.push(resolution)
Notification.warning
delay: 7000
positionY: 'bottom'
closeOnClick: false
templateUrl: 'revertible-action-toast.html'
onClose: resolution
.then (toastScope) ->
toastScope.operationName = operationName
toastScope.revertAction = ->
defer.reject()
defer.promise.finally ->
resolved = yes
toastScope.kill()
OPERATIONS_TO_UNDO.splice(OPERATIONS_TO_UNDO.indexOf(resolution), 1)
defer.promise
operation.flush = ->
$q.all((resolution() for resolution in OPERATIONS_TO_UNDO))
existingBeforeUnload = window.onbeforeunload
window.onbeforeunload = ->
message = existingBeforeUnload?()
if not message and OPERATIONS_TO_UNDO.length > 0
operation.flush()
message = 'Some operations are unfinished.'
message or undefined
operation
describe 'RevertibleAction', ->
beforeEach ->
module('xxx')
inject (@RevertibleAction, @Notification, $q) ->
@notificationScope = {}
@operation = jasmine.createSpy().and.returnValue('THE RESULT')
@operationThen = jasmine.createSpy()
@operationCatch = jasmine.createSpy()
spyOn(@Notification, 'warning').and.returnValue($q.when(@notificationScope))
@RevertibleAction('', @operation).then(@operationThen, @operationCatch)
@args = @Notification.warning.calls.mostRecent().args[0]
afterEach(-> window.onbeforeunload = ->)
it 'displays notification', ->
expect(@Notification.warning).toHaveBeenCalled()
expect(@operation).not.toHaveBeenCalled()
it 'executes operation when the notification is hidden because of timeout', ->
@args.onClose()
expect(@operation).toHaveBeenCalled()
it 'does not execute operation when is cancelled by user', ->
inject(($rootScope) -> $rootScope.$apply())
@notificationScope.revertAction()
inject(($rootScope) -> $rootScope.$apply())
@args.onClose()
expect(@operation).not.toHaveBeenCalled()
it 'executes operation on flush', ->
@RevertibleAction.flush()
expect(@operation).toHaveBeenCalled()
it 'resolves the promise when the operation is confirmed', ->
@RevertibleAction.flush()
inject(($rootScope) -> $rootScope.$apply())
expect(@operationThen).toHaveBeenCalledWith('THE RESULT')
it 'rejects the promise when the operation is cancelled', ->
inject(($rootScope) -> $rootScope.$apply())
@notificationScope.revertAction()
inject(($rootScope) -> $rootScope.$apply())
expect(@operationThen).not.toHaveBeenCalled()
expect(@operationCatch).toHaveBeenCalled()
angular.module('xxx').factory 'RevertibleArrayItemRemoval', (RevertibleAction, $q) ->
(message, removeCallback, array, element) ->
indexOfElement = array.indexOf(element)
array.splice(indexOfElement, 1)
RevertibleAction(message, removeCallback).catch (response) ->
if response?.status isnt 404 # if the element has not been removed yet
array.splice(indexOfElement, 0, element) # then the error is different and the element should be put back into the array
$q.reject(response)
else
response
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment