One of the more powerful feature of Ruby is adding a modules methods to an existing class using either extend and include. For example:
module Sortable
extend ActiveSupport::Concern
included do
scope (:ordered), -> { order(:priority) }
def self.sort(params)
self.transaction do
params.each_with_index do |id, index|
self.find(id).update(priority: index.next)
end
end
end
end
end
class Task < ActiveRecord::Base
include Sortable
end
rails console
Task.sort([1,3,2])
# BEGIN
# UPDATE tasks SET priority = 1 WHERE id = 1;
# UPDATE tasks SET priority = 2 WHERE id = 3;
# UPDATE tasks SET priority = 3 WHERE id = 2;
# COMMIT
Similar functionality is also available in JavaScript / CoffeeScript, but requires a bit of setup.
To get started create the following file mixins/base/mixin.js.coffee:
App.Mixin =
extend: (mixin) ->
for key, value of mixin when key not in ['extend','include']
@[key] = value
mixin.extended?.apply(@)
return @
include: (mixin) ->
for key, value of mixin when key not in ['extend','include']
@::[key] = value
mixin.included?.apply(@)
return @
This 'mixin' can be added with Underscore JS using extend to Backbone routers, models, collections, or views:
class App.Router extends Backbone.Router
present: (view) ->
App.view.remove() if App.view?
App.view = view
$('#container').html(view.el)
view.render()
_.extend App.Router, App.Mixins
class App.View extends Backbone.View
_.extend App.View, App.Mixins
class App.Model extends Backbone.Model
_.extend App.Model, App.Mixins
class App.Collection extends Backbone.Collection
_.extend App.Collection, App.Mixins
Now that the extend and include methods have been added to our basic models, collections and views they can be used to extract out common code. To start create a basic task app by:
Adding app/assets/javascripts/application/models/task.js.coffee:
class App.Models.Task extends App.Model
defaults:
notes: null
url: -> if @id? then "/tasks/#{@id}" else "/tasks"
Then app/assets/javascripts/application/routers/tasks.js.coffee:
class App.Routers.Tasks extends App.Router
routes:
"tasks/new" : "new"
new: ->
model = new App.Models.Tasks
view = new App.Views.Tasks.New(model: model)
view.present()
Then app/assets/javascripts/application/views/tasks/new.js.coffee:
class App.Views.Tasks.New extends App.View
template: HanldebarsTemplates['tasks/new']
render: ->
@$el.html(@template())
Then app/assets/javascripts/application/templates/tasks/new.hamlbars:
%form
.alert
.field.notes
%input{ type: 'text', name: 'notes' }
.error
%button.save Save
%span.processing{ style: 'display:none' } ...
At this point a form helper can be added app/assets/javascripts/application/helpers/form.js.coffee:
class App.Helpers.Form
constructor: (view) ->
@view = view
$: (selector) ->
@view.$(selector)
submit: ->
@process() unless @processing()
process: ->
@reset()
unless @save()
for field, message of @model.errors
@$(".#{field} .error").text(message)
return
reset: ->
@$('.error').empty()
@$('.processing').show()
return
save: ->
@view.model.save @parameters(),
success: _.bind(@success, @)
error: _.bind(@error, @)
parameters: ->
parameters = {}
for attr in @view.attrs
parameters[attr] = @$("[name='#{attr}']").val()
return parameters
success: (model, response, options) ->
@$('.processing').hide()
@$('.alert').text("Hurrah, your changes have been saved!")
return
error: (model, response, options) ->
@$('.processing').hide()
@$('.alert').text("Whoops, it looks like something went wrong!")
return
processing: ->
@$('.processing').is(":visible")
Finally the mixin can be added app/assets/javascripts/application/mixins/form.js.coffee:
App.Mixins.Form =
events:
'submit': 'submit'
submit: (event) ->
event.preventDefault()
event.stopPropagation()
new App.Helpers.Form(@).submit()
return
Now the view can be modified to include the mixin app/assets/javascripts/application/views/tasks/new.js.coffee:
class App.Views.Tasks.New extends App.View
@include App.Mixins.Form
template: HandlebarsTemplates['tasks/new']
attrs: ['notes']
render: ->
@$el.html(@template())
And that's it! For every subsequent form that is added to the project all that needs to be done is include the App.Mixins.Form.
Mixins are fantastic, but do come with a few caveats. In this example, the majority of code was extracted into a helper (breaking the Law of Demeter). However, this is a common practice for mixins to fully encapsulate functionality. This also ensures that the objects prototype isn't polluted and provides a form of name spacing to the helper methods.