Skip to content

Instantly share code, notes, and snippets.

@ggoodman
Created December 15, 2011 23:53
Show Gist options
  • Save ggoodman/1483563 to your computer and use it in GitHub Desktop.
Save ggoodman/1483563 to your computer and use it in GitHub Desktop.

Lumbar.View

TRY ME OUT AT: http://bl.ocks.org/1483563

As is evident from the name chosen for this library - lumbar - this has been heavily influenced by the incredible Backbone.js. In fact, the full lumbar framework is designed to sit on top of Backbone.js for the time being. This is because the model, collection and router components of Backbone.js are 10x better than anything I can write.

I wrote this because I'm not a major fan of the way Backbone.js' View system works. This system is very similar, but has a few key differences. Please refer to the last section below.

Caveats

This library probably won't suit your needs as using it in anything but Coffee-Script would be an exercise in self-mutilation. Furthermore, documentation simply does not exist.

Suggested usage

lumbar.view "myapp.article",
  mountPoint: "<article></article>"
  template: ->
    h1 @title
    p @content
    div ".about", ->
      strong "Author:"
      text @author

myapp.root = new class extends lumbar.View,
  mountPoint: "body" # This takes over the body
  template: ->
    header ->
      h1 "MyApp title"
      section "#articles", ->
        $v("myapp.article", article) for article in $m("myapp.articles")

# Now, when you're ready, you can render the whole shebang
myapp.root.render()

Public API

lumbar.view(name, definition)

Define a view for as name, using the definition hash to augment the view's prototype.

Returns the generated view class.

# Default usage:
lumbar.view "app.section",
  # The jQuery expression to find or create the containing element to be bound to @$
  mountPoint: "<div/>"

  # Value/object to be passed as the second argument at construction
  mountArgs: null

  # Method to be called on @$ in order to add the rendered template to the container
  mountMethod: "html" 

  # The template for the view in CoffeeKup (https://github.com/mauricemach/coffeekup)
  template: -> 

  # A hash of "event [selector]": callback | "event" pairs
  events: null 

  # The function that can be overwritten to handle construction
  initialize: -> 

lumbar.view(name)

Retrieve a the lumbar.View class that was previously defined.

# Default usage:
lumbar.view("app.section")

Public methods of lumbar.View instances

lumbar.View::render(locals)

Render a template with the supplied locals available under the @ namespace from within the template (see CoffeeKup documentation).

Special functions available inside view templates

$v(viewName, [locals])

Create an instance of the supplied view and render it with locals inline.

$m(modelPath)

Recursively look through globally defined variables according to a "." separated path. This also handles traversing on objects that supply a "get(key)" method such as Backbone.js.

This will eventually also tell the parent view that the view depends on the model. This will allow sub-views to intelligently refresh by listening to chances to their dependent models.

Divergence from Backbone.js

  • Templates are defined in-line in the template using CoffeeKup.
  • The @$ property of views refers to a jQuery collection, not a namespaced jQuery.find(). This is similar, but not the same as Backbone.js' View.el.
  • Views can be defined using lumbar.view for referencing within view templates.
  • They are written in Coffee-Script. Caveat emptor.
  • They are built to reflect the hierarchical nature of typical webapps.
