In this episode, this Rails view code was shown as an example of tension between the patterns “Tell, Don’t Ask” and “Single Responsibility Principle”:
<% if @account.invitations_remaining? %>
<p>
You have
<span class="count">
<%= @account.invitations_remaining %>
</span>
remaining.
<%= link_to "Invite?". new_invitation_path %>
</p>
<% else %>
<p>
You have no invitations remaining.
<%= link_to "Upgrade?", edit_plan_path(@account.plan) %>
<p>
<% end %>
I’m curious whether folks feel that a JavaScript-based view+template solution helps respect both patterns. See the following Backbone example.
invitations-remaining-view.coffee:
define [
"backbone"
"./invitations-remaining-view.tmpl"
], (
Backbone
template
) ->
class InvitationsRemainingView extends Backbone.View
render: ->
super
@$el.html template(invitations_remaining: @model.get("invitations_remaining"))
this
shouldBeDisplayed: ->
@model.get("invitations_remaining") > 0
invitations-remaining.handlebars:
<p>
You have <span class="count">{{ invitations_remaining }}</span> remaining.
<a href="/invitations/new">Invite?</a>
</p>
no-invitations-remaining-view.coffee:
define [
"backbone"
"./no-invitations-remaining-view.tmpl"
], (
Backbone
template
) ->
class NoInvitationsRemainingView extends Backbone.View
render: ->
super
@$el.html template(plan_id: @model.get("plan_id"))
this
shouldBeDisplayed: ->
@model.get("invitations_remaining") == 0
no-invitations-remaining-view.handlebars:
<p>
You have no invitations remaining.
<a href="/plans/{{ plan_id }}/edit">Upgrade?</a>
<p>
There might be a wrapper view like invitations-view.coffee:
define [
"backbone"
"./invitations-view.tmpl"
], (
Backbone
template
) ->
class InvitationsView extends Backbone.View
initialize: ->
super
@_removeDisplayPolicy()
render: ->
@$el.html template
@_renderDisplayPolicy()
this
remove: ->
super
@_removeDisplayPolicy()
_renderDisplayPolicy: ->
unless @displayPolicy?
@displayPolicy = new SectionDisplayPolicy({
el: @$('.sections')
model: @model
sectionViews:
'.invitations-remaining-container': InvitationsRemainingView
'.no-invitations-remaining-container': NoInvitationsRemainingView
}).render()
_removeDisplayPolicy: ->
@displayPolicy?.remove()
@displayPolicy = undefined
invitations-view.handlebars:
<article class="sections">
<div class="invitations-remaining-container"></div>
<div class="no-invitations-remaining-container"></div>
</article>
That SectionDisplayPolicy object is the secret sauce. Different JavaScript frameworks may have something like this built in, or there may be some great plugins that do something similar, but for the purpose of example so you can read an implementation, here’s one that @seangriffin and I wrote:
define [
"lodash"
"backbone"
], (
_
Backbone
) ->
# SectionDisplayPolicy is responsible for managing the sections of a page by
# rendering and removing them from the DOM.
class SectionDisplayPolicy extends Backbone.View
initialize: (options) ->
@sectionViews = _.mapValues options.sectionViews, (View) =>
new View(model: @model)
@hiddenSections = _.values(@sectionViews)
@displayedSections = []
@listenTo @model, "change", @_updateSections
render: ->
@_updateSections()
this
remove: ->
@stopListening()
_.each @displayedSections, (section) ->
section.remove()
this
_updateSections: =>
@_removeSections()
@_renderSections()
_renderSections: ->
_.each @hiddenSections, (section) =>
if section.shouldBeDisplayed()
@_containerFor(section).html(section.render().el)
@displayedSections.push(section)
@hiddenSections = _.without(@hiddenSections, section)
_removeSections: ->
_.each @displayedSections, (section) =>
unless section.shouldBeDisplayed()
section.remove()
@hiddenSections.push(section)
@displayedSections = _.without(@displayedSections, section)
_containerFor: (target) ->
selector = _.findKey(@sectionViews, target)
@$(selector)
This style has many small objects that use the shouldBeDisplayed interface defined by the SectionDisplayPolicy object to contain the knowledge of when to display themselves based on the model’s data.
There is no conditional logic in the template (no logic at all, actually), no conditional methods defined on the model (it can be a dumb data container) and I believe it does not violate “Single Responsibility Principle” or “Tell, Don’t Ask”, but I’d love to hear others’ opinions.
There is more code to write in this version, but the majority of the logic is in a library object, SectionDisplayPolicy, which is very versatile. We’re using it heavily in our current Backbone app. The rest of the code is very boiler-plate-y and straightforward. It’s using Require.js to define the dependencies and the bare minimum necessary for a Backbone.View to do its work.