Skip to content

Instantly share code, notes, and snippets.

@kerbyfc
Last active August 29, 2015 14:19
Show Gist options
  • Save kerbyfc/7333ca2018060e16d70a to your computer and use it in GitHub Desktop.
Save kerbyfc/7333ca2018060e16d70a to your computer and use it in GitHub Desktop.
Marionette ItemView behavior to prevent navigation and model changes loss
"use strict"
App.Behaviors.Common ?= {}
###*
* Behavior to prevent data loss while navigation, it should be invoked
* with browser history state changes
*
* @note all passed options should be passed to ConfirmDialog view
* @note you should manually destroy view in #accept & #omit methods
*
* @example primary usage
*
* class ProtectedItemView extends Marionette.ItemView
*
* ...
*
* behaviors:
* Guardian:
* title: "Warning"
* content: "Data wasn't saved. Are you sure ...?"
*
* # Specify the url `scope` to determine situations
* # to do all proper checks with guardian
* #
* urlMatcher: ->
* "/item/#{this.model.id}"
*
* # do things if urlMatcher was changed
* # you can prevent confirmatino by returning false
* #
* needConfirmation: (urlPath) ->
* this.navigateAfterConfirm = urlPath
* console.log "allow routing"
* return false
*
* omit: (urlPath) ->
* console.log "data wasn't modified"
* this.destroy()
*
* reject: ->
* console.log "data was modified,
* but view close was rejected by user"
*
* accept: ->
* console.log "data was modified,
* and user confirmed view close"
* this.model.rollback()
* App.vent.trigger "nav", this.navigateAfterConfirm
*
* always: ->
*
* ...
*
###
class App.Behaviors.Common.Guardian extends Marionette.Behavior
ui:
inputs : "input, textarea, select"
name : "[name='DISPLAY_NAME']"
events:
"change @ui.inputs" : "_changed"
"keyup @ui.inputs" : "_changed"
modelEvents:
"change" : "guard"
"rollback" : "cleanup"
###*
* Protected options (callbacks), that shouldn't been computed
* for Confirm dialog constructor
* @type {Array}
###
callbacks: [
# callbacks to be passed to Confirm dialog
"accept"
"reject"
"always"
# business logic callbacks
"needConfirmation"
"omit"
]
###*
* Default behavior options
* @note All functions will be called (by _.result) in view scope!
* @type {Object}
###
defaults:
###*
* Computed property to examine guardian necessity while route changes
* @property {String|RegEx|Function} urlMatcher
###
urlMatcher: false
###*
* Sign to determine if view should be protected instantly
* @property {Boolean} initial
###
initial: false
###*
* Confirmation dialog title
* @property {String|jQuery|Function} title
###
title: ->
@t "global.edit"
###*
* Confirmation dialog content
* @property {String|jQuery|Function} content
###
content: ->
"#{@t 'global.cancel_success'}?"
###*
* Make unique cache key with entity class name and it's id
* (used as key in local storage)
* @property {String|Function} key
###
key: ->
entity = @model.constructor.name.replace /[A-Z]/g, (letter) ->
":" + letter.toLowerCase()
"#{entity.slice 1}:#{@model.id}"
###*
* Do things if urlMatcher was changed
* @property {Boolean|Function} needConfimation confirmation needfull(less)
* @note you can prevent confirmation by returning false (omit will not be invoked)
###
needConfirmation: true
###*
* Callback to be invoked if guardian logic wasn't affected
* @function
###
omit: -> null
###*
* Do things anyway after confirmation (unless omit)
* @function
###
always: -> null
###*
* Confirmation acceptance callback
* @function
* @note will be wrapped by guardian
* @see #approveNavigation
###
accept: -> null
###*
* Confirmation rejection callback
* @function
###
reject: -> null
initialize: ->
# check view takes the model
unless @view.model ?= @view.options.model
throw new Error "Guardian cant find `model` in view or view options"
# restore model state from cache if it exists
# this action will activate data loss protection
if cache = localStorage.getItem @_invoke "key"
@view.model.set JSON.parse cache
else
if @_invoke "initial"
@guard()
###*
* Cleanup with behavior (or view) destruction
###
destroy: ->
@cleanup()
super
###########################################################################
# PRIVATE
###*
* Compute option value by key
* @note computing is in view scope!
* @param {String} key - option key
* @param {Array} args... arguments
* @return {Any}
###
_invoke: (key, args...) ->
if opt = @options[key]
if _.isFunction opt
opt.apply @view, args
else
opt
else
null
###*
* Handle inputs changes
* @param {Event} e
###
_changed: (e) =>
@view.model.set Backbone.Syphon.serialize @view
###########################################################################
# PUBLIC
###*
* Patch view with method to be invoked on navigation
* (use view as facade)
###
override: =>
@originCond ?= @view.approveNavigation or -> true
@view.approveNavigation = @approveNavigation
###*
* Mark model as protected and cache its data
###
guard: =>
@override()
# cache data
localStorage.setItem @_invoke("key"), JSON.stringify @view.model.toJSON()
# register guardian
unless @view.model.guardian
@view.model.guardian = this
@view.trigger "guardian:activate"
###*
* Replacement of proper view method
* if view model data needs to be protected
* it will compute options and instantiate
* confirm dialog with them
* @return {Boolean} destroy decision
###
approveNavigation: =>
fragment = Backbone.history.fragment
# check if url change was in acceptable scope
if fragment.match(@_invoke "urlMatcher")? or
# needConfirmation hook can cancel confirmation by returning false
@_invoke("needConfirmation", fragment) is false
# permit routing
return true
# check if model is protected by guardian
if @view.model?.guardian is this
# compute options
opts = _.reduce @options, (acc, opt, key) =>
acc[key] = if key in @callbacks
_.isFunction(opt) and opt.bind(@view) or opt
else
@_invoke key
acc
, {}
# show confirmation dialog
App.Helpers.confirm _.extend {}, opts,
accept: =>
@cleanup()
# apply buisness logic
@_invoke "accept", arguments...
# prevent
false
else
@cleanup()
# apply buisness logic
@_invoke "omit"
# permit routing
true
###*
* Revert view destroy condition, cleanup model cache
* and stop model protection
###
cleanup: =>
# cleanup view
@view.approveNavigation = @originCond
delete @originCond
# clean cache
localStorage.removeItem @_invoke "key"
# remove guardian
if @view.model?.guardian
delete @view.model.guardian
@view.trigger "guardian:deactivate"
"use strict"
require "behaviors/common/guardian.coffee"
module.exports = class ReportView extends Marionette.ItemView
template: "reports/report"
ui:
save : "[data-action='save']"
exec : "[data-action='save-n-execute']"
cancel : "[data-action='cancel']"
name : "[name='DISPLAY_NAME']"
events:
"click @ui.save" : "_save"
"click @ui.exec" : "_saveAndExec"
"click @ui.cancel" : "_cancel"
behaviors:
Guardian:
initial: true
title: ->
action = @model.isNew() and 'add' or 'edit'
@t "reports.report.#{action}_title"
urlMatcher: ->
"reports/#{@model.id}"
content: ->
@t "reports.cancel_confirm"
needConfirmation: (url) ->
@model.navOnDestroy = url
@markAsActive()
accept: ->
@model.rollback()
# state was changed by activation another node in tree
if @model.navOnDestroy
App.vent.trigger "nav", @model.navOnDestroy
onShow: ->
@ui.name.focus().select()
onDestroy: ->
if @model.isNew()
_.defer =>
@model.destroy()
serializeData: ->
_.extend super, isNew: @model.isNew()
###########################################################################
# PRIVATE
###*
* Handle cancel button click
* @param {Event} e
###
_cancel: (e) ->
e.preventDefault()
App.vent.trigger "nav:back", "reports"
###*
* Save report
* @param {Event} e
###
_save: (e) =>
e.preventDefault()
@save()
###*
* Save and then execute report
* @param {Event} e
###
_saveAndExec: (e) =>
e.preventDefault()
@save().done @exec
###########################################################################
# PUBLIC
###*
* Save report
* @return {jQuery.Deferred}
###
save: =>
data = Backbone.Syphon.serialize @
data.IS_PERSONAL = +data.IS_PERSONAL
isNew = @model.isNew()
if isNew
@model.unset "QUERY_REPORT_ID", silent: true
@model.save data,
wait: true
success: =>
App.vent.trigger "reports:report:save", @model, isNew
error: =>
if isNew
@model.set QUERY_REPORT_ID: "new"
# TODO show errors
###*
* Run report
###
exec: =>
@model.execute()
###*
* Make active proper tree node
###
markAsActive: =>
App.reqres.request "reports:tree:set:active:node", "report:#{@model.id}", true,
noEvents: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment