From 50d03061546d1f4aa94e43eac95c3ad69ed71b59 Mon Sep 17 00:00:00 2001 From: dsc Date: Mon, 27 Feb 2012 20:32:14 -0800 Subject: [PATCH] Implements .fromKVPairs() with nested object support for most all of the models. --- lib/base.co | 70 ++++++++++++++++++++++++++++----------- lib/graph/model.co | 36 ++++---------------- lib/graph/view.co | 39 +++++++-------------- lib/scaffold/model.co | 53 ++++++++++++++++-------------- lib/scaffold/view.co | 46 ++++++++++++++++--------- lib/underscore/object.co | 82 +++++++++++++++++++++++++++++++++++++-------- 6 files changed, 195 insertions(+), 131 deletions(-) diff --git a/lib/base.co b/lib/base.co index a7d23c9..f3766f4 100644 --- a/lib/base.co +++ b/lib/base.co @@ -2,46 +2,76 @@ _ = require 'kraken/underscore' op = require 'kraken/util/op' +/** + * @class Base model, extending Backbone.Model, used by scaffold and others. + * @extends Backbone.Model + */ BaseModel = exports.BaseModel = Backbone.Model.extend do # {{{ - ctorName : 'BaseModel' - # List of methods to bind on initialize; set on subclass + # A list of method-names to bind on initialize; set this on a subclass to override. __bind__ : [] - - idAttribute : 'id' - valueAttribute : 'value' - + ctorName : 'BaseModel' initialize: -> _.bindAll this, ...@__bind__ if @__bind__.length @__super__ = @constructor.__super__ - - toKVPairs: (kv_delimiter='=') -> - idAttr = @idAttribute or 'id' - key = @[idAttr] or @get idAttr - valAttr = @valueAttribute or 'value' - value = @get 'value' - if key and value? - "#{encodeURIComponent key}#kv_delimiter#{encodeURIComponent value}" - else - '' - - toString: -> "#{@ctorName}(id=#{@id}, value=#{@get 'value'})" + /** + * Like `.toJSON()` in that it should return a plain object with no functions, + * but for the purpose of `.toKVPairs()`, allowing you to customize the values + * included and keys used. + * @returns {Object} + */ + toKVObject: -> + _.collapseObject @toJSON() + + /** + * Serialize the model into a `www-form-encoded` string suitable for use as + * a query string or a POST body. + * @returns {String} + */ + toKVPairs: (item_delim='&', kv_delim='=') -> + _.toKVPairs @toKVObject(), item_delim, kv_delim + + toString: -> "#{@ctorName}(id=#{@id})" # Class Methods BaseModel import do - fromKVPairs: (o) -> - o = _.fromKVPairs o if typeof o is 'string' + /** + * Factory method which constructs an instance of this model from a string of KV-pairs. + * This is a class method inherited by models which extend {BaseModel}. + * @static + * @param {String|Object} o Serialized KV-pairs (or a plain object). + * @returns {BaseModel} An instance of this model. + */ + fromKVPairs: (o, item_delim='&', kv_delim='=') -> + o = _.fromKVPairs o, item_delim, kv_delim if typeof o is 'string' Cls = if typeof this is 'function' then this else this.constructor - new Cls o + new Cls _.uncollapseObject o + +# }}} +/** + * @class Base collection, extending Backbone.Collection, used by scaffold and others. + * @extends Backbone.Collection + */ +BaseList = exports.BaseList = Backbone.Collection.extend do # {{{ + ctorName : 'BaseList' + + toKVObject: -> + _.collapseObject @toJSON() + + toKVPairs: (item_delim='&', kv_delim='=') -> + _.toKVPairs @toKVObject(), item_delim, kv_delim + + toString: -> "#{@ctorName}(length=#{@length})" # }}} /** - * @class Base View, used by scaffold and others. + * @class Base view, extending Backbone.View, used by scaffold and others. + * @extends Backbone.View */ BaseView = exports.BaseView = Backbone.View.extend do # {{{ ctorName : 'BaseView' diff --git a/lib/graph/model.co b/lib/graph/model.co index 7932914..9a0cf32 100644 --- a/lib/graph/model.co +++ b/lib/graph/model.co @@ -89,33 +89,12 @@ GraphOption = exports.GraphOption = Field.extend do # {{{ for k, v in o o[k] = '' if v!? o - - toKVPairs: -> - key = @get 'name' - value = @get 'value' - if value? - "#{encodeURIComponent key}=#{encodeURIComponent value}" - else - '' - - # }}} GraphOptionList = exports.GraphOptionList = FieldList.extend do # {{{ ctorName : 'GraphOptionList' model : GraphOption - - /** - * Transforms this list into form-encoded KV-pairs, excluding null values and - * @returns {String} - */ - toKVPairs: (keepDefaults=false) -> - @models - .filter -> it.get('name') and it.getValue()? and (keepDefaults or not it.isDefault()) - .map -> "#{encodeURIComponent it.get 'name'}=#{encodeURIComponent it.serializeValue()}" - .join '&' - # }}} @@ -132,21 +111,24 @@ GraphModel = exports.GraphModel = BaseModel.extend do # {{{ if name and not (@id or @has 'id') @id = @attributes.id = _.underscored name - defaults : -> + defaults: -> { + id : 'graph' name : 'Kraken Graph' dataset : '/data/pageviews_by.timestamp.language.csv' + desc : '' + width : 'auto' + height : 'auto' options : {} } hasOption: (key) -> options = @get 'options', {} - key in options + options[key]? getOption: (key, def) -> - options = @get 'options', {} - if key in options then options[key] else def + @get('options', {})[key] ? def setOption: (key, value, opts={}) -> options = @get 'options', {} @@ -161,10 +143,8 @@ GraphModel = exports.GraphModel = BaseModel.extend do # {{{ @trigger "change:options:#key", this, value, key, opts unless opts.silent - toKVPairs: -> - ... - toString: -> "#{@ctorName}(id=#{@id}, name=#{@get 'name'}, dataset=#{@get 'dataset'})" + # }}} diff --git a/lib/graph/view.co b/lib/graph/view.co index f8c335c..a734073 100644 --- a/lib/graph/view.co +++ b/lib/graph/view.co @@ -29,13 +29,6 @@ GraphOptionView = exports.GraphOptionView = FieldView.extend do # {{{ 'click .collapsed' : 'onClick' - update: -> - val = @$el.find('.value').val() - current = @model.get 'value' - return if val is current - console.log "#this.update( #current -> #val )" - @model.setValue val, {+silent} - render: -> @__super__.render ... @$el.addClass 'collapsed' if @isCollapsed @@ -80,21 +73,17 @@ GraphOptionsScaffold = exports.GraphOptionsScaffold = Scaffold.extend do # {{{ # itemPositionDataEnabled : true # animationEngine : 'jquery' + /** + * Add a GraphOption to this scaffold, rerendering the isotope + * layout after collapse events. + */ addOne: (field) -> - # console.log "#this.addOne!", field - - field.on 'change:value', ~> - key = field.get 'name' - value = field.getValue() - @trigger "change:#key", this, value, key, field - @trigger "change", this, value, key, field - view = @__super__.addOne ... view.on 'change:collapse render', @render view toKVPairs: -> - @collection.toKVPairs() + @collection.toKVPairs ... # }}} @@ -114,19 +103,18 @@ GraphView = exports.GraphView = BaseView.extend do # {{{ initialize : (o={}) -> - @render = _.debounce @render, DEBOUNCE_RENDER - @renderAll = _.debounce @renderAll, DEBOUNCE_RENDER - @model or= new GraphModel BaseView::initialize ... # console.log "#this.initialize!" + @render = _.debounce @render, DEBOUNCE_RENDER + @renderAll = _.debounce @renderAll, DEBOUNCE_RENDER + @model.on 'destroy', @remove, this + @model.on 'change', @render, this @model.on 'change:options', ~> changes = @model.changedAttributes() console.log 'Model.changed(options) ->', changes @chartOptions changes, {+silent} - @model.on 'change', @render, this - @model.on 'destroy', @remove, this @build() @viewport = @$el.find '.viewport' @@ -136,13 +124,13 @@ GraphView = exports.GraphView = BaseView.extend do # {{{ @scaffold.collection.reset that if o.graph_spec @scaffold.on 'change', (scaffold, value, key, field) ~> - console.log "scaffold.change", value, key, field + console.log "scaffold.change!", value, key, field @model.setOption key, value, {+silent} options = @model.get 'options', {} @chartOptions options, {+silent} - _.delay @render, DEBOUNCE_RENDER + _.delay @renderAll, DEBOUNCE_RENDER chartOptions: (values, opts) -> @@ -204,9 +192,8 @@ GraphView = exports.GraphView = BaseView.extend do # {{{ @render() false - - toKVPairs: (keepDefaults=false) -> - @scaffold.toKVPairs keepDefaults + toKVPairs: -> + @model.toKVPairs ... toString: -> "#{@ctorName}(#{@model})" # }}} diff --git a/lib/scaffold/model.co b/lib/scaffold/model.co index 083c0e3..91ff110 100644 --- a/lib/scaffold/model.co +++ b/lib/scaffold/model.co @@ -1,6 +1,6 @@ _ = require 'kraken/underscore' op = require 'kraken/util/op' -{BaseModel} = require 'kraken/base' +{BaseModel, BaseList} = require 'kraken/base' @@ -68,19 +68,9 @@ Field = exports.Field = BaseModel.extend do # {{{ null - /* * * Serializers * * */ - serializeValue: -> - v = @getValue() - if v!? - v = '' - else if _.isArray(v) or _.isObject(v) - v = JSON.stringify v - String v - - /* * * Value Accessors * * */ getValue: (def) -> - @getParser() @get 'value', def + @getParser() @get @valueAttribute, def setValue: (v, options) -> def = @get 'default' @@ -88,45 +78,58 @@ Field = exports.Field = BaseModel.extend do # {{{ val = null else val = @getParser()(v) - @set 'value', val, options + @set @valueAttribute, val, options clearValue: -> - @set 'value', @get('default') + @set @valueAttribute, @get('default') isDefault: -> - @get('value') is @get('default') + @get(@valueAttribute) is @get('default') + /* * * Serializers * * */ + serializeValue: -> + v = @getValue() + if v!? + v = '' + else if _.isBoolean v + v = Number v + else if _.isArray(v) or _.isObject(v) + v = JSON.stringify v + String v + toJSON: -> {id:@id} import do _.clone(@attributes) import { value:@getValue(), def:@get('default') } + toKVObject: -> + { "#{@id}": @serializeValue() } - toString: -> "(#{@id}: #{@get 'value'})" + toString: -> "(#{@id}: #{@serializeValue()})" # }}} -FieldList = exports.FieldList = Backbone.Collection.extend do # {{{ +FieldList = exports.FieldList = BaseList.extend do # {{{ ctorName : 'FieldList' - model : Field + model : Field /** * Collects a map of fields to their values, excluding those set to `null` or their default. * @returns {Object} */ - values: -> + values: (keepDefaults=false) -> _.synthesize do - @models.filter -> not it.isDefault() + if keepDefaults then @models else @models.filter -> not it.isDefault() -> [ it.get('name'), it.getValue() ] toJSON: -> @values() - toKVPairs: -> - @models - .filter -> it.get('name') and it.getValue()? - .map -> "#{encodeURIComponent it.id}=#{encodeURIComponent it.get 'value'}" - .join '&' + toKVObject: -> + _.collapseObject @toJSON() + + toKVPairs: (item_delim='&', kv_delim='=') -> + _.toKVPairs @toKVObject(), item_delim, kv_delim toString: -> "#{@ctorName}(length=#{@length})" # }}} diff --git a/lib/scaffold/view.co b/lib/scaffold/view.co index e9c9c89..b16cceb 100644 --- a/lib/scaffold/view.co +++ b/lib/scaffold/view.co @@ -10,20 +10,21 @@ FieldView = exports.FieldView = BaseView.extend do # {{{ className : 'field' events : - 'blur .value' : 'onUIChange' - 'submit .value' : 'onUIChange' + 'blur .value' : 'update' + 'submit .value' : 'update' # initialize: -> # # console.log "#this.initialize!" # BaseView::initialize ... - onUIChange: -> - val = @$el.find('.value').val() - current = @model.get 'value' - return if val is current - console.log "#this.onUIChange( #current -> #val )" + update: -> + val = @model.getParser() @$el.find('.value').val() + current = @model.getValue() + return if _.isEqual val, current + console.log "#this.update( #current -> #val )" @model.setValue val, {+silent} + @trigger 'update', this render: -> return @remove() if @model.get 'hidden', false @@ -74,10 +75,17 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ # console.log "[S] #this.addOne!", @__super__ _.remove @subviews, field.view if field.view + # avoid duplicating event propagation + field.off 'change:value', @change, this + + # propagate value-change events as key-value change events + field.on 'change:value', @change, this + SubviewType = @subviewType view = new SubviewType model:field @subviews.push view @$el.append view.render().el unless field.get 'hidden' + view.on 'update', @change.bind(this, field) @render() view @@ -88,18 +96,22 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{ @collection.each @addOne this - get: (id) -> - @collection.get id - - at: (idx) -> - @collection.at idx - - pluck: (prop) -> - @collection.pluck prop + change: (field) -> + key = field.get 'name' + value = field.getValue() + @trigger "change:#key", this, value, key, field + @trigger "change", this, value, key, field + this - values: -> - @collection.values() toString: -> "#{@ctorName}(collection=#{@collection})" + + + +# Proxy collection methods +<[ get at pluck invoke values toJSON toKVObject toKVPairs ]> + .forEach (methodname) -> + Scaffold::[methodname] = -> @collection[methodname].apply @collection, arguments + # }}} diff --git a/lib/underscore/object.co b/lib/underscore/object.co index de10a0c..22f7fe1 100644 --- a/lib/underscore/object.co +++ b/lib/underscore/object.co @@ -1,6 +1,26 @@ _ = require 'underscore' +OBJ_PROTO = Object.prototype +getProto = Object.getPrototypeOf + _obj = do + + isPlainObject : (o) -> + !! o and _.isObject(o) and OBJ_PROTO is getProto(o) + + /** + * Converts the collection to a list of its items: + * - Objects become a list of `[key, value]` pairs. + * - Strings become a list of characters. + * - Arguments objects become an array. + * - Arrays are copied. + */ + items: (obj) -> + if _.isObject(obj) and not _.isArguments(obj) + _.map obj, (v, k) -> [k, v] + else + [].slice.call obj + /** * In-place removal of a value from an Array or Object. */ @@ -17,35 +37,61 @@ _obj = do - toKVPairs: (o, item_delimiter='&', kv_delimiter='=') -> + toKVPairs: (o, item_delim='&', kv_delim='=') -> _.reduce do o (acc, v, k) -> - acc.push encodeURIComponent(k)+kv_delimiter+encodeURIComponent(v) if k + acc.push encodeURIComponent(k)+kv_delim+encodeURIComponent(v) if k acc [] - .join item_delimiter - + .join item_delim - fromKVPairs: (qs, item_delimiter='&', kv_delimiter='=') -> + fromKVPairs: (qs, item_delim='&', kv_delim='=') -> _.reduce do - qs.split item_delimiter + qs.split item_delim (acc, pair) -> - [k, v] = pair.split kv_delimiter + idx = pair.indexOf kv_delim + if idx is not -1 + [k, v] = [pair.slice(0, idx), pair.slice(idx+1)] + else + [k, v] = [pair, ''] acc[ decodeURIComponent k ] = decodeURIComponent v if k acc {} - + + /** + * @returns {Object} Copies and flattens any sub-objects into namespaced keys on the parent object, + * such that `{ "foo":{ "bar":1 } }` becomes `{ "foo.bar":1 }`. + */ + collapseObject: (obj, parent={}, prefix='') -> + prefix += '.' if prefix + _.each obj, (v, k) -> + if _.isPlainObject v + _.collapseObject v, parent, prefix+k + else + parent[prefix+k] = v + parent + + uncollapseObject: (obj) -> + _.reduce do + obj + (acc, v, k) -> + _.setNested acc, k, v, true + acc + {} /** * Searches a heirarchical object for a given subkey specified in dotted-property syntax. * @param {Object} base The object to serve as the root of the property-chain. * @param {Array|String} chain The property-chain to lookup. + * @param {Boolean} [ensure=false] If true, intermediate keys that are `null` or + * `undefined` will be filled in with a new empty object `{}`, ensuring the get will + * return valid metadata. * @retruns {null|Object} If found, the object is of the form `{ key: Qualified key name, obj: Parent object of key, val: Value at obj[key] }`. Otherwise `null`. */ - getNestedMeta : (obj, chain) -> + getNestedMeta : (obj, chain, ensure=false) -> chain = chain.split('.') if typeof chain is 'string' - return _.reduce do + _.reduce do chain (current, key, idx, chain) -> return null unless current? @@ -56,8 +102,11 @@ _obj = do key : key val : current[key] - if key in current - current[key] + val = current[key] + if val? + val + else if ensure + current[key] = {} else null obj @@ -82,10 +131,13 @@ _obj = do * @param {Object} obj The object to serve as the root of the property-chain. * @param {Array|String} chain The property-chain to lookup. * @param {Any} value The value to set. - * @retruns {null|Object} If found, returns the old value, and otherwise `null`. + * @param {Boolean} [ensure=false] If true, intermediate keys that are `null` or + * `undefined` will be filled in with a new empty object `{}`, ensuring the set + * will succeed. + * @retruns {null|Any} If found, returns the old value, and otherwise `null`. */ - setNested : (obj, chain, value) -> - meta = _obj.getNestedMeta obj, chain + setNested : (obj, chain, value, ensure=false) -> + meta = _obj.getNestedMeta obj, chain, ensure if meta meta.obj[meta.key] = value meta.val -- 1.7.0.4