Last active
August 29, 2015 14:19
-
-
Save kerbyfc/7333ca2018060e16d70a to your computer and use it in GitHub Desktop.
Marionette ItemView behavior to prevent navigation and model changes loss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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