Skip to content

Instantly share code, notes, and snippets.

@ggoodman
Created January 11, 2012 02:20
Show Gist options
  • Save ggoodman/1592575 to your computer and use it in GitHub Desktop.
Save ggoodman/1592575 to your computer and use it in GitHub Desktop.
Backbone.js TODO app ported to lumbar

GISTER

Gister is a utility for creating, editing and debugging small web pages. It can easily be used to test css and javascript all on the client side.

Gister is hooked into github and so saving your work in gister will save your work to github.

https://github.com/ggoodman/gister

Try clicking Preview above!

Note on username / password

Until github add's a client side OAuth2 flow, I'm forced to require a username / password-based login. Please know that your username / password is being sent over https:// to github and is exchanged for an OAuth2 token. Your username / password is never stored anywhere.

<!doctype html>
<html>
<head>
<title>Lumbar Demo: Todos</title>
<link href="todos.css" media="all" rel="stylesheet" type="text/css"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="http://documentcloud.github.com/underscore/underscore-min.js"></script>
<script src="http://documentcloud.github.com/backbone/backbone-min.js"></script>
<script src="https://github.com/documentcloud/backbone/raw/master/examples/backbone-localstorage.js"></script>
<script src="http://coffeescript.org/extras/coffee-script.js"></script>
<script src="http://coffeekup.org/coffeekup.js"></script>
<script type="text/coffeescript" src="lumbar.coffee"></script>
<script type="text/coffeescript" src="lumbar.model.coffee"></script>
<script type="text/coffeescript" src="lumbar.view.coffee"></script>
<script type="text/coffeescript" src="todos.coffee"></script>
</head>
<body>
</body>
</html>
window.lumbar =
version: "0.0.1"
start: ->
console.log "lumbar.start", arguments...
Backbone.history.start()
_.mixin obj: (key, value) ->
hash = {}
hash[key] = value
hash
class lumbar.Emitter
_.extend lumbar.Emitter.prototype, Backbone.Events
class lumbar.Router extends Backbone.Router
routes: []
constructor: ->
routes = @routes
@routes = []
super()
for route, name of @routes
@route(new RegExp("^#{route}", "i"), name, @[name])
lumbar = window.lumbar
lumbar.Deferred = jQuery.Deferred
class lumbar.Model extends Backbone.Model
@persist: (attribute, filterOrOptions = {}) ->
@persisted ||= {}
@persisted[attribute] = _.extend {},
if _.isFunction(filterOrOptions)
save: filterOrOptions
load: (value) -> value
else _.defaults filterOrOptions,
save: (value) -> value
load: (value) -> value
@
@expose: (attribute, dependentAttributes, valueBuilder) ->
@exposed ||= {}
@exposed[attribute] = {dependentAttributes, valueBuilder}
@
constructor: ->
self = @
oldInit = self.initialize
self.initialize = ->
if self.constructor.exposed
for attribute, {dependentAttributes, valueBuilder} of self.constructor.exposed
for dependency in dependentAttributes
self.bind "change:#{dependency}", (model) ->
args = []
args.push model.get(attr) for attr in dependentAttributes
model.set attribute: valueBuilder.apply(model, args.push(attribute))
oldInit.call(self, arguments...)
super(arguments...)
toJSON: ->
raw = super()
unless @constructor.persisted then raw
else
json = {}
json.id = @id if @id
for attribute, {save} of @constructor.persisted
json[attribute] = save.call(@, raw[attribute], attribute)
json
toViewModel: ->
unless @constructor.exposed then _.clone(@attributes)
else
json = {}
json[attribute] = filter(attribute, raw[attribute]) for attribute, filter of @constructor.exposed
json
__save: (attrs, options = {}) ->
self = @
dfd = new lumbar.Deferred
super attrs, _.extend {}, options,
success: _.bind(dfd.resolve, self)
error: (args...) -> dfd.rejectWith(self, args)
dfd.promise()
__destroy: (options = {}) ->
self = @
dfd = new lumbar.Deferred
super _.extend {}, options,
success: (args...) -> dfd.resolveWith(self, args)
error: (args...) -> dfd.rejectWith(self, args)
dfd.promise()
__fetch: (options = {}) ->
self = @
dfd = new lumbar.Deferred
super _.extend {}, options,
success: (args...) -> dfd.resolveWith(self, args)
error: (args...) -> dfd.rejectWith(self, args)
dfd.done -> self.saved = _.clone(self.attributes )
dfd.promise()
class lumbar.Collection extends Backbone.Collection
__create: (attrs, options = {}) ->
self = @
dfd = new lumbar.Deferred
super attrs, _.extend {}, options,
success: _.bind(dfd.resolve, self)
error: (args...) -> dfd.rejectWith(self, args)
dfd.promise()
__fetch: (options = {}) ->
self = @
dfd = new lumbar.Deferred
super _.extend {}, options,
success: (args...) -> dfd.resolveWith(self, args)
error: (args...) -> dfd.rejectWith(self, args)
dfd.promise()
lumbar = window.lumbar
lumbar.uid = do ->
index = 0
-> "uid-#{+new Date}-#{index++}"
window.log = do ->
repeat = (char, times) ->
str = ""
for i in [0...times] then str += char
str
stack = []
enter: (method, args...) ->
return unless lumbar.view.DEBUG
console.log repeat(".", stack.length) + ">", method, args
stack.push(method)
exit: (args...) ->
return unless lumbar.view.DEBUG
method = stack.pop()
console.log repeat(".", stack.length) + "<", method, args
lumbar.view = (viewName, constructor) ->
log.enter "lumbar.view", arguments...
if constructor
constructor::viewName = viewName
lumbar.view.constructors[viewName] = constructor
log.exit()
lumbar.view.constructors[viewName]
lumbar.view.constructors = {}
lumbar.view.DEBUG = true
lumbar.view.registry = {}
lumbar.view.getRegisteredInstance = (dependent, viewName, locals = {}, args = {}) ->
log.enter "lumbar.view.getRegisteredInstance", arguments...
uid = dependent.uid or dependent.uid = lumbar.uid()
reg = lumbar.view.registry
reg[uid] ||= {}
unless reg[uid][viewName]
unless viewClass = lumbar.view(viewName)
throw new Error("View not defined: #{viewName}")
reg[uid][viewName] = new viewClass(args)
reg[uid][viewName].render(locals)
log.exit reg[uid][viewName]
reg[uid][viewName]
lumbar.view.childViews = {}
lumbar.view.registerChildView = (parentView, childView) ->
lumbar.childViews[parentView.uid] ||= {}
lumbar.childViews[parentView.uid][childView.uid] = childViews
lumbar.view.renderStack = []
lumbar.view.renderStack.peek = (n = lumbar.view.renderStack.length - 1) ->
lumbar.view.renderStack[n]
lumbar.view.renderChildView = (viewName, locals = {}) ->
log.enter "lumbar.view.renderChildView", arguments...
parentView = lumbar.view.renderStack.peek()
view = lumbar.view.getRegisteredInstance(parentView, viewName, locals)
parentView.childViews.push(view)
log.exit """<div id="#{view.uid}">PLACEHOLDER DIV</div>"""
# Return a placeholder div
#div id: view.uid, -> "PLACEHOLDER DIV THAT YOU SHOULDN'T SEE!"
"""<div id="#{view.uid}">PLACEHOLDER DIV</div>"""
lumbar.view.renderIteratedChildView = (viewName, model) ->
log.enter "lumbar.view.renderIteratedChildView", arguments...
parentView = lumbar.view.renderStack.peek()
view = lumbar.view.getRegisteredInstance(model, viewName, model.toViewModel(), model: model)
lumbar.view.registerDependentModel(view, model)
parentView.childViews.push(view)
log.exit """<div id="#{view.uid}">PLACEHOLDER DIV</div>"""
# Return a placeholder div
"""<div id="#{view.uid}">PLACEHOLDER DIV</div>"""
lumbar.view.renderIteratedView = (modelPath, viewName, locals = {}) ->
log.enter "lumbar.view.renderIteratedView", arguments...
collection = lumbar.view.resolveModel(modelPath)
placeholders = ""
collection.each (model) ->
placeholders += lumbar.view.renderIteratedChildView(viewName, model)
log.exit(placeholders)
text placeholders
lumbar.view.registerDependentModel = (view, model) ->
view.rerender ||= ->
view.render(model.toViewModel())
model.unbind "change", view.rerender
model.bind "change", view.rerender
lumbar.view.registerDependentAttribute = (view, model, key) ->
model.unbind "change:#{key}", view.render
model.bind "change:#{key}", view.render
lumbar.view.registerDependentCollection = (view, model) ->
console.log "lumbar.view.registerDependentCollection", arguments...
model.unbind "add", view.render
model.unbind "remove", view.render
model.unbind "reset", view.render
model.bind "add", view.render
model.bind "remove", view.render
model.bind "reset", view.render
lumbar.view.resolveModel = (modelPath) ->
log.enter "lumbar.view.resolveModel", arguments...
parentView = lumbar.view.renderStack.peek()
model = window
segments = modelPath.split(".")
checkSegment = (parentView, model, segment) ->
if model instanceof Backbone.Model
model.uid ||= lumbar.uid()
lumbar.view.registerDependentAttribute(parentView, model, segment)
else if model instanceof Backbone.Collection
model.uid ||= lumbar.uid()
lumbar.view.registerDependentCollection(parentView, model)
for segment in segments
if model[segment] then model = model[segment]
else
checkSegment(parentView, model, segment)
model = model.get(segment)
checkSegment(parentView, model)
log.exit model
model
class lumbar.View extends lumbar.Emitter
@register = (name) -> lumbar.view(name, @)
mountPoint: "<div></div>"
mountOptions: null
mountMethod: "html"
template: ->
initialize: ->
constructor: (args) ->
_.extend(@, args)
@uid = lumbar.uid()
@childViews = []
@initialize(arguments...)
detach: ->
@$.detach if @$
@
attachChildViews: ->
for childView in @childViews
@$.find("##{childView.uid}").replaceWith(childView.$)
@
detachChildViews: ->
while @childViews.length
childView = @childViews.pop()
childView.detach()
@
create: ->
args = [@mountPoint]
args.push(if _.isFunction(@mountOptions) then @mountOptions.call(@) else @mountOptions) if @mountOptions?
@$ = $.apply($, args)
@trigger "create", @
update: ->
@$.prop(if _.isFunction(@mountOptions) then @mountOptions.call(@) else @mountOptions) if @mountOptions?
@trigger "update", @
detach: ->
@$.detach()
@trigger "detach", @
getRenderOptions: (locals) ->
_.extend locals,
parent: @
hardcode:
$v: lumbar.view.renderChildView
$c: lumbar.view.renderIteratedView
$m: lumbar.view.resolveModel
generateMarkup: (locals) ->
@markup = CoffeeKup.render(@template, @getRenderOptions(locals))
@trigger "generate", @
bindEvents: ->
@boundEvents ||= {}
if @events
for mapping, callback of @events
unless @boundEvents[mapping]
[event, selector...] = mapping.split(" ")
selector = selector.join(" ")
@boundEvents[mapping] = if _.isFunction(callback) then callback else _.bind(@[callback], @)
callback = @boundEvents[mapping]
if event and selector then @$.undelegate(selector, event, callback).delegate(selector, event, callback)
else if event then @$.off(event, callback).on(event, callback)
@
# Full re-render
render: (locals = {}) =>
log.enter @viewName, arguments...
lumbar.view.renderStack.push(@)
@detachChildViews()
if @$ then @update()
else @create()
@generateMarkup(locals)
@$[@mountMethod] @markup
@trigger "mount", @
@attachChildViews()
lumbar.view.renderStack.pop()
@bindEvents()
log.exit()
@trigger "render"
window.todos =
version: "0.0.2"
class Task extends lumbar.Model
@persist "description"
@persist "done"
defaults:
done: false
description: ""
editing: false
toggle: -> @save done: !@get("done")
todos.tasks = new class extends lumbar.Collection
model: Task
localStorage: new Store("todos")
lumbar.view "todos.item", class extends lumbar.View
mountPoint: "<li></li>"
mountOptions: ->
"class": "todo" + (if @model?.get("done") then " done" else "")
template: ->
unless @editing
div ".display", ->
if @done then input ".check", type: "checkbox", checked: "checked"
else input ".check", type: "checkbox"
div ".todo-text", @description
span ".todo-destroy", ""
else
div ".edit", ->
input ".todo-input", type="text", value=""
events:
"click .check" : (e) -> @model.save done: [email protected]("done")
"dblclick div.todo-text" : (e) -> @model.set editing: true
"click span.todo-destroy" : (e) -> @model.destroy()
"keypress .todo-input" : (e) -> @model.save description: $(e.target).val(), editing: false if e.keyCode is 13
todos.view = new class extends lumbar.View
mountPoint: "body"
template: ->
div "#todoapp", ->
div ".title", ->
h1 "Todos"
div ".content", ->
div "#create-todo", ->
input "#new-todo", placeholder: "What needs doing?", type: "text"
span ".ui-tooltip-top", style: "display: none", "Press Enter to save this task"
div "#todos", ->
ul "#todo-list", ->
$c("todos.tasks", "todos.item")
events:
"keypress #new-todo" : (e) ->
if e.keyCode is 13 and value = $(e.target).val()
todos.tasks.create description: value
$(e.target).val("")
todos.tasks.fetch()
todos.view.render()
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-weight: inherit;
font-style: inherit;
font-size: 100%;
font-family: inherit;
vertical-align: baseline;
}
body {
line-height: 1;
color: black;
background: white;
}
ol, ul {
list-style: none;
}
a img {
border: none;
}
html {
background: #eeeeee;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.4em;
background: #eeeeee;
color: #333333;
}
#todoapp {
width: 480px;
margin: 0 auto 40px;
background: white;
padding: 20px;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
}
#todoapp h1 {
font-size: 36px;
font-weight: bold;
text-align: center;
padding: 20px 0 30px 0;
line-height: 1;
}
#create-todo {
position: relative;
}
#create-todo input {
width: 466px;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
padding: 6px;
border: 1px solid #999999;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}
#create-todo input::-webkit-input-placeholder {
font-style: italic;
}
#create-todo span {
position: absolute;
z-index: 999;
width: 170px;
left: 50%;
margin-left: -85px;
}
#todo-list {
margin-top: 10px;
}
#todo-list li {
padding: 12px 20px 11px 0;
position: relative;
font-size: 24px;
line-height: 1.1em;
border-bottom: 1px solid #cccccc;
}
#todo-list li:after {
content: "\0020";
display: block;
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
#todo-list li.editing {
padding: 0;
border-bottom: 0;
}
#todo-list .editing .edit {
display: block;
}
#todo-list .editing input {
width: 444px;
font-size: 24px;
font-family: inherit;
margin: 0;
line-height: 1.6em;
border: 0;
outline: none;
padding: 10px 7px 0px 27px;
border: 1px solid #999999;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}
#todo-list .check {
position: relative;
top: 9px;
margin: 0 10px 0 7px;
float: left;
}
#todo-list .done .todo-text {
text-decoration: line-through;
color: #777777;
}
#todo-list .todo-destroy {
position: absolute;
right: 5px;
top: 14px;
display: none;
cursor: pointer;
width: 20px;
height: 20px;
background: url('http://documentcloud.github.com/backbone/examples/todos/destroy.png') no-repeat 0 0;
}
#todo-list li:hover .todo-destroy {
display: block;
}
#todo-list .todo-destroy:hover {
background-position: 0 -20px;
}
#todo-stats {
*zoom: 1;
margin-top: 10px;
color: #777777;
}
#todo-stats:after {
content: "\0020";
display: block;
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
#todo-stats .todo-count {
float: left;
}
#todo-stats .todo-count .number {
font-weight: bold;
color: #333333;
}
#todo-stats .todo-clear {
float: right;
}
#todo-stats .todo-clear a {
color: #777777;
font-size: 12px;
}
#todo-stats .todo-clear a:visited {
color: #777777;
}
#todo-stats .todo-clear a:hover {
color: #336699;
}
#instructions {
width: 520px;
margin: 10px auto;
color: #777777;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#instructions a {
color: #336699;
}
#credits {
width: 520px;
margin: 30px auto;
color: #999;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#credits a {
color: #888;
}
/*
* François 'cahnory' Germain
*/
.ui-tooltip, .ui-tooltip-top, .ui-tooltip-right, .ui-tooltip-bottom, .ui-tooltip-left {
color:#ffffff;
cursor:normal;
display:-moz-inline-stack;
display:inline-block;
font-size:12px;
font-family:arial;
padding:.5em 1em;
position:relative;
text-align:center;
text-shadow:0 -1px 1px #111111;
-webkit-border-top-left-radius:4px ;
-webkit-border-top-right-radius:4px ;
-webkit-border-bottom-right-radius:4px ;
-webkit-border-bottom-left-radius:4px ;
-khtml-border-top-left-radius:4px ;
-khtml-border-top-right-radius:4px ;
-khtml-border-bottom-right-radius:4px ;
-khtml-border-bottom-left-radius:4px ;
-moz-border-radius-topleft:4px ;
-moz-border-radius-topright:4px ;
-moz-border-radius-bottomright:4px ;
-moz-border-radius-bottomleft:4px ;
border-top-left-radius:4px ;
border-top-right-radius:4px ;
border-bottom-right-radius:4px ;
border-bottom-left-radius:4px ;
-o-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
-moz-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
-khtml-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
-webkit-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
background-color:#3b3b3b;
background-image:-moz-linear-gradient(top,#555555,#222222);
background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#555555),color-stop(1,#222222));
filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222);
-ms-filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222);
}
.ui-tooltip:after, .ui-tooltip-top:after, .ui-tooltip-right:after, .ui-tooltip-bottom:after, .ui-tooltip-left:after {
content:"\25B8";
display:block;
font-size:2em;
height:0;
line-height:0;
position:absolute;
}
.ui-tooltip:after, .ui-tooltip-bottom:after {
color:#2a2a2a;
bottom:0;
left:1px;
text-align:center;
text-shadow:1px 0 2px #000000;
-o-transform:rotate(90deg);
-moz-transform:rotate(90deg);
-khtml-transform:rotate(90deg);
-webkit-transform:rotate(90deg);
width:100%;
}
.ui-tooltip-top:after {
bottom:auto;
color:#4f4f4f;
left:-2px;
top:0;
text-align:center;
text-shadow:none;
-o-transform:rotate(-90deg);
-moz-transform:rotate(-90deg);
-khtml-transform:rotate(-90deg);
-webkit-transform:rotate(-90deg);
width:100%;
}
.ui-tooltip-right:after {
color:#222222;
right:-0.375em;
top:50%;
margin-top:-.05em;
text-shadow:0 1px 2px #000000;
-o-transform:rotate(0);
-moz-transform:rotate(0);
-khtml-transform:rotate(0);
-webkit-transform:rotate(0);
}
.ui-tooltip-left:after {
color:#222222;
left:-0.375em;
top:50%;
margin-top:.1em;
text-shadow:0 -1px 2px #000000;
-o-transform:rotate(180deg);
-moz-transform:rotate(180deg);
-khtml-transform:rotate(180deg);
-webkit-transform:rotate(180deg);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment