Class in Sqimitive Core

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

Defined in: sqimitive.js, line 105

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 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 (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: sqimitive.js, line 363Show 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 you don’t want this overhead for some property (even though it’s usually tiny) then list it in _shareProps or assign its value in Base.init() or postInit().

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 (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: sqimitive.js, line 435Show code

_shareProps: [],
_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: sqimitive.js, line 1168Show 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: sqimitive.js, line 1172

Methods

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.

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 built-in classes like Date.

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']

Defined in: sqimitive.js, lines 977-993 (17 lines) • Show code

deepClone: function (obj) {
  // This method has a big impact on performance because it's called in
  // each Sqimitive constructor so we avoid using _'s methods and access
  // standard methods directly (saving 10% of time on each access).
  if (typeof obj == 'object' && obj != null) {
    if (Array.isArray(obj)) {
      obj = obj.map(Core.deepClone)
    } else {
      obj = objectAssign({}, obj)
      for (var prop in obj) {
        obj[prop] = Core.deepClone(obj[prop])
      }
    }
  }

  return obj
},
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: sqimitive.js, lines 770-784 (15 lines) • Show code

expandFunc: function (func, obj) {
  if (typeof func == 'string') {
    var parts = func.split(/([.-].*)/)
    if (parts.length > 1) {
      return Core.masker(parts[0], parts[1], obj)
    } else {
      return function () {
        var callCx = obj || this
        return callCx[func].apply(callCx, arguments)
      }
    }
  } 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 debuggers (as the function/constructor – “class” names). Defaults to “Sqimitive”.
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 ones from newClass (or merges according to _mergeProps), adds staticProps, hardwires events into class definition (fuse()) and calls finishMixIn().
  • The staticProps argument is applied by extend() before mixIn() applies the staticProps key of protoProps so the latter win on key conflict. If giving both (which you’d rarely do) results may be unexpected:
    var Class = Sqimitive.Base.extend({
      staticProps: {prop: 'foo'},
    }, {
      prop: 'bar',
    })
      // Class.prop is 'foo'
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: sqimitive.js, lines 548-588 (41 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' && Array.prototype.shift.call(arguments)

  var child = extend(name || 'Sqimitive', this, arguments[0])
  //! +ig
  // Since base class has its own __super__, make sure child's (set up by
  // extend() above) isn't overwritten.
  _.extend(child, this, arguments[1], {__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})

  arguments[0] && child.mixIn(arguments[0])

  // 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] )

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 turn 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).

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 false (see the example in fuse()).

Other notes:

  • This is an internal method and there is no reason to call it directly.
  • funcs can be “falsy”, in this case undefined is returned.
  • funcs is cloned so it may be changed while ::fire() is running (e.g. a handler may be removed thanks to once()) without affecting its outcome.
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')

Defined in: sqimitive.js, lines 1055-1100 (46 lines) • Show code

fire: function (funcs, args) {
  var res

  // No funcs (not even an array) can be e.g. when calling change_XXX.
  if (funcs) {
    if (!args) { args = [] }

    // Cloning function list because it might change from the outside (e.g.
    // when a handler is removed from the same event as it's being fired;
    // may happen when once() is used).
    _.find(funcs.concat(), function (eobj) {
      if (eobj.args != null && eobj.args != args.length) {
        var thisRes = eobj.sup ? eobj.sup(eobj.cx || this, args) : undefined
      } else {
        if (!eobj.sup && !eobj.res) {
          var thisArgs = args
        } else {
          var thisArgs = Array.prototype.slice.call(args)
          eobj.sup && thisArgs.unshift(eobj.sup)
          eobj.res && thisArgs.unshift(res)
        }

        // A benchmark avoiding Function's apply() was done on Chrome,
        // instead 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) {
        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
},
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 un: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: sqimitive.js, lines 925-948 (24 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(/-+$/, '')
  }

  return function () {
    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)
  }
},
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).
options An arbitrary value (usually an object) given to newClass.finishMixIn(), allowing creation of parametrized mix-ins (basically, generics).
newClassobject The mix-in, the object “mixed into” this.

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)

Other notes:

  • mixIn() is doing most of extend()’s job, which is just creating a special form of a mix-in.
  • mixInDoesmixIn() applies “sub mix-ins” (calls mixIn() if newClass contains the mixIns key; this is recursive), overwrites fields of this with ones from 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
// 2 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: sqimitive.js, lines 600-602 (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: prefix (like +, see evtpf), args (trailing _) and name (everything else).

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

Example
Sqimitive.Core.parseEvent('foo.bar')
  //=> {prefix: '', name: 'foo.bar', args: ''}

Sqimitive.Core.parseEvent('-foo.bar___')
  //=> {prefix: '-', name: 'foo.bar', args: '___'}

Defined in: sqimitive.js, lines 1009-1020 (12 lines) • Show code

parseEvent: function (str) {
  // Colons were reserved for use instead of '_' in 'change_OPT' but turned
  // out to be troublesome to declare since you'd have to quote them.
  // They're no longer used but perhaps will find another use in the
  // future.
  //
  // Dots are used in _forward'ed event names, and we need prefix symbols
  // (+-=) in the middle of event name for the same reason: '.-change'.
  var match = str.match(/^([+\-=]?)([\w\d.:+\-=]+?)(_*)$/)
  if (!match) { throw new SyntaxError('Bad event name: ' + str) }
  return {prefix: match[1], name: match[2], args: match[3]}
},
picker ( prop [, args] )

Modifiers: static

Part of: tag_Utilities

Returns a function that fetches prop property of the object it’s given, possibly calling it with args.

The function expects one argument (an object) that, when called, returns the value of the given object’s prop property (if it’s not a function) or the result of calling that function (if it is one, giving it args). In other cases (not an object or no prop) the function returns undefined.

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

Arguments
Name Types Notes
propstring Property name.
argsarray Optional arguments if prop resolves to a function.
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 values = ['foo', null, ['bar'], obj]
_.map(values, picker('one'))
  //=> [undefined, undefined, undefined, 1]
ExampleUsually picker()’s result is given to some filtering function (util). Example from chld:
getIncomplete: function () {
  return this.reject(MyToDoList.picker('get', 'isCompleted'))
},

Defined in: sqimitive.js, lines 722-731 (10 lines) • Show code

picker: function (prop, args) {
  args = Core.toArray(args)
  return function (obj) {
    if (obj instanceof Object && prop in obj) {
      return typeof obj[prop] == 'function'
        ? obj[prop].apply(obj, args)
        : obj[prop]
    }
  }
},
stub ( )

Modifiers: static

Simply an empty function that returns undefined.

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

ExampleUse stub() 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,
})

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

Exact optimizations are implementation details and may change in the future, but at this time if you were to put success into events it would be kept because on() doesn’t check existing handlers for stub:

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

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)

Defined in: sqimitive.js, line 652Show 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 (Array.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: sqimitive.js, lines 1129-1137 (9 lines) • Show code

  toArray: function (value) {
    if (_.isArguments(value)) {
      return Array.prototype.slice.call(value)
    } else if (!Array.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

Defined in: sqimitive.js, lines 675-677 (3 lines) • Show code

unique: function (prefix) {
  return this._unique[prefix] = 1 + (this._unique[prefix] || 0)
},
autoOff ( [sqim [, events [, cx]]] )

Part of: tag_Events

A utility method for tracking object connections.

If any arguments are given, adds sqim (object) to this object’s list of tracked objects (_autoOff). This 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. Returns sqim.

events, if given, is an object in on() format (keys are comma-separated event references and values are their handlers - expandFunc()). 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).

Giving sqim without events only tracks the object without hooking any events.

If no arguments are given, calls off(this) on all tracked objects to remove listeners of this and clears the list. Similar to doing _.invoke(this._autoOff, 'off', this). Returns this.

Other notes:

  • One object can be tracked several times without causing problems.

from chevaoff

Example
this.autoOff(sqim)

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

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

// Unbind this instance's events from all sqimitives previously
// enlisted by this.autoOff(sqim) and clear the track list:
this.autoOff()
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-',
  },
})

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: ...})
    },
  },
})

Defined in: sqimitive.js, lines 2208-2229 (22 lines) • Show code

autoOff: function (sqim, events, cx) {
  if (arguments.length < 1) {
    var list = this._autoOff
    this._autoOff = []
    _.invoke(list, 'off', this)
    return this
  } else {
    this._autoOff.push(sqim)
    arguments.length > 2 || (cx = this)

    // on({event: map}) would fuse the handlers. autoOff() only mimicks
    // {event} format since it's meant for binding "unbindable" hooks.
    for (var name in events) {
      var names = name.split(', ')
      for (var i = 0, l = names.length; i < l; ++i) {
        sqim.on(names[i], events[name], cx)
      }
    }

    return sqim
  }
},
constructor ( )