# Define model where we organize our model hierarchically in a top-level namespace
window.app =
people: [{name: "ggoodman"}, {name: "pgoodman"}]
# Define a view that
lumbar.view "app.name",
mountPoint: "<li>" # Define our mountPoint which is what is passed to jQuery to create the markup, if-needed
template: ->
text "Name: #{@name}"
root = new class extends lumbar.View
mountPoint: "body" # Attach this model to the body
template: ->
h1 "Hello world"
ul ->
$v("app.name", person) for person in $m("app.people")
$ -> root.render()
<!doctype html>
<html>
<head>
<title>Lumbar.View - Templates in 100% Coffee-Script</title>
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
<script src="http://code.jquery.com/jquery-1.7.1.js"></script>
<script src="https://github.com/ggoodman/gister/raw/rewrite/lib/underscore.js"></script>
<script src="https://github.com/ggoodman/gister/raw/rewrite/lib/coffeekup.js"></script>
<script src="https://github.com/ggoodman/gister/raw/rewrite/lib/coffee-script.js"></script>
<script type="text/coffeescript" src="lumbar.view.coffee"></script>
<script type="text/coffeescript" src="demo.coffee"></script>
</head>
<body>
</body>
</html>
window.lumbar =
version: "0.0.1"
window.console ||=
log: (args...) ->
alert(args.join(" "))
class lumbar.Emitter
bind: (event, listener) =>
@listeners ?= {}
(@listeners[event] ?= []).push(listener)
@
trigger: (event, args...) =>
@listeners ?= {}
listener(args...) for listener in @listeners[event] if @listeners[event]
@
lumbar.views = {}
lumbar.view = (name, definition) ->
if definition?
cls = class extends lumbar.View
_.extend cls.prototype, definition
lumbar.views[name] = cls
lumbar.views[name]
lumbar.view.stack = []
lumbar.view.top = -> lumbar.view.stack[lumbar.view.stack.length - 1]
class lumbar.View extends lumbar.Emitter
mountPoint: "<div></div>"
mountArgs: null
mountMethod: "html"
template: ->
compiled: null
initialize: ->
sequence: do ->
sequence = 0
(increment = true) -> if increment then sequence++ else sequence
constructor: ->
@dependentModels = []
@dependentViews = []
@initialize(arguments...)
create: (mountPoint = @mountPoint, mountArgs = @mountArgs) ->
args = [mountPoint]
args.push(mountArgs) if mountArgs
@$ = $.apply($, args)
@
destroy: =>
@clearDependents()
@$.remove() if @$
@
clearDependents: ->
while @dependentModels.length then @dependentModels.pop().unbind("all")
while @dependentViews.length then @dependentViews.pop().destroy()
@attachQueue = []
@
# Attach the child to this view and return a temporary mountPoint
attachDependentView: (view) ->
id = "mount-#{@sequence()}"
@dependentViews.push(view)
@attachQueue.push(id: id, view: view)
id
attachDependentModel: (model) ->
if model.bind then model.bind "all", _.throttle(@render, 100)
@dependentModels.push(view)
@
createCompileOptions: ->
locals:
parent: @
createRenderOptions: (locals = {}) ->
_.extend locals,
parent: @
hardcode:
$v: (viewName, locals = {}) ->
unless viewClass = lumbar.view(viewName)
throw new Error("View not defined: #{viewName}")
view = new viewClass()
view.render(locals)
id = lumbar.view.top().attachDependentView(view)
div id: id, "Mount point"
$m: (modelPath) ->
path = modelPath.split(".")
model = window
for step in path
model = model[step] or model.get(step)
model
bindEvents: ->
if @events
for mapping, callback of @events
[event, selector...] = mapping.split(" ")
selector = selector.join(" ")
callback = if _.isFunction(callback) then callback else =>
@trigger callback
if event and selector then @$.delegate selector, event, callback
else if event then @$.on event, callback
@
generateMarkup: (locals = {}) ->
@markup = CoffeeKup.render(@template, @createRenderOptions(locals))
@trigger "generate", @
@
attachChildViews: ->
while @attachQueue.length
{id, view} = @attachQueue.pop()
$("##{id}", @$).replaceWith(view.$)
view.trigger "attach", view
@
render: (locals = {}) =>
# Clear dependent models and views, removing listeners and DOM elements
@clearDependents()
# Create the container that will hold the view's DOM
@create() unless @$
lumbar.view.stack.push(@)
# Render the generated markup into the container
@$[@mountMethod] @generateMarkup(locals).markup
lumbar.view.stack.pop()
@trigger "mount", @
# Child views have been rendered as empty divs; replace those with the markup
@attachChildViews()
@trigger "render", @
@bindEvents()
@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment