* Subsequent listeners added on this event will be auto-triggered.
* @returns {this}
*/
- triggerReady: ->
- return this if @ready
- @ready = true
- @trigger 'ready', this
+ triggerReady: (lock='ready', event='ready') ->
+ return this if @[lock]
+ @[lock] = true
+ @trigger event, this
this
/**
* 'ready-reset' event.
* @returns {this}
*/
- resetReady: ->
- return this unless @ready
- @ready = false
- @trigger 'ready-reset', this
+ resetReady: (lock='ready', event='ready') ->
+ return this unless @[lock]
+ @[lock] = false
+ @trigger "#event-reset", this
this
/**
* @param {Object} [context]
* @returns {this}
*/
- on: (events, callback, context) ->
+ on: (events, callback, context=this) ->
return this if not callback
Backbone.Events.on ...
if @ready and _.contains events.split(/\s+/), 'ready'
- callback.call context or this, this
+ callback.call context, this
this
+ makeHandlersForCallback: (cb) ->
+ success : ~> cb.call this, [null].concat arguments
+ error : ~> cb.call this, it
+
+
### Synchronization
self.unwait(); fn ...
+
+ ###
+
getClassName: ->
"#{@..name or @..displayName}"
@__superclass__ = @..__super__.constructor
@waitingOn = 0
Backbone.Model ...
- @trigger 'create', this
+ # @..trigger 'create', this
#
+ ### Data & Model Loading
+
+ /**
+ * Override to customize what data or assets the model requires,
+ * and how they should be loaded.
+ *
+ * By default, `load()` simply calls `loadModel()` via `loader()`.
+ *
+ * @see BaseModel#loader
+ * @see BaseModel#loadModel
+ * @returns {this}
+ */
+ load: ->
+ console.log "#this.load()"
+ @loader do
+ start : @loadModel
+ completeEvent : 'fetch-success'
+ this
+
+ /**
+ * Wraps the loading workflow boilerplate:
+ * - Squelches multiple loads from running at once
+ * - Squelches loads post-ready, unless forced
+ * - Triggers a start event
+ * - Triggers "ready" when complete
+ * - Wraps workflow with wait/unwait
+ * - Cleans up "loading" state
+ *
+ * @protected
+ * @param {Object} [opts={}] Options:
+ * @param {Function} opts.start Function that starts the loading process. Always called with `this` as the context.
+ * @param {String} [opts.startEvent='load'] Event to trigger before beginning the load.
+ * @param {String} [opts.completeEvent='load-success'] Event which signals loading has completed.
+ * @param {Boolean} [opts.force=false] If true, move forward with the load even if we're ready.
+ * @returns {this}
+ */
+ loader: (opts={}) ->
+ opts = { -force, startEvent:'load', completeEvent:'load-success', ...opts }
+ @resetReady() if opts.force
+ return this if not opts.start or @loading or @ready
+
+ @wait()
+ @loading = true
+ @trigger opts.startEvent, this
+
+ # Register a handler for the post-load event that will run only once
+ @once opts.completeEvent, ~>
+ console.log "#{this}.onLoadComplete()"
+ @loading = false
+ @unwait() # terminates the `load` wait
+ @trigger 'load-success', this unless opts.completeEvent is 'load-success'
+ @triggerReady()
+
+ # Finally, start the loading process
+ opts.start.call this
+ this
+
+ /**
+ * Runs `.fetch()`, triggering a `fetch` event at start, and
+ * `fetch-success` / `fetch-error` on completion.
+ *
+ * @protected
+ * @returns {this}
+ */
+ loadModel: ->
+ @wait()
+ @trigger 'fetch', this
+ @fetch do
+ success : ~> @unwait(); @trigger 'fetch-success', this
+ error : ~> @unwait(); @trigger 'fetch-error', this, ...arguments
+ this
### Serialization
toString: ->
"#{@getClassName()}[#{@length}]"
- # toString: ->
- # modelIds = @models
- # .map -> "\"#{it.id ? it.cid}\""
- # .join ', '
- # "#{@getClassName()}[#{@length}](#modelIds)"
+ toStringWithIds: ->
+ modelIds = @models
+ .map -> "\"#{it.id ? it.cid}\""
+ .join ', '
+ "#{@getClassName()}[#{@length}](#modelIds)"
# }}}
.parMap_ (next, id) ~>
return next.ok(that) if @cache.get id
@register @createModel id
- .on 'ready', next
+ .on 'ready', -> next.ok it
.load()
.unflatten()
.seq (models) ->
} = require 'kraken/dataset/dataset-view'
{ MetricEditView,
} = require 'kraken/dataset/metric-edit-view'
-
+{ DataSource,
+} = require 'kraken/dataset/datasource-model'
/**
- * @class
+ * @class DataSet selection and customization UI (root of the `data` tab).
*/
DataView = exports.DataView = BaseView.extend do # {{{
- __bind__ : <[ ]>
+ __bind__ : <[ onMetricsChanged ]>
tagName : 'section'
className : 'data-ui'
template : require 'kraken/template/data'
datasources : null
-
+ /**
+ * @constructor
+ */
constructor: function DataView
BaseView ...
@graph_id = @options.graph_id
BaseView::initialize ...
@metric_views = new ViewList
- @datasources = @model.sources
+ @datasources = DataSource.getAllSources()
@model.metrics
- .on 'add', @addMetric, this
- .on 'remove', @removeMetric, this
- @on 'ready', @onReady, this
- @load()
+ .on 'add', @addMetric, this
+ .on 'remove', @removeMetric, this
+ @model.once 'ready', @onReady, this
onReady: ->
+ console.log "#this.onReady! #{@model.metrics}"
dataset = @model
@model.metrics.each @addMetric, this
- # @metric_edit_view = @addSubview new MetricEditView {@graph_id, dataset, @datasources}
- # @metric_edit_view
- # .on 'update', @onUpdateMetric, this
-
@dataset_view = new DataSetView {@model, @graph_id, dataset, @datasources}
@addSubview @dataset_view
.on 'add-metric', @onMetricsChanged, this
.on 'edit-metric', @editMetric, this
@render()
+ @triggerReady()
this
-
- load: ->
- @wait()
- # $.getJSON '/datasources/all', (@data) ~>
- # _.each @data, @canonicalizeDataSource, this
- # @model.sources.reset _.map @data, -> it
- @unwait()
- # @render()
- @triggerReady()
-
/**
* Transform the `columns` field to ensure an Array of {label, type} objects.
*/
attrs = _.clone @model.attributes
{ @graph_id, @datasources } import attrs
- # Don't rebuild HTML, simply notify subviews
- # render: ->
- # @renderSubviews()
- # @trigger 'render', this
- # this
-
addMetric: (metric) ->
console.log "#this.addMetric!", metric
return metric if @metric_views.findByModel metric
- view = new MetricEditView {model:metric, @graph_id, dataset, @datasources}
+ view = new MetricEditView {model:metric, @graph_id, dataset:@model, @datasources}
+ view.on 'update', @onUpdateMetric, this
@metric_views.push @addSubview view
metric
@metric_views.invoke 'hide'
@metric_edit_view = @metric_views.findByModel metric
@metric_edit_view?.show()
- @onMetricsChanged()
+ _.delay @onMetricsChanged, 10
onMetricsChanged: ->
oldMinHeight = parseInt @$el.css 'min-height'
newMinHeight = Math.max do
- oldMinHeight
@dataset_view.$el.height()
@metric_edit_view?.$el.height()
# console.log 'onMetricsChanged!', oldMinHeight, '-->', newMinHeight
*/
DataSet = exports.DataSet = BaseModel.extend do # {{{
urlRoot : '/datasets'
- ready : false
/**
* @type DataSourceList
initialize : ->
BaseModel::initialize ...
- @sources = new DataSourceList
@metrics = new MetricList @attributes.metrics
@on 'change:metrics', @onMetricChange, this
load: (opts={}) ->
@resetReady() if opts.force
- return this if @ready
+ return this if @loading or @ready
+
+ unless @metrics.length
+ return @triggerReady()
+
+ console.log "#this.load()..."
@wait()
+ @loading = true
@trigger 'load', this
- Seq _.unique @metrics.pluck 'source_id'
- .parMap_ (next, source_id) ->
- DataSource.lookup source_id, next
- .seqEach_ (next, source) ~>
- @sources.add source
- next.ok source
+ Seq @metrics.models
+ .parEach_ (next, metric) ->
+ metric.once 'ready', next.ok .load()
+ # .parEach_ (next, metric) ->
+ # metric.on 'load-data-success', next.ok .loadData()
.seq ~>
+ console.log "#{this}.load() complete!"
+ @loading = false
@unwait() # terminates the `load` wait
@triggerReady()
this
m
onMetricChange: ->
- @metrics.reset @get 'metrics'
+ @resetReady()
+ # @metrics.reset @get 'metrics'
+ @load()
# TODO: toJSON() must ensure columns in MetricList are ordered by index
# ...in theory, MetricList.comparator now does this