Skip to content

Instantly share code, notes, and snippets.

@dmonagle
Last active December 12, 2015 12:39
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmonagle/4773420 to your computer and use it in GitHub Desktop.
Save dmonagle/4773420 to your computer and use it in GitHub Desktop.
Ember mixins for CRUD functionality.
Ember.EditController = Ember.Mixin.create(
saveError: false
saveInvalid: false
isEditing: false
# Set associations to be associations of the content. These will then be checked for validity on save
# and all of the flags, such as isDirty and isLoaded, will take these associations into consideration.
#
# Eg: A user may have an address model which is edited within the same transaction.
# In this case you would put:
# associations: ['address']
#
associations: []
# Build an array of models that we will check for things such as isSaving, isLoaded, isDirty etc...
models: (->
$.map($.merge(['content'], @get('associations')), (value, i) =>
@get(value)
)
).property('content', 'associations')
# This will be called from the edit action, or can be called from a setupController on a route.
startEdit: ->
unless @get('isEditing')
@set('isEditing', true)
# This action exists to be called from an edit view
edit: ->
@startEdit()
this.send('editing', @get('content'))
cleanup: ->
@set('isEditing', false)
@set('saveError', false)
@set('saveInvalid', false)
@set('errors', null)
if @get('isDirty') and !@get('isDeleted')
@get('transaction').rollback()
cancel: ->
@cleanup()
# Send action to the router
@send("cancelled", @get('content'))
resetTransaction: ->
@get('models').forEach((model) =>
if model.get('isError') or not model.get('isValid')
if model.get('id')
# Horrible force the model state back to loaded.updated so it looks like an object loaded from the server
# ready to be committed.
model.get('stateManager').transitionTo('loaded.updated.uncommitted')
else
# Horrible force the model state back to created so it looks like an new object ready
# to be committed again.
# For some reason, model.isDirty is false now and I don't know how to get it to be dirty again.
model.get('stateManager').transitionTo('loaded.created.uncommitted')
)
save: ->
# Check before save
if typeof @beforeSave == 'function'
return unless @beforeSave()
# Remove the errors
@set('errors', [])
@set('saveError', false)
@set('saveInvalid', false)
# Observe the record until it is finished saving
@addObserver('isSaving', this, ->
return if @get('isSaving')
@set('saveError', @get('isError'))
unless @get('isError') or @get('isInvalid')
# The content is valid, we can remove the observer
@removeObserver('isSaving')
# If this is a new record, we want to wait until the id is populated before firing the saved event
unless @get('content.id')
@addObserver('content.id', this, ->
# We have an id, remove the observer and fire the event
@removeObserver('content.id')
@set('isEditing', false)
@send("saved", @get('content'))
)
else
# This is an existing record, we can fire the saved event
@set('isEditing', false)
@send("saved", @get('content'))
else
# The record is either in error or invalid, we will reset the transaction.
@removeObserver('isSaving')
@resetTransaction()
)
# Now lets try to save
@get('transaction').commit()
isSaving: (->
@get('models').filterProperty('isSaving', true).get('length') > 0
).property('models.@each.isSaving')
isDirty: (->
@get('models').filterProperty('isDirty', true).get('length') > 0
).property('models.@each.isDirty')
isLoaded: (->
@get('models').filterProperty('isLoaded', false).get('length') == 0
).property('models.@each.isLoaded')
isError: (->
return false if @get('saveError')
@get('models').filterProperty('isError', true).get('length') > 0
).property('models.@each.isError')
isValid: (->
# Because we are forcing the model to become unsaved again, it clears the isValid flag. However certain
# things (namely ember-bootstrap) rely on this flag to show validation errors, so if we have had an invalid
# save we return false.
return false if @get('saveInvalid')
@get('models').filterProperty('isValid', false).get('length') == 0
).property('models.@each.isValid')
isInvalid: (->
!@get('isValid')
).property('isValid')
)
Ember.DestroyController = Ember.Mixin.create(
delete: ->
@addObserver('content.isDeleted', this, ->
if @get('content.isDeleted')
@removeObserver('content.isDeleted')
@send("deleted", @get('content'))
)
@get('content').deleteRecord()
@get('transaction').commit()
)

I am using this method in a couple of projects at the moment. I don't believe for a second that this is the way things are ultimately envisioned to be done in ember-data, but this is what I have working in a project at the moment.

At this point in time, using this mixin, the controller is able to edit and save a record and recover if the attempted save is not valid or it errors (two different states). In the case of invalid records, the errors are available to the view, and if you use ember-bootstrap, the validations will appear inline on the fields.

I still have questions about the transaction system - after the record fails to save, it will be in the defaultTransaction rather than in the transaction that the model was loaded/created in. I'm not sure I like that, but this is working for the time being and I hope to refine it as more ember-data functionality comes to light.

Example Route using this controller:

# /product/:product_id/edit
App.ProductEditRoute = Ember.Route.extend(
  renderTemplate: ->
    @render('products/edit', {into: 'products/product', outlet: 'product', controller: 'product'})

  deactivate: ->
    @controllerFor('product').cleanup()

  events:
    saved: (product) ->
      @transitionTo('product.index', product)
    cancelled: (product) ->
      @transitionTo('product.index', product)
    deleted: (product) ->
      @transitionTo('products.index')
)
@wulftone
Copy link

Looks great, but being a total noob to ember, how do I use it? : \

@Bouke
Copy link

Bouke commented Mar 5, 2013

Thanks, I've been puzzled by ember-data's transactions but this makes sense. Hopefully future releases of ember-data replaces such constructs. Regarding the EditRoute, it wouldn't work for me. I think it should use ember's events for sending the cancel event? Or am I missing something here?

Ember.EditRoute = Ember.Mixin.create
  # binding on `exit` for now, `exit` is private and should be changed to
  # `deactivate`, however it is not yet available in ember-pre4 version.
  exit: ->
    @_super()
    this.controllerFor(this.get('routeName')).send 'cancel'

@dmonagle
Copy link
Author

dmonagle commented Mar 7, 2013

Hi Bourke,

Yes I'm not sure what I was thinking when I wrote that route. It caused some trouble like that. Realistically, I think case specific what you do here. The cancel event should be called when a user has verified that they wish to cancel. I think you should implement your own deactivate to handle this situation. I've updated the gist to do the absolute basics without using the cancel action at all.

@vijayj
Copy link

vijayj commented Sep 27, 2013

We are using the latest ember data 1.0 beta and the resetTransaction method had to be modified to this. Note that their is no state manager and we don't write the entire state path

  resetTransaction: function() {   //<--  we call is resetModelError in our code
    if (this.get('isError') || !this.get('isValid')) {
      if (this.get('id')) {
        return this.transitionTo('saved');
      } else {
        return this.transitionTo('uncommitted');
      }
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment