Class in Sqimitive Core

class Sqimitive.Core
Implements inheritance and event system without any other Sqimitive (Base) functionality

Defined in: main.js, line 94

Description (skip)

Implements inheritance and event system without any other Sqimitive (Base) functionality.

In a typical scenario you don’t need to use Core directly – use Sqimitive.Base instead.

In special cases, since Core lacks any specialized functionality it can be used as a base class for your classes that need better inheritance and/or events (like a more elaborate alternative to https://github.com/primus/eventemitter3)…

var MyEventAwareClass = Sqimitive.Core.extend({
  doFoo: function () {
    this.fire('beforeFoo', ['arg', 'arg'])
    // ...
    this.fire('afterFoo', ['...'])
  }
})

// Now clients can subscribe to its events:
myEventAwareObject.on('beforeFoo', function () {
  alert('Boo!')
})

// ...

myEventAwareObject.doFoo()    //=> Boo!

…Or as an instance object held in an internal property if you want to avoid exposing any Sqimitive functionality at all:

var MyEventAwareClass = function () {
  var _events = new Sqimitive.Core

  this.on   = function() { _events.on.apply(_events, arguments) }
  this.off  = function() { _events.off.apply(_events, arguments) }
  this.fire = function() { _events.fire.apply(_events, arguments) }

  // Just like in the above example:
  this.doFoo = function () { ... }
}

myEventAwareObject.on('beforeFoo', ...)
myEventAwareObject.doFoo()

Properties

_mergeProps

Modifiers: protected, static

Part of: tag_Extension

from setOnDeclViaPush

May only be set after Core::extend() using MyClass.prop.push(...).

Specifies which instance properties are to be merged rather than overwritten when redefined in a subclass.

Value Types
Types Notes
array of string Property names.

Properties must be of these types:

Members
Name Types Notes
array Merged using Array.concat(baseClass[prop], childClass[prop]). Subclass’ values are added after parents’ values: ['a', 'b'] + ['c'] = ['a', 'b', 'c'].
object Merged using _.extend(baseClass[prop], childClass[prop]). Keys defined in parents are kept unless also defined in a subclass: {a: 1, b: 2} + {a: 3, c: 4} = {a: 3, b: 2, c: 4}.
Example
var MyParent = Sqimitive.Base.extend({
  _objectProperty: {key1: 'value1', key2: 'value2'},
  _arrayProperty: ['item1', 'item2'],
})

MyParent._mergeProps.push('_objectProperty', '_arrayProperty')

var MyChild = MyParent.extend({
  _objectProperty: {key2: 'CHILD1', key3: 'CHILD2'},
  _arrayProperty: ['item3', 'item4'],
})

// MyChild._objectProperty is now
// {key1: 'value1', key2: 'CHILD1', key3: 'CHILD2'}

// MyChild._arrayProperty is now ['item1', 'item2', 'item3', 'item4']

mergePropsExtendWarning: when passing _mergeProps or _shareProps inside staticProps (second argument of extend()) all inherited items will be removed. The correct way to add your properties while keeping those in the base classes is this:

var MySqimitive = Sqimitive.Base.extend({
  // Define instance fields...
}, {
  // Define static fields if you need, else don't pass this parameter.
})

// extend() has copied the inherited _mergeProps list which we can now
// append to or modify using regular Array functions.
MySqimitive._mergeProps.push('prop1', 'prop2', ...)
// Same with _shareProps.
MySqimitive._shareProps.push('prop1', 'prop2', ...)

These are wrong ways to append to these properties:

var MySqimitive = Sqimitive.Base.extend({
  // WRONG: _mergeProps is static so it won't be read from here.
  _mergeProps: ['prop'],
}, {
  // WRONG: technically fine but will entirely replace base class'
  // merge list.
  _mergeProps: ['prop'],
})

// WRONG: works but once again will replace all the inherited items.
MySqimitive._mergeProps = ['prop']

// CORRECT:
MySqimitive._mergeProps.push('prop')

Other notes:

  • extend() always clones _mergeProps and _shareProps.
  • By default, Base class adds Base._opt, Base.elEvents and _respToOpt to this list.
  • See instance .mixIn() for the implementation.
  • Removing a parent-of-parent’s property from _mergeProps doesn’t “un-merge” it since merging happens in extend() (mixIn()) so after extend() the new class already has merged properties of all of its ancestors. However, removal does affect new subclasses:
    var ClassA = Sqimitive.Base.extend({array: ['in A']})
    ClassA._mergeProps.push('array')
      // (new ClassA).array is ['in A']
    
    var ClassB = ClassA.extend({array: ['in B']})
      // (new ClassB).array is ['in A', 'in B']
    
    var ClassC = ClassB.extend({array: ['in C']})
    ClassC._mergeProps = []
      // (new ClassC).array is ['in A', 'in B', 'in C']
    
    var ClassD = ClassC.extend({array: ['in D']})
      // (new ClassD).array is ['in D'] - not merged!

Complex inherited value modification

_mergeProps doesn’t allow deleting members or otherwise changing the value. However, in some contexts null or undefined does the job for objects (in contrast with delete such properties still appear when using for..in, etc.):

var MyParent = Sqimitive.Base.extend({
  _objectProperty: {key1: 'value1', key2: 'value2'},
})

MyParent._mergeProps.push('_objectProperty')

var MyChild = MyParent.extend({
  _objectProperty: {key1: null},
})

// MyChild._objectProperty is now {key1: null, key2: 'value2'}

for (var key in new MyChild) { alert(key) }
  //=> key1
  //=> key2

Use Base.init() or postInit() to modify inherited values in other ways:

var MyParent = Sqimitive.Base.extend({
  _objectProperty: {key1: 'value1', key2: 'value2'},
})

var MyChild = MyParent.extend({
  events: {
    init: function () {
      // MyParent's _objectProperty is unaffected, see _shareProps.
      delete this._objectProperty.key1
      this._objectProperty.key2 += 'foo'
    },
  },
})

// MyChild._objectProperty is now {key2: 'value2foo'}

for (var key in new MyChild) { alert(key) }
  //=> key2

from inBB

In Backbone…

In Backbone, when you extend a parent class with a property that it already has you end up with a completely new property. This doesn’t always make sense – for example, if a class has its own bb:View-events then what you really need is merge its own (base) events with the events of new subclass. Same thing with bb:Model-attributes and bb:Model-defaults, bb:Router-routes and others. Example (http://jsfiddle.net/Proger/u2n3e6ex/):

var MyView = Backbone.View.extend({
  events: {
    'click .me': function () { alert('You clicked it!') },
  },
})

var MyOtherView = MyView.extend({
  // This object entirely replaces MyView's event map.
  events: {
    'keypress .me': function () {
      alert('Oh noes, we broke the button :(')
    },
  },
})

Defined in: main.js, line 430Show code

_mergeProps: [],
_shareProps

Modifiers: protected, static

Part of: tag_Extension

from setOnDeclViaPush

May only be set after Core::extend() using MyClass.prop.push(...).

Specifies which instance properties are not to be cloned upon construction. They will be shared by all instances of a class (where the property was defined, i.e. where given to extend()).

Value Types
Types Notes
array of string Property names.

Unlisted instance properties are cloned (using deepClone()) upon new object instantiation (in constructor). If using a complex object (Date, Node, etc. – not just {}) then assign it in Base.init() or postInit(). If using an instance property as if it were static, either list it in _shareProps or move to staticProps (extend()) and access as this.constructor.foo (yes, JavaScript makes it pretty inconvenient).

ExampleOne particular case when the default cloning causes problem is when you are assigning “classes” to properties – recursive copying of such a value not just breaks it (MyClass === myObj._model no longer works) but is also very heavy.

Either use _shareProps

var MyView = Sqimitive.Base.extend({
  _model: MyApp.MyModel,
})

// _shareProps is a static property.
MyView._shareProps.push('_model')

…Or assign the value after instantiation, which is less declarative:

var MyView = Sqimitive.Base.extend({
  _model: null,   // MyApp.MyModel¹

  events: {
    init: function () {
      this._model = MyApp.MyModel
    },
  },
})

¹ It’s customary in Sqimitive to leave a comment with the type’s name next to such property.

from mergePropsExtend

Warning: when passing _mergeProps or _shareProps inside staticProps (second argument of extend()) all inherited items will be removed. The correct way to add your properties while keeping those in the base classes is this:

var MySqimitive = Sqimitive.Base.extend({
  // Define instance fields...
}, {
  // Define static fields if you need, else don't pass this parameter.
})

// extend() has copied the inherited _mergeProps list which we can now
// append to or modify using regular Array functions.
MySqimitive._mergeProps.push('prop1', 'prop2', ...)
// Same with _shareProps.
MySqimitive._shareProps.push('prop1', 'prop2', ...)

These are wrong ways to append to these properties:

var MySqimitive = Sqimitive.Base.extend({
  // WRONG: _mergeProps is static so it won't be read from here.
  _mergeProps: ['prop'],
}, {
  // WRONG: technically fine but will entirely replace base class'
  // merge list.
  _mergeProps: ['prop'],
})

// WRONG: works but once again will replace all the inherited items.
MySqimitive._mergeProps = ['prop']

// CORRECT:
MySqimitive._mergeProps.push('prop')

Other notes:

from inBB

In Backbone…

In Backbone, values of all properties inherited by a subclass are shared among all instances of the base class where they are defined. Just like in Python, if you have ...extend( {array: []} ) then doing this.array.push(123) will affect all instances where array wasn’t overwritten with a new object. This poses a typical problem in day-to-day development.

Example (http://jsfiddle.net/Proger/vwqk67h8/):

var MyView = Backbone.View.extend({foo: {}})
var x = new MyView
var y = new MyView
x.foo.bar = 123
alert(y.foo.bar)

Can you guess the alert message? It’s 123!

Defined in: main.js, line 505Show code

_shareProps: [],
lastFired

Modifiers: static

Holds recently fire()’d event hooks (cloned eobj-s with self set to call context) if ::trace is on.

from trc

Sometimes you find yourself wondering why a hook was called or who caused a property to be updated. Given that hooks often point to generic functions or that _opt changes are batch()’ed, finding initiators can be a tricky business.

Effects of ::trace:

  • Every sqimitive stores constructor()-time stack trace in .trace
  • Last bunch of fire()’d event hooks is recorded under lastFired (in form of their eobj, cloned, with self set to call context)
  • Objects of _events (eobj) hold stack trace of their registration (fuse()) under trace
  • batch()’ed events receive new trace field in options, along with batchID and others
  • Various wrapper functions (picker(), firer(), once(), etc.) store their arguments under trace

All added properties are meant for inspection in debugger.

Example
var sq1 = new Sqimitive.Core
alert(sq1.trace)      //=> undefined
Sqimitive.Core.trace = true
var sq2 = new Sqimitive.Core
alert(sq2.trace)      //=> 'Error\n  at ...'

Note: setting trace on subclasses of Core will have no effect.

By default, Chrome limits trace depth to 10 which usually prevents you from seeing actually important frames. One way to increase it is:

Error.stackTraceLimit = 100

Other ways: https://stackoverflow.com/questions/9931444.

Defined in: main.js, line 253Show code

lastFired: [],
trace

Modifiers: static

Enables tracing of certain events to aid in debugging. Can be changed on run-time.

trcSometimes you find yourself wondering why a hook was called or who caused a property to be updated. Given that hooks often point to generic functions or that _opt changes are batch()’ed, finding initiators can be a tricky business.

Effects of ::trace:

  • Every sqimitive stores constructor()-time stack trace in .trace
  • Last bunch of fire()’d event hooks is recorded under lastFired (in form of their eobj, cloned, with self set to call context)
  • Objects of _events (eobj) hold stack trace of their registration (fuse()) under trace
  • batch()’ed events receive new trace field in options, along with batchID and others
  • Various wrapper functions (picker(), firer(), once(), etc.) store their arguments under trace

All added properties are meant for inspection in debugger.

Example
var sq1 = new Sqimitive.Core
alert(sq1.trace)      //=> undefined
Sqimitive.Core.trace = true
var sq2 = new Sqimitive.Core
alert(sq2.trace)      //=> 'Error\n  at ...'

Note: setting trace on subclasses of Core will have no effect.

By default, Chrome limits trace depth to 10 which usually prevents you from seeing actually important frames. One way to increase it is:

Error.stackTraceLimit = 100

Other ways: https://stackoverflow.com/questions/9931444.

Defined in: main.js, line 247Show code

trace: false,
_cid

Modifiers: protected

from readOnly

May only be read, not changed.

An identifier of this object. Unique among all instances of Sqimitive.Core or its subclasses created during this session (page load).

Currently begins with “p” for “primitive” followed by a positive number as generated by unique(). This is unlikely to change but in any case it’s guaranteed to remain a valid identifier of only Latin symbols, i.e. begin with a letter followed by zero or more letters, digits and underscores.

Can be used to namespace DOM events as in this.el.on('click.' + this._cid) (jQuery.attach() does this).

Historically “cid” stands for “client identifier” – a term originating from Backbone (bb:Model-cid) but probably not holding much meaning at this point.

Example
;(new Sqimitive.Core)._cid      //=> 'p1'
;(new Sqimitive.Base)._cid      //=> 'p2'
;(new Sqimitive.Core)._cid      //=> 'p3'
;(new Sqimitive.jQuery)._cid    //=> 'p4'

Defined in: main.js, line 1672Show code

_cid: '',
events

Part of: tag_Events

There is no such property per se but this key can be passed to extend() and mixIn() to set up new event handlers of a “subclass”.

Giving events is the same as calling this.on({events}) after extend()/mixIn() so see on() (with an object argument) for details.

Since in Sqimitive everything is an event (evt) this is the way you do inheritance, override methods, etc. Such events are “fused” into the new class declaration so there is no overhead of applying them on each class instantiation.

See the Events overview (evt) for examples.

from es6thiswarn

Warning: avoid using ES6 arrow functions as handlers due to their fixed this (es6this).

P.S: again, events is private to extend()/mixIn() and does not become this.events.

Defined in: main.js, line 1676

trace

Stores the stack trace at the time this instance was constructor’ed, if ::trace was on.

from trc

Sometimes you find yourself wondering why a hook was called or who caused a property to be updated. Given that hooks often point to generic functions or that _opt changes are batch()’ed, finding initiators can be a tricky business.

Effects of ::trace:

  • Every sqimitive stores constructor()-time stack trace in .trace
  • Last bunch of fire()’d event hooks is recorded under lastFired (in form of their eobj, cloned, with self set to call context)
  • Objects of _events (eobj) hold stack trace of their registration (fuse()) under trace
  • batch()’ed events receive new trace field in options, along with batchID and others
  • Various wrapper functions (picker(), firer(), once(), etc.) store their arguments under trace

All added properties are meant for inspection in debugger.

Example
var sq1 = new Sqimitive.Core
alert(sq1.trace)      //=> undefined
Sqimitive.Core.trace = true
var sq2 = new Sqimitive.Core
alert(sq2.trace)      //=> 'Error\n  at ...'

Note: setting trace on subclasses of Core will have no effect.

By default, Chrome limits trace depth to 10 which usually prevents you from seeing actually important frames. One way to increase it is:

Error.stackTraceLimit = 100

Other ways: https://stackoverflow.com/questions/9931444.

Defined in: main.js, line 154Show code

Core.trace && (this.trace = (new Error).stack)

Methods

batchGuard ( index, func [, options] )

Modifiers: static

Part of: tag_Options

Returns a function for use as a change or compatible hook that calls func only once per batch (batchID).

Result Types
Types Notes
function returning undefined if skipping func due to batchID
Arguments
Name Types Notes
indexint index in returned function’s arguments of the options object with batchID key (_batchOptions())
str calculate by pattern: c (for change) or c_ (for change_OPT) preceded by any number of . + = (event prefixes evtpf that prepend extra arguments)
func Subject to expandFunc()
optionsmissing
object

Possible options keys (all optional):

Arguments
Name Types Notes
seenSet Give the same value in seen (or the same options object) to link several batchGuard-s together (to call func once across all guards, not once per guard). you can set seen to a Set or give {} to the first batchGuard() and then read the value it has written to this key. Treat Set as a blackbox (it conceals multiple nuances explained in skb).
cxobject If not given then context is unchanged.
skipfunction Called when skipping a seen batch. Useful when figuring why func isn’t getting called when it should be.
Example
function titleChanged(task, now) {
  console.log('New title of ' + task._cid + ' is ' + now)
}

tasks.on('.change_title', Sqimitive.Core.batchGuard('.c_', titleChanged))
tasks.on('.change_title', Sqimitive.Core.batchGuard(3, titleChanged))

Do not use batchGuard() in class declaration (functions given to extend()) unless you want all instances of the class to share the same guard:

var Class = Sqimitive.Core.extend({
  events: {
    change: batchGuard(0, () => alert('Called')),
  },
})
var o1 = new Class
var o2 = new Class
o1.batch([o2], function () {
  o1.set('foo', 123)    // alerts
  o2.set('foo', 123)    // doesn't
})

Instead, assign such a property or hook in init:

var Class = Sqimitive.Core.extend({
  events: {
    init: function () {
      this.fuse('change', batchGuard(...))
    },
  },
})

Remember that func is called once per batch, not per batch per object. This matters if using batchGuard() to handle events originating from different objects:

var Collection = Sqimitive.Base.extend({
  _childEvents: ['change'],
})
var col = new Collection
col.on('.change', batchGuard('.c', function ...))
var child1 = col.nested(1)
var child2 = col.nested(2)
child1.batch([child2], function () {
  child1.set('foo', 1)
  child2.set('bar', 2)
})

In the above example, batchGuard() calls func just once and options that func receives may come from either of the batched sqimitives which means options.batch is arbitrary and does not list all queued events (it has either change_foo or change_bar). In case multiple origins are possible, instead of batch read options.batched[i][1] – but only process objects (...[0]) that you expect:

col.on('.change', batchGuard('.c', function (child, name, now, old, options) {
  // WRONG:
  options.batch.forEach(...)
}))
col.on('.change', batchGuard(4, function (child, name, now, old, options) {
  // WRONG:
  options.batched.forEach(function ([child, batch]) {
    batch.forEach(...)
  })
}))
col.on('.change', batchGuard('.c', function (child, name, now, old, options) {
  // CORRECT:
  options.batched.forEach(function ([child, batch]) {
    if (col.nested(child)) {    // or: child instanceof ..., etc.
      batch.forEach(...)
    }
  })
}))

Checking objects is important because a batch may include unrelated sqimitives:

child1.batch([child2, unrelated], function () {
  child1.set('foo', 1)
  child2.set('bar', 2)
  unrelated.set('quux', 3)  // not part of col, must ignore in our guard
})

Defined in: main.js, lines 1560-1604 (45 lines) • Show code

batchGuard: function (index, func, options) {
  if (typeof index == 'string') {
    if (!index.match(/^[.+=]*c_?$/)) {
      throw new Error('batchGuard: Invalid index: ' + index)
    }
    // function ([...] [name,] now, old, options)
    index = index.length + (_.last(index) == '_' ? 0 : 2)
  }

  options || (options = {})
  func = Core.expandFunc(func, options.cx)
  var seen = options.seen || (options.seen = new Set)
  var unique = Core.unique('bg') + 'bg'

  function batchGuard_() {
    var eventOptions = arguments[index]
    var id = eventOptions.batchID

    if (!id) {
      throw new Error('batchGuard: No batchID in ' + index)
    }

    if (seen.size == seen.add(id).size) {
      options.skip && options.skip.apply(options.cx || this, arguments)
    } else {
      var event = unique + id
      var remaining = 0

      _.forEach(eventOptions.batched, function (item) {
        if (item[1].length) {
          remaining++
          item[1].push([event])
          item[0].once(event, function () {
            --remaining || seen.delete(id)
          })
        }
      })

      return func.apply(this, arguments)
    }
  }

  Core.trace && (batchGuard_.trace = arguments)
  return batchGuard_
},
deepClone ( obj )

Modifiers: static

Part of: tag_Utilities

Returns a version of the argument with recursively-copied arrays and {} objects so that any modification to either obj or the returned value (obj’s copy) won’t affect its counterpart.

deepclonebasedeepClone() is used in Core.constructor to copy non-shared properties (_shareProps). Think of it as of recursively calling un:clone() or jq:extend() or using LoDash’s cloneDeep(). It’s deliberately dumb to remain simple and will attempt to copy everything, even classes like Date, RegExp, DOM nodes, etc. (resulting in invalid objects).

Use deepClone() when need to clone an object just once or twice. Use deepCloner() when doing this often (such as upon new object instantiation) – it’s faster by 8-9 times and produces code close to what you’d have written by hand.

Example
var obj = {array: [{sub: 'prop'}]}
var obj2 = obj
var obj3 = Sqimitive.Base.deepClone(obj)

obj2.array.push('new')
  // obj.array is now [{sub: 'prop'}, 'new']
  // obj3.array is still [{sub: 'prop'}]

delete obj3.array[0].sub
  // obj3.array is now [{}]
  // obj.array is still [{sub: 'prop'}, 'new']

var cloner = Sqimitive.Base.deepCloner(obj)
cloner.toString()   //=> return {"array":[{"sub":"prop",},],}
var objClone = cloner()

Defined in: main.js, lines 1174-1187 (14 lines) • Show code

deepClone: function (obj) {
  if (typeof obj == 'object' && obj != null) {
    if (_.isArray(obj)) {
      obj = obj.map(Core.deepClone)
    } else {
      obj = _.assign({}, obj)
      for (var prop in obj) {
        obj[prop] = Core.deepClone(obj[prop])
      }
    }
  }

  return obj
},
deepCloner ( [obj [, options]] )

Modifiers: static

Part of: tag_Utilities

Returns a function constructing obj with recursively-copied arrays and {} objects – compiled version of deepClone().

Arguments
Name Types Notes
objmixed
optionsmissing create new cloner non-scalar values are held in shared array, passed to the returned function via s variable.
object merge multiple cloners
Result Types
Types Notes
function returning obj copy
object if no arguments or if options given, call its compile() to get the function

from deepclonebase

deepClone() is used in Core.constructor to copy non-shared properties (_shareProps). Think of it as of recursively calling un:clone() or jq:extend() or using LoDash’s cloneDeep(). It’s deliberately dumb to remain simple and will attempt to copy everything, even classes like Date, RegExp, DOM nodes, etc. (resulting in invalid objects).

Use deepClone() when need to clone an object just once or twice. Use deepCloner() when doing this often (such as upon new object instantiation) – it’s faster by 8-9 times and produces code close to what you’d have written by hand.

Example
var obj = {array: [{sub: 'prop'}]}
var obj2 = obj
var obj3 = Sqimitive.Base.deepClone(obj)

obj2.array.push('new')
  // obj.array is now [{sub: 'prop'}, 'new']
  // obj3.array is still [{sub: 'prop'}]

delete obj3.array[0].sub
  // obj3.array is now [{}]
  // obj.array is still [{sub: 'prop'}, 'new']

var cloner = Sqimitive.Base.deepCloner(obj)
cloner.toString()   //=> return {"array":[{"sub":"prop",},],}
var objClone = cloner()

Defined in: main.js, lines 1206-1255 (50 lines) • Show code

deepCloner: function (obj, o) {
  o = o || {
    func: ['return '],
    shared: [],
    compile: function () {
      // Clearing o.func to free old members which may be quite numerous.
      return _.partial(new Function('s', o.func.splice(0).join('')), o.shared)
    },
  }

  if (!arguments.length) { return o }

  switch (typeof obj) {
    case 'string':
      if (obj.length < 50) {
        o.func.push(JSON.stringify(obj))
        break
      }
    default:
      o.func.push('s[' + (o.shared.push(obj) - 1) + ']')
      break
    case 'object':
      if (obj == null) {
        // Fall through.
      } else if (_.isArray(obj)) {
        o.func.push('[')
        for (var i = 0; i < obj.length; i++) {
          Core.deepCloner(obj[i], o)
          o.func.push(',')
        }
        o.func.push(']')
        break
      } else {
        o.func.push('{')
        for (var prop in obj) {
          o.func.push(JSON.stringify(prop) + ':')
          Core.deepCloner(obj[prop], o)
          o.func.push(',')
        }
        o.func.push('}')
        break
      }
    case 'undefined':
    case 'boolean':
    case 'number':
      o.func.push(obj + '')
  }

  return arguments[1] ? o : o.compile()
},
expandFunc ( func [, obj] )

Modifiers: static

Expands a function reference func of object obj (this if not given) into a real Function.

expandFunc() is used in on(), events and others to short-reference the instance’s own methods.

If func is a string and contains a dot or a dash (.-) – returns masked (masker()) version of this method (mask starts with the first such character). If it’s a string without them – returns a function that calls the method named func on obj (or this if omitted). In other cases returns func as is (if obj is omitted) or _.bind(func, obj) (if obj is given).

Example
var func = Sqimitive.Base.expandFunc('meth')
  // returned function will call this.meth(arguments, ...)

var obj = {meth: function (s) { alert(s) }}
func.call(obj, 123)
  // alerts 123

var func = Sqimitive.Base.expandFunc('meth-.', obj)
  // this function works in obj context, calling meth with just one
  // argument (2nd it was given) - see masker()

_.each({k1: 1, k2: 2}, func)
  // each() calls func(1, 'k1') and func(2, 'k2')
  // func calls obj.meth('k1') and obj.meth('k2')
  // alerts twice: 'k1' and 'k2'

_.each({k1: 1, k2: 2}, _.bind(func, obj))
  // if we didn't give obj to expandFunc() previous example would
  // fail - func() would be called on window which has no 'meth'
  // method

Defined in: main.js, lines 954-970 (17 lines) • Show code

expandFunc: function (func, obj) {
  if (typeof func == 'string') {
    var parts = func.split(/([.-][\d.-]*)$/)
    if (parts.length > 1) {
      return Core.masker(parts[0], parts[1], obj)
    } else {
      function expandFunc_() {
        var callCx = obj || this
        return callCx[func].apply(callCx, arguments)
      }
      Core.trace && (expandFunc_.trace = arguments)
      return expandFunc_
    }
  } else {
    return obj ? _.bind(func, obj) : func
  }
},
extend ( [name, ] [protoProps [, staticProps]] )

Modifiers: static

Part of: tag_Extension

Creates a subclass of the class on which extend() is called.

Arguments
Name Types Notes
namestring Optional convenience string displayed in the debugger (as the function/constructor – “class” name). Defaults to name of base class.
var MyClass = Sqimitive.Base.extend('My.Class')
MyClass.name                      //=> 'My.Class'
;(new MyClass).constructor.name   //=> 'My.Class'
protoPropsobject New instance fields (properties or methods). May contain special non-field keys (see mixIn()).
staticPropsobject New static fields.

protoProps fields become accessible as (new MyClass).instanceSomething() while staticProps – as MyClass.staticSomething().

Any argument may be null or omitted. If all are such then you get a copy of the base class (and yet BaseClass !== SubClass).

Other notes:

  • In Sqimitive, subclassing is a special case of mix-ins (multi-parent inheritance in OOP). extend() simply creates a new “blank” class and mixes the base class into it. Therefore most of the job is performed by mixIn() (which also allows changing particular object’s prototype after class construction on run-time).
  • extend() creates a new prototype, sets its parent, assigns __super__ (a static property pointing to the base class), calls ::mixIn() and resolves _childClass if it’s a string (since it can’t be done before the prototype is created).

from mixInDoes

  • mixIn() applies “sub mix-ins” (calls mixIn() if newClass contains the mixIns key; this is recursive), overwrites fields of this with deepClone-s of those in newClass (or merges according to _mergeProps), adds staticProps, hardwires events into class definition (fuse()) and calls finishMixIn().
  • If staticProps argument is given, it replaces protoProps.staticProps. As expected, this key (or argument) is applied by mixIn() after applying staticProps of mixIns.
    var MixIn = {
      staticProps: {sp: 'm'}
    }
    var Class = Sqimitive.Base.extend({
      mixIns: [MixIn]
    }, {
      sp: 'c'
    })
    // Equivalent:
    var Class = Sqimitive.Base.extend({
      staticProps: {sp: 'c'},
      mixIns: [MixIn]
    })
    // Above, Class.sp is 'c'. However, if mixing-in later, it'd be 'm':
    Class.mixIn(MixIn)
ExampleIn case of duplicated field names, subclass’ fields take precedence and overwrite fields in the parent class except for fields listed in _mergeProps:
// First we extend a base Sqimitive class with our own properties.
var MyBase = Sqimitive.jQuery.extend({
  _somethingBase: 123,
  _somethingNew: 'foo',

  el: {tag: 'nav', id: 'nav'},

  _opt: {
    baseOption: 'boo',
    baseMore: 'moo',
  },
})

// Now if we extend MyBase...
var MySubclass = MyBase.extend({
  _somethingSub: 'bar',
  _somethingBase: 987,

  el: {tag: 'footer'},

  _opt: {
    subOption: 'sub',
    baseMore: 'bus',
  },
})

/*
  ...we get the following class, after merging with its parent:

    MySubclass = {
      // Got new value - overridden in MySubclass.
      _somethingBase: 987,
      // Retained old value from MyBase.
      _somethingNew: 'foo',
      // New property - introduced in MySubclass.
      _somethingSub: 'bar',

      // Got new value in MySubclass.
      el: {tag: 'footer'},

      // Unlike el, _opt is listed in _mergeProps by default so its
      // keys are merged and not entirely replaced.
      _opt: {
        // Retained.
        baseOption: 'boo',
        // Introduced.
        subOption: 'sub',
        // Overridden.
        baseMore: 'bus',
      },
    }
*/

Defined in: main.js, lines 638-684 (47 lines) • Show code

extend: function (name, protoProps, staticProps) {
  // this = base class.
  // Only works in strict mode which disconnects parameter vars from members
  // of arguments.
  name = typeof arguments[0] == 'string' ? ap.shift.call(arguments) : this.name

  var child = extend(name, this, arguments[0])
  //! +ig
  // Since base class has its own __super__, make sure child's (set up by
  // extend() above) isn't overwritten.
  _.assign(child, this, {__super__: child.__super__})

  // Ensure changing these in a subclass doesn't affect the parent:
  //   var Sub = Sqimitive.Base.extend()
  //   Sqimitive.Base._mergeProps.length  //=> 3
  //   Sub._mergeProps.push('new')
  //   Sqimitive.Base._mergeProps.length  //=> 4
  child._mergeProps = (child._mergeProps || this._mergeProps).concat()
  child._shareProps = (child._shareProps || this._shareProps).concat()

  //! +ig
  // Function.prototype.length confuses "isArrayLike" functions.
  // Just `[delete Core.length`] doesn't work.
  Object.defineProperty(child, 'length', {value: 'NotAnArray'})

  name && Object.defineProperty(child, 'name', {value: name})

  if (arguments[1]) {
    (arguments[0] = arguments[0] || {}).staticProps = arguments[1]
  }
  arguments[0] && child.mixIn(arguments[0])

  // _childClass is technically part of Base, not Core but doing it here for
  // simplicity.
  //
  // String class path is relative to the base class; instead of searching
  // all prototypes to find where this property was introduced (without this
  // all children will reference to them as the base class), we "fixate" it
  // when declaring the class. Done here, not in mixIn(), to avoid questions
  // like "is the string, if given by a mix-in, relative to the mix-in or
  // the target class?".
  if (typeof child.prototype._childClass == 'string') {
    child.prototype._childClass = [child, child.prototype._childClass]
  }

  return child
},
fire ( funcs [, args [, inBatch]] )

Modifiers: static

Invokes event handlers in response to firing an event (see instance .fire()).

funcs is an array of event registration objects of an internal format. ::fire() calls each handler in order according to its type (such as expecting a fixed number of arguments, accepting current result value, affecting return value, etc. according to prefix in on(), see evtref) while giving it args (array or Arguments).

If a handler returns something other than undefined and it’s eligible for changing return value (as it’s the case for +event and =event, see evtpf), then current result is replaced by that handler’s return value. Returns the ultimate return value after calling every handler, unless stopped by any eobj.post callback setting eobj.stop to true (see the example in post).

Other notes:

  • This is an internal method and there is no reason to call it directly.
  • Hook order (priority; evtref) is maintained by fuse(), not fire().
  • funcs can be non-isArray, in this case undefined is returned.
  • funcs is cloned; members added or removed while ::fire() is running are ignored; changes to a member (eobj) affect the call only if that member wasn’t yet called.
  • In Core, a removed hook that was not yet called still isn’t called (because _unregHandler() clears eobj.func). See the second example below.
Example
var funcs = [
  // The event object (eobj) stored within sqim._events:
  {func: function () { ... }, cx: window, res: true}
]
var res = Sqimitive.Core.fire(funcs, [5, 'foo'])
  // same as: var res = func.call(window, 5, 'foo')
ExampleRemoving hooks from within ::fire():
var sqim = new Sqimitive.Core
var ev1 = sqim.on('evt', () => console.log(1))    // to be called first
sqim.on('evt', () => sqim.off(ev1).off(ev2))      // to be called second
var ev2 = sqim.on('evt', () => console.log(2))    // to be called last
sqim.fire('evt')
  // console logs 1 but not 2

Defined in: main.js, lines 1331-1387 (57 lines) • Show code

fire: function (funcs, args, inBatch) {
  var res
  args = args || []

  // funcs can be undefined, e.g. when firing change_OPT.
  // It may be non-array if non-own property was read (_events.toString).
  //
  // Cloning function list to ignore the outside changes (e.g. when a
  // handler is removed from the same event as it's being fired; may happen
  // when once() is used).
  _.isArray(funcs) && funcs.concat().some(function fire_(eobj) {
    if (inBatch != null && eobj.batch !== inBatch) { return }

    Core.trace &&
      Core.lastFired.push(_.assign({self: this}, eobj)) > 50 &&
      Core.lastFired.shift()

    // func is null if off() was called on a wrapped handler.
    // Non-null eobj.args is implementing "ev__" (#argDanger).
    var call = eobj.func && (eobj.args == null || eobj.args == args.length)

    if (!call) {
      // If the handler is not to be called, pretend it's not there and call
      // the handlers it wraps (supList),
      var thisRes = eobj.sup ? eobj.sup(eobj.cx || this, args) : undefined
    } else {
      if (!eobj.sup && !eobj.res) {
        var thisArgs = args
      } else {
        var thisArgs = ap.slice.call(args)
        eobj.sup && thisArgs.unshift(eobj.sup)
        eobj.res && thisArgs.unshift(res)
      }

      // A benchmark avoiding Function's apply() was done on Chrome
      // using a direct call construct from EventEmitter3:
      // switch (args.len) { case 0: f(); case 1: f(args[0]); ... }
      // It made no differences at all in performance.
      var thisRes = eobj.func.apply(eobj.cx || this, thisArgs)
    }

    if (eobj.ret && thisRes !== undefined) {
      res = thisRes
    }

    if (eobj.post && call) {
      eobj.stop = false
      // Attention: args may be an array-like object (often an Arguments),
      // not a real array.
      res = eobj.post.call(eobj.cx || this, eobj, res, args)
      // eobj could be modified by post, including unsetting post.
      if (eobj.stop) { return true }
    }
  }, this)

  return res
},
firer ( event [, args [, cx]] )

Modifiers: static

Part of: tag_Events

Returns a function that fire()-s an event with arguments of its call.

The returned function calls fire(event, ...firerArgs, ...givenArgs) in context of cx.

Arguments
Name Types Notes
eventstring Like change_caption
argsarray Push some parameters in front of the function’s arguments (firerArgs).
cxobject If not given then context is unchanged.
Examplefirer() is used by on() to “convert” method calls into events making it unnecessary to directly call fire() in most cases (see evt):
var MyClass = Sqimitive.Base.extend({
  upper: function (s) { return s.toUpperCase() },
})

var obj = new MyClass
  // obj.upper is the function given to extend(), not an event
var res = obj.upper('booh!')
  //=> 'BOOH!'

obj.on('+upper', () => 'baah!')
  // now obj.upper is the result of firer() and yet it's called as if
  // it was a regular method
var res = obj.upper('booh!')
  //=> 'baah!'
// Same as writing:
var res = obj.fire('upper', ['booh!'])
  //=> 'baah!'

Essentially, using firer() is just a short way of writing:

(function () { return this.fire(event, args.concat(arguments)) }).bind(cx)

Defined in: main.js, lines 1430-1443 (14 lines) • Show code

firer: function (event, args, cx) {
  if (arguments.length > 1) {
    args = Core.toArray(args)
    var firer_ = function () {
      return (cx || this).fire(event, args.concat( ap.slice.call(arguments) ))
    }
  } else {
    var firer_ = function () {
      return (cx || this).fire(event, arguments)
    }
  }
  Core.trace && (firer_.trace = arguments)
  return firer_
},
masker ( func[, mask[, cx[, args]]] )

Modifiers: static

Part of: tag_Utilities

Returns a version of func with arguments reordered according to mask.

Arguments
Name Types Notes
masknumber to skip that many leading arguments alike to no:rest()
null/omitted to assume the number 1 (skip first argument)
string pattern maskerPattern
funcstring method name Called on cx
function
cxobject The context for func
null/omitted use this
argsarray Extra left-side arguments to func

masker() is similar to LoDash’s rearg().

Masking is a way to work around argDanger and avoid writing callback function wrappers that only ignore or reorder arguments. It’s implicitly used in string events values since they are passed to expandFunc().

Examplees6thisES6 arrow functions could be useful for this but they are ill-suited for use as handlers when extend()’ing because of their permanent this.

var MyClass = Sqimitive.Base.extend({
  events: {
    // WRONG: will pass res as s, value as chars and break clean():
    '+normalize_caption': 'clean',
    // WRONG: this is window/self, not an instance of MyClass:
    '+normalize_caption': (res, value) => this.clean(value),
    // CORRECT:
    '+normalize_caption': function (res, value) { return this.clean(value) },
    // CORRECT: skip first argument, give second:
    '+normalize_caption': 'clean-.',
  },

  clean: function (s, chars) {
    chars = chars || ' \t\r\n'
    while (s.length && chars.indexOf(s[0]) != -1) {
      s = s.substr(1)
    }
    while (s.length && chars.indexOf(s[s.length - 1]) != -1) {
      s = s.replace(/.$/, '')
    }
    return s
  },
})

Example
$.ajax({
  url: 'api/route',
  dataType: 'json',
  context: sqim,

  // WRONG: success' second argument is textStatus which gets assigned
  // as assignResp(data, options) breaking the latter (textStatus is
  // not options):
  success: sqim.assignResp,

  // CORRECT: we indicate that we are only interested in the first
  // argument which is passed through to assignResp():
  success: Sqimitive.Base.masker('assignResp', '.'),
})
ExampleIt is customary to alias masker() with a shorter name and use the alias in the code:
var m = Sqimitive.Base.masker

var MyModel = Sqimitive.Base.extend({
  // Note that it's different from the first example: normalize_OPT
  // is a method, not an event handler, so no need for '+...' - but
  // no automatic masking too (string value can be used only in
  // events, in properties it must be a function).
  //
  // Unmasked, _.trim() takes two arguments: (str, chars).
  // normalize_OPT() is passed: (value, options).
  // As we see, options is given as chars which is incorrect.
  normalize_caption: m(_.trim, '.'),
})
Example
// Giving an explicit cx, the context object (col):
_.each(arrayOfSqims, m('nest', '21', col))
  // here we call col.nest() on each item in arrayOfSqim with swapped
  // arguments, effectively nesting each member into the col object.
  // _.each() calls the iterator as (value, key) while nest() takes
  // (key, sqim)

m('nest', 1)
  // returns a function that preserves all but the first argument:
  // function () { return this.nest.apply(this, _.rest(arguments)) }

m('nest')
  // the same - omitted mask defaults to number 1

m('nest', 0, cx)
  // doesn't change arguments at all (_.rest(a, 0) == a) but binds
  // function to cx

m('nest', 0, null, ['left', 'left2'])
  // doesn't bind result but pushes 'left' and 'left2' arguments
  // before all given arguments: nest('left', 'left2', other args...)

m(function (a1, a2) { alert(a1 + ' ' + a2) }, '')
  //=> always 'undefined undefined'

Mask patternmaskerPattern

In a string mask, each symbol maps arguments given to the masked function (returned by masker) to arguments for the original func (the argument to masker). Each symbol represents a particular argument and can be one of these:

Arguments
Name Types Notes
dot. Gets argument at index of this dot in the mask string (-..-. equals to -23-5).
dash- Ignores argument (doesn’t give to func); trailing dashes are meaningless (arguments past the end of mask are never given unless mask is a number).
number1-9 Gets argument by index: 1 gets the first masked argument, etc.

For example, if the wrapped function received arguments arg1, arg2, arg3 then the mask of -.1 (same as -21) gives the original func arguments arg2, arg1.

Empty mask passes zero arguments (as do -, --, etc.).

Note: the mask of '3' (string) is different from 3 (number) - '3' passes 3rd wrapper’s argument as the first func’s argument while 3 skips first 3 arguments and passes all others.

Defined in: main.js, lines 1108-1134 (27 lines) • Show code

masker: function (func, mask, cx, args) {
  mask == null && (mask = 1)
  var isMethod = typeof func == 'string'
  var isSkipFirst = typeof mask == 'number'
  args || (args = [])

  if (!isSkipFirst) {
    mask = mask
      .replace(/\./g, function (ch, i) { return i > 8 ? '-' : i + 1 })
      .replace(/[^1-9.\-]+/g, '-')
      .replace(/-+$/, '')
  }

  function masker_() {
    var callCx = cx || this
    var callArgs = isSkipFirst ? args.concat(_.rest(arguments, mask)) : args.concat()

    for (var i = 0; i < mask.length; i++) {
      mask[i] != '-' && callArgs.push(arguments[mask[i] - 1])
    }

    return (isMethod ? callCx[func] : func).apply(callCx, callArgs)
  }

  Core.trace && (masker_.trace = arguments)
  return masker_
},
mixIn ( newClass, options )

Modifiers: static

Part of: tag_Extension

Extends this class with a behaviour of another “class” (a “mix-in”).

The instance .mixIn() works the same way but allows extending a particular object instance on run-time. Its description follows.

from mixInDesc

Arguments
Name Types Notes
this The object receiving new “mixed-in” fields (for static ::mixIn() this is the “child” class).
newClassobject The mix-in, the object “mixed into” this.
options An arbitrary value (usually an object) given to newClass.finishMixIn(), allowing creation of parametrized mix-ins (basically, generics).

Possible newClass keys:

Arguments
Name Types Notes
staticPropsobject Static fields made available as newClass.something
eventsobject Event listeners (see events).

Because this is not a real field, keys in newClass.events do not override keys in this.events. If both this and newClass have the foo event key (i.e. if both listen to this event) then two listeners are set up, not one. Compare with Base.elEvents overriding (elEventsMixIn).

finishMixInfunction Called before returning.

this = newClass. arguments = child, options where child is the prototype of the updated class

mixInsarray Each item is either a mixed-in class or an array: [class, options]
* Other keys represent instance fields (the protoProps argument of extend()).

_mergeProps is respected, but of this (not of newClass).

A string form of _childClass is only allowed in extend(), not here; other forms (array, object) are allowed in both places.

Example
var MixIn = {
  staticProps: {
    staticMethod: function () { ... },
  },
  events: {
    '-init': function () {
      // (1) Define a property only if it's not defined yet.
      this._someProp = this._someProp || 'buzz'
    },
  },
  finishMixIn: function (targetProto) {
    alert("I'm now mixed into " + targetProto.toString())

    // (2) Or could do it here, more performance-efficient since ran
    // only once for each mixed-in class, not once for each such class
    // instantiation:
    //targetProto._someProp = targetProto._someProp || 'buzz'
    //targetProto.constructor.someStatic = 123
  },
  _opt: {
    correctly: 'merged',
  },
  instanceMethod: function () { ... },
}

var Base1 = Sqimitive.Base.extend({})

var Base2 = Sqimitive.Base.extend({
  _opt: {
   a: 'base',
 },
  // Not overridden by MixIn.
  _someProp: 123,
})

Base1.mixIn(MixIn)   // alerts
  //=> _someProp = 'buzz'
  //=> opt = {correctly: 'merged'}

Base2.mixIn(MixIn)   // alerts again
  //=> _someProp = 123
  //=> opt = {a: 'base', correctly: 'merged'}

Warning: this is modified in-place; no new class is created (mixIn() returns no value). If you want a base class without a given mix-in and a subclass with that mix-in – first extend() the base class and then mix into the new sub-class:

// CORRECT:
var Base = Sqimitive.Base.extend()
var Sub = Base.extend({mixIns: [SomeMixIn]})

// WRONG: will modify Base, not return a new subclass:
var Base = Sqimitive.Base.extend()
var Sub = Base.mixIn(SomeMixIn)

ExampleThere is no way to determine if a class or object has some mixIn or not. For example, listing a mix-in in _childClass is useless because Base.nestEx() is using instanceof and it works on real classes only. You can add a field to work around this – static (only for mix-ins applied to declarations) or instance (for mix-ins applied on run-time):
var MyMixIn = {staticProps: {myMixInIsMixedIn: true}}
var instanceofMyMixIn = 'myMixInIsMixedIn' in myObj.constructor

var sqim1 = new Sqimitive.Core
var sqim2 = new Sqimitive.Core
sqim1.mixIn(MyMixIn)
sqim2.constructor.myMixInIsMixedIn    //=> true (!)

var MyMixIn = {myMixInIsMixedIn: true}
var sqim3 = new Sqimitive.Core
var sqim4 = new Sqimitive.Core
sqim3.mixIn(MyMixIn)
sqim4.myMixInIsMixedIn                //=> false

Other notes:

  • mixIn() is doing most of extend()’s job, which is just creating a special form of a mix-in.
  • Hooking init and similar events for a mix-in applied on run-time is useless since they will be never invoked.
  • mixInDoesmixIn() applies “sub mix-ins” (calls mixIn() if newClass contains the mixIns key; this is recursive), overwrites fields of this with deepClone-s of those in newClass (or merges according to _mergeProps), adds staticProps, hardwires events into class definition (fuse()) and calls finishMixIn().
  • Not to be confused with Underscore’s un:mixin(). However, LoDash’s mixin() is of a similar purpose.

Merging of elEventselEventsMixIn

Base lists Base.elEvents in _mergeProps so the former are merged but unlike with newClass.events keys (which can never have a conflict and be dropped), keys in elEvents get overridden on name collisions. This is sometimes desirable (to override the parent’s handler), sometimes not (then use a unique .ns suffix):

var Base = Sqimitive.Base.extend({
  elEvents: {
    click: function () { alert("Click-click-click!') },
  },
})

var ChildA = Base.extend({
  elEvents: {
    click: function () { alert("Overridden!') },
  },
})

var ChildB = Base.extend({
  elEvents: {
    'click.new-in-child-B': function () { alert("Combined!') },
  },
})

// in ChildA, 'click' has 1 listener (Base's dropped)
// in ChildB, 'click' has 2 listeners (Base's and ChildB's)

Mix-in inheritance

mixIns is applied before setting other properties which allows extending mix-ins themselves (later mix-ins override preceding just like with normal inheritance on classes). For example:

var ParentMixIn = {
  someEvent: function () { /* parent */ },
]

var ChildMixIn = {
  mixIns: [ParentMixIn],
  events: {
    someEvent: function () { /* child */ },
  },
}

var myClass = Sqimitive.Base.extend({
  mixIns: [ChildMixIn],
})

// myClass had ParentMixIn added, then ChildMixIn, and now has two
// listeners on someEvent

ParentMixIn could also have the mixIns property to specify its own parent.

In the above example, the mix-in specified its parent, which is usually intuitive. Still, it could be specified in the final class’ mixIns alone:

var ParentMixIn = {
  someEvent: function () { /* parent */ },
]

var ChildMixIn = {
  // mixIns property is missing.
  events: {
    someEvent: function () { /* child */ },
  },
}

var myClass = Sqimitive.Base.extend({
  // Added ParentMixIn in front of ChildMixIn.
  mixIns: [ParentMixIn, ChildMixIn],
})

// or (equivalent, no mixIns property, mixIn() calls instead):
var myClass = Sqimitive.Base.extend()
myClass.mixIn(ParentMixIn)
myClass.mixIn(ChildMixIn)

// or (equivalent for a particular object instance):
var myClass = Sqimitive.Base.extend()
var obj = new myClass
obj.mixIn(ParentMixIn)
obj.mixIn(ChildMixIn)

// in any case, MyClass (or obj) has 'someEvent' firer() with
// 2 listeners: of ParentMixIn and of ChildMixIn

Warning: calling mixIn() in finishMixIn() would have a different effect. If ChildMixIn were defined as follows then MyClass or obj would have someEvent not as a firer() but as the ParentMixIn’s function (because it was mixed-in after ChildMixIn and overrode its events handler):

var ChildMixIn = {
  finishMixIn: function (newClass) {
    newClass.mixIn(ParentMixIn)
  },
  events: {
    someEvent: function () { /* child */ },
  },
}

This will override the handler introduced in the declaration of Class for the same reason – MixIn is added to Class after Class’ own fields:

var MixIn = {
  some: function () { ... },
}

var Class = Sqimitive.Base.extend({
  events: {
    some: function () { ... },
  }
})

Class.mixIn(MixIn)

This is the correct way (using mixIns property when extend()’ing Class):

var MixIn = {
  some: function () { ... },
}

var Class = Sqimitive.Base.extend({
  mixIns: [MixIn],
  events: {
    some: function () { ... },
  }
})

Edge cases

See the source code for details.

  • Given a base class B and subclass C, adding mix-ins to B after C has been extend()’ed from B when C declares events will lead to C not having events of the newly mixed-in objects of B.
  • Declaration-time events of B are fuse()’d and their eobj-s are shared among all subclasses of B and should not be changed (list of events may change, just not properties of inherited handlers):
    var ClassB = Sqimitive.Core.extend({
      events: {change: 'render'},
    })
    
    ClassB.prototype._events.change[0].post    //=> undefined
    
    var ClassC = ClassB.extend()
    ClassC.prototype._events.change[0].post = function () { ... }
    ClassB.prototype._events.change[0].post    //=> Function
    
    // In this version instances' _events are deepClone()'d so changing it
    // (not the prototype) doesn't affect other classes/objects:
    var obj = new ClassC
    obj._events.change[0].post = 'foo'
    ClassB.prototype._events.change[0].post    //=> still Function

Defined in: main.js, lines 696-698 (3 lines) • Show code

mixIn: function (newClass, options) {
  return this.prototype.mixIn(newClass, options)
},
parseEvent ( str )

Modifiers: static

Extracts portions of the given event identifier as recognized by on().

Returns an object with keys: batch (leading ^), priority (number before ^), prefix (like +, see evtpf), args (number of trailing _ or null), event (event name, everything else) and trace (null if ::trace is off).

Errors if str doesn’t look like a proper event reference.

Example
Sqimitive.Core.parseEvent('foo.bar')
  //=> {batch: true, priority: 0, prefix: '', event: 'foo.bar', args: null}

Sqimitive.Core.parseEvent('^-3^+foo.bar___')
  //=> {batch: false, priority: -3, prefix: '+', event: 'foo.bar', args: 3}

Defined in: main.js, lines 1273-1282 (10 lines) • Show code

parseEvent: function (str) {
  // Dots are used in _forward'ed event names, and we need prefix symbols
  // (+-=) and ^ in the middle of event name for the same reason:
  // '.2^-change'.
  var match = str.match(/^(\^?)(-?\d+\^)?([+\-=]?)(.+?)(_*)$/)
  if (!match) { throw new SyntaxError('Bad event name: ' + str) }
  return {batch: !match[1], priority: parseInt(match[2]) || 0,
          prefix: match[3], event: match[4], args: match[5].length || null,
          trace: Core.trace && (new Error).stack}
},
picker ( prop [, args] )

Modifiers: static

Part of: tag_Utilities

Returns a function accepting an object and returning value of the property at prop, accessible via that object.

Arguments
Name Types Notes
propstring dotted property path
array already split path, empty array to return the a’rgument itself (or result of calling it)
other stringified and split
argsarray List of argument lists for function-type properties.
mixed = [[args]]
omitted = []

The returned function (f) expects one argument (a). When called, f walks prop, treating each item as a key (“own” or not) in the “current” object (which starts as a) and returns the last “current” object.

If there is a function at that key (or a itself is one) and its name is not a string starting with a capital letter (i.e. not a class constructor), f calls it in previous “current” object’s context (undefined if calling a) with the next unused member of args ([] if none) and stores the result as the new “current” object.

f returns undefined when trying to descend into undefined/null “current” value.

picker() is similar to un:result() in Underscore and LoDash.

Example
var obj = {
  one: 1,
  two: function () { return 2 },
  some: function (a, b) { return a + '--' + b },
}

var picker = Sqimitive.Base.picker
picker('one')(obj)                  //=> 1
picker('two')(obj)                  //=> 2
picker('some', [['A', 'B']])(obj)   //=> 'A--B'

var collection = ['foo', null, ['bar'], obj]
_.map(collection, picker('one'))
  //=> [undefined, undefined, undefined, 1]

'toString' in obj           //=> true
_.has(obj, 'toString')      //=> false
picker('toString')(obj)     //=> '[object Object]'

picker([])(obj)             //=> obj
picker([])('foo')           //=> 'foo'
picker([])(() => 123)       //=> 123
// typeof Date is 'function' but Date.name[0] is upper-case.
picker([])(Date)            //=> Date
picker(0)('foo')            //=> 'f'
picker('0.0.0')('foo')      //=> 'f'
picker([])(null)            //=> null
picker(0)(null)             //=> undefined
ExampleUsually picker()’s result is given to some filtering function (util). Example from chld:
getIncomplete: function () {
  return this.reject(MyToDoList.picker('get', 'isCompleted'))
},
Exampleargs is a list of multiple argument lists, not a single argument list. This enables picker() to call methods of objects returned by other methods. However, prop often references just one property which is either not a method or a method taking nothing (so you can omit args), or a method taking one scalar argument (so you can pass that value directly):
picker('remove')(sqim)        //= sqim.remove()
picker('get')(sqim)           //= sqim.get()
picker('get', 'opt')(sqim)    //= sqim.get('opt')

picker('nested.get', [['ch1'], ['opt']])(sqim)
// Same as:
sqim.nested('ch1').get('opt')

picker('nested.get', [['ch1']])(sqim)
// Same as:
picker('nested.get', 'ch1')(sqim)
// Same as:
sqim.nested('ch1').get()

// WRONG, will fail:
picker('nested.get', ['ch1', 'opt'])(sqim)
picker('nested.get', ['ch1'])(sqim)

Defined in: main.js, lines 884-915 (32 lines) • Show code

picker: function (prop, args) {
  _.isArray(prop) || (prop = (prop + '').split('.'))

  if (arguments.length > 1 && !_.isArray(args)) {
    args = [[args]]
  }

  function picker_(obj) {
    var cx
    var ai = 0

    function picker_call() {
      if (typeof obj == 'function' && !/^[A-Z]/.test(obj.name)) {
        obj = obj.apply(cx, args && args[ai++])
      }
    }

    picker_call()

    for (var i = 0; i < prop.length; i++) {
      if (obj == null) { return }
      cx = obj
      obj = obj[prop[i]]
      picker_call()
    }

    return obj
  }

  Core.trace && (picker_.trace = arguments)
  return picker_
},
stub ( )

Modifiers: static

Simply an empty function that returns undefined.

stub() is similar to un:noop() in Underscore and LoDash.

ExampleUse stub() or undefined in places where you don’t want to supply any implementation – this lets Sqimitive optimize things when it knows that a function (acting as a method or an event handler) can be simply discarded or overridden.
var MySqim = Sqimitive.Base.extend({
  success: Sqimitive.Base.stub,
  error: Sqimitive.Base.stub,
  // Equivalent:
  success: undefined,
  error: undefined,
})

var my = new MySqim
// Replaces the empty handler entirely.
my.on('success', function () { alert('Good!') })
  //=> my._events.success.length is 1

Otherwise, if you are not a performance purist you can just use function () {} or new Function:

var MySqim = Sqimitive.Base.extend({
  success: function () { },
})

var my = new MySqim
my.on('success', function () { alert('Good!') })
  //=> my._events.success.length is 2

Special values stub() and undefined are only recognized in properties, not events:

var MySqim = Sqimitive.Base.extend({
  events: {
    // Registers new hook that is called but has no effect on anything.
    success: Sqimitive.Base.stub,
    // Itself does nothing but previously registered hooks are removed.
    '=success': Sqimitive.Base.stub,
    // Simply wrong, undefined is not callable.
    success: undefined,
  },
})

var my = new MySqim
my.on('success', function () { alert('Good!') })
  //=> my._events.success.length is 2 (existing stub handler kept)
my.on('success', Sqimitive.Base.stub)
  //=> my._events.success.length is 3 (new stub handler added)
my.on('success', undefined)
  // simply wrong, undefined is not callable

Defined in: main.js, line 757Show code

stub: function Sqimitive_stub() { },
toArray ( value )

Modifiers: static

Part of: tag_Utilities

Attempts to cast value into a native Array object.

The Arguments object becomes an array, Array (no:isArray()) is returned as is while anything else is wrapped into an array to become its sole member. This means that false, null and undefined all result in [value], not in [].

Example
;(function () {
  Sqimitive.Core.toArray(arguments)   //=> [5, 'foo']
})(5, 'foo')

Sqimitive.Core.toArray([5, 'foo'])    //=> [5, 'foo']
Sqimitive.Core.toArray(5)             //=> [5]
Sqimitive.Core.toArray()              //=> [undefined]

Note: toArray() does not clone the result:

var a = [5]
Sqimitive.Core.toArray(a).push('foo')
  // a is now [5, 'foo']

Defined in: main.js, lines 1633-1641 (9 lines) • Show code

toArray: function (value) {
  if (_.isArguments(value)) {
    return ap.slice.call(value)
  } else if (!_.isArray(value)) {
    return [value]
  } else {
    return value
  }
},
unique ( [prefix] )

Modifiers: static

Part of: tag_Utilities

Returns a sequential number starting from 1 that is guaranteed to be unique among all calls to unique() with the same prefix during this session (page load, etc.).

_cid receives one such value in constructor.

unique() is similar to un:uniqueId() in Underscore and LoDash.

Example
unique('my')    //=> 3
unique()        //=> 87
unique('my')    //=> 4
unique('some')  //=> 21
unique('my')    //=> 5

Well-known prefix’es:

Arguments
Name Types Notes
p Used in _cid (sqimitive’s ID).
e Used in on() (event handler’s ID).
o Used in set() (operation’s ID).
b Used in batch() (batch’s ID – group of operations).
bg Used in batchGuard() (guard’s instance).

Defined in: main.js, lines 787-789 (3 lines) • Show code

unique: function (prefix) {
  return this._unique[prefix] = 1 + (this._unique[prefix] || 0)
},
_batchOptions ( id [, options] )

Modifiers: protected

Part of: tag_Options

Returns information about currently active batch() for use in batched events.

Result Types
Types Notes
object cloned options with extra keys
Arguments
Name Types Notes
idnumber Active batch’s identifier as given to batch()’s func
objectobject Optional keys to merge into result (batch-specific keys override them).

Call _batchOptions() once for each “operation” when adding custom batch events, then pass the result to all operation’s events (e.g. ifSet() gives both change_OPT and change the same options object).

You may pass the same object to all events of the same operation (to allow clients persist data across related events) or you may clone it (to avoid interference) – but never pass it to events of different operations or batches.

Defined in: main.js, lines 3547-3586 (40 lines) • Show code

_batchOptions: function (id, options) {
  // It might be tempting to optimize cloning away if options was given by
  // mutating and returning options - but that would be erroneous:
  //
  //    var sq1 = new Sqimitive.Base
  //    var sq2 = new Sqimitive.Base
  //    var options = {}
  //
  //    sq1.batch(null, function () {
  //      sq1.set('a', 1, options)
  //
  //      sq2.batch(null, function () {
  //        sq2.set('b', 2, options)
  //
  //        // Expected batched events at this point:
  //        // sq2._batch = [['change_b', 2, undefined, {batchID: 2, ...}]]
  //        // sq1._batch = [['change_a', 1, undefined, {batchID: 1, ...}]]
  //        //
  //        // If _batchOptions() called by set() did not clone options,
  //        // sq1._batch would be [['change_a', ... {batchID: 2, ...}]] -
  //        // note the different batchID. This is because change_a's
  //        // options object was mutated by sq2.set() and now all
  //        // operations where it was used have wrong values (originating
  //        // from last called set()) for those operations.
  //        //
  //        // For example, when change_a is fired its options.batch must
  //        // be [['change_a', 1, undefined, {options...}]] but because
  //        // this key (batch) was overridden (in options) by sq2.set()
  //        // and because change events of b were already dispatched,
  //        // options.batch for change_a would be in fact []!
  //      })
  //    })
  return _.assign({}, options, {
    batch: this._batch,
    batched: this._batched,
    batchID: id,
    operationID: Core.unique('o'),
    trace: Core.trace && (new Error).stack,
  })
},
_forward ( prefix, events, sqim, prepend )

Modifiers: protected

Part of: tag_Events

Forwards events occurring on sqim to this, with arguments prepend’ed.

Result Types
Types Notes
sqim

Forwarding is done by firing “prefix + event_name” on this (the object on which _forward() is called) with prepend ([sqim] if omitted) pushed in front of original event’s arguments. event_name is a complete reference string as given to parseEvent(), e.g. =foo_.

_forward() is used to set up _childEvents, with the prefix of ..

Example
d._forward('.', ['render'], o)
d.on('.render', function (o) { alert(o._cid) })
 // now whenever 'render' is fired on o, '.render' is fired on d where
 // it shows the _cid of the object where the original 'render' occurred
Example
destination._forward('dlg-', ['change', '-render'], origin, [])

This example fires dlg-change and dlg--render events on destination (a Sqimitive) whenever change and render are fired on origin, keeping original arguments. -render simply means the forwarded events occur on destination before other handlers of origin are executed, as per evtpf.

Defined in: main.js, lines 3511-3523 (13 lines) • Show code

_forward: function (prefix, events, sqim, prepend) {
  _.forEach(events.concat(), function (event) {
    var name = prefix + event
    function _forward_() {
      ap.unshift.apply(arguments, prepend || [sqim])
      return this.fire(name, arguments)
    }
    Core.trace && (_forward_.trace = [prefix, events, sqim, prepend])
    sqim.on(event, _forward_, this)
  }, this)

  return sqim
},
autoOff ( [sqim [, events [, cx]]] | func [, cx] )

Part of: tag_Events

A utility method for tracking object connections.

this.autoOff(sqim, {})          // add sqim to the list
this.autoOff(sqim, {foo: ...})  // ...and hook some events; cx = this
this.autoOff(sqim, ['foo', ...])        // equivalent
this.autoOff(sqim, {foo: ...}, null)    // ...cx = sqim
this.autoOff(sqim, {foo: ...}, other)   // ...cx = other

this.autoOff()      // off() all sqims tracked so far and clear the list
this.autoOff(sqim)  // off() and un-list sqim, only if it was tracked
this.autoOff(sqim, true)  // just un-list sqim, keep hooks on it

// Iterate over tracked objects:
var found = this.autoOff(sqim => sqim.get('foo'))

autoOff() has three main forms – tracking, untracking and iterating:

  • Adding sqim (an object) to this object’s Set of tracked objects (_autoOff), optionally hooking events on it – returns sqim.
  • Removing all objects from this list and hooks of this on them - returns this.
  • Removing one object, optionally with hooks of this on it – returns sqim.
  • Iterating over objects in this list – calls func for every object until it returns a value that is not undefined or null; autoOff() returns this value, or last func’s result, or undefined if the list was empty.

The list is not used by Sqimitive but your application can use it to unbind all listeners added by this object to other objects in one go. For example, you can do this.autoOff() when this is about to be destroyed so that events on other objects to which this had any connections won’t trigger its handlers.

events, if given and non-true, is an object in on() format (keys are comma-separated event references and values are their handlers - expandFunc()) or an array (hooked in order): ['evt', func, 'evt2', func2, ...]. Commas in event references are allowed even though on() only accepts them when fuse’ing.

With events, cx sets the context in which handlers will be called. If cx is null then sqim is used, if no cx argument is given then this is used (i.e. the object on which autoOff() was called). sqim’s on() always receives cx of this.

Other notes:

  • autoOff() can be called multiple times for one object. This will cause problems only if supplying the same events (hooks will be added twice); doing just autoOff(sqim, {}) twice is fine, and duplicates won’t inflate the list because it’s a Set.
  • The form autoOff(sqim) doesn’t call off() if sqim wasn’t tracked (better for performance). This works if the code doesn’t manually set up hooks (on()) on objects-to-be-autoOff’d, which is recommended. If it does, do this.autoOff(sqim).off(this) or this.autoOff(sqim, {}) and later this.autoOff(sqim).
  • It’s safe to call autoOff() from within func to track or untrack objects – func will receive each object exactly once. Re-tracking the same object until the iterating autoOff() returns will call func twice for that object (Set’s behaviour). From func, calling autoOff() without arguments will call func for every object remaining to be iterated over before that call to autoOff() was made, regardless of subsequent autoOff() calls.

from chevaoff

Example
// Track sqim and hook sqim.remove (calls this.remove):
this.autoOff(sqim, {remove: this.remove})

// Equivalent:
this.autoOff(sqim, {remove: 'remove'})

// Track and hook 2 events (both call sqim.render - not this.render!):
this.autoOff(sqim, {'change_foo, change_bar': 'render'}, null)

// Set up a one-time listener that is removed either when sqim fires
// 'loaded' or this calls autoOff() (typically happens in remove()):
var ev = this.autoOff(sqim, {}).once('loaded', 'render', this)
// ...It's also possible to stop listening at any time:
this.cancel = () => sqim.off(ev)

// Be sure to pass correct cx to on/once for autoOff's off() to work:
autoOff(sqim, {}).once('loaded', 'render')         // WRONG
autoOff(sqim, {}).once('loaded', 'render', sqim)   // CORRECT

// Unbind this instance's events from all sqimitives previously
// enlisted by this.autoOff(sqim, {}) and clear the tracking list:
this.autoOff()

// Unbind from and unlist just one sqimitive:
this.autoOff(sqim)

// Conditionally untrack some objects:
this.autoOff(function (sqim) { sqim.get('foo') && this.autoOff(sqim) })

// Build an array from tracked objects:
var filtered = []
this.autoOff(function (sqim) {
  if (sqim.get('foo')) {
    filtered.push(sqim)
  }
})
ExampleArray form of events is convenient when keys are not constants:
this.autoOff(sqim, ['change_' + prop, 'render', name, 'update'])

// Could be done with computed names in ES6 but it's often longer:
this.autoOff(sqim, {['change_' + prop]: 'render', name: 'update'})

// Equivalent to manual object construction:
var events = {}
events['change_' + prop] = 'render'
events[name] = 'update'
this.autoOff(sqim, events)
ExampleCanonical usage:
var MyNotifyBar = Sqimitive.Base.extend({
  events: {
    owned: function () {
      this.autoOff(new View.LoginForm, {
        loggedIn: function () { alert('Hi there!') },
        '-multiple, events': ...,
      })
    },

    // WRONG: if unnest() is called with any arguments (for whatever
    // the reason) then autoOff()'s behaviour would change and it will
    // not untrack objects. See the note on ev__ danger in on().
    //'-unnest': 'autoOff',

    // WRONG: ES6 'this' is bound to whatever context is calling
    // extend(), see #es6this:
    '-unnest': () => this.autoOff(),

    // CORRECT:
    '-unnest': function () {
      this.autoOff()
    },

    // CORRECT: masked version:
    '-unnest': 'autoOff-',
  },
})

Gotcha with the above usage: if you hook using autoOff() in init() then those hooks will be removed as soon as the sqimitive is nested anywhere because Base.nestEx() calls unnest(). Consider hooking in owned() if the class is guaranteed to become nested prior to actual usage, or check for set _parent in -unnest before calling autoOff().

As a general rule, autoOff() is not used for permanently connected objects, i.e. ones that are destroyed at the same time as this. Such objects are typically set to “_protected” properties and init’ialized during construction:

var MyGame = Sqimitive.Base.extend({
 // Created together with MyGame and only referenced by MyGame:
  _timer: null,

  events: {
    init: function () {
      this._timer = new MyTimer({interval: 1000})
      this._timer.on({expired:  () => alert("Time's up!")})
        // on() instead of autoOff({expired: ...})
    },
  },
})

ExampleautoOff() only really calls on and off methods on sqim and can be used with non-sqimitives if their signatures are compatible. For example, jQuery’s are partially compatible: jq:on() is usable (it ignores cx but you can bind() or use ES6 arrow functions) while jq:off() is not:
// Tracking:
this.autoOff($('body'), [
  'onclick.' + this._cid,
  // Need to bind() or use => as jQuery doesn't accept context object.
  function (e) {
    this.clicked(e.target)
  }.bind(this),
])

// Untracking:
this.autoOff(function (el) {
  if (el instanceof $) {
    el.off('.' + this._cid)
    // Giving true removes object from the list without calling off().
    this.autoOff(el, true)
  }

  // Or, one-liner:
  ;(el instanceof $) && this.autoOff(el, true).off('.' + this._cid)
})

from unnestedoff

Base’s implementation of unnested() calls sqim.off(this) to unregister all event handlers that might have been previously attached to the removed child by this instance (provided they were not hardwired with fuse() and used cx === this).

Because of this, there’s no need to track and explicitly unsubscribe from children (regardless of _owning):

var MyClass = Sqimitive.Base.extend({
  events: {
    init: function () {
      var child = this.nest(new MyClass.Child)
      this.autoOff(child, {changed: 'recalc'})
    },

    unnested: function (sqim) {
      this.autoOff(sqim)
    },
  },

  recalc: ...
})

The above is redundant and can be simplified, provided that the context given to on() is this as below:

var MyClass = Sqimitive.Base.extend({
  events: {
    init: function () {
      var child = this.nest(new MyClass.Child)
      // this (_parent) will call off(this) on child when child is unnested().
      child.on('changed', 'recalc', this)
    },
  },

  recalc: ...
})

If you are hooking all children then use _childEvents to further simplify the task:

var MyGame = Sqimitive.Base.extend({
  _childEvents: ['changed'],

  events: {
    '.changed': 'recalc',
  },

  recalc: ...
})

Garbage collection

autoOff() prevents sqim from being garbage-collected by creating a reference from this to sqim. (WeakSet would be a solution if only it supported enumeration.)

Normally, sqim.on(..., this) results in a reference to this being stored in sqim so that if sqim is not referenced anywhere else (e.g. it is not nest’ed) then it will be freed even if this remains alive.

However, autoOff() additionally lists sqim in the _autoOff Set of this, creating a two-way link; now both sqim and this may only be freed together. Imagine a long-term “server” object (this) and short-term “clients” (sqim): this-server autoOff()-s (tracks) new sqim-client, then unnest()-s the latter at some point. Even though clients are not referenced anywhere except server’s _autoOff, they will be kept alive until the server itself is destroyed.

Addressing this is application-specific but one common way is to hook sqim’s unnest (or Base.remove()) and remove it from _autoOff (this = server):

this.autoOff(client, {
  ...,
  remove: function () {
    this.autoOff(client)
  },
})

Defined in: main.js, lines 3070-3126 (57 lines) • Show code

autoOff: function (sqim, events, cx) {
  var list = this._autoOff

  switch (arguments.length) {
    case 0:
      if (list && list.size) {
        this._autoOff = null
        list.forEach(function (sqim) { sqim.off(this) }, this)
      }
      return this

    case 1:
    case 2:
      if (typeof sqim == 'function') {
        var res
        list && list.forEach(function (item) {
          // Can't break out of Set's forEach().
          res = res == null && sqim.call(events || this, item)
        }, this)
        return res
      } else if (arguments.length == 1 || events === true) {
        if (list && list.delete(sqim)) {
          events === true || sqim.off(this)
        }
        return sqim
      }

      cx = this
    default:
      ;(list || (this._autoOff = new Set))
        .add(sqim)

      var self = this

      function bind(func, name) {
        // For sqim.off(this) to work, on()'s cx must be this. If user wants
        // another context, bind the func.
        if (cx !== self) {
          func = Core.expandFunc(func, cx || sqim)
        }
        var names = name.split(', ')
        for (var i = 0, l = names.length; i < l; i++) {
          sqim.on(names[i], func, self)
        }
      }

      if (_.isArray(events)) {
        for (var i = 1, l = events.length; i < l; i += 2) {
          bind(events[i], events[i - 1])
        }
      } else {
        _.forEach(events, bind)
      }

      return sqim
  }
},
batch ( sqims, func [, cx] )

Part of: tag_Options

Calls func and defers certain events it has produced on this and sqims until func returns.

batch() is the backend used in Sqimitive to defer change and change_OPT events produced by ifSet() (and its set() wrapper). Clients may use it for other events.

Result Types
Types Notes
mixed as returned by func
Arguments
Name Types Notes
sqimsnull Sqimitives to defer and to share the same batch ID with. If array, fails if this or any sqims has an active batch. First member is ignored if it’s this. If you need several sqimitives to be batched but don’t require special effects of having them in one batch, do nested batch() calls with sqims of null.
array
func Receives current batch id. May push to sqim._batch.
cxobject The context for func
null/omitted use this

Some options may be related so that changing one causes a refresh based on values of other options. For example, 2D coordinates consist of x and y; set()’ing them one by one (e.g. first x, then y) will cause multiple refreshes but only the last will use actual (new) values and others may use partially updated state (resulting in an object moving from x1/y1 to x2/y1, then to x2/y2 rather than straight to x2/y2). batch() groups change events and fires them after setting all values instead of after setting each value.

Once func returns, the batch ends and batch() starts dispatching the events. During this process, if a hook on a change event (generated by func) creates a new batch, that batch’s events will be dispatched after events of the current batch. Therefore, batch() may return much later than func as there may be numerous pending batches.

Special fields in options of batched events (added by _batchOptions()):

Arguments
Name Types Notes
batcharray Events in the batch being dispatched, in this._batch format; custom eventIndex property points to the entry handled by the current ^ handler (naor); for regular non-^ handlers it’s null and batch itself omits already fired events (current event is the first member hence batch is never empty); can be mutated except for the current member (eventIndex’th if ^, else 0); doesn’t include events of the later pending batches.
batchIDint A unique batch ID; tells if two change events originated from the same func
batchedarray of [sqim, batch] this and sqims given to the “main” batch(), with their respective batch arrays.

From inside a change listener, each batched member can be classified as a sqimitive that:

  • …is currently firing batched events (always exactly one such member per batched; its batched array is === options.batch)
  • …has finished firing events of this batchID (batched array is empty; do not change it, the batch won’t “resume”)
  • …is yet to start firing batched events (after the current options.batch becomes empty)
operationIDint A unique identifier of the event group; tells if events originated from the same ifSet() call.
tracestr Set to the stack trace of the batch entry’s creation if Core.trace was enabled.
null

Other notes:

  • Given two members (sqims) of the same batch, options.batchID of their change events are equal but sqim._batch are equal only for events of the same sqimitive (IDs are shared but _batch instances are not).
  • Any combination of recursive set()/batch() is allowed. batch() inside a batch() just calls func (or fails depending on sqims). batch() after func (i.e. by a hook on a change) defers events to a new group dispatched after the original func’s events are done.
  • An exception in any change handler drops all pending events, both of the same batch and of pending batches.
  • Batched events across multiple sqims are dispatched not in order of their operation calls (e.g. O1 on sqim1, O2 on sqim2, O3 on sqim1) but in groups per each member of sqims, starting from the last one (O2 on sqim2, O1 on sqim1, O3 on sqim1). Order within a group is guaranteed but order of groups is an internal detail that should not be relied upon.
ExampleConsider an object with width and height Base._opt’ions bound to a DOM node:
var Rect = new Sqimitive.jQuery.extend({
  _opt: {width: 0, height: 0},
  events: {
    change_width: '_recalc',
    change_height: '_recalc',
  },
  _recalc: function () {
    var w = this.get('width')
    var h = this.get('height')
    this.el.css({width: w, height: h})
  },
})
Calling set() would trigger two pairs of events:
var obj = new Rect
obj.set('width', 10)
  // fires change_width and change
  // obj's options = {width: 10, height: 0}
  // el's size = 10*0
obj.set('height', 20)
  // fires change_height and change
  // obj's options = {width: 10, height: 20}
  // el's size = 10*20
As seen, el would undergo two dimension changes of which the first is redundant because after width the caller immediately supplies height. In contrast, doing so in a batch:
obj.batch(null, function () {
  obj.set('width', 10)
  obj.set('height', 20)
})
  // fires change_width, change, change_height, change - all after
  // the batch'ed function returns; change_width sets el's size to
  // 10*20, as does change_height
There are still two dimension changes (_recalc calls) but the second sets the same size and is optimized by the browser.

If _recalc were doing heavy calculations, it could skip them by using an internal option like so:

var Rect = new Sqimitive.jQuery.extend({
  _opt: {width: 0, height: 0, _recalced: ''},
  events: {
    change_width: '_ifRecalc',
    change_height: '_ifRecalc',
    change__recalced: '_recalc',
  },
  _ifRecalc: function () {
    this.set('_recalced', this.get('width') + '*' + this.get('height'))
  },
  _recalc: function () { /* as above */ },
})

ExampleDemonstration of batch ID:
me.on('change', function (name, now, old, options) {
  console.log(name + ' during batch ' + options.batchID)
})

me.set('foo', 123)      //=> foo during batch 1 (implicit batch)
me.set('foo', 456)      //=> foo during batch 2 (implicit batch)

me.batch(null, function () {
  me.set('foo', 789)    //=> foo during batch 3
  me.set('bar', 123)    //=> bar during batch 3
})

me.batch(null, function () {
  me.set('bar', 456)    //=> bar during batch 4
})
Batch-after-batch:
me.on('change_foo', function () {
  me.set('bar', 456)
})

me.batch(null, function () {
  me.set('foo', 123)    //=> foo during batch 1
})
  // Logged after func returns but before batch() returns:
  //=> bar during batch 2
Nested batch() vs sqims and null sqims vs array:
me.batch(null, function () {
  you.batch(null, function () {
    me.set('foo', 123)    // options.batchID == 1
    you.set('bar', 456)   // options.batchID == 2 (different)
  })
})

me.batch([you], function () {
  me.set('foo', 123)      // options.batchID == 3
  you.set('bar', 456)     // options.batchID == 3 (same)
})

me.batch([], function () {
  me.batch([], ...)       // throws - a batch already active on me
})

“Natural” ordernaor

Normally, batch() calls accumulated events in order of changes so that last change dispatches its events last while events in response to a batch (by hooks on events it’s dispatched) are fired after that batch’s own events.

To illustrate the issue, consider this class:

Sqimitive.Base.extend({
  _opt: {
    original: '123',
    linked: null,
  },

  events: {
    change_original: function (now) { this.set('linked', now) },
    change: function (opt) { console.log('change of ' + opt) },
  },
})

When calling set('original', 'new') on such an object, logically you would expect these events: change_original, change of original, change_linked, change of linked. However, with a naive implementation handling change_original would trigger change_linked, then change of linked and only then – change of original.

This would be also a problem if changing the same option from within its own event handler because change with the new value would be fired first (since set() was called last), then when the original set() returns it would also fire change but this time with the original (now old!) value.

Notwithstanding, rarely a handler must be notified of the change immediately (sort of “out of band data”). batch() calls listeners whose event name starts with ^ immediately after func returns, even if there are events of earlier-created batches yet to be dispatched. While normal listeners can be seen as an asynchronous event bus, ^ are closer to direct function calls.

For example, let’s assume two objects: one an array of 2D points, another its index (array of points on the given Y axis). The index must be kept in sync so that clients accessing it at any point in time see no discrepancies with the main array (the only exception we allow is inside the same batch() func).

var Points = Sqimitive.Base.extend({
  points: [],

  add: function (point) {
    this.points.push(point)
    this.batch(null, id => this._batch.push(['added', point]))
  },

  added: Sqimitive.Core.stub,
})

var Index = Sqimitive.Base.extend({
  byY: {},

  events: {
    init: function (opt) {
      this.autoOff(opt.points, {
        added: '_added',
        //'^added': '_added',
      })
    },
  },

  _added: function (point) {
    (this.byY[point.y] || (this.byY[point.y] = [])).push(point)
  },
})

var points = new Points
var index = new Index({points})
points.add({x: 0, y: 0})
  // index.byY = {0: [{...}]}
points.add({x: 1, y: 1})
  // index.byY = {0: [{...}], 1: [{...}]}

The above implementation works but at the first glance only:

var points = new Points
var index = new Index({points})

points.on('change_foo', function () {
  console.dir(index.byY)      // (1)
  points.add({x: 2, y: 2})
  console.dir(index.byY)      // (2)
})

points.add({x: 0, y: 0})
points.set('foo', 123)
console.dir(index.byY)        // (3)

Both (1) and (2) output the same result: {0} while (3) outputs {0, 2}. Why output of (2) is the same as of (3)? Because ifSet() uses batch, it delays dispatching events of other batches (added in our case) until its own events are finished. change_foo calls add() which creates a batch (queued after the currently executing ifSet’s batch) and pushes added to it (to be dispatched after set('foo') returns, hence (3) showing expected result). In contrast, if Index hooks '^added' then it becomes exempt from this delay and causes _added() to be called after points.add({x: 2, y: 2}) returns (or, precisely, upon returning from func id => ... given to batch() inside add()) and the index to be updated.

^ makes it “closer” to direct function call but not exactly that: such handler is called once the batch closes (func returns), not once a corresponding event if queued (“function is called”). Below, add() runs inside an already active batch so no index update takes place then:

points.batch(null, function () {
  points.add({x: 3, y: 3})
    // points._batch = [['added', {x: 3, y: 3}]]
  console.dir(index.byY)
    //=> {}
})
console.dir(index.byY)
  //=> {3: [{...}]}

^ has no special meaning for dispatching initiated by fire() and, consequently, in =wrapped handlers (evtpf; compare eventIndex with null to determine the phase).

^ separates firing into two groups which are then ordered by priority. This means that in response to a batch, ^0^event will run before -1^event even though the latter has lower priority.

sqim.on('^0^event', () => console.log(0))
sqim.on('-1^event', () => console.log(-1))

sqim.fire('event')      // outputs -1, 0

sqim.batch(null, function () {
  this._batch.push(['event'])
})    // outputs 0, -1

Custom events

You can defer arbitrary events from within func by pushing an array to sqim._batch, with first member being event name and others being its arguments. Because sqims is rarely used, sqim here is typically this.

For consistency with Sqimitive’s ifSet(), provide hooks with information (_batchOptions()) about the current batch.

this.batch(function (id) {
  this._batch.push(['firstOff', 'pa', 'ra', 'ms'])
  // Or, better:
  var options = {batch: this._batch, batchID: id, operationID: Core.unique('o')}
  this._batch.push(['firstOff', 'pa', 'ra', 'ms', options])
  // Or, best:
  this._batch.push(['firstOff', 'pa', 'ra', 'ms', this._batchOptions(id)])
})
  // does fire('firstOff', ['pa', 'ra', 'ms'...]) when func returns

Once batched events start firing, the batch is considered “closed”. This typically means that _batch will no longer change, allowing heavy update functions to process all batched events as one unit in response to the first event such a function is interested in, remembering its batch ID and ignoring subsequent events with the same ID. Remembering the ID might be simpler than maintaining a hash of current state (_recalced) as in the earlier Rect example:

var Rect = new Sqimitive.jQuery.extend({
  _opt: {width: 0, height: 0},
  _lastBatch: 0,
  events: {
    change_width: '_ifRecalc',
    change_height: '_ifRecalc',
  },
  _ifRecalc: function (now, old, options) {
    if (this._lastBatch != options.batchID) {
      this._lastBatch = options.batchID
      this._recalc()
    }
  },
  _recalc: function () {
    var w = this.get('width')
    // ...
  },
})
Or the ID itself may be seen as a kind of hash:
var Rect = new Sqimitive.jQuery.extend({
  _opt: {width: 0, height: 0, _lastBatch: 0},
  events: {
    change_width: '_ifRecalc',
    change_height: '_ifRecalc',
    change__lastBatch: '_recalc',
  },
  _ifRecalc: function (now, old, options) {
    this.set('_lastBatch', options.batchID)
  },
  _recalc: function () { /* as above */ },
})

Skipping known batchesskb

Things get hairy if your “_ifRecalc” handler is attached to multiple sqimitives and processes all batched sqimitives as one, rather than each batched sqimitive’s events as one:

function update(options) {
  if (!skip(options)) {    // read below
    _.each(options.batched, function (item) {
      // item[0] is a sqimitive.
      _.each(item[1], function (event) {
        // Any change must cause an update. Or can check event[1] if
        // only some _opt'ions cause it. Don't use event[0] == 'change_OPT'
        // as it will never match since change_OPT is fired prior to
        // change and is already removed from batched by the time update()
        // runs.
        if (event[0] == 'change') {
          // ...
  }
}

sqim1.on('change', (name, now, old, options) => update(options))  // (1)
sqim2.on('change', (name, now, old, options) => update(options))  // (2)
sqim3.on('change', (name, now, old, options) => update(options))  // (3)

Now, update() should ignore batches that it has seen. Previously used != fails short since there may be events of other sqimitives fired between batched events of one sqimitive, as below:

var lastBatch = 0
function skip(options) {
  if (lastBatch != options.batchID) {
    lastBatch = options.batchID
  } else {
    return true
  }
}

sqim3.on('change', function () {    // (4)
  sqim2.set('quux', 789)
})
sqim1.batch([sqim3], function () {  // (5)
  sqim1.set('foo', 123)
  sqim3.set('bar', 456)
})
  // When batch()'s func returns, first event to be fired happens to be
  // sqim3 change_bar, handled by (3). skip() receives options.batchID
  // of N, storing it in lastBatch. Then, change_bar is handled by (4),
  // which starts another batch (N + 1) that is closed immediately after
  // set() in (4). This triggers (2), where skip() stores N + 1 in
  // lastBatch. But now, the pending sqim1 change_foo from (5) triggers
  // (1) where skip() sees options.batchID of N and considers this batch
  // to be "new", even though it was already processed in response to (3).

Another incorrect solution would be to skip all batches with the same or lower ID than previously seen. After all, batchID is guaranteed to be unique and growing. However, the ID is generated when a batch starts (before calling func), not when the first batched event is dispatched:

var lastBatch = 0
function skip(options) {
  if (lastBatch < options.batchID) {
    lastBatch = options.batchID
  } else {
    return true
  }
}

sqim1.batch(null, function () {
  sqim1.set('foo', 123)
  sqim3.set('bar', 456)   // not part of sqim1's batch!
})
  // batch() has allocated the batchID of N before calling func.
  // Then, before batch()'s func returns, sqim3 change_bar is fired
  // since this time sqim3 is not part of batch()'s sqims list (the
  // batchID for change_bar is N + 1). This triggers (3), where
  // skip() stores N + 1 in lastBatch. Then sqim3.set() returns, then
  // sqim1.batch()'s func returns and fires the delayed sqim1 change_foo,
  // to which (1) responds. skip() compares lastBatch of N + 1 with
  // options.batchID of N and decides this batch is not "new".

To summarize, a handler hooked onto multiple objects must store the actual list of batchID-s it has seen. Most straightforward approach is to have a round-robin array holding up to N last IDs but choosing N is not trivial – make it too large and you will be wasting memory; make it too small and you will be identifying old batches as new.

Instead of hardcoding any such value, we may rely on the fact that a given batchID can never reappear after the batch ends and that options.batched may not change after the batch has started. We can keep only as many IDs in the list as there are currently active batches. On the first event, push batchID to the list and push an internal event to each of the batched sqimitive’s current batch. When the event fires N times, we pop that batchID (N = count of batched sqimitives, except ones with empty – already drained – batches).

var seenBatches = new Set
function skip(options) {
  if (seenBatches.size == seenBatches.add(options.batchID).size) {
    // Already saw this batchID.
    return true
  } else {
    // Event is firing in the batch for the first time, across all
    // batched sqimitives. Wait for all sqimitives to drain this batch.
    var event = 'mySkip' + Core.unique('mySkip')
    var remaining = 0
    _.each(options.batched, function (item) {
      if (item[1].length) {
        remaining++
        item[1].push([event])
        item[0].once(event, function () {
          --remaining || seenBatches.delete(options.batchID)
        })
      }
    })
    // Instead of hooking every batched sqimitive, we could hook only
    // batched[0] since it will fire its events last. However, the order
    // in which batched sqimitives are processed is an internal detail.
  }
}

Use batchGuard() that implements this algorithm in a generalized form.

Listening for batch end

Occasionally you need to do some clean-up when all batched events finish but before the next batch starts firing. One way would be to override batch() itself, tap into func and push some internal event into _batch as the client-provided func returns. This is the only option when you need to react to every batch, but usually the clean-up is necessary only if certain events occurred during a batch, similarly to try/finally (except an exception breaks the batch). In this case you can push the internal event to options.batch or options.batched from within a listener as done in the example in skb.

ExampleWith the above Rect class, we know that change_width and change_height may trigger a lengthy synchronous process (as a result of the _ifRecalc listener) and we want to show hourglass cursor to the user until _recalc returns, i.e. the batch ends:
var rect = new Rect
var sym = Symbol()
var waiting = 0
rect.on('-change_width, -change_height', function (now, old, options) {
  if (!options.batch[sym]) {
    $('body').css('cursor', 'wait')
    options.batch.push(['restoreCursor'])
    // Fire restoreCursor only once per batch, even if there are other
    // change events pending.
    options.batch[sym] = true
    // This counter is in case we hook several Rect-s or other objects
    // controlling the cursor.
    // We could also use the counter alone, ditching Symbol and allowing
    // duplicate restoreCursor events per batch but that'd be slightly
    // less efficient.
    waiting++
  }
})
rect.on('restoreCursor', function () {
  --waiting || $('body').css('cursor', '')
})
In a real application you should make lengthy processes async to avoid freezing the UI. Luckily, Async exists just for that.

Generally speaking, it is permitted to modify options.batch in other ways as well, except touching the member corresponding to the event being fired. However, this is not recommended as it may quickly become unmanageable. In particular, since the order in which listeners are called is hard to predict, there is no guarantee that the batch update function alike to _recalc above will be affected by these modifications (such as an added or removed change_width) because it might have already executed. Or, with the skip() function example in skb, appending a new change event that would trigger update() will cause skip() to wrongly report a batch with such event as “new” because skip()’s internal on-batch-end listener has already deleted batchID from seenBatches.

This problem does not exist for internal events like restoreCursor and mySkip that are not used outside of their private scope.

Defined in: main.js, lines 4169-4257 (89 lines) • Show code

batch: function (sqims, func, cx) {
  var id = this._batchID || (this._batchID = Core.unique('b'))

  if (sqims) {
    if (this._batch) {
      // Disallowing as active batch's ID cannot change.
      throw new Error('Batch already active')
    }

    var i = +(sqims[0] === this)

    if (sqims.length > i) {
      var sqim = sqims[i]
      // Start a batch across multiple sqimitives.
      if (!sqim._batch) {
        // If !!_batch, don't overwrite (call batch() and let it throw).
        sqim._batchID = id
        sqim._batched = this._batched
      }
      func = sqim.batch.bind(sqim, sqims.slice(i + 1), func, cx || this)
    }
  }

  // Batch already active, append.
  if (this._batch) {
    return func.call(cx || this, id)
  }

  var batch = this._batch = []
  var outermost = !this._batches
  outermost ? this._batches = [batch] : this._batches.push(batch)
  this._batched.push([this, batch])

  try {
    try {
      var res = func.call(cx || this, id)
    } finally {
      // Multi-sqim batch must not only start simultaneously but also finish
      // (i.e. clear _batch, etc.) on all sqims at once. If each sqim would
      // finish after its own func has returned, this would fail:
      //
      //   sqim2.on('^change_foo', function () {
      //     sqim1.batch([sqim2], function () {   // (1)
      //       sqim2.set('foo', 456)
      //     })
      //   })
      //
      //   sqim2.set('foo', 123)                  // (2)
      //
      // (2) triggers change_foo which calls (1), whose func queues another
      // change_foo. Before sqim1.batch() returns, sqim2.batch([]) (called
      // by the former) would first clear _batch on itself, i.e. sqim2 (but
      // not sqim1!), then proceed to dispatching queued events on sqim2.
      // After that sqim2.batch() would return and sqim1.batch() would clear
      // _batch on sqim1. In the example, sqim2.batch() dispatches
      // change_foo, reentering the hook which calls (1) and fails with
      // "batch active on sqim1" because sqim2._batch would be null but
      // sqim1._batch would be not.
      _.forEach(this._batched, function (item) {
        item[0]._batch = item[0]._batchID = null
        item[0]._batched = []
      })
    }

    batch.eventIndex = -1

    for (var item; item = batch[++batch.eventIndex]; ) {
      this.fire(item[0], item.slice(1), false)
    }

    batch.eventIndex = null

    // Outermost batch has finished but hooks on events that it has produced
    // could have added new batches. Process them until the queue is
    // exhaused.
    if (outermost) {
      while (batch = this._batches.shift()) {
        while (batch.length) {
          this.fire(batch[0][0], batch[0].slice(1), true)
          batch.shift()
        }
      }
    }

    return res
  } finally {
    outermost && (this._batches = null)
  }
},
constructor ( )

Modifiers: constructor

Assigns new unique _cid (a string: p + unique positive number) and clones all instance properties of self that are not listed in _shareProps. Cloning puts a stop to accidental static sharing of properties among all instances of a class where those properties are defined.

Defined in: main.js, line 143

fire ( event [, args [, inBatch]] )

Part of: tag_Events

Triggers an event giving args as parameters to all registered listeners. Returns result of the last called listener that was eligible for changing it.

The actual event processing is performed by static ::fire().

It’s safe to add/remove new listeners while fire() is executing – they will be in effect starting with the next fire() call (even if it’s nested).

fireOverrideWarning: to override fire, fuse and on, don’t use the usual on('fire') as it will lead to recursion (even the =fire form, evtpf). Use the old-school prototype overriding (__super__ is set up by extend()) – see Async’s source code for an example:

fire: function (event, args) {
  // Do your stuff...
  return MyClass.__super__.fire.apply(this, arguments)
},

The all eventfireAll

fire() first triggers the special all event. If all’s return value is anything but undefined – fire() returns it bypassing the actual handlers of event.

all’s handlers get event put in front of other args (e.g. ['eventName', 'arg1', 2, ...]).

Note that the all event is only triggered for actual events so if, for example, render() isn’t overridden then it will be called as a regular member function without triggering all or any other event:

var ClassA = Sqimitive.Base.extend({
  events: {
    render: function () { return this },
  },
})

;(new ClassA)
  .on({all: () => alert('booh!')})
  .render()
    // alert() happens, render is a firer

var ClassB = Sqimitive.Base.extend({
  render: function () { return this },
})

;(new ClassB)
  .on({all: () => alert('booh!')})
  .render()
    // no alert(), render of extend() is not an event handler but a
    // method

;(new ClassB)
  .on({all: () => alert('booh!')})
  .on({render: function () { return this }})
  .render()
    // alert() happens because of on('render') which converted
    // the render function from ClassB's declaration to a
    // firer('render') which when called triggers fire('render') which
    // in turn triggers 'all'

Defined in: main.js, lines 2318-2327 (10 lines) • Show code

fire: function (event, args, inBatch) {
  if (this._events.all && event != 'all') {
    var allArgs = arguments.length < 2 ? [] : Core.toArray(args).concat()
    allArgs.unshift(event)
    var res = this.constructor.fire.call(this, this._events.all, allArgs)
    if (res !== undefined) { return res }
  }

  return this.constructor.fire.call(this, this._events[event], args, inBatch)
},
fuse ( event, func[, cx] )

Part of: tag_Events

Registers a single permanent event handler.

Unlike with on() and once(), this handler cannot be removed with off().

Arguments
Name Types Notes
eventstring A single event reference (no comma notation).
funcfunction Masked method name (expandFunc()).
string
cxobject The context for func
null/omitted use this
Result Types
Types Notes
object An internal event registration object (eobj) that should be discarded.

fuse() is a low-level method. You are advised to call on({event}) instead.

Example
sqim.on({
  something: function () { ... },
  someone: function () { ... },
})

// Identical to the above:
sqim.fuse('something', function () { ... })
sqim.fuse('someone', function () { ... })

// A masked callback - receives (a1, a2, a3), passes (a3, a1, a1, a1)
// to sqim.meth().
sqim.fuse('somewhere', 'meth-3111', sqim)

The post callbackpost

One case when eobj can be accessed is to assign its post key (this is considered advanced usage). If set to a function, ::fire() calls post(eobj, res, args) after executing the handler (func), within the handler’s cx. post must return the new result (replaces res in any case – even if undefined and regardless of evtpf). args is the event’s arguments, an array-like object (possibly Arguments). If post sets eobj.stop to true then remaining handlers are skipped. post is only called along with its associated handler and not called, for example, if it was attached to a =wrapped hook that was off’ed, or to a hook with mismatching arguments (ev__, see argDanger).

ExampleSee the source code of Sqimitive\Async for a practical example on post.

var sqim = new Sqimitive.Base

sqim.on('+event', function () {
  console.log(1)
  return 'test'
})

var eobj = sqim.fuse('event', function () {
  console.log(2)
}, this)

eobj.post = function (eobj, res, args) {
  console.log(3)
  eobj.stop = true
  return res.toUpperCase()    // res is 'test', args is []
}

sqim.on('event', function () {
  console.log(4)
})

sqim.fire('event')      //=> 'TEST'
  // console logs: 1, 2, 3 but not 4

from fireOverride

Warning: to override fire, fuse and on, don’t use the usual on('fire') as it will lead to recursion (even the =fire form, evtpf). Use the old-school prototype overriding (__super__ is set up by extend()) – see Async’s source code for an example:

fire: function (event, args) {
  // Do your stuff...
  return MyClass.__super__.fire.apply(this, arguments)
},

Defined in: main.js, lines 3206-3253 (48 lines) • Show code

fuse: function (event, func, cx) {
  func = Core.expandFunc(func)
  var eobj = Core.parseEvent(event)
  // Don't just do _events[name] or (name in _events) because if name is
  // toString or other Object.prototype member, this will fail (array won't
  // be created).
  var list = _.has(this._events, eobj.event)
    ? this._events[eobj.event] : (this._events[eobj.event] = [])
  this._wrapHandler(eobj.event)
  eobj.func = func
  eobj.cx = cx

  if (eobj.prefix == '+') {
    eobj.res = eobj.ret = true
  } else if (eobj.prefix == '=') {
    eobj.ret = true
    eobj.supList = list.splice(0)

    // function (this[, arguments])
    // sup() itself is removed if present as arguments[0].
    var sup = eobj.sup = function (self, args) {
      if (args && args[0] === sup && _.isArguments(args)) {
        args = ap.slice.call(args, 1)
      }

      return Core.fire.call(self, eobj.supList, args)
    }

    Object.defineProperty(sup, 'name', {value: eobj.event})
  }

  this._insertHandler(list, eobj)

  // A benchmark with optimized event handlers was done on Chrome: it was
  // discovered that in a certain application 60% of fire() calls were on
  // events with 1 handler so I made it bypass fire() if there were no
  // handlers (replaced the wrapped method, firer() with stub) or 1 handler
  // (replaced with direct call to it); this was also maintained on event
  // handler changes (on()/off()).
  //
  // However, performance savings from this optimization were ~5% (on 30k
  // fire() calls) while the added complexity was significant (due to
  // different eobj properties like cx and post which implementation needed
  // to be duplicated in both fire() and the fire()-less wrapper) so it was
  // discarded.

  return eobj
},
logEvents ( [enable] )

Part of: tag_Events

A debug method for logging all triggered events to the console.

logEvents() allows enabling event logging as well as acts as an event handler for all. The handler can be used on its own, without enabling logging with logEvents(true).

Arguments
Name Types Notes
true Enable logging; do nothing if browser doesn’t provide console.log()
-no arguments Same as true (enable logging).
false Disable logging, if enabled.
stringevent name Log this event; other arguments are the event’s arguments.
Result Types
Types Notes
this if enable is bool
undefined if string
Example
sqim.logEvents(true)   // enable logging
sqim.logEvents()       // same as above
sqim.logEvents(false)  // disable

Note: logEvents logs only events passing through the instance .fire(). This means that only real event calls are tracked, not calls of non-overridden methods (i.e. of regular functions, not firer). See fireAll.

Extending default logger

You can override or extend the default logging behaviour by testing the argument for string:

var MyLoggee = Sqimitive.Base.extend({
  events: {
    logEvents: function (event) {
      // logEvents() calls itself when an event occurs and the first
      // argument is the event name - a string. In other cases it's
      // not the logger being called.
      if (typeof event == 'string') {
        console.log('el.' + this.el[0].className)
      }
    },
  },
})

// Logs both standard logEvents() info and our class name line.
;(new MyLogger).logEvents().fire('something')

Note: returning non-undefined from the all handler prevents calling real event handlers (see .fire()). In the above example it never happens since logEvents event is hooked with no prefix (evtpf) meaning the handler’s return value is ignored (even if it does return anything). However, not in this example:

events: {
  '=logEvents': function (sup, event) {
    if (typeof event == 'string') {
      console.log(this.el[0].className)
    } else {
      sup(this, arguments)
    }
    return 'break!'
      // returning non-undefined from an 'all' handler bypasses real
      // event handlers
  },
},

Defined in: main.js, lines 2401-2427 (27 lines) • Show code

logEvents: function (enable) {
  if (typeof enable == 'string') {
    // function (event, eventArg1, arg2, ...)
    var info = this._cid

    if (this.el) {
      //! +ig
      var el = this.el.nodeType ? this.el : this.el[0] // jQuery
      info += '\t\t' + el.tagName
      var className = el.className.trim()

      if (className != '') {
        info += (' ' + className).replace(/\s+/g, '.')
      }
    }

    console.log(enable + ' (' + (arguments.length - 1) + ') on ' + info)
    return undefined
  } else if (!enable && arguments.length) {
    this.off(this._logEventID)
    this._logEventID = null
  } else if (console && console.log && !this._logEventID) {
    this._logEventID = this.on('all', Core.logEvents)
  }

  return this
},
mixIn ( newClass, options )

Part of: tag_Extension

Extends this object instance with a behaviour of another “class” (a “mix-in”).

The static ::mixIn() exists which affects all objects of a class.

Arguments
Name Types Notes
thismixInDesc The object receiving new “mixed-in” fields (for static ::mixIn() this is the “child” class).
newClassobject The mix-in, the object “mixed into” this.
options An arbitrary value (usually an object) given to newClass.finishMixIn(), allowing creation of parametrized mix-ins (basically, generics).

Possible newClass keys:

Arguments
Name Types Notes
staticPropsobject Static fields made available as newClass.something
eventsobject Event listeners (see events).

Because this is not a real field, keys in newClass.events do not override keys in this.events. If both this and newClass have the foo event key (i.e. if both listen to this event) then two listeners are set up, not one. Compare with Base.elEvents overriding (elEventsMixIn).

finishMixInfunction Called before returning.

this = newClass. arguments = child, options where child is the prototype of the updated class

mixInsarray Each item is either a mixed-in class or an array: [class, options]
* Other keys represent instance fields (the protoProps argument of extend()).

_mergeProps is respected, but of this (not of newClass).

A string form of _childClass is only allowed in extend(), not here; other forms (array, object) are allowed in both places.

Example
var MixIn = {
  staticProps: {
    staticMethod: function () { ... },
  },
  events: {
    '-init': function () {
      // (1) Define a property only if it's not defined yet.
      this._someProp = this._someProp || 'buzz'
    },
  },
  finishMixIn: function (targetProto) {
    alert("I'm now mixed into " + targetProto.toString())

    // (2) Or could do it here, more performance-efficient since ran
    // only once for each mixed-in class, not once for each such class
    // instantiation:
    //targetProto._someProp = targetProto._someProp || 'buzz'
    //targetProto.constructor.someStatic = 123
  },
  _opt: {
    correctly: 'merged',
  },
  instanceMethod: function () { ... },
}

var Base1 = Sqimitive.Base.extend({})

var Base2 = Sqimitive.Base.extend({
  _opt: {
   a: 'base',
 },
  // Not overridden by MixIn.
  _someProp: 123,
})

Base1.mixIn(MixIn)   // alerts
  //=> _someProp = 'buzz'
  //=> opt = {correctly: 'merged'}

Base2.mixIn(MixIn)   // alerts again
  //=> _someProp = 123
  //=> opt = {a: 'base', correctly: 'merged'}

Warning: this is modified in-place; no new class is created (mixIn() returns no value). If you want a base class without a given mix-in and a subclass with that mix-in – first extend() the base class and then mix into the new sub-class:

// CORRECT:
var Base = Sqimitive.Base.extend()
var Sub = Base.extend({mixIns: [SomeMixIn]})

// WRONG: will modify Base, not return a new subclass:
var Base = Sqimitive.Base.extend()
var Sub = Base.mixIn(SomeMixIn)

ExampleThere is no way to determine if a class or object has some mixIn or not. For example, listing a mix-in in _childClass is useless because Base.nestEx() is using instanceof and it works on real classes only. You can add a field to work around this – static (only for mix-ins applied to declarations) or instance (for mix-ins applied on run-time):
var MyMixIn = {staticProps: {myMixInIsMixedIn: true}}
var instanceofMyMixIn = 'myMixInIsMixedIn' in myObj.constructor

var sqim1 = new Sqimitive.Core
var sqim2 = new Sqimitive.Core
sqim1.mixIn(MyMixIn)
sqim2.constructor.myMixInIsMixedIn    //=> true (!)

var MyMixIn = {myMixInIsMixedIn: true}
var sqim3 = new Sqimitive.Core
var sqim4 = new Sqimitive.Core
sqim3.mixIn(MyMixIn)
sqim4.myMixInIsMixedIn                //=> false

Other notes:

  • mixIn() is doing most of extend()’s job, which is just creating a special form of a mix-in.
  • Hooking init and similar events for a mix-in applied on run-time is useless since they will be never invoked.
  • mixInDoesmixIn() applies “sub mix-ins” (calls mixIn() if newClass contains the mixIns key; this is recursive), overwrites fields of this with deepClone-s of those in newClass (or merges according to _mergeProps), adds staticProps, hardwires events into class definition (fuse()) and calls finishMixIn().
  • Not to be confused with Underscore’s un:mixin(). However, LoDash’s mixin() is of a similar purpose.

Merging of elEventselEventsMixIn

Base lists Base.elEvents in _mergeProps so the former are merged but unlike with newClass.events keys (which can never have a conflict and be dropped), keys in elEvents get overridden on name collisions. This is sometimes desirable (to override the parent’s handler), sometimes not (then use a unique .ns suffix):

var Base = Sqimitive.Base.extend({
  elEvents: {
    click: function () { alert("Click-click-click!') },
  },
})

var ChildA = Base.extend({
  elEvents: {
    click: function () { alert("Overridden!') },
  },
})

var ChildB = Base.extend({
  elEvents: {
    'click.new-in-child-B': function () { alert("Combined!') },
  },
})

// in ChildA, 'click' has 1 listener (Base's dropped)
// in ChildB, 'click' has 2 listeners (Base's and ChildB's)

Mix-in inheritance

mixIns is applied before setting other properties which allows extending mix-ins themselves (later mix-ins override preceding just like with normal inheritance on classes). For example:

var ParentMixIn = {
  someEvent: function () { /* parent */ },
]

var ChildMixIn = {
  mixIns: [ParentMixIn],
  events: {
    someEvent: function () { /* child */ },
  },
}

var myClass = Sqimitive.Base.extend({
  mixIns: [ChildMixIn],
})

// myClass had ParentMixIn added, then ChildMixIn, and now has two
// listeners on someEvent

ParentMixIn could also have the mixIns property to specify its own parent.

In the above example, the mix-in specified its parent, which is usually intuitive. Still, it could be specified in the final class’ mixIns alone:

var ParentMixIn = {
  someEvent: function () { /* parent */ },
]

var ChildMixIn = {
  // mixIns property is missing.
  events: {
    someEvent: function () { /* child */ },
  },
}

var myClass = Sqimitive.Base.extend({
  // Added ParentMixIn in front of ChildMixIn.
  mixIns: [ParentMixIn, ChildMixIn],
})

// or (equivalent, no mixIns property, mixIn() calls instead):
var myClass = Sqimitive.Base.extend()
myClass.mixIn(ParentMixIn)
myClass.mixIn(ChildMixIn)

// or (equivalent for a particular object instance):
var myClass = Sqimitive.Base.extend()
var obj = new myClass
obj.mixIn(ParentMixIn)
obj.mixIn(ChildMixIn)

// in any case, MyClass (or obj) has 'someEvent' firer() with
// 2 listeners: of ParentMixIn and of ChildMixIn

Warning: calling mixIn() in finishMixIn() would have a different effect. If ChildMixIn were defined as follows then MyClass or obj would have someEvent not as a firer() but as the ParentMixIn’s function (because it was mixed-in after ChildMixIn and overrode its events handler):

var ChildMixIn = {
  finishMixIn: function (newClass) {
    newClass.mixIn(ParentMixIn)
  },
  events: {
    someEvent: function () { /* child */ },
  },
}

This will override the handler introduced in the declaration of Class for the same reason – MixIn is added to Class after Class’ own fields:

var MixIn = {
  some: function () { ... },
}

var Class = Sqimitive.Base.extend({
  events: {
    some: function () { ... },
  }
})

Class.mixIn(MixIn)

This is the correct way (using mixIns property when extend()’ing Class):

var MixIn = {
  some: function () { ... },
}

var Class = Sqimitive.Base.extend({
  mixIns: [MixIn],
  events: {
    some: function () { ... },
  }
})

Edge cases

See the source code for details.

  • Given a base class B and subclass C, adding mix-ins to B after C has been extend()’ed from B when C declares events will lead to C not having events of the newly mixed-in objects of B.
  • Declaration-time events of B are fuse()’d and their eobj-s are shared among all subclasses of B and should not be changed (list of events may change, just not properties of inherited handlers):
    var ClassB = Sqimitive.Core.extend({
      events: {change: 'render'},
    })
    
    ClassB.prototype._events.change[0].post    //=> undefined
    
    var ClassC = ClassB.extend()
    ClassC.prototype._events.change[0].post = function () { ... }
    ClassB.prototype._events.change[0].post    //=> Function
    
    // In this version instances' _events are deepClone()'d so changing it
    // (not the prototype) doesn't affect other classes/objects:
    var obj = new ClassC
    obj._events.change[0].post = 'foo'
    ClassB.prototype._events.change[0].post    //=> still Function

Defined in: main.js, lines 2129-2237 (109 lines) • Show code

mixIn: function (newClass, options) {
  //! +ig=4
  // Don't expose internal inheritance fields on the final classes.
  var merged = {mixIns: undefined, finishMixIn: undefined, staticProps: undefined, events: undefined}

  _.forEach(newClass.mixIns || [], function (mixIn) {
    // mixIns items are either objects or arrays. concat() ensures mixIn()
    // is called either with (class, options) or (class) alone.
    this.mixIn.apply(this, [].concat(mixIn))
  }, this)

  _.forEach(this.constructor._mergeProps, function (prop) {
    if ((prop in this) && (prop in newClass)) {
      if (_.isArray(newClass[prop])) {
        merged[prop] = this[prop].concat(newClass[prop])
      } else {
        merged[prop] = _.assign({}, this[prop], newClass[prop])
      }
    }
  }, this)

  _.assign(this, newClass, merged)
  _.assign(this.constructor, newClass.staticProps || {})

  if (newClass.events) {
    // Core has no __super__ but it doesn't use mix-ins either so no check
    // for null. This condition will evaluate to true except when a class
    // has mix-ins (via mixIns property or mixIn() method) - we could clone
    // it in the second case too but this would be a waste.
    //
    // Warning: this operates under assumption that the base class is
    // finalized (all mix-ins applied) before any of its sub-classes is
    // created.
    //
    //   var Base = Sqimitive.extend()
    //   Base.mixIn(...)    // fine
    //   var Child = Base.extend()
    //   Base.mixIn(...)    // wrong
    //
    // Adding mix-ins after Child was declared may have unexpected side
    // effects - if the mix-in adds an event and if Child had its own events
    // block, then Child won't receive new mix-in's events. This is an
    // implementation detail - "officially", adding mix-ins after declaring
    // a subclass leads to undefined behaviour and should never be used (but
    // it's fine to mix-in into live instances at any time).
    if (this._events === this.constructor.__super__._events) {
      //! +ig
      // Could use deepClone but it's more intense - we don't clone eobj-s
      // which theoretically could be changed before instantiation but we
      // ignore this possibility.
      this._events = _.assign({}, this._events)

      for (var ev in this._events) {
        this._events[ev] = this._events[ev].concat()
      }
    }

    this.on(newClass.events)
  }

  if (this === this.constructor.prototype) {   // static mixIn()
    //! +ig
    // Constructor fills this one when the class is created for the first
    // time. It should be regenerated for every class and not be copied to
    // subclasses to avoid the following:
    //
    //   var Base = Sqimitive.Base.extend()
    //   new Base
    //   Base._copyProps      //=> [...]
    //   var Sub = Base.extend({newProp: []})
    //   new Sub
    //   Sub._copyProps       //=> same as Base; newProp is missing
    //   ;(new Sub).newProp.push(123)
    //   ;(new Sub).newProp   //=> [123] instead of []
    //
    // Additionally, it should be cleared when a mix-in is added since it
    // may have provided new properties (in finishMixIn if not via
    // newClass). This is not done for instance mixIn() since the latter is
    // called after constructor.
    this.constructor._copyProps = initCopyProps
  } else {    // instance mixIn()
    // When mixing-in to a declaration, inherited and mixed-in properties'
    // values are cloned by the constructor. Obviously, the latter isn't
    // called when mixing-in to an instance so need to clone now.
    //
    //   // Instance mix-in:
    //   var MixIn = {newProp: []}
    //   var sqim = new (Sqimitive.Base.extend())
    //   sqim.mixIn(MixIn)
    //   sqim.newProp.push(123)
    //   MixIn.newProp    //=> must be [], not [123]
    //
    //   // Compare with declaration mix-in (cloning done by constructor):
    //   var sqim = new (Sqimitive.Base.extend({
    //     mixIns: [MixIn]
    //   }))
    //   // Or, the same:
    //   var Decl = Sqimitive.Base.extend()
    //   Decl.mixIn(MixIn)
    //   var sqim = new Decl
    for (var prop in newClass) {
      if (!(prop in merged) && this._mustClone(prop)) {
        this[prop] = this.constructor.deepClone(this[prop])
      }
    }
  }

  newClass.finishMixIn && newClass.finishMixIn(this, options)
},
off ( key )

Part of: tag_Events

Removes non-fuse()’d event listener(s).

Result Types
Types Notes
this

key can be one of:

Arguments
Name Types Notes
stringevent name Like “render”; removes all listeners to that event.
numberlistener ID As returned by on(); removes that particular listener from that particular event.
objectcontext The cx to which listeners were registered; removes all listeners to all events with that context.
array Containing any of the above values including more sub-arrays; identical to multiple off() calls.

Does nothing if no matching events, contexts or listeners were found (thus safe to call multiple times).

When unregistering a wrapping handler (=event, see evtpf) its underlying handlers are reinserted into the list of handlers (“undoing” method override) maintaining priority (evtref) but not necessary the original order (calls to on()).

See evt for a nice example on this subject and once() for attaching one-shot listeners.

Example
// key = 'evtname' | 12345 | {cx} | [key, key, ...]

var id = sqim.on('=superseded', function () { ... }, this)
sqim.off(id)
sqim.off('superseded')
sqim.off(this)

// Just like the above 3 calls:
sqim.off([id, 'superseded', this])

from unnestedoff

Base’s implementation of unnested() calls sqim.off(this) to unregister all event handlers that might have been previously attached to the removed child by this instance (provided they were not hardwired with fuse() and used cx === this).

Defined in: main.js, lines 3387-3412 (26 lines) • Show code

off: function (key) {
  if (_.isArray(key)) {
    // List of identifiers of some kind.
    _.forEach(key, this.off, this)
  } else if (key instanceof Object) {
    // By context.
    var list = this._eventsByCx.get(key)
    if (list) {
      this._eventsByCx.delete(key)
      _.forEach(list, this._unregHandler.bind(this, {cx: true}))
    }
  } else if (typeof key == 'string') {
    // By event name.
    var list = this._events[key]
    if (list) {
      delete this._events[key]
      _.forEach(list, this._unregHandler.bind(this, {sup: true}))
    }
  } else {
    // By handler ID.
    var eobj = this._eventsByID[key]
    eobj && this._unregHandler({}, eobj)
  }

  return this
},
on ( event(s) [, func] [, cx] )

Part of: tag_Events

Registers a single event handler and returns its ID or permanently hardwires multiple events and/or handlers.

Arguments
Name Types Notes
eventstring to register one handler and return ID that can be used to unregister it with off() Object keys are event references (evtref), values are handlers.

If an object, keys can contain multiple event references separated with  – this is identical to multiple on() calls but shorter. Note: a space after the comma is mandatory (unlike with jQuery selectors).

object to fuse() one or multiple handlers and return this
cxonOnceobject Context in which the handler(s) are called. this is the object on which on() was called so on('e', 'h') will call h() on the object where the event occurs (and in that object’s context).
null/omitted use this

The handler (func or event object value) can be either a (masked) method reference (see expandFunc()) or function. Strings are resolved when the event is fired so their handlers are always up-to-date and don’t even have to exist by the time on() is called.

Errors if an event reference can’t be parsed.

ExampleObject form – on( {events} [, cx] ):
sqim.on({'-get_, nest': 'render'})
  //=> this (handlers cannot be unbound)
  // call render() on sqim when get() and nest() happen

// WRONG: no space, seen as a single event name "get_,nest":
sqim.on({'-get_,nest': 'render'})

String form – on( 'event', func [, cx] ):

var id = sqim.on('change', this.render, this)
  //=> 12345
  // handler called in a different context (this, not sqim)
sqim.off(id)

// WRONG: comma notation only accepted by on({events}):
sqim.on('change, nest', this.render, this)

ExampleObject-event form is used behind the scenes when extend()’ing or mixIn() a class and supplying the events key in protoProps, therefore events format is exactly the events argument of on().

Warning: when giving extend() or mixIn() a method (as a property, not as an events member) which name is already used for an event it will conceal existing handlers and generally misbehave – see evtconc.

Warning: this semantics does not apply to Base.elEvents!

var MyClass = Sqimitive.Base.extend({
  events: {
    // Calls render() after 'name' option change and before
    // 'birthday' change.
    'change_name, -change_birthday': 'render',

    // Calls fadeOut() when 'close' gets fired.
    close: function () {
      this.el.fadeOut(_.bind(this.remove, this))
    },
  },
})

obj.set('name', 'miku')   // render() called
obj.close()               // fadeOut() called

from fireOverride

Warning: to override fire, fuse and on, don’t use the usual on('fire') as it will lead to recursion (even the =fire form, evtpf). Use the old-school prototype overriding (__super__ is set up by extend()) – see Async’s source code for an example:

fire: function (event, args) {
  // Do your stuff...
  return MyClass.__super__.fire.apply(this, arguments)
},

Other notes:

  • To register one-time handlers use once() instead of on() + off().
  • on() with an object event is like fuse() but with added comma notation in event keys.
  • Treat event ID as an opaque scalar value. Its type and format may change but not its meaning in regards to off() and other methods.
  • Fusing is a bit more efficient since no tracking information about handlers is stored. If your handler is meant to stay with the object for its lifetime – use on({event: func}, cx) or fuse() (this also clearly conveys your intention of keeping it forever).
  • Specifically, fuse() affects one internal value (the list of hooks), single-handler on() – two (+ list of event IDs) or three (+ list of events by context if cx is not null), off() – also two or three.

Event reference formatevtref

An event reference is a string with four parts: [priority^][prefix]event[args].

Arguments
Name Types Notes
priority Optional; an integer ending on ^ determining the order of calling hooks of the same event (defaults to 0, may be negative), lower called first. If begins with ^ or is ^ alone, marks the handler out-of-batch: when due for calling in response to batch()’ed events, it is called immediately after func returns – without draining accumulated batches (see naor).
prefix Optional; changes the way event handler is bound and called as explained in evtpf.
event Event name – exactly what is given to fire() when triggering an event.
args Zero or more underscores (_); if present, the handler gets called only if event was given that exact number of arguments (it’s not possible to match zero arguments).

For example, eve__ registers a handler that is called for fire('eve', [1, 2]) but is not called for fire('eve', [1]) or fire('eve', [1, 2, 3]).

In case of =event (overriding handler), if argument count differs then all handlers superseded by this one get called while the superseding handler itself is not called (as if the superseding handler was just return sup(this, arguments)).

Generally, usage of args is frowned upon because of its unintuitive nature – see argDanger.

Event prefixesevtpf

Arguments
Name Types Notes
noneevArgs… Add new handler after existing handlers. It neither receives the current event result nor it can change it (the handler’s return value is ignored). This form is used most often and it’s perfect for “attaching” extra behaviour to the end of the original code, retaining the original result.
-evArgs… Add new handler before existing handlers of the same priority, otherwise identical to “no prefix”.
+res, evArgs… Add new handler after existing handlers. It receives the current event return value (res) and can change it if the handler returns anything but undefined.
=sup, evArgs… Wrap around existing handlers – they are removed, regardless of their priority (own priority of = sorts it in relation to non-= handlers added later). It receives a callable sup of form function (this, args) which it may call or not (alike to calling super in Java). sup.name equals plain event name, handy with comma notation. If it returns undefined the current event return value is unchanged.

First argument of sup is the context (normally the object that initiated the event, i.e. the handler’s own this). Second argument is an array of arguments the overridden handlers receive. The handler may change these arguments to trick the underlying (wrapped) handlers into believing the event received a different set of data or it can pass the arguments object that the handler itself has received – in this case args[0] (which is sup) is removed and the rest is given to the underlying handlers.

Example
// sup.name == 'someEvent'.
'=someEvent': function (sup, a1, a2) {
  // Passes original context and arguments unchanged.
  return sup(this, arguments)
  // Identical to above but longer - sup() removes itself from the
  // first argument.
  return sup(this, _.rest(arguments))
  // Not identical to above - if event was given >2 arguments they
  // will be omitted here but passed as is by above.
  return sup(this, [a1, a2])
  // Changes first argument and omits 3rd and other arguments (if
  // given).
  return sup(this, [1, a2])
  // Gives no arguments at all to the underlying handlers.
  return sup(this)
  // Passes sup verbatim because removal doesn't happen for array.
  return sup(this, [sup])
},

Note: old handlers are not technically “removed” – they are kept around and restored if the =wrapping handler is removed so treat this prefix like any other. It can be even used with once():

sqim.once('=update', function () { alert('Holding tight!') })
sqim.update()   // alerts

// Old 'update' handlers are now restored.

sqim.update()   // no more alerts

This works even if more handlers were added to the event after wrapping:

sqim.on('-update', ...)
  // 1 handler
var id = sqim.on('=update', ...)
  // 1 handler - '-update' superseded
sqim.on('update', ...)
  // 2 handler2: ['=update', 'update']
sqim.off(id)
  // 2 handler2: ['-update', 'update'] - '=update' removed

Warning: don’t pass a wrong this by accident, as when =overriding from an outside object using on()/fuse() with a cx:

sqim.on('=foo', function (sup) {
  // Likely an error: this in '=foo' is not sqim.
  sup(this, arguments)
}, this)   // <- note explicit context

Wrapped handlers are sorted by their priorities, as usual. However, their out-of-batch marks (leading ^) have no use because the main handler determines when/if they are called.

sqim.on('foo', ...)       // (1)
sqim.on('^=foo', ...)     // (2)
  // (1) is never called by itself; if (2) calls its sup() immediately
  // then (1) will be processed out-of-batch

sqim.on('^foo', ...)      // (3)
sqim.on('=foo', ...)      // (4)
  // the opposite: (4) forces (3) to be called in-batch, if called at all

It’s impossible for a handler to set the event’s result to undefined as it is considered a “keep current result” marker. This can be worked around using eobj.post whose result is taken literally:

sqim.fuse('event', new Function)
  .post = function (eobj, res, args) { return undefined }

The danger of ev__argDanger

In JavaScript, functions accept extra arguments with ease; often you would provide an iterator and only care for some of its arguments. However, if using the ev__ form (underscores are the args part of evtref) then the event must have been given that exact number of arguments even if none of its handlers uses the rest:

var MySqimitive = Sqimitive.Base.extend({
  accessor: function (prop, value) {
    if (arguments.length == 1) {
      return this._foo[prop]
    } else {
      this._foo[prop] = value
      return value
    }
  },
})

var sqim = new MySqimitive
// This handler should always be called when a property is being
// set... right?
sqim.on('accessor__', function (prop, value) {
  alert('Now ' + prop + ' is ' + value)
})

// Alerts "Now name is val". So far so good.
sqim.accessor('name', 'val')

var propsToSet = {prop: 'foo', bar: 123}
// map() calls sqim.accessor() for every key in propsToSet and
// properties are indeed properly set - but no alerts appear! This is
// because map() passes 3 arguments to the iterator: value, key and
// the list itself (object in our case). Therefore even if accessor()
// uses just 2 of these arguments (like it does above) the actual
// event fired is "accessor___" (3 underscores), not "accessor__" (the
// one we've hooked).
_.map(_.invert(propsToSet), sqim.accessor, sqim)

Defined in: main.js, lines 2701-2715 (15 lines) • Show code

on: function (event, func, cx) {
  if (event instanceof Object) {
    for (var name in event) {
      var names = name.split(', ')
      for (var i = 0, l = names.length; i < l; i++) {
        this.fuse(names[i], event[name], func)
      }
    }
    return this
  } else if (arguments.length >= 2) {
    return this._regHandler( this.fuse(event, func, cx) )
  } else {
    throw new TypeError('on: Bad arguments')
  }
},
once ( event, [retainer, ] func[, cx] )

Part of: tag_Events

Regsiters a single one-shot event handler that removes itself as soon as func returns non-retainer (or after the first call).

In all other aspects once() is identical to on() with the string argument, i.e. on(event, func, cx). Returns new event ID suitable for off() so you can unregister it before it’s called (or during or after - nothing will happen). Doesn’t allow registering multiple events/handlers in one call.

Example
sqim.once('+normalize_foo', () => 123, this)
sqim.off(this)   // removes the above hook, along with others of this
sqim.once('=render', () => { /* skips one call, keeps event's result */ })

sqim.once('change_visible', null, now => now ? this.update() : null)
  // Returns null until sqim._opt.visible becomes truthy, retaining
  // the handler. Then returns result of update() and unhooks.
  // In any case, the hook's result is ignored because of no event prefix.
Arguments
Name Types Notes
eventstring
retainermixed except string, function, NaN (not comparable) Compared using ===, acts as event result if returned; typical choices are undefined, null, true and false
omitted
funcstring
function

from onOnce

Arguments
Name Types Notes
cxobject Context in which the handler(s) are called. this is the object on which on() was called so on('e', 'h') will call h() on the object where the event occurs (and in that object’s context).
null/omitted use this

The handler (func or event object value) can be either a (masked) method reference (see expandFunc()) or function. Strings are resolved when the event is fired so their handlers are always up-to-date and don’t even have to exist by the time on() is called.

Errors if an event reference can’t be parsed.

If the handler somehow gets called again after it should not have been, or if event occurs again while running (reenters), or if func throws - the handler returns undefined without calling func and unbinds. This is in line with no:once().

Defined in: main.js, lines 2754-2783 (30 lines) • Show code

once: function (event, retainer, func, cx) {
  var args = arguments
  var self = this
  var running = 0

  switch (typeof retainer) {
    case 'string':      // expandFunc()
    case 'function':
      ap.splice.call(args, 1, 0, retainer = {})
  }

  function once_() {
    var incomplete = {}
    var res = incomplete

    try {
      if (!running++) {
        res = Core.expandFunc(args[2]).apply(this, arguments)
      }
    } finally {
      res === retainer ? running-- : self.off(id)
    }

    return res === incomplete ? undefined : res
  }

  Core.trace && (once_.trace = arguments)
  var id = this.on(event, once_, args[3])
  return id
},