From 9eb939e38f3fc0368305d3219f68495cf26fa9bb Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Fri, 24 Apr 2015 17:16:43 +0200 Subject: [PATCH] helpers: CoffeeScript to ES6. --- src/helpers.coffee | 338 ----------------------------------------- src/helpers.js | 367 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+), 338 deletions(-) delete mode 100644 src/helpers.coffee create mode 100644 src/helpers.js diff --git a/src/helpers.coffee b/src/helpers.coffee deleted file mode 100644 index cd5136587..000000000 --- a/src/helpers.coffee +++ /dev/null @@ -1,338 +0,0 @@ -# FIXME: This file name should reflect what's inside! - -#===================================================================== - -$clone = require 'lodash.clone' -$forEach = require 'lodash.foreach' -$isArray = require 'lodash.isarray' -$isEmpty = require 'lodash.isempty' -$isFunction = require 'lodash.isfunction' - -#===================================================================== - -$asArray = (val) -> if $isArray val then val else [val] -$asFunction = (val) -> if $isFunction val then val else -> val - -$first = (collection, def) -> - if (n = collection.length)? - return collection[0] unless n is 0 - else - return value for own _, value of collection - - # Nothing was found, returns the `def` value. - def - -$removeValue = (array, value) -> - index = array.indexOf value - return false if index is -1 - array.splice index, 1 - true - -#--------------------------------------------------------------------- - -# TODO: currently the watch can be updated multiple times per -# “$MappedCollection.set()” which is inefficient: it should be -# possible to address that. - -$watch = (collection, { - # Key(s) of the “remote” objects watched. - # - # If it is a function, it is evaluated in the scope of the “current” - # object. (TODO) - # - # Default: undefined - keys - - # Alias for `keys`. - key - - # Rule(s) of the “remote” objects watched. - # - # If it is a function, it is evaluated in the scope of the “current” - # object. (TODO) - # - # Note: `key`/`keys` and `rule`/`rules` cannot be used both. - # - # Default: undefined - rules - - # Alias for `rules`. - rule - - # Value to add to the set. - # - # If it is a function, it is evaluated in the scope of the “remote” - # object. - # - # Default: -> @val - val - - # Predicates the “remote” object must fulfill to be used. - # - # Default: -> true - if: cond - - # Function evaluated in the scope of the “remote” object which - # returns the key of the object to update (usually the current one). - # - # TODO: Does it make sense to return an array? - # - # Default: undefined - bind - - # Initial value. - init - - # Function called when a loop is detected. - # - # Usually it is used to either throw an exception or do nothing to - # stop the loop. - # - # Note: The function may also returns `true` to force the processing - # to continue. - # - # Default: (number_of_loops) -> throw new Error 'loop detected' - loopDetected -}, fn) -> - val = if val is undefined - # The default value is simply the value of the item. - -> @val - else - $asFunction val - - loopDetected ?= -> throw new Error 'loop detected' - - # Method allowing the cleanup when the helper is no longer used. - #cleanUp = -> # TODO: noop for now. - - # Keys of items using the current helper. - consumers = Object.create null - - # Current values. - values = Object.create null - values.common = init - - # The number of nested processing for this watcher is counted to - # avoid an infinite loop. - loops = 0 - - updating = false - - process = (event, items) -> - return if updating - - # Values are grouped by namespace. - valuesByNamespace = Object.create null - - $forEach items, (item, key) -> # `key` is a local variable. - return unless not cond? or cond.call item - - if bind? - key = bind.call item - - # If bind did not return a key, ignores this value. - return unless key? - - namespace = "$#{key}" - else - namespace = 'common' - - # Computes the current value. - value = val.call item - - (valuesByNamespace[namespace] ?= []).push value - - return - - # Stops here if no values were computed. - return if $isEmpty valuesByNamespace - - if loops - return unless (loopDetected loops) is true - previousLoops = loops++ - - # For each namespace. - for namespace, values_ of valuesByNamespace - - # Updates the value. - value = values[namespace] - ctx = { - # TODO: test the $clone - value: if value is undefined then $clone init else value - } - changed = if event is 'enter' - fn.call ctx, values_, {} - else - fn.call ctx, {}, values_ - - # Notifies watchers unless it is known the value has not - # changed. - unless changed is false - values[namespace] = ctx.value - updating = true - if namespace is 'common' - collection.touch consumers - else - collection.touch (namespace.substr 1) - updating = false - - loops = previousLoops - - processOne = (event, item) -> - process event, [item] - - # Sets up the watch based on the provided criteria. - # - # TODO: provides a way to clean this when no longer used. - keys = $asArray (keys ? key ? []) - rules = $asArray (rules ? rule ? []) - if not $isEmpty keys - # Matching is done on the keys. - - throw new Error 'cannot use keys and rules' unless $isEmpty rules - - $forEach keys, (key) -> collection.on "key=#{key}", processOne - - # Handles existing items. - process 'enter', (collection.getRaw keys, true) - else if not $isEmpty rules - # Matching is done the rules. - - $forEach rules, (rule) -> collection.on "rule=#{rule}", process - - # TODO: Inefficient, is there another way? - rules = do -> # Minor optimization. - tmp = Object.create null - tmp[rule] = true for rule in rules - tmp - $forEach collection.getRaw(), (item) -> - processOne 'enter', item if item.rule of rules - else - # No matching done. - - collection.on 'any', process - - # Handles existing items. - process 'enter', collection.getRaw() - - # Creates the generator: the function which items will used to - # register to this watcher and to get the current value. - generator = do (key) -> # Declare a local variable. - -> - {key} = this - - # Register this item has a consumer. - consumers[key] = true - - # Returns the value for this item if any or the common value. - namespace = "$#{key}" - if namespace of values - values[namespace] - else - values.common - - # Creates a helper to unregister an item from this watcher. - generator.unregister = do (key) -> # Declare a local variable. - -> - {key} = this - delete consumers[key] - delete values["$#{key}"] - - # Creates a helper to get the value without using an item. - generator.raw = (key) -> - values[if key? then "$#{key}" else 'common'] - - # Returns the generator. - generator - -#===================================================================== - -$map = (options) -> - options.init = Object.create null - - $watch this, options, (entered, exited) -> - changed = false - - $forEach entered, ([key, value]) => - unless @value[key] is value - @value[key] = value - changed = true - return - $forEach exited, ([key, value]) => - if key of @value - delete @value[key] - changed = true - return - - changed - -#--------------------------------------------------------------------- - -# Creates a set of value from various items. -$set = (options) -> - # Contrary to other helpers, the default value is the key. - options.val ?= -> @key - - options.init = [] - - $watch this, options, (entered, exited) -> - changed = false - - $forEach entered, (value) => - if (@value.indexOf value) is -1 - @value.push value - changed = true - return - - $forEach exited, (value) => - changed = true if $removeValue @value, value - return - - changed - -#--------------------------------------------------------------------- - -$sum = (options) -> - options.init ?= 0 - - $watch this, options, (entered, exited) -> - prev = @value - - $forEach entered, (value) => @value += value - $forEach exited, (value) => @value -= value - - @value isnt prev - -#--------------------------------------------------------------------- - -# Uses a value from another item. -# -# Important note: Behavior is not specified when binding to multiple -# items. -$val = (options) -> - # The default value. - def = options.default - delete options.default - - options.init ?= def - - # Should the last value be kept instead of returning to the default - # value when no items are available! - keepLast = !!options.keepLast - delete options.keepLast - - $watch this, options, (entered, exited) -> - prev = @value - - @value = $first entered, (if keepLast then @value else def) - - @value isnt prev - -#===================================================================== - -module.exports = { - $map - $set - $sum - $val -} diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 000000000..b46e8e152 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,367 @@ +// FIXME: This file name should reflect what's inside! + +// =================================================================== + +import $clone from 'lodash.clone' +import $forEach from 'lodash.foreach' +import $isArray from 'lodash.isarray' +import $isEmpty from 'lodash.isempty' +import $isFunction from 'lodash.isfunction' + +// =================================================================== + +const $asArray = (val) => $isArray(val) ? val : [val] +const $asFunction = (val) => $isFunction(val) ? val : () => val + +const $first = (collection, defaultValue) => { + const {length} = collection + if (length == null) { + for (let key in collection) { + return collection[key] + } + } else if (length) { + return collection[0] + } + + // Nothing was found, returns the `def` value. + return defaultValue +} + +const $removeValue = (array, value) => { + const index = array.indexOf(value) + if (index === -1) { + return false + } + + array.splice(index, 1) + return true +} + +// ------------------------------------------------------------------- + +// TODO: currently the watch can be updated multiple times per +// “$MappedCollection.set()” which is inefficient: it should be +// possible to address that. + +const $watch = (collection, { + // Key(s) of the “remote” objects watched. + // + // If it is a function, it is evaluated in the scope of the “current” + // object. (TODO) + // + // Default: undefined + keys, + + // Alias for `keys`. + key, + + // Rule(s) of the “remote” objects watched. + // + // If it is a function, it is evaluated in the scope of the “current” + // object. (TODO) + // + // Note: `key`/`keys` and `rule`/`rules` cannot be used both. + // + // Default: undefined + rules, + + // Alias for `rules`. + rule, + + // Value to add to the set. + // + // If it is a function, it is evaluated in the scope of the “remote” + // object. + // + // Default: -> @val + val, + + // Predicates the “remote” object must fulfill to be used. + // + // Default: -> true + if: cond, + + // Function evaluated in the scope of the “remote” object which + // returns the key of the object to update (usually the current one). + // + // TODO: Does it make sense to return an array? + // + // Default: undefined + bind, + + // Initial value. + init, + + // Function called when a loop is detected. + // + // Usually it is used to either throw an exception or do nothing to + // stop the loop. + // + // Note: The function may also returns `true` to force the processing + // to continue. + loopDetected = () => { throw new Error('loop detected') } +}, fn) => { + val = val == null ? + // The default value is simply the value of the item. + function () { return this.val } : + $asFunction(val) + + // Method allowing the cleanup when the helper is no longer used. + // cleanUp = -> // TODO: noop for now. + + // Keys of items using the current helper. + const consumers = Object.create(null) + + // Current values. + const values = Object.create(null) + values.common = init + + // The number of nested processing for this watcher is counted to + // avoid an infinite loop. + let loops = 0 + + let updating = false + + const process = (event, items) => { + if (updating) return + + // Values are grouped by namespace. + const valuesByNamespace = Object.create(null) + + $forEach(items, (item) => { + if (cond && !cond.call(item)) return + + const namespace = (function () { + if (bind) { + const key = bind.call(item) + + return key && `$${key}` + } else { + return 'common' + } + })() + + // If not namespace, ignore this item. + if (!namespace) return + + ( + valuesByNamespace[namespace] || + (valuesByNamespace[namespace] = []) + ).push(val.call(item)) + }) + + // Stops here if no values were computed. + if ($isEmpty(valuesByNamespace)) return + + if (loops && loopDetected(loops) !== true) return + const previousLoops = loops++ + + // For each namespace. + $forEach(valuesByNamespace, (values_, namespace) => { + // Updates the value. + const value = values[namespace] + + const ctx = { + // TODO: test the $clone + value: value == null ? $clone(init) : value + } + const changed = event === 'enter' ? + fn.call(ctx, values_, {}) : + fn.call(ctx, {}, values_) + + // Notifies watchers unless it is known the value has not + // changed. + if (changed !== false) { + values[namespace] = ctx.value + updating = true + if (namespace === 'common') { + collection.touch(consumers) + } else { + collection.touch(namespace.substr(1)) + } + updating = false + } + }) + + loops = previousLoops + } + + const processOne = (event, item) => process(event, [item]) + + // Sets up the watch based on the provided criteria. + // + // TODO: provides a way to clean this when no longer used. + keys = $asArray(keys || key || []) + rules = $asArray(rules || rule || []) + if (!$isEmpty(keys)) { + // Matching is done on the keys. + + if (!$isEmpty(rules)) { + throw new Error('cannot use both keys and rules') + } + + $forEach(keys, key => { + collection.on(`key=${key}`, processOne) + }) + + // Handles existing items. + process('enter', collection.getRaw(keys, true)) + } else if (!$isEmpty(rules)) { + // Matching is done the rules. + + $forEach(rules, rule => { + collection.on(`rule=${rule}`, process) + }) + + // TODO: Inefficient, is there another way? + rules = (function (rules) { // Minor optimization. + const tmp = Object.create(null) + for (let rule of rules) { + tmp[rule] = true + } + return tmp + })(rules) + $forEach(collection.getRaw(), item => { + if (rules[item.rule]) { + processOne('enter', item) + } + }) + } else { + // No matching done. + + collection.on('any', process) + + // Handles existing items. + process('enter', collection.getRaw()) + } + + // Creates the generator: the function which items will used to + // register to this watcher and to get the current value. + const generator = function () { + const {key} = this + + // Register this item has a consumer. + consumers[key] = true + + // Returns the value for this item if any or the common value. + const namespace = `$${key}` + return (namespace in values) ? + values[namespace] : + values.common + } + + // Creates a helper to unregister an item from this watcher. + generator.unregister = function () { + const {key} = this + delete consumers[key] + delete values[`$${key}`] + } + + // Creates a helper to get the value without using an item. + generator.raw = (key) => values[key != null ? `$${key}` : 'common'] + + // Returns the generator. + return generator +} + +// =================================================================== + +export const $map = function (options) { + options.init = Object.create(null) + + return $watch(this, options, function (entered, exited) { + let changed = false + + $forEach(entered, ([key, value]) => { + if (this.value[key] !== value) { + this.value[key] = value + changed = true + } + }) + $forEach(exited, ([key, value]) => { + if (key in this.value) { + delete this.value[key] + changed = true + } + }) + + return changed + }) +} + +// ------------------------------------------------------------------- + +// Creates a set of value from various items. +export const $set = function (options) { + // Contrary to other helpers, the default value is the key. + if (!options.val) { + options.val = function () { return this.key } + } + + options.init = [] + + return $watch(this, options, function (entered, exited) { + let changed = false + + $forEach(entered, (value) => { + if (this.value.indexOf(value) === -1) { + this.value.push(value) + changed = true + } + }) + + $forEach(exited, (value) => { + if ($removeValue(this.value, value)) { + changed = true + } + }) + + return changed + }) +} + +// ------------------------------------------------------------------- + +export const $sum = function (options) { + if (!options.init) { + options.init = 0 + } + + return $watch(this, options, function (entered, exited) { + const prev = this.value + + $forEach(entered, (value) => { this.value += value }) + $forEach(exited, (value) => { this.value -= value }) + + return this.value !== prev + }) +} + +// ------------------------------------------------------------------- + +// Uses a value from another item. +// +// Important note: Behavior is not specified when binding to multiple +// items. +export const $val = function (options) { + // The default value. + const def = options.default + delete options.default + + if (!options.init) { + options.init = def + } + + // Should the last value be kept instead of returning to the default + // value when no items are available! + const keepLast = !!options.keepLast + delete options.keepLast + + return $watch(this, options, function (entered, exited) { + const prev = this.value + + this.value = $first(entered, keepLast ? this.value : def) + + return this.value !== prev + }) +}