Anatomy of a Sqimitive

Objects in Sqimitive are homogeneous and mostly deal with other “sqimitives” only – the basic building blocks provided by the library, the “primitive”.

Each sqimitive has three fundamental features that in different combinations cover an extremely wide range of tasks: opt options, chld children and evt events. Below is a quick high-level overview to demonstrate the main idea; consult the API documentation on Core, Base and others with hundreds of examples if you want to get all out of Sqimitive.

Optionsopt

Options are bb:Model-attributes in Backbone’s terms – set of key/value pairs defined under Base._opt which trigger events on change or access, can be normalized and can be virtual (i.e. you can write your accessor that won’t correspond to a “physical”, listed option). They are solely accessed via set() and get() methods to create a sort-of public object interface.

Sample code below defines a class with two options – isCompleted (boolean) and caption (string). When one of them is changed, the associated DOM node is updated (see the sample To-Do application for more: https://squizzle.me/js/sqimitive/demo/to-do.html).

var MyToDoItem = Sqimitive.jQuery.extend({
  _opt: {
    isCompleted: true,
    caption: 'Do me at home!',
  },

  events: {
    // When task becomes complete or incomplete its DOM element gets that
    // class added or removed on the fly.
    change_isCompleted: function (newValue) {
      this.el.toggleClass('done', newValue)
    },

    change_caption: 'render',
  },

  // HTML template for this node's contents as used below.
  _tpl: '<h3><%- caption %></h3>',

  normalize_isCompleted: function (value) {
    // Turn whatever is given as a new value for isCompleted into a boolean.
    // If the result is identical to the current value - 'change' is not
    // fired.
    return !!value
  },

  // Trim whitespace around the caption.
  normalize_caption: function (value) {
    return _.trim(value)
  },

  render: function () {
    // Retrieve the map of all options/values and pass them as variables to
    // the template. Note that template() implementation differs in NoDash,
    // Underscore and LoDash.
    var vars = this.get()   // = {isCompleted: false, caption: 'foo'}
    this.el.html(_.template(this._tpl, vars))
  },
})

Childrenchld

Children are zero or more “sqimitives” nested into zero or more parent “sqimitives”. Their events may be forwarded to parent – but only while they are still part of that parent (_childEvents); upon removal they are automatically unbound (autoOff). When a child is added or removed its parent, if any, gets notified (unnested). Also, most of Underscore.js is available as methods to easily filter or transform children into native arrays or objects (util).

Parent sqimitives can be of two types: _owning (by default) and non-owning. First represent a typical tree where each child has exactly one _parent and which you can traverse the tree in either direction starting from any node. If you nest a child into another parent it’s automatically removed from the former owner. Second type is more of a list where you can only traverse from the outside because a child doesn’t know in what other sqimitives it might be listed in, if at all, and no automatic removal is done when a child becomes part of another parent.

Sample code below defines a to-do list that is meant for storing MyToDoItems from the above example. Note that in Backbone you would have at least two classes: one bb:Collection for storing the list of to-do items and one bb:View for displaying that collection… Or, to be brutally honest, you would create four classes: a bb:Model holding data of a single to-do item, a bb:View displaying it, a bb:Collection holding to-do items as a whole and another bb:View holding the Collection holding the Models – and you still have to link each Model to its View and keep track of their events and DOM elements.

In Sqimitive you can still do all that but it’s the author’s opinion that such pure concepts are only good for academics and very large projects – most of the time you would rather have something more utilitarian and dirty, if you will. Sqimitive allows you a choice since everything is ultimately a primitive and can be “purified” to the point you need.

Note that the hierarchy of sqimitives (defined by nest) doesn’t necessary reflect the hierarchy in DOM (of jQuery.el) – _children can have their elements under their _parent’s el or elsewhere, or not have DOM elements at all (as “models” do).

var MyToDoList = Sqimitive.jQuery.extend({
  // Add extra protection against accidental foreign class being added as a
  // child.
  _childClass: MyToDoItem,
  // Leading dash means "listen before" - see next section about events.
  _childEvents: ['-change', 'change'],

  events: {
    // To avoid collisions between children-generated and self events those
    // forwarded from children get prepended with a period. If you have
    // another parent that is forwarding its child's children events then
    // another period appears - e.g. '..change'. Think of this as of regular
    // '../../path' notation where each period means "one level above".
    '.-change': function (sqim, optName, newValue, currentValue) {
      // Outputs something like "To-do item's caption ... from foo to bar".
      console.log('To-do item\'s ' + optName + ' is about to be changed' +
                  ' from ' + currentValue + ' to ' + newValue)
    },

    '.change': function (sqim, optName, newValue, currentValue) {
      console.log(optName + ' has changed to ' + newValue)
    },
  },

  postInit: function () {
    var itemOptions = {isCompleted: false, caption: 'Dummy item'}
    var sqim = this.nest(new MyToDoItem(itemOptions))
    sqim.set('caption', 'fire them!')
    // Because of forwarded events two new messages have appeared in the
    // console.

    // Can also assign an explicit name (if omitted _cid is used).
    this.nest('childName', new this._childClass)
    // Can retrieve the object like this:
    var sqim = this.nested('childName')
    sqim.unnest()
  },

  // Use Underscore to retireve only children with isCompleted being false.
  getIncomplete: function () {
    // picker() gets inherited from Sqimitive.Core and is simply a function
    // calling a method on the given object with given parameters. In other
    // words, equivalent to ES6's: (o) => o.get('isCompleted') LoDash has a
    // similar method called rearg().
    return this.reject(MyToDoList.picker('get', 'isCompleted'))
  },
})

Eventsevt

Events are Squimitive’s Swiss Army Knife to deal with everything from inheritance (OOP style – extend and mixIn) and prototyping (native JavaScript style) to dynamic property morphing and dispatching notifications in an Observer-like fashion. When defined upon class declaration handlers are “fuse’d” into the class (reducing their performance overhead), otherwise they work as regular event listeners that can be manipulated on run-time using on, off and others (for instance, they are automatically removed once a nested sqimitive is unnested).

When you try to listen to an event and there is a method of the same name, Sqimitive turns that method into an event slot and the method itself becomes its first listener. This way every method is potentially an event that you can manipulate on run-time as well as upon declaration (events), working with nearly conventional OOP as found in languages like C and PHP while still utilizing the power of dynamic object manipulation as it’s meant with JavaScript.

Likewise, if there is no method when you define an event – Sqimitive creates it so that calling it actually triggers the event (firer). This way you can always invoke a method without knowing if it’s a real function or an event trampoline, bridging the gap between the two.

An example to demonstrate how methods become events “on demand” (logEvents):

var MyBase = Sqimitive.Base.extend({
  // render() is essentially a function, regular "method".
  render: function () {
    this.el.text('Hello!')
  },
})

// What we're doing is calling a function. It's not an event and won't be
// caught by logEvents().
;(new MyBase).render()

var MyChild = MyBase.extend({
  events: {
    render: function () {
      this.el.append('...I extend...')
    },
  },
})

// Now we are in fact firing 'render' which is an event with two listeners:
// one from MyBase (called first) and another from MyChild. 'render' is fired
// whenever we call render() in MyChild and descendants, and for them
// logEvents() logs the call.
;(new MyChild).render()

// POTENTIALLY WRONG:
var MyChile = MyChild.extend({
  render: function () {
    alert('Boom!')
  },
})

// Now we're back to event-less render() - a mere function. Note that two
// former 'render' handlers are still present so if we attach a new listener
// to 'render' current render() ("Boom") will be prepended to the list of
// handlers as a 3rd handler and MyChile.render() itself will be replaced by
// firer('render'). It's a bad practice to supersede an "evented" function
// like this and usually indicates a mistake (forgetting that a method of the
// same name exists in some parent). Consequently, logEvents() here won't
// track anything. More on this below, in #evtconc.
;(new MyChile).render()

Compare Sqimitive-style inheritance (evtpf) and the traditional inheritance using __super__ (which still works in Sqimitive):

var MyBase = Sqimitive.Base.extend({
  effect: function (arg) {
    console.log('MyBase.effect(' + arg + ')')
    return this
  },
})

// Traditional JavaScript-OOP inheritance as supported by Backbone.
var JsOopSubclassing = Sqimitive.Base.extend({
  // This way you override the inherited method, entirely.
  effect: function (arg) {
    return 'foo'
  },

  // Calling the inherited implementation...
  effect: function (arg) {
    console.log('pre-actions')
    // We have to hardcode current class and method names, plus the call is
    // quite long.
    var result = JsOopSubclassing.__super__.effect.apply(this, arguments)
    console.log('post-actions')
    return result
  },
})

// Event-oriented Sqimitive inheritance.
var SqimitiveSubclassing = Sqimitive.Base.extend({
  events: {
    // This is how you override the entire method in Sqimitive.
    '=effect': function (sup, arg) {
      return 'foo'
    },

    // ...and this is how you call the inherited implementation.
    '=effect': function (sup, arg) {
      console.log('pre-actions')
      // No hardcoded class reference, concise calling format.
      var result = sup(this, arguments)
      console.log('post-actions')
      return result
    },

    // However, such a full override is rarely needed - most often you only
    // need to do something after the original method executes, keeping its
    // return value. This one is identical to the above in effect but without
    // logging 'pre-actions'.
    effect: function (arg) {
      console.log('post-actions')
    },

    // Sometimes we need to do just 'pre-actions' - this is how.
    '-effect': function (arg) {
      console.log('pre-actions')
    },

    // Yet at other times we need to call the original code and obtain and/or
    // change its return value.
    '+effect': function (result, arg) {
      console.log('post-actions')

      if (result === this) {
        // Return something other than the original code returned.
        return new That(arg)
      }

      // Returning undefined or not returning at all retains the current
      // result. These are identical:
      //return undefined
      //return
    },
  },
})

Finally, to demonstrate the usage of dynamic event binding and method overriding.

var DynamicEvents = Sqimitive.Base.extend({
  events: {
    slotA: function () {
      console.log('slotA')
      return 'slotA'
    },
  },

  slotB: function () {
    return 'slotB'
  },

  // Just a property that isn't a function.
  notASlot: 123,

  listeners: function () {
    // When slotA is fired, it outputs "slotA" and "post-effect" to the
    // console and returns 'slotA'. Exactly the same would be with slotB even
    // though it was't explicitly declared as an event - it becomes one as
    // soon as the first handler is attached.
    this.on('slotA', function () {
      console.log('post-effect')
    })

    // Nobody said we can't create events out of thin air without defining
    // them anywhere first. Note that since it's an event handler and not a
    // class method it cannot return any value (it would be ignored). This
    // way no disruption is caused if the class suddenly declares a method of
    // the same name (this handler will be called after it).
    this.on('slotC', function () {
      console.log('post-effect')
      return 'ignored'
    })

    this.slotC()        // both are
    this.fire('slotC')  // equivalent.

    // Of course, events can have prefixes seen in the previous sample.
    this.on('+slotC', function (result) {
      console.log('post-effect')
      return 'new result'
    },

    this.on('-slotC', function () {
      console.log('pre-effect')
      return 'ignored'
    })

    // You can do a full override as well - and the beauty is that you can
    // off() it any time later and original method(s) (now "inside" sup) will
    // be put back in place.
    this.on('=slotC', function (sup) {
      console.log('pre-effect')
      var result = sup(this, arguments)
      console.log('post-effect')
      return result
    })

    // If you try to turn a non-method into an event nothing will break - you
    // will add an event listener all right and fire('notASlot') will work
    // but doing notASlot() won't fire the event - only access that property.
    // Granted, it's confusing to have an event which works differently from
    // this.notASlot() so better avoid it.
    this.on('notASlot', function () {
      alert('Boo!')
    })

    alert(this.notASlot)    // alerts 123.
    this.fire('notASlot')   // alerts Boo!
  },

  dynamic: function () {
    var handler = function () { };
    var context = new SomeObject;

    // Unless "fused" (on class declaration time), each event handler gets a
    // unique ID that can be used to unbind it later (very fast). Contrary to
    // the common approach, Sqimitive offers no event namespaces (such as
    // my.handler) used to unbind group of events - by-context lookup is
    // available and it covers most of such use-cases (see below).
    var id = this.on('event', handler)
    this.off(id)

    // You are free to use dots and colons in event names for your needs.
    this.on('com.myapi.proc:group', handler)

    // Slower but removes all bindings to the given context object among all
    // events of this object in one go.
    this.on('withContext', handler, context)
    this.off(context)

    // You can also clear all listeners to a particular event.
    this.on('wipeEvent', handler)
    this.off('wipeEvent')
  },
})

Events vs methods on extensionevtconc

As demonstrated, method calls are often event triggers in disguise and so both obj.method() and obj.fire('method') work exactly the same way. However, there is a catch: when extend’ing a class or an object (mixIn) you can either add methods within the events property or list them as regular properties – and the latter is wrong. Compare:

// WRONG:
Sqimitive.Base.extend({
  method: function () { ... }
})

// CORRECT:
Sqimitive.Base.extend({
  events: {
    '=method': function () { ... }
  }
})

The first declaration is bad: it overrides the firer of method so if called as obj.method() it works as expected but as soon as a new handler is added with on/once/fuse the defined method() is moved to the beginning of handlers of the method event while retaining all previously defined handlers on that event (unlike with =method which removes old handlers entirely, see evtpf). Also, obj.fire('method') calls old (now concealed) handlers while obj.method() does not, with different side effects to the caller!

Follow this rule of a thumb: when introducing a method (or an event, which are very close things in Sqimitive) – declare it as a property; when overriding an existing method – always put it into events, without exceptions.

But then, if you want to be on the safe side you can declare all functions in events – they will work just like regular methods but you’ll need a bit more typing (as the empty event prefix ignores return value – evtpf).

Opening the viewsvw

Options, children and events compose 90% of what a sqimitive is. However, they all are mostly about logic and data; to make the user happy we should interact with him and present some visual information. This is when Views in MVC and Backbone terminology come into play.

By default, each sqimitive possesses a DOM element stored in this.el – a jQuery/Zepto object. It can be disabled for pure data classes (like Models or Collections) but if it isn’t then such a node is automatically created by the constructor and assigned to this property (jQuery.el). Then you can make use of automatic binding of DOM events via jQuery.elEvents and of convenient methods like jQuery.attach(), jQuery.remove(), this.$('sel.ector'), bubble('eventForAllParents') and others.

Sample code below creates a simple login form. It stores data in its own opt’ions but, as you must know by now, Sqimitive allows you to extract it into another Model-like object if you need more abstraction in your application.

var MyFormView = Sqimitive.jQuery.extend({
  // If omitted will create just a plain <div>.
  el: {tag: 'form', action: 'javascript:void 0', className: 'login-form'},

  _opt: {
    login: '',
    password: '',
    remember: false,
  },

  elEvents: {
    submit: function () {
      var data = this.el.serializeArray()
      $.each(data, _.bind(this.set, this))

      $.ajax({
        url: 'login',
        type: 'POST',
        data: data,
        context: this,
        success: this.loggedIn,
        error: function () {
          this.addClass('error')
        },
      })

      return false
    },

    'change [name=remember]': function (e) {
      this.set('remember', e.target.checked)
    },

    render: function () {
      this.el.empty()
        .append('<input name=login autofocus>')
        .append('<input name=password type=password>')
        .append('<input name=remember type=checkbox>')
        .append('<button type=submit>Log In</button>')

      this.update()
    },
  },

  update: function () {
    this.$('[name=login]').val(this.get('login'))
    this.$('[name=password]').val(this.get('password'))
    this.$('[name=remember]')[0].checked = this.get('remember')
  },

  // stub() is just a function that returns undefined (nothing). When it's
  // used in place of a method and if that method becomes an event (getting a
  // listener) then there's a small optimization - Sqimitive removes the old
  // method entirely without adding it as a listener for that event.
  //
  // Alternatively, you could just leave this undefined and always use
  // fire('loggedIn') but it's against Sqimitive conventions, more tricky and
  // less obvious if you ever get a loggedIn() method that for any reason
  // does something else.
  loggedIn: Sqimitive.jQuery.stub,
})

We can use the above class as follows:

// A typical use case - just create a new form object along with its DOM
// element:
var sqim = new MyFormView({login: 'default@login'})

// Or if we have an existing container element - use it:
var sqim = new MyFormView({login: 'default@login', el: '#loginForm'})

// Then we can listen to new sqimitive's events like so:
sqim.on('loggedIn', function () { alert('Hello, ' + this.get('login')) })

// ...or morph it dynamically - just like good old JavaScript but better:
sqim.on('=render', function (sup) {
  if (location.protocol != 'https:') {
    this.el.text('Your connection is not secure!')
  } else {
    // Only show the form over HTTPS.
    sup(this, arguments)
  }
})

// This is not Sqimitive-way: it's long-winded, overrides whatever is already
// defined as render() including all event listeners (if 'render' is an event
// slot, see #evtconc) and hardcodes parent class name and return value...
// but if you don't mind that - use it, Sqimitive doesn't care.
sqim.render = function () {
  if (location.protocol != 'https:') {
    this.el.text('Your connection is not secure!')
    return this
  } else {
    return MyFormView.render.apply(this, arguments)
  }
}

Defined in: HELP.chem, lines 651-1130 (480 lines)