Assigns new unique _cid (a string: p + unique 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: sqimitive.js, line 154

fire ( event [, args] )

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: sqimitive.js, lines 1672-1681 (10 lines) • Show code

fire: function (event, args) {
  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)
},
firer ( event [, args [, self]] )

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

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

Arguments
Name Types Notes
eventstring Like change_caption
argsarray Push some parameters in front of the function’s arguments (firerArgs).
selfobject 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(self)

Defined in: sqimitive.js, lines 1721-1732 (12 lines) • Show code

firer: function (event, args, self) {
  if (arguments.length > 1) {
    args = Core.toArray(args)
    return function () {
      return (self || this).fire(event, args.concat( Array.prototype.slice.call(arguments) ))
    }
  } else {
    return function () {
      return (self || this).fire(event, arguments)
    }
  }
},
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 Context in which func is called.
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)

One case when you may use eobj is to assign the post key (this is considered advanced usage). If set to a function, ::fire() calls post(eobj, res, args) after executing the handler within the handler’s cx; post must return the new result (replaces res in any case, 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.

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

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: sqimitive.js, lines 2305-2350 (46 lines) • Show code

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

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

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

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

  event.prefix == '-' ? list.unshift(eobj) : list.push(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: sqimitive.js, lines 1806-1832 (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).
options An arbitrary value (usually an object) given to newClass.finishMixIn(), allowing creation of parametrized mix-ins (basically, generics).
newClassobject The mix-in, the object “mixed into” this.

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)

Other notes:

  • mixIn() is doing most of extend()’s job, which is just creating a special form of a mix-in.
  • mixInDoesmixIn() applies “sub mix-ins” (calls mixIn() if newClass contains the mixIns key; this is recursive), overwrites fields of this with ones from 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
// 2 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: sqimitive.js, lines 1539-1600 (62 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}

  _.each(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)

  _.each(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] = _.extend({}, this[prop], newClass[prop])
      }
    }
  }, this)

  _.extend(this, newClass, merged)
  _.extend(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.
    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 = _.extend({}, this._events)

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

    this.on(newClass.events)
  }

  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.
stringlistener 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 put in place of the wrapper in the list of handlers (“undoing” method override).

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

Example
// key = 'evtname' | 'id/123' | {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])

Defined in: sqimitive.js, lines 2441-2461 (21 lines) • Show code

off: function (key) {
  if (arguments.length < 1) { throw new TypeError('off: Bad arguments') }

  if (_.isArray(key)) {
    // List of identifiers of some kind.
    _.each(key, this.off, this)
  } else if (key instanceof Object) {
    // By context.
    this._cxHandlers(key, function (list, i) {
      _.each(this._eventsByCx.splice(i, 1)[0].slice(1), this._unregHandler, this)
    })
  } else if (key.indexOf('/') == -1) {
    // By event name.
    _.each(this._events[key] || [], this._unregHandler, this)
  } else {
    // By handler ID.
    this._unregHandler( this._eventsByID[key] )
  }

  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.

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 selector)s

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)
  //=> 'change/2'
  // 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.
  • 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).

Event reference formatevtref

An event reference is a string with 3 parts: [prefix]event[args].

Arguments
Name Types Notes
prefix Optional; changes the way event handler is bound and called as explained in evtpf
event Event name (alphanumeric symbols, dots and colons) – 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, 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. It receives a callable sup of form function (this, args) which it may call or not (alike to calling super in Java).

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
'=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)
},

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

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: sqimitive.js, lines 2057-2071 (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, func[, cx] )

Part of: tag_Events

Regsiters a single one-shot event handler that removes itself after being called exactly once.

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 after, nothing will happen). Doesn’t allow registering multiple events/handlers.

Example
sqim.once('change', function () { ... }, this)
sqim.once('=render', function () { /* skip one next render() call */ })

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.

Defined in: sqimitive.js, lines 2094-2108 (15 lines) • Show code

once: function (event, func, cx) {
  if (arguments.length >= 2) {
    var id = this.on(event, function () {
      if (id) {
        this.off(id)
        id = null
        return Core.expandFunc(func, this).apply(cx || this, arguments)
      }
    })

    return id
  } else {
    throw new TypeError('once: Bad arguments')
  }
},