From: dsc Date: Thu, 12 Apr 2012 20:13:04 +0000 (-0700) Subject: Adds Backbone Base docs. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=cd6edeb2d2fc64af3a40e9f034ca5926b1c25411;p=limn-bak.git Adds Backbone Base docs. --- diff --git a/docs/github-readme.css b/docs/github-readme.css new file mode 100644 index 0000000..16f7c20 --- /dev/null +++ b/docs/github-readme.css @@ -0,0 +1,207 @@ +a, a:link { + color: #2e62c9; + vertical-align: baseline; } + +a:visited { + color: #244792; } + +a:hover, a:active { + color: #4596ff; } + +.site { margin: 0 auto; padding: 15px; width: 920px; } + +.highlight{background:#fff;} +.highlight .c{color:#998;font-style:italic;} +.highlight .err{color:#a61717;background-color:#e3d2d2;} +.highlight .k{font-weight:bold;} +.highlight .o{font-weight:bold;} +.highlight .cm{color:#998;font-style:italic;} +.highlight .cp{color:#999;font-weight:bold;} +.highlight .c1{color:#998;font-style:italic;} +.highlight .cs{color:#999;font-weight:bold;font-style:italic;} +.highlight .gd{color:#000;background-color:#fdd;} +.highlight .gd .x{color:#000;background-color:#faa;} +.highlight .ge{font-style:italic;} +.highlight .gr{color:#a00;} +.highlight .gh{color:#999;} +.highlight .gi{color:#000;background-color:#dfd;} +.highlight .gi .x{color:#000;background-color:#afa;} +.highlight .go{color:#888;} +.highlight .gp{color:#555;} +.highlight .gs{font-weight:bold;} +.highlight .gu{color:#800080;font-weight:bold;} +.highlight .gt{color:#a00;} +.highlight .kc{font-weight:bold;} +.highlight .kd{font-weight:bold;} +.highlight .kn{font-weight:bold;} +.highlight .kp{font-weight:bold;} +.highlight .kr{font-weight:bold;} +.highlight .kt{color:#458;font-weight:bold;} +.highlight .m{color:#099;} +.highlight .s{color:#d14;} +.highlight .na{color:#008080;} +.highlight .nb{color:#0086B3;} +.highlight .nc{color:#458;font-weight:bold;} +.highlight .no{color:#008080;} +.highlight .ni{color:#800080;} +.highlight .ne{color:#900;font-weight:bold;} +.highlight .nf{color:#900;font-weight:bold;} +.highlight .nn{color:#555;} +.highlight .nt{color:#000080;} +.highlight .nv{color:#008080;} +.highlight .ow{font-weight:bold;} +.highlight .w{color:#bbb;} +.highlight .mf{color:#099;} +.highlight .mh{color:#099;} +.highlight .mi{color:#099;} +.highlight .mo{color:#099;} +.highlight .sb{color:#d14;} +.highlight .sc{color:#d14;} +.highlight .sd{color:#d14;} +.highlight .s2{color:#d14;} +.highlight .se{color:#d14;} +.highlight .sh{color:#d14;} +.highlight .si{color:#d14;} +.highlight .sx{color:#d14;} +.highlight .sr{color:#009926;} +.highlight .s1{color:#d14;} +.highlight .ss{color:#990073;} +.highlight .bp{color:#999;} +.highlight .vc{color:#008080;} +.highlight .vg{color:#008080;} +.highlight .vi{color:#008080;} +.highlight .il{color:#099;} +.type-csharp .highlight .k{color:#00F;} +.type-csharp .highlight .kt{color:#00F;} +.type-csharp .highlight .nf{color:#000;font-weight:normal;} +.type-csharp .highlight .nc{color:#2B91AF;} +.type-csharp .highlight .nn{color:#000;} +.type-csharp .highlight .s{color:#A31515;} +.type-csharp .highlight .sc{color:#A31515;} + +#readme{ font:13.34px helvetica,arial,freesans,clean,sans-serif; } +#readme.announce{margin:1em 0;} +#readme.blob{;} +#readme span.name{font-size:140%;padding:.8em 0;} +#readme div.plain,#readme div.wikistyle{background-color:#f8f8f8;padding:.7em;} +#readme.announce div.plain,#readme.announce div.wikistyle{border:1px solid #e9e9e9;} +#readme.blob div.plain,#readme.blob div.wikistyle{border-top:none;} +#readme div.plain pre{font-family:'Bitstream Vera Sans Mono','Courier',monospace;font-size:85%;color:#444;white-space:pre-wrap;word-wrap:break-word;width:74em;} +#missing-readme{font:13.34px helvetica,arial,freesans,clean,sans-serif;text-align:center;background-color:#ffc;padding:.7em;border:1px solid #ccc;} +#readme.rst .borderless,#readme.rst table.borderless td,#readme.rst table.borderless th{border:0;} +#readme.rst table.borderless td,#readme.rst table.borderless th{padding:0 .5em 0 0!important;} +#readme.rst .first{margin-top:0!important;} +#readme.rst .last,#readme.rst .with-subtitle{margin-bottom:0!important;} +#readme.rst .hidden{display:none;} +#readme.rst a.toc-backref{text-decoration:none;color:black;} +#readme.rst blockquote.epigraph{margin:2em 5em;} +#readme.rst dl.docutils dd{margin-bottom:.5em;} +#readme.rst div.abstract{margin:2em 5em;} +#readme.rst div.abstract p.topic-title{font-weight:bold;text-align:center;} +#readme.rst div.admonition,#readme.rst div.attention,#readme.rst div.caution,#readme.rst div.danger,#readme.rst div.error,#readme.rst div.hint,#readme.rst div.important,#readme.rst div.note,#readme.rst div.tip,#readme.rst div.warning{margin:2em;border:medium outset;padding:1em;} +#readme.rst div.admonition p.admonition-title,#readme.rst div.hint p.admonition-title,#readme.rst div.important p.admonition-title,#readme.rst div.note p.admonition-title,#readme.rst div.tip p.admonition-title{font-weight:bold;font-family:sans-serif;} +#readme.rst div.attention p.admonition-title,#readme.rst div.caution p.admonition-title,#readme.rst div.danger p.admonition-title,#readme.rst div.error p.admonition-title,#readme.rst div.warning p.admonition-title{color:red;font-weight:bold;font-family:sans-serif;} +#readme.rst div.dedication{margin:2em 5em;text-align:center;font-style:italic;} +#readme.rst div.dedication p.topic-title{font-weight:bold;font-style:normal;} +#readme.rst div.figure{margin-left:2em;margin-right:2em;} +#readme.rst div.footer,#readme.rst div.header{clear:both;font-size:smaller;} +#readme.rst div.line-block{display:block;margin-top:1em;margin-bottom:1em;} +#readme.rst div.line-block div.line-block{margin-top:0;margin-bottom:0;margin-left:1.5em;} +#readme.rst div.sidebar{margin:0 0 .5em 1em;border:medium outset;padding:1em;background-color:#ffe;width:40%;float:right;clear:right;} +#readme.rst div.sidebar p.rubric{font-family:sans-serif;font-size:medium;} +#readme.rst div.system-messages{margin:5em;} +#readme.rst div.system-messages h1{color:red;} +#readme.rst div.system-message{border:medium outset;padding:1em;} +#readme.rst div.system-message p.system-message-title{color:red;font-weight:bold;} +#readme.rst div.topic{margin:2em;} +#readme.rst h1.section-subtitle,#readme.rst h2.section-subtitle,#readme.rst h3.section-subtitle,#readme.rst h4.section-subtitle,#readme.rst h5.section-subtitle,#readme.rst h6.section-subtitle{margin-top:.4em;} +#readme.rst h1.title{text-align:center;} +#readme.rst h2.subtitle{text-align:center;} +#readme.rst hr.docutils{width:75%;} +#readme.rst img.align-left,#readme.rst .figure.align-left,#readme.rst object.align-left{clear:left;float:left;margin-right:1em;} +#readme.rst img.align-right,#readme.rst .figure.align-right,#readme.rst object.align-right{clear:right;float:right;margin-left:1em;} +#readme.rst img.align-center,#readme.rst .figure.align-center,#readme.rst object.align-center{display:block;margin-left:auto;margin-right:auto;} +#readme.rst .align-left{text-align:left;} +#readme.rst .align-center{clear:both;text-align:center;} +#readme.rst .align-right{text-align:right;} +#readme.rst div.align-right{text-align:left;} +#readme.rst ol.simple,#readme.rst ul.simple{margin-bottom:1em;} +#readme.rst ol.arabic{list-style:decimal;} +#readme.rst ol.loweralpha{list-style:lower-alpha;} +#readme.rst ol.upperalpha{list-style:upper-alpha;} +#readme.rst ol.lowerroman{list-style:lower-roman;} +#readme.rst ol.upperroman{list-style:upper-roman;} +#readme.rst p.attribution{text-align:right;margin-left:50%;} +#readme.rst p.caption{font-style:italic;} +#readme.rst p.credits{font-style:italic;font-size:smaller;} +#readme.rst p.label{white-space:nowrap;} +#readme.rst p.rubric{font-weight:bold;font-size:larger;color:maroon;text-align:center;} +#readme.rst p.sidebar-title{font-family:sans-serif;font-weight:bold;font-size:larger;} +#readme.rst p.sidebar-subtitle{font-family:sans-serif;font-weight:bold;} +#readme.rst p.topic-title{font-weight:bold;} +#readme.rst pre.address{margin-bottom:0;margin-top:0;font:inherit;} +#readme.rst pre.literal-block,#readme.rst pre.doctest-block{margin-left:2em;margin-right:2em;} +#readme.rst span.classifier{font-family:sans-serif;font-style:oblique;} +#readme.rst span.classifier-delimiter{font-family:sans-serif;font-weight:bold;} +#readme.rst span.interpreted{font-family:sans-serif;} +#readme.rst span.option{white-space:nowrap;} +#readme.rst span.pre{white-space:pre;} +#readme.rst span.problematic{color:red;} +#readme.rst span.section-subtitle{font-size:80%;} +#readme.rst table.citation{border-left:solid 1px gray;margin-left:1px;} +#readme.rst table.docinfo{margin:2em 4em;} +#readme.rst table.docutils{margin-top:.5em;margin-bottom:.5em;} +#readme.rst table.footnote{border-left:solid 1px black;margin-left:1px;} +#readme.rst table.docutils td,#readme.rst table.docutils th,#readme.rst table.docinfo td,#readme.rst table.docinfo th{padding-left:.5em;padding-right:.5em;vertical-align:top;} +#readme.rst table.docutils th.field-name,#readme.rst table.docinfo th.docinfo-name{font-weight:bold;text-align:left;white-space:nowrap;padding-left:0;} +#readme.rst h1 tt.docutils,#readme.rst h2 tt.docutils,#readme.rst h3 tt.docutils,#readme.rst h4 tt.docutils,#readme.rst h5 tt.docutils,#readme.rst h6 tt.docutils{font-size:100%;} +#readme.rst ul.auto-toc{list-style-type:none;}.highlight .gc{color:#999;background-color:#EAF2F5;} + +.wikistyle h1,.wikistyle h2,.wikistyle h3,.wikistyle h4,.wikistyle h5,.wikistyle h6{border:0!important;} +.wikistyle h1{font-size:170%!important;border-top:4px solid #aaa!important;padding-top:.5em!important;margin-top:1.5em!important;} +.wikistyle h1:first-child{margin-top:0!important;padding-top:.25em!important;border-top:none!important;} +.wikistyle h2{font-size:150%!important;margin-top:1.5em!important;border-top:4px solid #e0e0e0!important;padding-top:.5em!important;} +.wikistyle h3{margin-top:1em!important;} +.wikistyle hr{border:1px solid #ddd;} +.wikistyle p{margin:1em 0!important;line-height:1.5em!important;} +.wikistyle a.absent{color:#a00;} +.wikistyle ul,#wiki-form .content-body ul{margin:1em 0 1em 2em!important;} +.wikistyle ol,#wiki-form .content-body ol{margin:1em 0 1em 2em!important;} +.wikistyle ul li,#wiki-form .content-body ul li,.wikistyle ol li,#wiki-form .content-body ol li{margin-top:.5em;margin-bottom:.5em;} +.wikistyle ul ul,.wikistyle ul ol,.wikistyle ol ol,.wikistyle ol ul,#wiki-form .content-body ul ul,#wiki-form .content-body ul ol,#wiki-form .content-body ol ol,#wiki-form .content-body ol ul{margin-top:0!important;margin-bottom:0!important;} +.wikistyle blockquote{margin:1em 0!important;border-left:5px solid #ddd!important;padding-left:.6em!important;color:#555!important;} +.wikistyle dt{font-weight:bold!important;margin-left:1em!important;} +.wikistyle dd{margin-left:2em!important;margin-bottom:1em!important;} +.wikistyle table{margin:1em 0!important; width:100%; border-spacing:1px;} +.wikistyle table thead {border:1px solid #bbb; border-width: 1px 0; padding:1em; background-color:#F2F2F2; color:#333; } +.wikistyle table th, td{text-align:left; vertical-align:middle; margin:0 1px; } +.wikistyle table th{border-bottom:1px solid #bbb!important;} +.wikistyle table td{border-bottom:1px solid #ddd!important;} +.wikistyle a code,.wikistyle a:link code,.wikistyle a:visited code{color:#4183c4!important;} +.wikistyle img{max-width:100%;} +.wikistyle pre.console{margin:1em 0!important;font-size:12px!important;background-color:black!important;padding:.5em!important;line-height:1.5em!important;color:white!important;} +.wikistyle pre.console code{padding:0!important;font-size:12px!important;background-color:black!important;border:none!important;color:white!important;} +.wikistyle pre.console span{color:#888!important;} +.wikistyle pre.console span.command{color:yellow!important;} +.wikistyle .frame{margin:0;display:inline-block;} +.wikistyle .frame img{display:block;} +.wikistyle .frame>span{display:block;border:1px solid #aaa;padding:4px;} +.wikistyle .frame span span{display:block;font-size:10pt;margin:0;padding:4px 0 2px 0;text-align:center;line-height:10pt;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;} +.wikistyle .float-left{float:left;padding:.5em 1em .25em 0;} +.wikistyle .float-right{float:right;padding:.5em 0 .25em 1em;} +.wikistyle .align-left{display:block;text-align:left;} +.wikistyle .align-center{display:block;text-align:center;} +.wikistyle .align-right{display:block;text-align:right;} +.wikistyle pre{margin:1em 0;font-size:12px;background-color:#eee;border:1px solid #ddd;padding:5px;color:#444;overflow:auto;-webkit-box-shadow:rgba(0,0,0,0.07) 0 1px 2px inset;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.wikistyle pre::-webkit-scrollbar{height:8px;width:8px;} +.wikistyle pre::-webkit-scrollbar-track-piece{margin-bottom:10px;background-color:#e5e5e5;border-bottom-left-radius:4px 4px;border-bottom-right-radius:4px 4px;border-top-left-radius:4px 4px;border-top-right-radius:4px 4px;} +.wikistyle pre::-webkit-scrollbar-thumb:vertical{height:25px;background-color:#ccc;-webkit-border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(255,255,255,1);} +.wikistyle pre::-webkit-scrollbar-thumb:horizontal{width:25px;background-color:#ccc;-webkit-border-radius:4px;} +.wikistyle pre code{padding:0!important;font-size:12px!important;background-color:#eee!important;border:none!important;} +.wikistyle code{font-size:12px!important;background-color:#f8f8ff!important;color:#444!important;padding:0 .2em!important;border:1px solid #dedede!important;} + +.markdown-format .highlight pre{background:#fafafa;} +.markdown-format .highlight .gu,.markdown-format .highlight .gd,.markdown-format .highlight .gi{display:inline-block;width:100%;padding:0 0 0 5px;margin-left:-5px;} +.markdown-format .highlight .gu{font-weight:normal;color:#999;background:#EAF2F5;} +.markdown-format .highlight .gd{;} +.markdown-format .highlight .gi{;} diff --git a/docs/internals/backbone-base.html b/docs/internals/backbone-base.html new file mode 100644 index 0000000..6575886 --- /dev/null +++ b/docs/internals/backbone-base.html @@ -0,0 +1,128 @@ + + + + +Backbone Base Classes | GraphKit Docs + + + + +
+
+
+

Backbone Base Classes

+

Nearly all models, model collections, and views in the project derive from three base classes, aptly named BaseModel, BaseList, and BaseView. Herein we'll discuss some of the more arcane aspects of these classes, especially the default behavior they implement, and the ways in which subclasses can augment it.

+

Table of Contents

+ +

BaseMixin

+

Though these three do not share a common base class (as each derives from its Backbone counterpart), they do share a set of methods and properties defined in a BaseMixin (the mixin is applied to each Base itself, so you do not need to apply it to any of your subclasses).

+

Declarative Binding

+

Due to the fact that JavaScript functions are unbound, it is sometimes necessary to force a method to retain class context (this) for event handlers. The property __bind__ allows you to specify this declaratively; it is a list of method names to be bound when initialize() is called on a new instance. The __bind__ property recursively cascades up the inheritance chain, so all classes will have their binds applied.

+

Synchronization

+

Due to the asynchronous nature of UI programming, it's often necessary to notify the world that this class is waiting for something. It might not even matter what they're waiting for -- in many cases, you just want to show a spinner or disable modifications while "stuff happens".

+

To this end, all Base classes provide methods for this kind of synchronization: wait() and unwait() manipulate a simple counter, waitingOn. When it moves above zero through a call to wait(), the event start-waiting is fired. When it returns to zero through a call to unwait(), the event stop-waiting is fired. Finally, a decorator method unwaitAnd(fn) allows you to wrap another method, such that invoking the returned function will call unwait() and then delegate to the passed method.

+

BaseModel

+

BaseModel provides two simple but important features: nested lookups and KV-pairs serialization.

+

Nested Attribute Lookup

+

Calls to get(key) can specify a nested attribute lookup by using dot-delimited keys.

+
m = new BaseModel { foo:{bar:1} }
+m.get('foo.bar') is 1
+
+ + +

KV-Pairs Serialization

+

KV-Pairs, also known as www-form-encoding, is the default serialization format for data posted by HTML forms. The query string in a URL (the bit after the first ?, but before any #) also usually takes this format. Unfortunately, JavaScript provides no built-in serialization for objects to this format. These methods fill that gap.

+
m = new BaseModel { a:1, b:2 }
+m.toKV() is 'a=1&b=2'
+
+ + +

JavaScript 1.8 introduced a protocol for [JSON serialization][json], whereby objects can specify a toJSON() method that returns an object to be serialized, rather than forcing the programmer to reimplement serialization just to customize what data is serialized.

+

The KV-Pairs protocol functions similarly. It consists in a family of instance methods and a class method, any of which can be replaced via inheritance:

+
    +
  • toKV(item_delim='&', kv_delim='=') -> String performs the actual serialization of the object a www-form-encoded string by invoking toKVPairs() and passing the result to _.toKV (along with the delimiters).
  • +
  • toKVPairs() -> Object is the counterpart of toJSON() -- it is expected to return an object to be serialized. Unlike toJSON(), it must return a proper object, and no recursive application of this function will be applied. This is due to the fact that query strings, unlike JSON, are fundamentally one-dimensional; without a type system, nesting requires a convention for delimiting sub-objects. The default implementation handles sub-objects using _.collapseObject() (rather than encoding them), and serializes each value using serialize(v).
  • +
  • _.collapseObject(obj, parent={}, prefix='') -> Object is an [Underscore][underscore] extension that copies and flattens a tree of sub-objects into namespaced keys on the parent object, such that { "foo":{ "bar":1 } } becomes { "foo.bar":1 }. Note that only plain objects are collapsed -- it is assumed that object subclasses will correctly .toString() or otherwise handle their serialization in the serialize(v) step.
  • +
  • serialize(v) -> String is called by the default implementation of toKVPairs() to transform each non-function value to a string.
  • +
  • toURL(item_delim='&', kv_delim='=') -> String is used to create a URL that uniquely identifies the model state, typically for use with the HTML5 History API.
  • +
  • The class method fromKV(s, item_delim='&', kv_delim='=') -> Model uses _.fromKV to deserialize the www-form-encoded string, uncollapses it with _.uncollapseObject(), and then creates a new model instance with the result.
  • +
+
m = new BaseModel { a:1, b:2, foo:{ bar:3 } }
+m.toKV() is 'a=1&b=2&foo.bar=3'
+
+ + +

The Underscore extensions can be found in lib/util/underscore, specifically kv.co and object.co.

+

BaseList

+

BaseList mostly exists for completeness -- at present, it merely provides implementations of the KV-Pairs protocol fed by toJSON(). This typically has a poor result.

+

BaseView

+

BaseView provides a large number of convenience methods, most of which are simple enough to read about in the source-docs. Instead, we'll focus here on the view lifecycle.

+

Model Integration

+

The setModel(model) -> model method sets a variety of metadata on both the view and the model -- use it to change the BaseView's model object should that ever be necessary. It does the following:

+
    +
  • Sets model.view equal to the BaseView. This is mostly useful for debugging in the console, as models should otherwise need no knowledge of any views.
  • +
  • Sets data attributes for "model" and "view" on the view's DOM element.
  • +
  • Register listeners on change to render the view and destroy to remove itself from the DOM.
  • +
+

Render Lifecycle

+

Backbone has provides almost no help in managing a view's relationship with the DOM. Unfortunately for us, this is in no way a simple task, and it also happens to be the place most likely to have serious user-facing performance implications. BaseView takes some steps to smooth this over (though there is much more that could be done) by implementing a default rendering lifecycle that eliminates much boilerplate.

+
    +
  • Note that Backbone.View's constructor creates a DOM element for the view (as this.el), and wraps it with jQuery (as this.$el). This is basically all Backbone does, ever.
  • +
  • At the end of initialize(), BaseView invokes the build() method.
  • +
  • build() -> this should populate the view with HTML, but does not attach it to the DOM. The default implementation of build() is a no-op unless the view has a template() method. If it does, build() invokes this.$template(), attaches its innerHTML content to the view's DOM element, and updates the element's CSS classes to match the template's. Finally, it calls attachSubviews(), which we'll come to later.
  • +
  • $template(locals) -> jQuery invokes the view's template(), merging a set of default locals ($, op, the model, the view) with the result of the toTemplateLocals() method, and then with the supplied locals. It then wraps the templated result with jQuery.
  • +
  • toTemplateLocals() should return a plain object with (presumably) the model's attributes converted to presentation values used in the template. By default it merely calls model.toJSON().
  • +
  • The render() method invokes build() and then triggers a render event. As noted above, it is registered as a listener on Model change events.
  • +
+

These methods (plus the render event) provide ample places to customize the rendering lifecycle. Many views do; here are some notes:

+
    +
  • Nearly all views override toTemplateLocals() to format the locals for presentation.
  • +
  • Many override render() to avoid rebuilding the DOM. Once this pattern emerged, it became obvious I should modify render() to call an update(locals) method if it exists, rather than blowing away whatever DOM content is there.
  • +
  • Data-binding frameworks exist to integrate with DOM content, but all of them I've seen are either excessively verbose, heavy-weight, and presumptuous, or do not play well with Backbone. A simple data-binding convention would go a long way to eliminating boilerplate.
  • +
+

Subviews

+

BaseView provides simple subview management methods and hooks:

+
    +
  • addSubview([selector], view) -> view registers a new subview. You may optionally pass a selector as the first argument to specify its attachment point in the view. The subview is not attached to the DOM until attachSubviews() is called (which normally happens during build() and render()).
  • +
  • hasSubview(view) tests if a view is registered.
  • +
  • removeSubview(view) -> [view, selector] | null unregisters a subview. Note that it does not remove the subview from the DOM.
  • +
  • attachSubviews() -> this attaches each subview to the view's DOM element. If the given subview was registered with a selector, the element it picks out will become the subview's parent. Otherwise, it is appended to the view's element.
  • +
  • **renderSubviews() -> this** is a convenience method for invoking render on each subview; it is not called by anything in the render lifecycle. Note renderSubviews() does not call attachSubviews().
  • +
+

Future Enhancements

+

View handling could use a lot of work to eliminate boilerplate and simplify common tasks. The biggest pain-points I've noted are:

+
    +
  • Views have no way of being notified when their DOM element is actually parented (or unparented) in the DOM. This is generally a very hard problem in rich client development on the web, and there are no bulletproof solutions. Still, we should be providing some methods which in turn fire an event for this.
  • +
  • As mentioned in the discussion of the render lifecycle, it would help to integrate a default data-binding convention and related methods.
  • +
  • Events don't bubble through the view hierarchy as they do with the DOM. This means views must manually inform their subviews to re-render. I left this unimplemented mostly because of the render-vs-update issue above.
  • +
+

+
+
+
+ diff --git a/docs/internals/backbone-base.md b/docs/internals/backbone-base.md index 78d1f51..e5b0bed 100644 --- a/docs/internals/backbone-base.md +++ b/docs/internals/backbone-base.md @@ -1,6 +1,118 @@ -# Backbone Base classes +# Backbone Base Classes + +Nearly all models, model collections, and views in the project derive from three base classes, aptly named `BaseModel`, `BaseList`, and `BaseView`. Herein we'll discuss some of the more arcane aspects of these classes, especially the default behavior they implement, and the ways in which subclasses can augment it. + +## Table of Contents + +[TOC] + + +## BaseMixin + +Though these three do not share a common base class (as each derives from its Backbone counterpart), they do share a set of methods and properties defined in a `BaseMixin` (the mixin is applied to each Base itself, so you do not need to apply it to any of your subclasses). + +### Declarative Binding + +Due to the fact that JavaScript functions are unbound, it is sometimes necessary to force a method to retain class context (`this`) for event handlers. The property `__bind__` allows you to specify this declaratively; it is a list of method names to be bound when `initialize()` is called on a new instance. The `__bind__` property recursively cascades up the inheritance chain, so all classes will have their binds applied. + +### Synchronization + +Due to the asynchronous nature of UI programming, it's often necessary to notify the world that this class is waiting for something. It might not even matter what they're waiting for -- in many cases, you just want to show a spinner or disable modifications while "stuff happens". + +To this end, all Base classes provide methods for this kind of synchronization: `wait()` and `unwait()` manipulate a simple counter, `waitingOn`. When it moves above zero through a call to `wait()`, the event `start-waiting` is fired. When it returns to zero through a call to `unwait()`, the event `stop-waiting` is fired. Finally, a decorator method `unwaitAnd(fn)` allows you to wrap another method, such that invoking the returned function will call `unwait()` and then delegate to the passed method. + + +## BaseModel + +`BaseModel` provides two simple but important features: nested lookups and KV-pairs serialization. + +### Nested Attribute Lookup + +Calls to `get(key)` can specify a nested attribute lookup by using dot-delimited keys. + +```coffee +m = new BaseModel { foo:{bar:1} } +m.get('foo.bar') is 1 +``` + +### KV-Pairs Serialization + +KV-Pairs, also known as `www-form-encoding`, is the default serialization format for data posted by HTML forms. The query string in a URL (the bit after the first `?`, but before any `#`) also usually takes this format. Unfortunately, JavaScript provides no built-in serialization for objects to this format. These methods fill that gap. + +```coffee +m = new BaseModel { a:1, b:2 } +m.toKV() is 'a=1&b=2' +``` + +JavaScript 1.8 introduced a protocol for [JSON serialization][json], whereby objects can specify a `toJSON()` method that returns an object to be serialized, rather than forcing the programmer to reimplement serialization just to customize what data is serialized. + +The KV-Pairs protocol functions similarly. It consists in a family of instance methods and a class method, any of which can be replaced via inheritance: + +- **`toKV(item_delim='&', kv_delim='=') -> String`** performs the actual serialization of the object a `www-form-encoded` string by invoking `toKVPairs()` and passing the result to `_.toKV` (along with the delimiters). +- **`toKVPairs() -> Object`** is the counterpart of `toJSON()` -- it is expected to return an object to be serialized. Unlike `toJSON()`, it must return a proper object, and no recursive application of this function will be applied. This is due to the fact that query strings, unlike JSON, are fundamentally one-dimensional; without a type system, nesting requires a convention for delimiting sub-objects. The default implementation handles sub-objects using `_.collapseObject()` (rather than encoding them), and serializes each value using `serialize(v)`. +- **`_.collapseObject(obj, parent={}, prefix='') -> Object`** is an [Underscore][underscore] extension that copies and flattens a tree of sub-objects into namespaced keys on the parent object, such that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`. Note that only plain objects are collapsed -- it is assumed that object subclasses will correctly `.toString()` or otherwise handle their serialization in the `serialize(v)` step. +- **`serialize(v) -> String`** is called by the default implementation of `toKVPairs()` to transform each non-function value to a string. +- **`toURL(item_delim='&', kv_delim='=') -> String`** is used to create a URL that uniquely identifies the model state, typically for use with the HTML5 History API. +- The class method **`fromKV(s, item_delim='&', kv_delim='=') -> Model`** uses `_.fromKV` to deserialize the `www-form-encoded` string, uncollapses it with `_.uncollapseObject()`, and then creates a new model instance with the result. + +```coffee +m = new BaseModel { a:1, b:2, foo:{ bar:3 } } +m.toKV() is 'a=1&b=2&foo.bar=3' +``` + +The Underscore extensions can be found in `lib/util/underscore`, specifically `kv.co` and `object.co`. + + +## BaseList + +`BaseList` mostly exists for completeness -- at present, it merely provides implementations of the KV-Pairs protocol fed by `toJSON()`. This typically has a poor result. + + +## BaseView + +`BaseView` provides a large number of convenience methods, most of which are simple enough to read about in the source-docs. Instead, we'll focus here on the view lifecycle. + +### Model Integration + +The `setModel(model) -> model` method sets a variety of metadata on both the view and the model -- use it to change the `BaseView`'s model object should that ever be necessary. It does the following: + +- Sets `model.view` equal to the `BaseView`. This is mostly useful for debugging in the console, as models should otherwise need no knowledge of any views. +- Sets data attributes for `"model"` and `"view"` on the view's DOM element. +- Register listeners on `change` to render the view and `destroy` to remove itself from the DOM. + +### Render Lifecycle + +Backbone has provides almost no help in managing a view's relationship with the DOM. Unfortunately for us, this is in no way a simple task, and it also happens to be the place most likely to have serious user-facing performance implications. `BaseView` takes some steps to smooth this over (though there is much more that could be done) by implementing a default rendering lifecycle that eliminates much boilerplate. + +- Note that `Backbone.View`'s constructor creates a DOM element for the view (as `this.el`), and wraps it with jQuery (as `this.$el`). This is basically all Backbone does, ever. +- At the end of `initialize()`, `BaseView` invokes the `build()` method. +- **`build() -> this`** should populate the view with HTML, but does not attach it to the DOM. The default implementation of `build()` is a no-op unless the view has a `template()` method. If it does, `build()` invokes `this.$template()`, attaches its `innerHTML` content to the view's DOM element, and updates the element's CSS classes to match the template's. Finally, it calls `attachSubviews()`, which we'll come to later. +- **`$template(locals) -> jQuery`** invokes the view's `template()`, merging a set of default locals ($, op, the model, the view) with the result of the `toTemplateLocals()` method, and then with the supplied locals. It then wraps the templated result with jQuery. +- `toTemplateLocals()` should return a plain object with (presumably) the model's attributes converted to presentation values used in the template. By default it merely calls `model.toJSON()`. +- The `render()` method invokes `build()` and then triggers a `render` event. As noted above, it is registered as a listener on Model `change` events. + +These methods (plus the `render` event) provide ample places to customize the rendering lifecycle. Many views do; here are some notes: + +- Nearly all views override `toTemplateLocals()` to format the locals for presentation. +- Many override `render()` to avoid rebuilding the DOM. Once this pattern emerged, it became obvious I should modify `render()` to call an `update(locals)` method if it exists, rather than blowing away whatever DOM content is there. +- Data-binding frameworks exist to integrate with DOM content, but all of them I've seen are either excessively verbose, heavy-weight, and presumptuous, or do not play well with Backbone. A simple data-binding convention would go a long way to eliminating boilerplate. + +### Subviews + +`BaseView` provides simple subview management methods and hooks: + +- **`addSubview([selector], view) -> view`** registers a new subview. You may optionally pass a selector as the first argument to specify its attachment point in the view. The subview is not attached to the DOM until `attachSubviews()` is called (which normally happens during `build()` and `render()`). +- **`hasSubview(view)`** tests if a view is registered. +- **`removeSubview(view) -> [view, selector] | null`** unregisters a subview. Note that it does not remove the subview from the DOM. +- **`attachSubviews() -> this`** attaches each subview to the view's DOM element. If the given subview was registered with a selector, the element it picks out will become the subview's parent. Otherwise, it is appended to the view's element. +- `**renderSubviews() -> this**` is a convenience method for invoking `render` on each subview; it is not called by anything in the render lifecycle. Note `renderSubviews()` does not call `attachSubviews()`. + +### Future Enhancements + +View handling could use a lot of work to eliminate boilerplate and simplify common tasks. The biggest pain-points I've noted are: + +- Views have no way of being notified when their DOM element is actually parented (or unparented) in the DOM. This is generally a very hard problem in rich client development on the web, and there are no bulletproof solutions. Still, we should be providing some methods which in turn fire an event for this. +- As mentioned in the discussion of the render lifecycle, it would help to integrate a default data-binding convention and related methods. +- Events don't bubble through the view hierarchy as they do with the DOM. This means views must manually inform their subviews to re-render. I left this unimplemented mostly because of the render-vs-update issue above. + -- Properties, Methods -- Subclass customization -- Call flow -- Events diff --git a/docs/reset.css b/docs/reset.css new file mode 100755 index 0000000..4b2c2d0 --- /dev/null +++ b/docs/reset.css @@ -0,0 +1,159 @@ +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin:0; padding:0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; } + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { + display: block; } + +html { color:#000; background:#fff; width:100%; height:100%; overflow-y: scroll; } +body { position:absolute; width:100%; top:0; left:0; } + +address,caption,cite,code,dfn,em,strong,th,var,optgroup { font-style:inherit; font-weight:inherit; } +fieldset,img { border:0; } +caption,th { text-align:left; } +caption { margin-bottom:.5em; text-align:center; } + +h1,h2,h3,h4,h5,h6 { font-size:100%; font-weight:bold; } +h1 { font-size:250%; } +h2 { font-size:150%; } +h3 { font-size:125%; } +h4 { font-size:110%; } +h1,h2,h3 { margin:0.5em 0 1em; } + + +ins { background-color: #ff9; color: #000; text-decoration: none; } +mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; } +del { text-decoration: line-through; } +abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; } +abbr,acronym { border:0; font-variant:normal; border-bottom:1px dotted #000; cursor:help; } +em, i { font-style:italic; } +small { font-size: 85%; } +strong, b, th { font-weight: bold; } +u { text-decoration:underline; } + +/* Set sub, sup without affecting line-height: gist.github.com/413930 */ +sub, sup { font-size: 75%; line-height: 0; position: relative; } +sup { top: -0.5em; } +sub { bottom: -0.25em; } + + +blockquote, q { quotes: none; } +blockquote:before, blockquote:after, +q:before, q:after { content: ''; content: none; } +hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } + + +/** Font normalization inspired by YUI Library's fonts.css: developer.yahoo.com/yui/ */ +//body { font:13px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */ +body { font:16px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */ +select, input, textarea, button { font:99% sans-serif; } + +/* Normalize monospace sizing: en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */ +pre, code, kbd, samp { font-family: monospace, sans-serif; } +/* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */ +pre { + white-space: pre; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + + word-wrap: break-word; + padding: 15px; } + +p,fieldset,table,pre { margin-bottom:1em; } + + +/* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */ +a:hover, a:active { outline: none; } + + +/* Lists */ + +ul,ol,dl,blockquote { margin:0.5em; } +ol,ul,dl { margin-left:1.0em; } +li { list-style:none; } +ol { list-style-type: decimal; } +ol li { list-style:decimal inside; } +ul li { list-style:disc inside; } +dl dd { margin-left:0.5em; } + +/* Remove margins for navigation lists */ +nav ul, nav li { margin: 0; list-style:none; list-style-image: none; } + + +/* Forms */ + +input,select { vertical-align: middle; } +input,button,textarea,select,optgroup,option { font-family:inherit; font-size:inherit; font-style:inherit; font-weight:inherit; } +input,button,textarea,select { *font-size:100%; } +input[type=text],input[type=password],textarea { width:12.25em; *width:11.9em; } +textarea { overflow: auto; } /* www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */ + +legend { color:#000; } +.ie6 legend, .ie7 legend { margin-left: -7px; } + +/* Align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */ +input[type="radio"] { vertical-align: text-bottom; } +input[type="checkbox"] { vertical-align: bottom; } +.ie7 input[type="checkbox"] { vertical-align: baseline; } +.ie6 input { vertical-align: text-bottom; } + +/* Hand cursor on clickable input elements */ +a[href], input[type='button'], input[type='submit'], input[type='image'], label[for], select, button, .pointer { cursor: pointer; } + + +/* Webkit browsers add a 2px margin outside the chrome of form elements */ +button, input, select, textarea { margin: 0; } + +/* Colors for form validity */ +input:valid, textarea:valid { } +input:invalid, textarea:invalid { + border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; +} +.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; } + +/* Make buttons play nice in IE: + www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */ +button { width: auto; overflow: visible; } + + +/* Tables */ + +table { border-collapse:collapse; border-spacing:0; } +th,td { padding:.5em; } +th { font-weight:bold; text-align:center; } +td { vertical-align: top; } + + +/* Misc */ + +/* These selection declarations have to be separate + No text-shadow: twitter.com/miketaylr/status/12228805301 + Also: hot pink! */ +::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; } +::selection { background:#FF5E99; color:#fff; text-shadow: none; } + +/* j.mp/webkit-tap-highlight-color */ +a:link { -webkit-tap-highlight-color: #FF5E99; } + +/* Bicubic resizing for non-native sized IMG: + code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */ +.ie7 img { -ms-interpolation-mode: bicubic; } + + +/* Minimal Basic Classes */ + +.clearer { clear:both !important; float:none !important; margin:0 !important; padding:0 !important; } +.rounded { border-radius:1em; -moz-border-radius:1em; -webkit-border-radius:1em; } diff --git a/docs/solarized.css b/docs/solarized.css new file mode 100644 index 0000000..40acc45 --- /dev/null +++ b/docs/solarized.css @@ -0,0 +1,69 @@ +.hll { background-color: #eee8d5 } +.c { color: #93a1a1; background-color: #eee8d5 } /* Comment */ +.err { color: #eee8d5; background-color: #dc322f } /* Error */ +.g { color: #839496 } /* Generic */ +.k { color: #cb4b16 } /* Keyword */ +.l { color: #6c71c4 } /* Literal */ +.n { color: #657b83 } /* Name */ +.o { color: #073642 } /* Operator */ +.x { color: #657b83 } /* Other */ +.p { color: #657b83 } /* Punctuation */ +.cm { color: #93a1a1; background-color: #eee8d5 } /* Comment.Multiline */ +.cp { color: #93a1a1; background-color: #eee8d5 } /* Comment.Preproc */ +.c1 { color: #93a1a1; background-color: #eee8d5 } /* Comment.Single */ +.cs { color: #93a1a1; background-color: #eee8d5 } /* Comment.Special */ +.gd { color: #839496 } /* Generic.Deleted */ +.ge { color: #839496; font-style: italic } /* Generic.Emph */ +.gr { color: #839496 } /* Generic.Error */ +.gh { color: #93a1a1; font-weight: bold } /* Generic.Heading */ +.gi { color: #839496 } /* Generic.Inserted */ +.go { color: #839496 } /* Generic.Output */ +.gp { color: #839496 } /* Generic.Prompt */ +.gs { color: #839496; font-weight: bold } /* Generic.Strong */ +.gu { color: #93a1a1 } /* Generic.Subheading */ +.gt { color: #839496 } /* Generic.Traceback */ +.kc { color: #6c71c4 } /* Keyword.Constant */ +.kd { color: #cb4b16 } /* Keyword.Declaration */ +.kn { color: #cb4b16 } /* Keyword.Namespace */ +.kp { color: #cb4b16 } /* Keyword.Pseudo */ +.kr { color: #cb4b16 } /* Keyword.Reserved */ +.kt { color: #cb4b16; text-decoration: underline } /* Keyword.Type */ +.ld { color: #6c71c4 } /* Literal.Date */ +.m { color: #002b36 } /* Literal.Number */ +.s { color: #2aa198 } /* Literal.String */ +.na { color: #586e75 } /* Name.Attribute */ +.nb { color: #b58900 } /* Name.Builtin */ +.nc { color: #268bd2; text-decoration: underline } /* Name.Class */ +.no { color: #6c71c4 } /* Name.Constant */ +.nd { color: #859900; font-style: italic } /* Name.Decorator */ +.ni { color: #657b83 } /* Name.Entity */ +.ne { color: #b58900; text-decoration: underline } /* Name.Exception */ +.nf { color: #268bd2; font-weight: bold } /* Name.Function */ +.nl { color: #657b83 } /* Name.Label */ +.nn { color: #859900 } /* Name.Namespace */ +.nx { color: #657b83 } /* Name.Other */ +.py { color: #657b83 } /* Name.Property */ +.nt { color: #657b83; font-weight: bold } /* Name.Tag */ +.nv { color: #586e75 } /* Name.Variable */ +.ow { color: #073642 } /* Operator.Word */ +.w { color: #586e75 } /* Text.Whitespace */ +.mf { color: #002b36 } /* Literal.Number.Float */ +.mh { color: #002b36 } /* Literal.Number.Hex */ +.mi { color: #002b36 } /* Literal.Number.Integer */ +.mo { color: #002b36 } /* Literal.Number.Oct */ +.sb { color: #2aa198 } /* Literal.String.Backtick */ +.sc { color: #2aa198 } /* Literal.String.Char */ +.sd { color: #2aa198; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #2aa198 } /* Literal.String.Double */ +.se { color: #d33682 } /* Literal.String.Escape */ +.sh { color: #2aa198 } /* Literal.String.Heredoc */ +.si { color: #859900 } /* Literal.String.Interpol */ +.sx { color: #2aa198 } /* Literal.String.Other */ +.sr { color: #6c71c4 } /* Literal.String.Regex */ +.s1 { color: #2aa198 } /* Literal.String.Single */ +.ss { color: #d33682; text-decoration: underline } /* Literal.String.Symbol */ +.bp { color: #002b36 } /* Name.Builtin.Pseudo */ +.vc { color: #586e75 } /* Name.Variable.Class */ +.vg { color: #586e75 } /* Name.Variable.Global */ +.vi { color: #586e75 } /* Name.Variable.Instance */ +.il { color: #002b36 } /* Literal.Number.Integer.Long */ diff --git a/lib/base.co b/lib/base.co index 225ea6e..cc62b1f 100644 --- a/lib/base.co +++ b/lib/base.co @@ -75,8 +75,11 @@ BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{ */ toKVPairs: -> kvo = _.collapseObject @toJSON() - for k, v in kvo - kvo[k] = @serialize v + if v and _.isObject v + for k, v in kvo + kvo[k] = @serialize v unless typeof v is 'function' + else + @serialize v kvo /** diff --git a/lib/util/underscore/kv.co b/lib/util/underscore/kv.co index fdc31ba..4eb5389 100644 --- a/lib/util/underscore/kv.co +++ b/lib/util/underscore/kv.co @@ -32,7 +32,7 @@ _kv = do {} /** - * Copies and flattens any sub-objects into namespaced keys on the parent object, such + * Copies and flattens a tree of sub-objects into namespaced keys on the parent object, such * that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`. */ collapseObject: (obj, parent={}, prefix='') ->