text:
Sqimitive.js – Lightweight library to build concrete applications
Sqimitive.js is a fat-free implementation of well-known Backbone.js JavaScript frontend framework. Sqimitive avoids certain less used (usually project-specific) features while conceptually bringing the rest under one roof.
In particular, Sqimitive:
- Removes somewhat artificial separation for Model, Collection and View classes providing single unified "primitive" (Sqimitive) class. This way Views can use options (attributes with on-change notifications), Underscore.js filtering and nesting (just like Collections) and all other features, shared by all, not imposed on any.
- Greatly expands the idea of events making inheritance as sensible as it is possible given JavaScript's idea of OOP.
- Removes server-side communication layer (sync) while keeping routines to smart-update object states. Instead of overriding standard parts as it's often done with Backbone you implement them yourself just once given your unique project specification.
- Is half the size of Backbone while being loaded with even more comments. Underscore.js is still required to operate while jQuery/Zepto are optional..
- Still lets you work the way you used with Backbone – if you wish. You can create base Model, Collection and View, use traditional __super__-style inheritance, write your specific API sync routines, use Underscore.js filtering and jQuery/Zepto this.el and so on.
While different, Sqimitive will appear very familiar to current Backbone users so just give it a try – this classic To-Do application is a good place to start.
bower install sqimitive npm install sqimitive
Download for development / production (minified, 12K)
Check out sample To-Do App and its
ToDo-MVC version (under construction)
See a typo? Select the fragment and press Ctrl+Enter. Help us keep this memo free of vexatious mistakes! (Powered by Orphus.ru.)
This page is printer-friendly! Make yourself a PDF and take along.
Showdown: Why not Backbone?
There are already dozens of client-side frameworks. Why create yet another one? Simply enough – Sqimitive strives to fill in some gaps that Backbone implementation and others have left wide open.
OOP with a human face
In Backbone and other JS frameworks inheritance (overriding of parent properties and methods) is painful.
With properties, if it's a complex object that you would rather extend (append to) rather than replace entirely – as it is the case with Model attributes, defaults, View events (DOM bindings) and others – you have to add them in the constructor because if you define such a property in the child class you will entirely overwrite the inherited value. Moreover, if at some point base class that did not have that property decided to define it – all its subclasses overwrite it without notice... Unless you remember to update them, which we humans often do not.
With methods, you have to hardcode base class reference – that's not to mention the entire construct which is already ugly: My.Class.__super__.baseMethod.apply(this, arguments). So if you later rename base class or move this subclass under another parent, or rename the method, or just make a typo – the entire inheritance chain for this method is broken. You may or may not get an error, too.
This is the price for trying to push classic OOP into JavaScript prototype model.
Sqimitive solves both problems by using _mergeProps for listing which properties should be merged together on extension. For methods, there is a fantastic event-driven inheritance that makes JavaScript OO coding a breeze. Any method can be turned into an event without altering the base class.
At the same time, the __super__ way of doing this is still available if you are feeling a bit masochistic.
Below is just one simple example. Look for more details at the mentioned sections.
var MyBaseClass = Sqimitive.Sqimitive.extend({ complexProp: { base: 123, }, init: function () { alert('I init()!') }, }) // _mergeProps is a static property. MyBaseClass._mergeProps.push('complexProp') var MySubclass = MyBaseClass.extend({ complexProp: { sub: 'foo', }, events: { init: function () { alert('Now I init() too!') }, }, }) var obj = new MySubclass // 1. alert: I init()! // 2. alert: Now I init() too! alert(obj.complexrop.base) // alert: 123 alert(obj.complexrop.sub) // alert: foo
Built-in nesting
Backbone offers no nested views out of the box. There are plugis such as Babysitter (from Marionette.js) but they don't provide a complete solution.
In Sqimitive, nested views are native. When used, they are automatically managed: elements and event listeners removed when coresponding view is removed, DOM events rebound and root element reinserted when root element is moved (e.g. on render()), it is possible to filter nested views using Underscore methods (just like Collection contents in Backbone), notifications about newly nested/unnested/changed views and so on.
No more need for delegateEvents(), off(null, null, this) or return this (when overriding render()).
See more details and examples in the Views overview section.
Change-driven behaviour
In Backbone and other frameworks, you have to track changes to your data and view states. Backbone eases this task by providing standard events like change:attr event for Models. However, this approach must be explored even further to take its full advantage.
In Sqimitive, you get a special object property – _opt ("options") – that is a pool of trackable states, similar to Backbone's Model attributes. Items in this pool are read with get() and written with set(). When a new and different value is written a bunch of events occur. Within these events you can cancel or normalize the change (normalize_attr()) or perform an action (change_attr() and change()). For parent sqimitives, they can optionally be forwarded (_childEvents.
This can be used to create event observation spots too. Suppose that you have an object that's loading some data from the server. Since it's asynchronous you don't know when (or if) it will finish. Usually you would either create a callback property and call it or use a custom event (jQuery or Backbone) and call it once loading is done. In Sqimitive, you simply introduce some _opt.loading, set it to false and then once done – set it to true. Observers listen to change_loading event having the full set of event handling routines at their disposal instead of introducing isLoading(), abortLoading(), onLoading(), etc.
Now this works the other way too: an external object can set('loading', true) to force refreshing of the data. Better still – the loading object itself can set this option to trigger its own loading routine – naturally, avoiding code duplication.
Moreover, since options are not accessed directly you can always listen for their access with get or add custom behaviour with set. This leads to more stable code base.
Given this mechanism you can significantly lower your render() and custom event rates while being much more aware of what has triggered the change so you can perform a lightweight update. And since there's no distinct separation between Models and Views this universally works for any kind of object you might have.
var MyView = Sqimitive.Sqimitive.extend({ _opt: { loading: false, people: [], // array of names. }, // We will keep jqXHR object here to be able to abort the request. _loadingXHR: null, events: { init: function () { // Wait 300 msec and then start fetching server data. _.delay(_.bind(this.set, this, 'loading', true), 300) }, // Loading was either cancelled or started. change_loading: function (value) { // Update class name of our DOM element. this.el.toggleClass('loading', value) // Abort old request, if any. this._loadingXHR && this._loadingXHR.abort() if (value) { // Fetch the data, pass to _load(). this._loadingXHR = $.getJSON('some/long/process', _.bind(this._load, this)) } }, // _opt.people changed - update names. change_people: '_updatePeople', render: function () { // Retrieve all _opt to pass them to the template. var vars = this.get() // Overwrite with new HTML. this.el.html(_.template($('#MyViewTemplate').text(), vars)) // Add peoples' names. this._updatePeople() }, }, _updatePeople: function () { var el = this.$('.people').empty() _.each(this.get('people'), function (name) { $('').text(name).appendTo(el) }) }, _load: function (resp) { // Do something with resp, then finally: this.set('loading', false) }, })
Events do occur on events. No cheating
If you have used Backbone's add, remove and change events then you probably know that they do not occur always. For example, if you reset() a Collection it won't fire add for new Models, remove for gone or change for existing Models that were updated.
This smells like an optimization – after all, firing hundreds of events when resetting a huge Collection might be slow. However, this case is very rare and yet the availability of reset() makes it not trivial to track the updates because often you will end up with your own routine bound to reset as an event that will figure the difference between old Collection contents and new one.
This also happens with attribute normalization on Models which for some reason doesn't occur when assigning an API response.
Sqimitive fires events when things change, period. In case you do have a performance-heavy object you can always implement your own more silent update routines. However, 90% of the time you will rely on nest, unnested, change and others to keep you notified.
Cloned instance properties
In JavaScript, when you create a prototype with non-scalar values (like objects or arrays) what you actually create are shared instance properties. If at runtime you modify (but not reassign) such a property this operation will affect all objects where that property is defined, unless its value was overwritten with a new object.
Backbone inherits this behaviour which most of the time leads to very confusing results (from the human's perspective). Consider this short snippet (JSFiddle):
var MyView = Backbone.View.extend({foo: []}) var first = new MyView var second = new MyView first.foo.push(123) alert(second.foo[0]) // alert: 123
The only way around is to assign such properties in the constructor, which you should first override using the crazy My.Class.__super__.baseMethod.apply(this, arguments) construct. This gives your code -50 points in the ability to save kittens on this planet.
Sqimitive eliminates this problem by automatically deep cloning all complex values upon new object instantination. Of course, sometimes this is not desired – for this you can always assign them in the constructor just like before or list in _shareProps to prevent auto cloning.
Anatomy Of A Sqimitive
Anything in Sqimitive is, well, a sqimitive – a combination of "Squizzle" and "primitive", first being what we call "The Squizzle Way" – eliminating superfluous concepts to produce lean and reliable code. See squizzle.me for other goodness.
Each sqimitive has three fundamental features that sprout out all of its many use cases: options, children and events. Below is a quick high-level overview to demonstrate the main idea; remember to proceed to the detailed API docs if you want to get all of Sqimitive (and there's as much to learn as there's text, really).
Options are attributes in Backbone's terms – set of key/value pairs 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 associated DOM node is updated.
var MyToDoItem = Sqimitive.Sqimitive.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: '
<%- caption %>
', 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: _.trim, render: function () { // Retrieve map of all options/values and pass to the template. var vars = this.get() // = {isCompleted: false, caption: 'foo'} this.el.html(_.template(this._tpl, vars)) }, })
Children are zero or more sqimitives nested into one parent sqimitive. Their events may be forwarded to parent – but only while they are still part of that parent; upon removal they are automatically unbound. When a children is added or removed its parent, if any, gets notified. Also, all standard Underscore.js methods are available to filter or transform them into a native array or object.
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 you can traverse the tree in either direction starting from any node. If you nest one children 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. No automatic removal is done either.
Sample code below defines a to-do list that is meant for storing MyToDoItem's. Note that in Backbone you would have at least two classes: one Collection for storing the list of to-do items and one View for displaying that collection. Or, to be 100% correct, you would create four classes: Model holding single to-do item data, View to display it, Collection to hold to-do items and another View to hold the Collection to hold 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 that but let's be honest – such pure concepts are good for academics and very large projects but most of the time you would rather have something slightly more dirty but more practical. Sqimitive gives you this choice since everything is ultimately a primitive and can be purified to the point you need.
This kind of nesting doesn't necessary reflect the DOM – children can have their elements under their parent's el or elsewhere, or not have DOM elements at all.
var MyToDoList = Sqimitive.Sqimitive.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: function (o) { return o.get('isCompleted') } return this.reject(MyToDoList.picker('get', 'isCompleted')) }, })
Events are Squimitive's Swiss Army Knife to deal with everything from inheritance (OOP style) and prototyping (JavaScript-native style) to dynamic property morphing and dispatching notifications in an Observer-like fashion. When defined upon class declaration handlers are "fused" into the class (hence introducing no performance overhead at all), otherwise they work as regular event listeners that can be removed on runtime (this happens automatically once a nested sqimitive is unnested, for instance).
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 which you can manipulate on runtime as well as upon declaration. This way you can use "normal" 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. This way you can always invoke a method without knowing if it's a real function or an event trampoline.
Sample code below shows side-by-side how traditional and Sqimitive inheritance correlate with each other.
var MyBase = Sqimitive.Sqimitive.extend({ effect: function (arg) { console.log('MyBase.effect(' + arg + ')') return this }, }) // Traditional JS-OOP inheritance. var JsOopSubclassing = Sqimitive.Sqimitive.extend({ // This way you would override former method with yours, entirely. effect: function (arg) { return 'foo' }, // ...optionally calling the inherited implementation. effect: function (arg) { console.log('pre-actions') // We have to hardcode current class name and the whole 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.Sqimitive.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, full override is rarely needed - most often you need just // to do something after the original method and keep its return value. // This one is identical to the above but without '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 perhaps // change or read its return value. '+effect': function (result, arg) { console.log('post-actions') if (result === this) { // Return something other than the original code produced. return new That(arg) } // Returning undefined or not returning at all retains initial result. // These are identical: //return undefined //return }, }, })
Code below demonstrates the usage of dynamic event binding and method overriding.
var DynamicEvents = Sqimitive.Sqimitive.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 a 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 // class method it cannot return any value (it is ignored). This way no // disruption is caused if the class suddenly acquires native method // of the same name (this handler will be called after it). this.on('slotC', function () { console.log('post-effect') return 'ignored' }) this.slotC() 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 later // you can off() it and former (sup) method 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 }) // However, if you try to turn a non-method into an event nothing // will break - you will add event listener all right but doing // notASlot() won't fire the event - only access that property. 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; // Each event handler unless it's "fused" on class declaration time gets // a unique ID that can be used to very quickly unbind it later. // Contrary to common approach, Sqimitive offers no event namespaces // (such as my.handler) used to unbind group of events - by-context lookup // covers most cases and is available 9see 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') }, })
Opening The Views
Options, children and events are 90% of what a sqimitive is. However, they all are mostly about the logic and data; to make the user happy we should interact with him and present information in a good way. This is when Views, in MVC and Backbone terminology, come into play.
By default, each sqimitive possesses a DOM element – that familiar this.el – which is any jQuery/Zepto/other DOM-like object. This can be disabled for pure-data classes (like Models or Collections) but if it's not, such a node is automatically created upon the sqimitive construction and assigned to its el. It can utilize automatic binding of DOM events via elEvents property, as well as convenient methods like this.$('sel.ector'), bubble('eventForAllParents') and sink('eventForChildren').
Sample code below creates a simple login form. It stores data in its own options but, as you must know by now, Sqimitive allows you to separate it into another Model-like object if you need more abstraction in your application.
var MyFormView = Sqimitive.Sqimitive.extend({ // If omitted will create just a plain
. el: {tag: 'form', action: '#', 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() return this }, 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 (and gets a listener) then there's a small optimization - // Sqimitive will remove the old method entirely not putting it // as a listener for that event. // Alternatively, you could just leave this undefined and always use // fire('loggedIn') but it's more tricky and less obvious if you ever // get a loggedIn() method that for any reason does something else. loggedIn: Sqimitive.Sqimitive.stub, })
Given the above class we can use it as follows:
// A typical use case - just create new form object along with a DOM element: var sqim = new MyFormView({login: 'default@login'}) // Or if we have a ready-made container element - use it: var sqim = new MyFormView({login: 'default@login', el: '#loginForm'}) // Then we can listen to new sqimitive's events as: 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 { sup(this, arguments) } }) // This is not Sqimitive-way as it is long-winded and will override whatever is // already defined as render() including all events (if 'render' is an event slot) // and hardcodes class name and return value but if you don't mind - go ahead. sqim.render = function () { if (location.protocol != 'https:') { this.el.text('Your connection is not secure!') return this } else { return MyFormView.render.apply(this, arguments) } }
Examples, Tips & Tricks
The following tricks can be used to make Sqimitive appear more like Backbone API. It will not make it 100% identical, don't use it in production as it is – it's just to give you a good direction.
- General
In Backbone, _cid is named cid, el is $el with el counterpart (native DOM node), fire() is named trigger(), listenTo() and stopListening() are autoOff() flavours, toJSON() is basically get(), attributes is the same as _opt, defaults are declaration-time _opt.
extend({ cid: null, // alias to _cid; do not write to. events: { '-init': function () { this.cid = this._cid }, init: function () { this._childClass = this._childClass || this.model }, }, trigger: function (event, arg_1) { return this.fire(event, _.rest(arguments)) }, listenTo: function (sqim, event, func) { this.autoOff(sqim, _.object([[event, func]])) return this }, stopListening: function (sqim, event, func) { if (!arguments.length) { return this.autoOff() } else if (func || (sqim && event)) { throw 'Unsupported stopListening() call.' } else { return this.off(event || sqim) } }, })
- Collection
Collections have model (class reference) which simply specifies _childClass, reset() is alike to assignChildren (Collection's set() is entirely different though), push(), pop(), shift(), unshift() are shortcuts for nest() and company (there's no built-in ordering in Sqimitive), clone() is another shortcut for non-_owning lists. Models are keyed by their id attribute value (or idAttribute) which are like _defaultKey.
extend({ model: null, // alias to _childClass; do not write to. el: null, idAttribute: 'id', events: { '=assignChildren': function (sup, resp, options) { options = _.extend({eqFunc: this.idAttribute}, options) return sup(this, [resp, options]) }, }, _defaultKey: function (model) { return model.get(this.idAttribute) }, reset: function (models, options) { if (!arguments.length) { this.invoke('remove') return this } else { return this.assignChildren(models, { eqFunc: this.idAttribute || 'id', keepMissing: !('remove' in options) || !options.remove, }) } }, push: function (model) { this.nest(model) }, unshift: function (model) { this.nest(model) }, pop: function () { var model = this.last() model && model.remove() return model }, shift: function () { var model = this.first() model && model.remove() return model }, toJSON: function () { return this.invoke('get') }, clone: function () { if (this._owning) { throw 'clone() will clear base instance.' } var copy = new this.constructor(this.get()) this.each(function (child, key) { copy.nest(key, child) }) return copy }, })
- Model
Models have escape(), has() shortcuts for get(). id option ("attribute") is readable/writable as get('id')/set('id', X) and also readable directly as obj.id (write attempts won't be caught nor will they change "real" id).
extend({ attributes: {}, // alias to _opt; do not write to. defaults: {}, // alias to initial _opt; do not write to. id: null, // alias to get('id'); do not write to. _opt: { id: null, }, events: { '-init': function () { this.defaults = Sqimitive.Sqimitive.deepClone(this._opt) }, init: function () { this.attributes = this._opt }, change_id: function (id) { this.id = id }, }, toJSON: function () { return this.get() }, escape: function (opt) { return _.escape(this.get(opt)) }, has: function (opt) { return this.get(opt) != null }, })
- View
Views have setElement(), delegateEvents(), undelegateEvents() that do similarly what attach() does.
extend({ el: null, $el: null, // alias to el; do not write to. events: { init: function () { this.$el = this.el }, }, setElement: function (el) { this.undelegateEvents() this.el = this.$el = el return this.attach() }, delegateEvents: function (events) { this.elEvents = events return this.attach() }, undelegateEvents: function () { this.el.off('.sqim-' + this._cid) }, })
Babysitting Models
One of the most common problems when developing a complex client-side app is keeping track of multiple Models or a Collection connected to a particular View. When a new Model appears you need to create and display a new nested View; when it's removed – its View should go away you; when Model attributes change the View should be updated (this case is often but not always handled by that Model's View).
On top of that, when parent View acquires another Collection it should properly detach itself from the previously assigned Collection, attach to the new object and repopulate itself.
Things get even more complex with asynchronous operations – sometimes the user gets ahead of his connection and you don't want the interface to tangle up.
Sqimitive addresses these challenges with a full set of methods:
- Attach/detach event handlers to the tracked Collection with autoOff() or off(collection).
- Keep that Collection as an option so once it changes you receive change_collection(newCol, oldCol) event.
- Make sure only proper Collections are assigned by checking value-to-be-set in normalize_collection(newCol).
- Listen to new/gone Models with on('+nest') and 'unnested'.
- Listen to changed Models with _childEvents = ['change'] and on('.change').
- Automatically manage nested Views' DOM connection with attachPath and attach(). Quite often there's no need for render() at all.
Sample code below demonstrates these methods in practice. It's a good idea to make an abstract base class and reuse it throughout your project.
var ParentView = Sqimitive.Sqimitive.extend({ _childEvents: ['change'], _childClass: NestedView, _opt: { collection: null, // Collection.Foo. }, normalize_collection: function (newCol) { if (!(newCol instanceof Collection.Foo)) { throw 'Bad collection type.' } else { return newCol } }, change_collection: function (newCol, oldCol) { // Unbind self from the old collection, if any. oldCol && oldCol.off(this) // Clear existing nested Views, if any. this.invoke('remove') // Set up new collection link. if (newCol) { this.autoOff(newCol, { // Note the leading + which makes _modelAdded's first argument to be // nest()'s return value which is the new child. Without it if key // (optional nest()'s first argument) is present it will be _modelAdded's // first argument instead of the child. '+nest': '_modelAdded', unnested: '_modelRemoved', '.change': '_modelChanged', }) // Populate with the existing models. newCol.each(this._modelAdded, this) } }, _modelAdded: function (model) { // This is the place when new nested View gets created and linked to the model. // First argument to nest() is view's parent key by which it can be retrieved // later. ID is usually unique so it's a good candidate. If this key already // existed such a View will be removed and replaced by the new View. var view = this.nest(model.get('id'), new this._childClass({ model: model, attachPath: '.models', })) // Append view.el to this.el.find('.models') and bind its DOM event listeners. // This won't render() the View but it might listen to attach() and render // automatically. view.attach() // ...if it doesn't auto-render - no big deal: view.render() }, _modelRemoved: function (model) { var view = this.nested(model.get('id')) if (view) { // Removes its element from this.el and then unnests from the list of // this View's children, removing its event listeners on the parent View. view.remove() } else { console.warn('Removed a Model with no nested View.') } }, _modelChanged: function (model) { // Update something when model options change... }, })
Given the above code it can be used like this:
var col = new Collection.Foo col.nest(new Model.Foo({id: 1})) new ParentView({collection: col, attachPath: 'body'}) // Gets created with one nested View. Parent view was appended to . col.nest(new Model.Foo({id: 2})) // New View nested. col.nested(2).remove() // Just nested View got removed. col.nested(1).set('smth', 'foo') // '.change' event got fired on the parent View.
Figuring "class name"
With its concept of "functions as first-class citizens", JavaScript lacks any kind of "class name" references in instantinated objects. The best we can afford is duck-typing where we see if object X has properties Y and Z and if it does – it's probably that kind of object (i.e. "class").
Sometimes (or rather quite often while debugging) we need to figure what's the object we see. If your app has all Sqimitive classes defined under a certain object (like window – which is a bad practice – or window.MyApp) you can go through all prototypes on start up and add sqClassName property holding string reference to that prototype.
First, let's override extend() with our own that will mark each produced prototype so we know we're looking at something we have created:
var BaseSqimitive = Sqimitive.Sqimitive.extend() BaseSqimitive.extend = function (protoProps, staticProps) { var child = Sqimitive.Sqimitive.extend.apply(this, arguments) child.sqIsClass = true // If your classes are defined like AppRoot, AppRoot.View, AppRoot.View.Cart // the following code will skip subclasses when extending a class: // AppRoot.OtherBaseView = AppRoot.View.extend() // Without it OtherBaseView would get AppRoot.View.Cart as OtherBaseView.Cart. for (var prop in this) { if (/^[A-Z]/.test(prop) && typeof this[prop] == 'function' && this[prop].sqIsClass) { delete child[prop] } } return child }
Now on start up we go through all classes and add sqClassName like View.Cart:
// Define classes above or wrap the following into $(). ;(function (cls, prefix) { for (var key in cls) { var member = cls[key] if (/[A-Z]/.test(key[0]) && typeof member == 'function' && member.sqIsClass) { member.prototype.sqClassName = prefix + key arguments.callee(member, prefix + key + '.') } } })(window.MyAppRoot, '') // Replace MyAppRoot reference with your root.
Here's an example of what you get:
- MyAppRoot.BaseView.sqClassName = 'BaseView'
- MyAppRoot.BaseView.SubView.sqClassName = 'BaseView.SubView'
- MyAppRoot.BaseModel.sqClassName = 'BaseModel'
Sometimes you get a number of actions to be completed before performing a specific task. For example, you need to preload a bunch of images and work on them once they are all ready.
This synchronization task can be carried out with this simple class:
var Countdown = Sqimitive.Sqimitive.extend({ el: false, _opt: { count: 0, cx: null, }, events: { init: function (opt, onDone) { // This way dec() and inc() calls will always happen on this object instance. _.bindAll(this, 'dec', 'inc') onDone && (this.done = onDone) }, }, done: Sqimitive.Sqimitive.stub, error: Sqimitive.Sqimitive.stub, dec: function () { if (--this._opt.count == 0) { this.done.call(this.get('cx') || this) } else if (this._opt.count < 0) { console && console.warn('Countdown below zero.') } return this }, inc: function () { ++this._opt.count return this }, })
Usage is straightforward:
var images = ['pic1.jpg', 'pic2.jpg'] var countdown = new Countdown({count: images.length}, function () { // Ran when all images have been loaded. }); // Start loading images. _.each(images, function (path) { var img = new Image // dec() is bound to the instance so can be called with free context. img.onload = countdown.dec img.src = path })
Activity Pipeline
At other times, you would ran into a complex synchronization routine with multiple stages that in addition should have those stages easy to override, e.g. in a subclass.
For example, imagine a Page class. Its objects occupy all available window space and when switched from one to another must perform a visual effect (e.g. slide or fade). Effect can be changed in specific Page subclass, there are various actions to perform when it's done (e.g. freeing of data), and there are also conditions when pages should not be changed - e.g. when it's busy or asking for confirmation. And it must be singular - we don't want effects or other phases overlap.
Below is a helper class that represents a pipeline of actions: begins with prereq(), then passthru(), then transition() and finally done(). If the action has been stopped cancel() occurs at any point. They are all methods but can be turned into events by overriding with on() according to Sqimitive's event model.
One instance can be only active once at a time - ignores consequent start() calls until done() or cancel() are reached. done callbacks given during one run are all retained and called upon completion.
var Activity = Sqimitive.Sqimitive.extend({ el: false, _done: [], cx: null, // autoset to _opt.cx. _opt: { cx: null, active: false, }, events: { change_cx: function (value) { this.cx = value }, }, // (1) Proceed to 'passthru' if the activity can be performed (e.g. if // popup window can be closed without explicit user choice), otherwise // proceed to 'cancel'. prereq: function () { this.passthru() }, // (2) Proceed to 'transition' if need to perform any action (e.g. if // a window is visible, not hidden) or to 'done'. passthru: function () { this.transition() }, // (3) Proceed to 'done' when all actions are finished. transition: function () { this.done() }, // (4) Invokes all pending callbacks. done: function () { var funcs = this._done.splice(0, this._done.length) this.set('active', false) this._invoke(funcs) }, // (2) Remove all on-done callbacks (not possible to perform the activity). cancel: function () { this._done = [] this.set('active', false) }, _invoke: function (list) { _.each(list, function (item) { try { item[0].call(item[1]) } catch (e) { console && console.error('Activity callback exception: ' + e) } }) }, // If currently active func will be called upon completion. If not active // the activity will be started and func called when it's done. start: function (func, cx) { this.enqueue(func, cx, true) this.ifSet('active', true) && this.prereq() return this }, // Unlike start() doesn't run the activity but instead calls func if it's // currnetly active or calls func right away if not, without starting up. enqueue: function (func, cx, always) { cx = cx || this.cx || this if (_.isFunction(func)) { ;(always || this.get('active')) ? this._done.push([func, cx]) : func.call(cx) } return this }, })
When you want to start running a new activity use start() with an optional callback:
var MyPage = Sqimitive.Sqimitive.extend({ _activity: null, elEvents: { 'click .close': function () { this._activity.start(function () { alert('Completely went away...') }) }, }, events: { init: function () { this._activity = new Activity({ // Since Activity's event handlers are called within activity's // context we can use this.cx or this.get('cx') to access this page. cx: this, }) // You don't have to implement every method down here, this is just a sample. this._activity.on({ passthru: function () { if (this.el(':visible')) { this.transition() } else { this.done() } }, transition: function () { this.cx.el.fadeOut(_.bind(this.done, this)) }, done: function () { this.cx.remove() }, }) }, }, }) var MyAskingPage = MyPage.extend({ events: { init: function () { this._activity.on('prereq', function () { if (confirm('Really close this page?')) { this.passthru() } else { this.cancel() } }) }, }, })
When you want to have your callback fired right away without startign the activity and postpone if it is active - use enqueue():
(new MyPage)._activity.enqueue(function () { // At this point it's guaranteed that the activity is no more/was not running. })
API Reference
This section describes all of Sqimitive features in detail. Most of this can be found in extensive code comments (and it's usually more up to date). It makes sense to first read the overview before diving in.
Before we move on let's quickly review Squizzle.me Toolkit's coding conventions:
- Protected class fields begin with underscore (_) – such fields (data properties and methods) are only meant to be read or written by the class they are defined in, or by any of its subclasses. It's bad practice to try to access them from an ancestor or, mind you, from a disconnected class – this is only justified, if ever, by really severe optimizations (such as tons of _opt calls).
- Underscores separating camelCase – when an identifier (oneId) needs to be mixed into another (TwoId) it simply becomes oneId_twoId instead of oneIdTwoId, one_id_two_id or something else dictated by "pretty printing" rules. For example, event identifier for a change event of an attrName option is simply change_attrName.
- Semicolon-free zone – you don't have to follow this hype but the author believes that such a code is cleaner and faster to type with a few insignificant drawbacks. No need to pay more where you don't have to, right?
Core class
Events play fundamental role in Sqimitive – so fundamental that half of its code implements just that. For this reason Sqimitive per se is split into two classes: Core and Sqimitive (both reside under common Sqimitive namespace).
Core implements inheritance and event binding, tracking and unbinding. It lacks any serious functionality and can be easily mixed into any other classes of your choice. The class is accessible globally as Sqimitive.Core but you most likely won't need to access it directly, using Sqimitive.Sqimitive instead
Static properties and methods
- constructor ()
Assigns new unique _cid ("p" + unique number) and clones all instance properties of self that are not listed in _shareProps. This puts a stop to accidental static sharing of properties among all instances of a class where those properties are defined; see _shareProps for details.
- _mergeProps
In Backbone, when you extend a parent class with a property that it already has you end up with a completely new property. This makes sense but not always – for example, if a class has its own events then what you really need is merge its own (base) events with the events of new subclass. Same thing with Model attributes and defaults, Router routes and others.
Consider this example (JSFiddle):
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 a button :(') }, }, })
_mergeProps lists such properties (only instance) that you want to merge rather than overwrite when subclassing a Sqimitive – exactly as _.extend(parentProp, subclassProp). In human language, this means that if subclass defines a merging property and inside it has a key that also exists in the base class' property then subclass overwrites that, but only that, value. All other keys are retained and there is no way to delete a base class' key other than replacing it with null or undefined (in contrast with delete such properties are still iterable with for..in). You can delete them after the instance was constructed.
By default, Core class sets it to _shareProps while Sqimitive class adds _opt and elEvents.
When passing _mergeProps inside staticProps (second argument of extend()) all inherited items will be removed; correct way to add your properties while keeping those in base classes is this:
var MySqimitive = Sqimitive.Sqimitive.extend({ // Define instance fields... }, { // Define static fields - if you need. If not 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', ...)
These are wrong ways to append to this property:
var MySqimitive = Sqimitive.Sqimitive.extend({ // _mergeProps is static so it won't be read from here. _mergeProps: ['prop'], }, { // This is fine but it will entirely replace base class' list, if any. _mergeProps: ['prop'], }) // This will work but once again will replace all the inherited items. MySqimitive._mergeProps = ['prop'] // What you want to do is most often this: MySqimitive._mergeProps.push('prop')
In Backbone, every subclass gets values of its inherited properties 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.
Consider this short snippet (JSFiddle):
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. What a surprise!
In Sqimitive, every non-scalar property gets cloned upon object instantination. If you don't want this overhead (usually it's miniscule) – simply assign all complex values in the constructor, init() or postInit() or list such properties (only instance) in _shareProps. Be safe by default.
One particular case to be aware of is when you are assigning classes to properties, like extend({_model: MyApp.MyModel}) – it will be recursively copied resulting in a broken prototype. Do this instead:
var MyView = Sqimitive.Sqimitive.extend({ _model: MyApp.MyModel, }) // _shareProps is a static property. MyView._shareProps.push('_model')
Or you can assign the class after instantination, which is less elegant:
var MyView = Sqimitive.Sqimitive.extend({ _model: null, // MyApp.MyModel events: { init: function () { this._model = MyApp.MyModel }, }, })
By default, base Sqimitive class sets it to _childClass. When passing _shareProps inside staticProps (second argument of extend()) all inherited items will be removed; correct way to add your properties while keeping those in base classes is this:
var MySqimitive = Sqimitive.Sqimitive.extend(...) MySqimitive._shareProps.push('prop1', 'prop2', ...)
_shareProps inheritance works exactly the same way as _mergeProps' – see its description for more examples.
- extend (protoProps[, staticProps])
Lets you create a new subclass of the given class. protoProps are new instance fields (properties or methods; can include events pseudo-property) while staticProps are new static fields – i.e. the ones called as MyClass.staticSomething() as opposed to (new MyClass).instanceSomething(). Most of the time you will use just protoProps.
In case of duplicated names subclass' values take precedence to overwrite values in its parent class (except when such names are listed in _mergeProps).
// First we extend base Sqimitive class with our own properties. var MyBase = Sqimitive.Sqimitive.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 - overriden 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 // properties are merged and not entirely replaced. _opt: { // Retained. baseOption: 'boo', // Introduced. subOption: 'sub', // Overriden. baseMore: 'bus', }, } */
- events
There is no such property per se but it can be passed to extend() to add new event handlers (it only exists inside extend() and does not become this.events). Since in Sqimitive everything is an event 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 instantination.
Formally this is similar to calling this.on({events}) so see on() with an object argument for details. Also see Events overview for a good example
- stub ()
An empty function that returns undefined. Used in places where you don't want to supply any implementation – this lets Sqimitive optimize things when it knows that a function can simply be discarded. Technically if you are not a performance purist you can just use function () {} or new Function to achieve the same effect.
var MySqim = Sqimitive.Sqimitive.extend({ events: { success: Sqimitive.Sqimitive.stub, // or success: function () { }, error: Sqimitive.Sqimitive.stub, }, }) var my = new MySqim // Replaces empty handler entirely. my.on('success', function () { alert('Good!') })
- unique (prefix)
Generates and returns a number starting from 1 that is guaranteed to be unique among all calls to unique() with the same prefix during this page load. Used to assign _cid (unique sqimitive instance identifier).
Sqimitive.Sqimitive.unique('foo') // returns 1. Sqimitive.Sqimitive.unique('foo') // returns 2. Sqimitive.Sqimitive.unique('bar') // returns 1. Sqimitive.Sqimitive.unique('foo') // returns 3.
- picker (prop[, args])
Returns a function that expects one argument (an object) that, when called, checks if given object has prop property and if it does – returns its value (if it's a method then it's called with args (array) and the result returned), otherwise returns undefined (for non-objects or objects with no prop).
Usually it's given to some filtering function (see getIncomplete() example).
var obj = { one: 1, two: function () { return 2 }, some: function (a, b) { return a + '--' + b }, } var picker = Sqimitive.Sqimitive.picker; alert( picker('one') ) // alerts "1". alert( picker('two') ) // alerts "2". alert( picker('some', ['A', 'B']) ) // alerts "A--B".
- expandFunc (func[, obj])
Expands a function reference func of object obj (this if not given) into a real Function. Used in on(), events and others to short-reference instance's own methods.
If func is a string and contains a dot or a dash (. -) - returns masked version of this method (mask starts with the first such character). If it's a string without them - returns a function calling method named func on obj (or this if omitted). In other cases returns func as is if obj is omitted or _.bind(func, obj) otherwise.
var func = Sqimitive.Sqimitive.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.Sqimitive.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.
- masker (func[, mask[, cx[, args]]])
Returns a version of func with re-routed arguments according to mask. If mask is a number - skips that number of leading arguments as in _.rest(), if omitted - assumed to be number 1 (skip first argument), otherwise it's a mask string - see below.
func is either a string (method name as in expandFunc()) or a function - both called on cx or this if omitted or null. args is array of extra left-side arguments.
In string mask each symbol maps arguments given to masked func (result of masker()) to original func. It consists of:
- Dots - each is replaced by its index in the string (-..-. equals to -23-5).
- Dashes - represent arguments that are to be ignored; trailing dashes are ignored (arguments past the end of mask are never given unless mask is a number)
- Numbers 1-9 - read arguments by index: 1 reads 1st masked argument, etc.
For example, mask of -.1 equals to -21 and gives two arguments: (arg2, arg1). Empty mask passes zero arguments (so do -, --, etc.)
Note: mask of '3' is different from 3 (number) - the first passes 3rd argument as the first while the second skips first 3 arguments and passes all others.
Masking is a way to work around the "Danger of args__" described here and avoid writing simple callback functions which reorder arguments. It is common to alias a shorted reference like var m = Sqimitive.Sqimitive.masker and use it in your code since its main point is to be easy to call.
$.ajax({ url: 'api/route', dataType: 'json', context: sqim, // This is wrong: success' second argument is textStatus which gets pushed // to assignResp(data, options) breaking the latter (options must be an object). success: sqim.assignResp, // This is correct: we indicate that we are only interested in the first // argument which is passed through to assignResp(). success: Sqimitive.Sqimitive.masker('assignResp', '.'), })
var m = Sqimitive.Sqimitive.masker var MyModel = Sqimitive.Sqimitive.extend({ _opt: { caption: '', }, // Unmasked, _.trim() takes two arguments (str, chars) but normalize_OPT() // are passed (value, options); the latter interfere with each other. normelize_caption: m(_.trim, '.'), })
_.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 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 (_.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. m(function (a1, a2) { alert(a1 + ' ' + a2) }, '') // so func // will always alert 'undefined undefined'.
- deepClone (obj)
Returns a recursive copy of the argument so that any modification to either obj or returned value (obj copy) won't affect its counterpart. Think of this as of recursively calling _.clone() or $.extend().
- parseEvent (str)
Extracts portions of the given event identifier as recognized by on(). Returns an object with keys prefix, name and args, throwing an error if str doesn't look like a proper event reference.
- fire (funcs[, args])
Processes an event call chain. funcs is an array of event registration objects. Calls each handler in turn according to its type (expecting a fixed number of arguments, accepting current result value, affecting return value, etc. according to prefix in on()) while giving it args (array). If a handler returns something but undefined and it's eligible for changing return value (as it's the case for +event and =event), then current result is replaced by that handler's return value. Returns the ultimate return value after calling each handler. There is no way to skip remaining handlers (but if you really need – try all event for this).
funcs can be "falsy", in this case undefined is returned.
See also instance method fire().
- toArray (value)
Attempts to cast value into a native Array object. In particular, function arguments become an array, arrays are returned as is while anything else is wrapped into an array to become its sole member and returned. This means that even null and undefined result in [value] – not [].
Does not clone result.
- is$ (obj)
Determines if obj is a $ collection (like jQuery or Zepto).
is$(document.rootElement) //=> false is$($('html')) //=> true is$($('
')) //=> true is$(null) //=> false
Instance properties and methods
- _cid
An identifier of this object instance unique to all sqimitive's ever instantinated during this page load. Unique to all Sqimitive.Core instances regardless of subclass. Can be used to namespace DOM events as in this.el.on('click.' + this._cid). Begins with "p" for "primitive" followed by a 1-based number.
Historically expands to "Client Identifier" – a term originating from Backbone but probably not holding much meaning at this point.
- _events
An internal object holding all current event bindings. Note that it includes both "fused" and dynamic events (fused are produced by extend'ing a class so that subclass' event handlers cannot be removed on runtime). It's not advised to deal with this object directly – use on(), once(), off() and fire() instead.
_events keys are event names without any prefixes or suffixes (render, remove and so on), values – arrays of event registration objects of internal structure (study code comments for details). These value arrays are given to fire() when an event occurs.
- _autoOff
When autoOff(sqim) is called to keep track of sqim object to which this object has attached an event listener, such sqim object is put into _autoOff array. You can then do this.autoOff() in unnest() or another place to sever all connections between an object that is about to go away and those still in the world of living.
- fire (event[, args])
Triggers an event giving args as parameters to all registered listeners. First fires a special all event and if its return value was anything but undefined – returns it bypassing handlers of event entirely. all gets event put in front of other args (e.g. ['eventName', 'arg1', 2, ...]). It's safe to add/remove new listeners during the event – they will be in effect starting with the next fire() call (even if it's nested).
Note that all event is only triggered for actual events so if, for example, render() isn't overriden it will be called as a regular member function without triggering an event.
See also static method fire().
- firer (event[, prependArgs[, self]])
Returns a function that, once called, will call fire(event, args) in context of self (if not given context is unchanged). args can be used to push some parameters in front of that function's args. Just a short way of writing:
_.bind(function () { return this.fire(event, prependArgs.concat(arguments)) }, self)
- logEvents ([enable])
A debug method that enables logging of all triggering events to the console. Pass false to disable. Will do nothing if browser doesn't provide console.log(). Acts as a handler for special all event (see fire()).
Also acts as the logging handler itself if first argument is a string – this lets you override default behaviour as a regular event handler.
Note that methods calls that are not events won't be logged. If this sounds trivial remember that in Sqimitive methods only become events on demand:
var MyBase = Sqimitive.Sqimitive.extend({ // render() is essentially a function. 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' - it's an event with two listeners: // one from MyBase (called first) and another from MyChild. // This way logEvents() logs the call because 'all' event gets fired because // 'render' is, in MyChild and descendants, an event that gets fired in the // first place. (new MyChild).render() 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 put 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 // an error (forgetting about method of the same name existing among the parents). // Regardless of the morale, logEvents() here won't track anything. (new MyChile).render()
If you want to log some extra info or replace logEvent's logging with your own – use regular Sqimitive inheritance:
var MyLoggee = Sqimitive.Sqimitive.extend({ events: { logEvents: function (event) { // logEvents() calls itself when an event occurs and the first argument is // 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) } // Make sure you don't return any value because a non-undefined result // will override original event chain (before which 'all' is called). }, }, }) // Logs both standard logEvents() info and our class name line. (new MyLogger).logEvents().fire('something')
- on (event[, func[, cx]])
The heart of Sqimitive's event system. Lets you add new event listeners, both dynamic and "fused" (that cannot be unbound at a later time). It has several call forms. See also once() that lets you attach one-shot listeners. Throws an exception if event can't be parsed.
- on( {events} [, cx] )
Fuses multiple event handlers into current object state meaning that they cannot be unbound with off(). Returns this. Similar to fuse() with added event comma notation.
events is an object with one or more event references as keys (e.g. =over.ride__ – see below) and event handlers as values. Multiple references are separated with , (comma and a space; this is identical to registering them one-by-one). Handlers are either functions (closures) or strings (method names of the object to which listeners are added; resolved on call time so don't have to exist when binding the handler)
cx is optional context in which the handlers are called (defaults to this, the object on which on() is called).
This is the form used when extend'ing an object with events. It does not apply to elEvents.
sqimitive.on({ // Calls render() after 'name' option change and before 'birthday' change. 'change:name, -change:birthday': 'render', // Calls the function when close() gets fired. close: function () { this.el.fadeOut(_.bind(this.remove, this)) }, })
- on( 'event', func [, cx] )
Adds single event handler that can be dynamically removed with off(). Returns new event listener identifier that off() accepts (but the latter accepts other things too).
event is a single event reference (comma notation not accepted), func is the function or string method name (resolved when it gets called, optionally masked like func-..1 - see expandFunc()) being called when that event is fired, cx is the context in which it is called (defaults to this, the object on which on() is called).
Because of their dynamic nature, such event handlers are slightly less efficient than fused so usually if your handler is meant to stay with the object for its lifetime – consider using on({event: func}, cx) or fuse().
Event reference is a string with 3 parts: [prefix]event[argcount]. prefix Optional; changes the way event handler is bound and called as explained in the following table. 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 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 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 – does not (equivalent to doing return sup(this, arguments)).
Generally, usage of args is frowned upon because of its unintuitive nature – see the note below for details.
Event prefixes prefix arguments meaning evArgs... No prefix adds new handler after existing ones and neither gives event result to it nor changes it based on the handler's return value (which is ignored). Used most often to do some extra computations after original code has executed, retaining original result. - evArgs... Adds new handler before existing handlers, otherwise identical to no prefix + res, evArgs... Adds it after existing handlers but is passed current event return value res, and if this handler returns anything but undefined – replaces event result with that value. = sup, evArgs... Wraps around existing handlers by removing them and passing a single callable sup of form function (this, args) – first argument corresponds to the object which initiated the event, second is array of arguments that were passed with the event (can be modified to give underlying handlers different set of data). args can also be arguments that the handler received – in this case first parameter (sup) is removed and the rest is given to underlying handlers.
'=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 3+ arguments they will // be omitted here but passed through 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 to underlying handlers. return sup(this) },
The danger of args__
In JavaScript, functions accept extra arguments with ease; often you would use some iterator and only care for one of its arguments. However, with args__ you have to pass exact number of arguments to the function even if its declaration doesn't use the rest. Consider this example:
var MySqimitive = Sqimitive.Sqimitive.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 occur when a property is being set... right? sqim.on('accessor__', function (prop, value) { alert('Now ' + prop + ' is ' + value) }) sqim.accessor('name', 'val') // alerts "Now name is val". var propsToSet = {prop: 'foo', bar: 123} // Somewhat contrived example for the brevity sake. Properties are set correctly (map() // calls accessor() for each item in propsToStr); however, no alerts appear. // This is because map() passes 3 arguments to iterator: value, key and list itself. // Therefore even if accessor() processes just 2 of them actual event fired is // accessor___ (3 underscores). _.map(_.invert(propsToSet), sqim.accessor, sqim)
- once (event, func[, cx])
Adds a one-shot event listener that removes itself after being called exactly once. In all other aspects once() is identical to on(event, func, cx) (obviously, can only be used for dynamic handlers). Returns event ID suitable for off() so you can unregister it before it's called (or after, nothing will happen). Doesn't accept multiple events.
func can be a string – method name of the object to which the handler is bound (resolved when handler gets called). It can be masked like func-..1 (see expandFunc()).
- fuse (event, func[, cx])
Adds a permanent event listener that cannot be removed with off(). event is single event reference (+some:eveent__, no comma notation), func is a function or a string (method name), optionally masked like func-..1 (see expandFunc()). cx is context in which func is to be called (defaults to this). See on() for details. Returns internal event registration object that you should discard without tampering.
sqimitive.on({ something: function () { ... }, someone: function () { ... }, }) // Identical to the above. sqimitive.fuse('something', function () { ... }) sqimitive.fuse('someone', function () { ... }) // Masked callback - receives (a1, a2, a3), passes (a3, a1, a1, a1) to // sqim.meth(). sqimitive.fuse('somewhere', 'meth-3111', sqim)
- off (key)
Undoes the effect of on() – removes event listener(s) unless they were fuse'd (permanent). key can be:
- Array – members are anything accepted by off() including more arrays.
- Object – context (that cx) to which handlers are bound; all with the identical context are removed.
- Event name like render or other – all listeners are removed.
- Handler ID – as returned by on(); removes that particular handler from that particular event.
Returns this. Does nothing if no matching events/contexts/handlers were found. off() is safe to be called multiple times – it will do nothing if there are no registered handlers for given value. When unregistering a wrapping handler (=event) its underlying handlers are restored – put in place of the wrapper in the event chain.
Event overview has a nice example on this subject. See also once() that lets you attach one-shot listeners.
- autoOff ( [sqim[, events[, cx]]] )
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 created by this object in the domain of 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 it had any connections won't trigger its old stale handlers. Returns sqim.
events, if given, is an object – event map where keys are event references (comma notation supported) and values are their handlers. cx is the context in which handlers will be called (defaults to this, the object on which autoOff() was called). cx can be explicitly set to null to keep sqim's context. Similar to manually calling on({events}, cx), see its documentation for details.
If events is not given only tracks the object without binding any events. One object can be added to the list several times without problems.
If no arguments are given, uses off(this) to remove listeners of this from all previously listed objects and clears the list. Similar to doing _.invoke(this._autoOff, 'off', this). Returns this.
var MyNotifyBar = Sqimitive.Sqimitive.extend({ events: { owned: function () { this.autoOff(new View.LoginForm, { loggedIn: function () { alert('Hi there!') }, '-multiple, events': ..., }) }, // This would be an unsafe way - if unnest() was called with any arguments // (for whatever the reason) autoOff()'s behaviour would change. See the // note on args__ danger in on(). //'-unnest': 'autoOff', '-unnest': function () { this.autoOff() }, }, })
Sqimitive class
While Core implements the fundamental event framework, this class implements what makes Sqimitive the Sqimitive – options, el and elEvents, children and filtering and a bit extra.
Sqimitive has no static fields. The class is accessible globally as Sqimitive.Sqimitive but since it's usually the only class you need (and you probably want a shorter name too) – start your project with something like this:
var MyApp = {...} // ... MyApp.Sqimitive = Sqimitive.Sqimitive.extend() // Now refer to MyApp.Sqimitive everywhere throughout your code.
Instance properties and methods
- _opt
Defines initial object options – similar to attributes in Backbone.Model. When any option value is changed change:OPTION and change events occur but before that occurs normalize_OPTION. This object can be initially set with extend() but it's not advised to access it directly – use get() and set() instead or you will bypass normalization and event triggering.
_opt is an object where keys are option names and values are anything, of arbitrary type. _opt is listed in _mergeProps so subclasses defining it will add to their parents' options instead of overwriting them entirely.
There is currently one (two) built-in option:
- attachPath
Specifies where this object's el will get appended to when its _parent is rendered or attach() is called with no arguments. Value can be anything accepted by parent's $(), like a string (selector (.list > :last-child) or . (period) to specify the parent's element) or a DOM node.
If a string this will have no effect on non-nested (non-_owning) sqimitives since access to the parent's $() is required – even if a global selector such as 'body' is used; if so you can set attachPath to document.bodyElement or $('body') even on non-owned sqimitives.
- el
There is no such option but if el is given to the constructor as part of the option object it replaces default value of the el property (but it cannot be an object of attributes, only a DOM node, selector or false).
- _parent
References a sqimitive that owns this object. If there's none is set to null. You can read this property but writing is discouraged because it may very well break integrity – use nest(), unnest() and others.
Non-owning sqimitives (see _owning) never change their children' _parent.
- _parentKey
When this object is owned by another sqimitive this property is set to the key under which it's listed in its parent's _children and which can be given to nested() and others. This is always a string or, for non-owned sqimitives – null along with their _parent.
- _children
An object with keys being nested children' _parentKeys (always strings) and values being the children themselves. Note that this is a purely formal nesting and doesn't dictate any DOM structure (children can have their el's outside of the parent's node).
It's recommended to access children using nested() and other methods instead of manipulating _children directly. Both _owning sqimitives and not list their children here.
See the children overview for examples.
- _owning
Specifies if sqimitive manages its children or not (by default it does). Managed (owning) parent means that all of its children know who owns them, under which key (see _parentKey) and makes sure they only have one parent – itself. Unmanaged (false) parent simply acts as a collection of children still providing filtering and other Sqimitive features but not imposing any structure onto its children, which do not even know that they are listed here. More details are found in the children overview.
Many properties and methods including _childEvents can be used in both modes.
- _childClass
Ensures _children contains instances of the specified class as long as children are only added via nest() is used and this property isn't changed on runtime. Is meant to be a sub/class of Sqimitive (this is not checked though). Defaults to Sqimitive.
var MyToDoItem = Sqimitive.Sqimitive.extend() var MyToDoList = Sqimitive.Sqimitive.extend({ _childClass: MyToDoItem, }) (MyToDoList).nest({}) // throws an exception.
- _childEvents
Can be overriden in subclasses to automatically forward() events occurring in any of _children to this instance, with event name prefixed with a dot (e.g. render → .render). Identical to manually calling on() and off() so can even specify methods that will be turned into events. Event handlers receive the child instance as first argument.
Don't list unnest() here – _parent will off() itself before that and never receive the notification. Use unnested() instead or -unnest (but in this case if an exception occurs during unnesting your handler won't know this and will be called before the view is removed).
See Children overview for a comprehensive example.
var MyList = Sqimitive.Sqimitive.extend() _childEvents: ['-change'], events: { '.change': function (sqim, name) { alert('Option ' + name + ' is about to change on ' + sqim._cid) }, }, })
With multi-level nesting you can forward already forwarded _childEvents just like that:
var MyListGroup = Sqimitive.Sqimitive.extend({ // Indicate this object nests MyList instances. _childClass: MyList, // MyList forwards '-change' on its children as '.-change' on itself so // we can foward that event too on this grouping instance. There's no // limit - '....-change' is perfectly fine and works on 4th nesting level. // Each forward gets originating object pushed in front so '..-change' gets // MyList as first argument. '...-change' would get (MyListGroup, MyList). _childEvents: ['.-change'], }) // Listening to '-change' that occurred on a MyList child, with MyList being // nested into MyListGroup. (new MyListGroup).on('..-change', function (listGroup) { ... })
- _respToOpt
Specifies the way external input object (e.g. API response) is transformed into options when assignResp() is called. set() is used to assign new values so normalization and change events take place as usual. _respToOpt is an object where keys are input keys and values are one of the following:
- false
- Input item is skipped regardless of options.onlyDefined.
- true
- Input item becomes value for the option by the same name.
- string
- Same as true but changes the option's name.
- function (respValue, key, resp, options)
- Flexible transformation – is called in this object's context and must return ['optToSet', value]. _respToOpt's key only determines respValue given to this function; the latter can access entire resp for other data (key argument holds the original _respToOpt's key), options is the object given to assignResp(). If optToStr is returned as false – the input item is skipped, otherwise it's option name to set value to.
Missing keys may or may not be passed through to this._opt unchanged – this depends on the options.onlyDefined flag of assignResp().
- el
After init() has ran is either a DOM node wrapped in $() or null if node creation was disabled. Not advised to change directly, treat as read-only.
When extend'ing this can be set to false (no element is created, this.el will be null – useful for data structures aka Models), a string (DOM selector) or an object of HTML attributes plus the following special keys:
- tag
- String – tag name like li. Defaults to div.
- className
- The same as class (CSS class) to work around the reserved word.
See Views overview for the high-level idea.
- elEvents
Lists automatically bound DOM event listeners for el. Format is inherited from Backbone and is an object with keys of click[ .sel .ector] form and values being functions (closures) or strings (method names, resolved when event occurs so they can be defined later, optionally masked like func-..1 - see expandFunc()). In the latter case be aware of the "Danger of args__" described here – it's called as function (eventObject).
Listeners are automatically rebound by attach(). See also attachPath.
elEvents is listed in _mergeProps so subclasses defining it add to their parents' events instead of overwriting them entirely.
var MyView = Sqimitive.Sqimitive.extend() el: {tag: 'form'}, elEvents: { // Attach listener to this object's el. submit: function (e) { e.preventDefault() // ... }, // React on change originating from an element with specific name attribute. 'change [name=login]': function () { this.$('[name=password]').val('') }, // Call render() whenever value of an element with name attribute changes. 'change [name]': 'render', // Masked callback - only gives first argument to _linkClicked(). 'click a': '_linkClicked.', }, })
- length
An integer specifying how many nested _children this object contains. Just like $(...).length or (new Backbone.Collection).length.
- constructor ([opt])
Triggers Core's constructor which assigns _cid, clones all but _shareProps, fires init which creates this.el, assigns jQuery el.data('sqimitive', this) (so you can reverse-lookup a Sqimitive instance from its DOM node – a deplorable practice) and calls set() to replace default values of this._opt according to opt (object, if given). Finally fires postInit which you should override instead of constructor to put your object initialization logic into. Both events receive the same arguments as the constructor was given, which in turn gets them from new.
opt can contain el to override default this.el (a DOM node or a selector but not an object of attributes). Note that el is not automatically attached anywhere after it's created, nor are its elEvents bound – call attach() for this.
- init ([opt])
Creates this.el, if necessary. Sets this._opt from object passed to the constructor, if any. See constructor description for details.
- postInit ([opt])
An event called after init() has done its job. Useful to add bindings to nodes and objects that have been created during init(). Is called once in each object's life.
var MySqimitive = Sqimitive.Sqimitive.extend({ _button: null, events: { postInit: function () { this._button = this.nest(new Button) }, }, })
- render ()
Placeholder for populating el with the actual contents of this instance (aka View). Default implementation re-inserts all nested Sqimitive under their corresponding attachPath's under this.el with attach(). It doesn't render them. Returns this.
- attach ([parent])
Appends el to parent (DOM selector or node). If no argument is given uses attachPath option (if present) to determine the parent (use this option to keep element always attached to a point in the DOM). If parent was changed recursively calls attach() on all children of self to rebind their DOM listeners (doesn't happen if no parent was found or this.el is already direct child of the found parent node so performance penalty of subsequent attach() calls is small).
Ultimately, clears existing event listeners and binds those defined in elEvents under .sqim-CID namespace.
sqim.attach('#nav') //= sqim.el.appendTo('#nav') sqim.attach('
') //= sqim.el.appendTo($('
')) or just appendTo('
') sqim.attach('#nothinghereever!') // does nothing sqim.attach() // uses sqim._opt.attachPath, if available sqim.attach(sqim.get('attachPath')) // the same
- get ([opt])
Reads one option named opt or, if there are no arguments – shallow-copies and returns all options (_opt) – it's safe to change the object itself (add/remove properties) but changing its values will indirectly change these options inside the sqimitive.
Don't access hits._opt directly because you will lose ability to change the "getter" behaviour – e.g. you can read non-existing options or transform them like this:
var MySetter = Sqimitive.Sqimitive.extend({ _opt: { foo: 'Foo!', }, events: { // Now any option can be read as option:up to turn its value into upper case. '+get': function (res, opt) { if (opt = opt.match(/^([^:]+):up$/)) { return this.get(opt[1]).toUpperCase() } }, }, }) // Popup: FOO! alert((new MySetter).get('foo:up'))
- set (opt, value[, options])
Same as ifSet() but returns this instead of true/false indicating if the new value was different from the old one or if options.forceFire was set (and so change events were fired).
Note: if you are overriding a "setter" you should override ifSet instead of set() which calls the foremer.
See ifSet() for details.
There's no standard set() version that writes multiple options at once. You might be looking for assignResp (useful when assigning a backend response) or $.each(opts, _.bind(model.set, model)).
- ifSet (opt, value[, options])
Writes one option (this._opt). First calls normalize_OPT on value, then fires change and change_OPT events if the normalized value was different (not _.isEqual()) or if options.forceFire was set. Returns true if events were fired (value differs or options.forceFire given).
options can be used to propagate custom data to event listeners on normalize_OPT(), change_OPT() and change().
It is safe to write more options from within ifSet(), normalize or change handlers – they are written immediately but consequent normalize/change events are deferred in FIFO fashion (first set – first fired).
See also set() and Options overview.
var MySetter = Sqimitive.Sqimitive.extend({ _opt: { readOnly: 'foo', }, events: { // Our handler will be called before inherited ifSet() which will prevent // modification of this._opt.readOnly when they are done via set/ifSet - // the recommended way. // // Make sure not to use '-set' though because set() calls ifSet() and it // would be still possible to change readOnly with ifSet('readOnly', 'bar'). '-ifSet': function (opt) { if (opt == 'readOnly') { throw 'You shouldn\'t really change what is called readOnly, you know?' } }, }, })
You can take advantage of ifSet()'s return value to perform interlocking operations (not necessary concurrently-safe but at least saving a call to get()):
if (sqim.ifSet('eventsBound', true)) { // eventsBound was previous false (not === true) and it was now changed to // true so we can do what we need, once. }
…as a short form of:
if (!sqim.get('eventsBound')) { sqim.set('eventsBound', true) // ... }
- normalize_OPT (value, options)
When setting any _opt, new value first gets normalized by calling function/event named this way, if it is defined. It's a good place to throw an error on wrong format too. options is an object with contents originally given to set() or ifSet(). There is no global normalization function but you can override ifSet() for this purpose.
var MyNorm = Sqimitive.Sqimitive.extend({ _opt: { stringie: '', }, // Now stringie is guaranteed to have no surrounding whitespace and be // lower case - as long as it's not written directly as this._opt.stringie. normalize_stringie: function (value) { return _.trim(value).toLowerCase() }, }) // Popup: foo alert( (new MyNorm).set('stringie', ' Foo\n').get('stringie') )
- change_OPT (value, old, options)
When new normalized option value is different from current one (given as old), an event named this way gets called after writing the value to _opt. options is an object with contents originally given to set() or ifSet(). See also change() that gets called after any change (after corresponding change_OPT).
If you refer to the handler by its string name as in the example below – be aware of the "Danger of args__" described here.
var MyNorm = Sqimitive.Sqimitive.extend({ _opt: { caption: '', }, events: { // When caption is changed - calls render() to update the interface. change_caption: 'render', }, })
Here is how you can propagate custom options:
sqim.on('change_foo', function (value, old, options) { if (!options || !options.noSync) { $.ajax({ url: 'update', type: 'POST', data: this.get(), }) } }) // The handler above performs an AJAX request: sqim.set('foo', 123) // But not now: sqim.set('foo', 123, {noSync: true}) // assignResp() passes options given to it through to set() so the handler // doesn't perform the request: sqim.assignResp({foo: 123}, {noSync: true})
- change (opt, value, old, options)
Gets called after each change_OPT with the same parameters as it and the changed option name put in front. See its description for details.
- nest ([key,] sqim[, options])
Adds new contained Sqimitive instance sqim to self. Unless this._owning is false, one Sqimitive can only have one parent or none thus forming a bi-directional tree (more details here). If key is omitted _defaultKey() is called to determine it, which by default returns sqim._cid (unique instance identifier). Updates sqim._parent and _parentKey. options are currently unused but can be used to propagate custom data to event listeners (it's also passed through by assignChildren()).
Errors if trying to nest object of wrong class (not this._childClass). Unnests sqim from its former parent, if any. Forwards its events according to this._childEvents. Finally, calls sqim.owned() to notify new child of the parent change.
Errors if key is undefined, null or object (as given or returned by _defaultKey()). Converts key to string.
Returns sqim. Does nothing if it's already contained in this instance under the same key (if key differs removes and nests sqim again).
There's no standard nest() version that adds multiple child objects at once. You might be looking for assignChildren() (useful when assigning a backend response).
sqim.nest(new Sqimitive) // _parentKey = 'p123' sqim.nest('key', new Sqimitive) // _parentKey = 'key' sqim.nest('key', null) // If sqim._owning is false - removes and returns the child under 'key', if any. // For _owning sqim this call form will produce an exception.
When listening to nest as an event and if you need to retrieve the nested object – use +nest or =nest event forms. This will pass result returned by nest() – the child – as the callback's first argument. With nest or -nest first argument will be whatever was given to nest() – which might be sqim but might also be key.
sqim.on('+nest', function (nested) { alert(nested._cid) }) sqim.on('=nest', function (sup, nested) { var nested = sup(this, arguments) alert(nested._cid) return nested }) // In contrast... sqim.on('nest', function (nested) { console.dir(nested) }) // Everything's okay. sqim.nest(new MySqimitive) // But not now - 'nested' above is string "key". sqim.nest('key', new MySqimitive)
- _defaultKey (sqim)
Is called when nest() wasn't given an explicit key to determine one. sqim is the sqimitive that is about to be nested into this instance. Similar to Backbone's idAttribute.
If you're trying to index children by some "ID" attribute (like Backbone's Collection) note that _parentKey will not be auto updated if that attribute changes. You should react to the change yourself, for example:
var MyCollection = Sqimitive.Sqimitive.extend({ _childEvents: ['.change_id'], events: { // '.change_id' will only occur for models which _parent is this. // They will be re-nested with nest() expanding to // nest(this._defaultKey(sqim), sqim) which is like nest(newID, sqim). // This will cause sqim to be unnest()'ed and then nested with the new key. '.change_id': function (sqim) { this.nest(sqim) }, }, _defaultKey: function (sqim) { return sqim.get('id') }, })
- owned ()
Is called after this instance has been nest()'ed into an _owning sqimitive (changed parents/got a first parent). this._parent and _parentKey are already set. Takes no arguments, can return anything. Not to be called directly.
See also unnest() that gets called before parent is changed/removed.
var MyChild = Sqimitive.Sqimitive.extend({ events: { // Will append this.el to parent's .some-point node as soon as this // instance gets a new parent. owned: function () { this.attach(this._parent.$('.some-point')) }, }, })
- _forward (prefix, events, sqim)
Forwards each of events to sqim instance by firing prefix + event_name on this instance (the one _forward is called on) with sqim pushed in front of original event arguments. This is used to forward _childEvents, with prefix = ..
For example, origin._forward('dlg-', ['change', 'render'], destination) will fire dlg-change and dlg-render events on destination (a Sqimitive) whenever change and render are fired on origin.
- unlist (key)
If this._owning is unset, unnests a child by its key or instance (does nothing if this key/child is not contained). If _owning is set (it is by default) checks if key is nested and if it is calls remove().
Returns the unnested sqimitive or undefined.
In contrast to unnest() this method is called on the parent object because there's no reverse child → parent relationship in non-owning mode.
collection.unlist('foo') //=> Sqimitive or undefined collection.unlist(collection.nested('foo')) // identical to above collection.unlist(child) //=> Sqimitive (child) or undefined
- unnest ()
Removes this instance from its parent object, if any. This effectively creates new detached tree (if this has nested objects) or leaf (if not) – see more about the Children concept. It may be called even if this._parent was already null. Calls unnested(this) on former parent.
Note that it doesn't remove this.el from its parent node – use remove() for this.
var MyChild = Sqimitive.Sqimitive.extend({ events: { '-unnest': function () { // this._parent and _parentKey are still set if this instance was // attached to any parent, otherwise they are null. this._parent && alert("I am about to be... unnested! :'(") }, unnest: function () { // At this point _parent and _parentKey are certainly null but there's // no telling if they were so - or if this instance had any parent before // unnest() was called. alert('I am now as free as the wind!') }, // In contrast to the above, here we can reliably determine if this // sqimitive was previously nested and if it was - do something after // it was unnested by calling standard handler. '=unnest': function (sup) { var hadParent = this._parent sup(this, arguments) hadParent && alert('I was abducted but you saved me!') return res }, }, })
- remove ()
Similar to unnest() but before unnesting removes this.el from its parent DOM node. Note that this doesn't recursively remove all nested _children as it might not be desired and slow; if they need to do some on-removal actions like removing event handlers – you can do this.sink('remove') (recursive).
var child = new Sqimitive.Sqimitive({el: '
Some text.
'}) var parent = new Sqimitive.Sqimitive({el: '
'}) parent.nest(child).attach(parent.el) // Now we have

Some text.

. child.unnest() // Now we still have

Some text.

 even though // child is no more listed under parent._children. // But should we have done this... child.remove() /// ...as we would have

 with child removed both // from parent's children list and its el.
- unnested (sqim)
Is called right after a child sqim was detached from its parent (this). Default implementation unregisters all event handlers that might have been previously attached to sqim by this object (provided they were not hardwired with fuse() and used cx === this). At this point sqim._parent and sqim._parentKey are already null.
If this._owning is false this is called when unnesting a child via nest('key', null).
Can return anything. Not to be called directly.
var MyParent = Sqimitive.Sqimitive.extend({ events: { unnested: function (sqim) { alert('Where thou go, my ' + sqim._cid + '...') }, }, })
- nested ([key])
Returns all nested sqimitives if key is not given, or a child by its key (_parentKey, case-sensitive) or its object instance. Returns undefined if given key/object key isn't nested in this instance or key is null or undefined.
sqim.nested() //=> {childKey1: Sqimitive, ...} sqim.nested('childKey1') //=> Sqimitive sqim.nested('foobarbaz!') //=> undefined var child = sqim.nested('childKey1') sqim.nested(child) //=> child sqim.nested(new Sqimitive) //=> undefined - argument not listed in sqim._children
- slice (start[, length])
Regular Array.slice(), treats this instance's children as an ordered array instead of {key: Sqimitive} object.
sqim.slice(1, 1) // get 2nd child as an array sqim.slice(0, -1) // get last child as an array; last() is more convenient sqim.slice(5, 3) // get 6th, 7th and 8th children as an array sqim.slice(0, 0) //=> [] - empty array
- at (index)
Similar to slice() but returns individual children, not an array.
sqim.at(0) //=> Sqimitive sqim.at(999) //=> undefined sqim.at(-1) //=> Sqimitive - identical to last()
- assignChildren (resp[, options])
Merges external "response" object/array resp into _children by updating existing nested sqimitives, adding new and removing unlisted ones. New sqimitives are created as _childClass.
If resp is an object with data key – uses its value (Python's Flask wraps array response into an object to prevent a JS attack). If resp (or resp.data) were not arrays uses _.values() to turn it into one (ignoring keys).
If options.eqFunc is null or omitted removes all children thus resetting the list. options.eqFunc can be a string (option name) given to get(), or a function (existingSqim, {opt}) returning true if existingSqim is the "same" as given opt object (a resp item) so the former should be retained and updated with assignResp(). Children that have never matched options.eqFunc considered not listed in given resp and removed unless options.keepMissing is set (if so they are just left unchanged).
If options.keepMissing is set while options.eqFunc is not – existing children are preserved and for each item in resp a new child is nested. Be aware that on duplicate keys (see options.keyFunc) only the last child will be kept, all others will be removed.
options.keyFunc is a function (sqim) that should return a _parentKey for given sqimitive that is about to be nest()'ed. If not present defaults to _defaultKey (returns _cid by default).
options as a whole is passed through to assignResp() and nest() so you can use it to set their options and to pass custom data along to your event listeners as they are eventually passed through to set(), change_OPT() and others.
There's no standard assignChildren() version that takes ready-made objects (like Backbone's Collection.set()) because many questions arise: do you need to keep old duplicate children or replace them with supplied objects and what to do with event listeners on the old children if you do, or with listeners on the new if you don't, what to consider "duplicate" (some ID attribute? exact same object?), do you want to keep old attributes, etc. Instead of making Sqimitive figure them you should just do exactly what you need.
To demonstrate most common cases:
assignChildren({data: [ {key: 'foo'}, {key: 'bar'} ]}, {eqFunc: 'key'}) // inputs 2 children with _opt = {key: foo} and {key: bar}; // "same" sqimitives are those having the same 'key' option. assignChildren({data: [ {key: 'foo'}, {key: 'bar'} ]}, {eqFunc: function (sqim, opt) { return sqim._opt.key == opt.key }}) // identical to the above. assignChildren([ {key: 'foo'}, {key: 'bar'} ], {eqFunc: 'key'}) // identical to the above (resp not wrapped in 'data'). assignChildren({ foosh: {key: 'foo'}, barrsh: {key: 'bar'} }, {eqFunc: 'key'}) // identical to the above (resp turned to array, keys ignored). assignChildren({ foosh: {key: 'foo'}, barrsh: {key: 'bar'} }, {eqFunc: 'key', keepMissing: true}) // identical to the above but keeps old children. assignChildren([ {key: 'foo'}, {key: 'bar'} ]) // similar but removes all nested sqimitives and adds 2 new ones. assignChildren(..., {onlyDefined: true, forceFire: true}) // passes onlyDefined to assignResp() and forceFire - to set()
A complete example:
var MyList = Sqimitive.Sqimitive.extend({) var MyItem = Sqimitive.Sqimitive.extend({ _opt: { foo: '', }, }) var list = new MyList var item1 = new MyItem({foo: 'first'}) list.nest(item1) var resp = [{foo: 'incoming#1'}, {foo: 'first'}] list.assignChildren(resp) // item1 was removed from list, which in turn got two new items: // 'incoming#1' and 'first' (the latter having identical _opt with the // removed item but being a newly created and nested object still). // Let's restore the list. list.invoke('remove').nest(item1) list.assignChildren(resp, {eqFunc: function (sqim, opt) { return sqim.get('foo') == opt.foo }}) // Now item1 wasn't removed because the function we passed compared its foo // option with resp's foo value and got a match. In addition to item1, list // got one new item - 'incoming#1'. // But we could have used this short form too if we're simply matching some // option with the same member in resp: list.assignChildren(resp, {eqFunc: 'foo'}) list.assignChildren([]) // The list was cleared as there were no items in resp.
This method is named "assign" to emphasize that data may undergo transformations before being assigned by the sqimitive.
- assignResp (resp[, options])
Filters and/or transforms external input (e.g. API response) into this._opt using defined rules (see _respToOpt). Calls set() to assign resulting values one by one, normalizing them and firing corresponding change events.
If options.onlyDefined is set then keys in resp that are missing from _respToOpt are ignored, if it's unset they are passed through as if they were all true.
options as a whole is passed through to set() so you can use it to pass custom data along to your event listeners on normalize_OPT(), change_OPT() and change().
Subclass can override this method to force certain value of options.onlyDefined like this:
var MySqimitive = Sqimitive.Sqimitive.extend({ events: { // With this override any call to assignResp() will omit resp keys // that are not present in this._respToOpt. '=assignResp': function (sup, resp, options) { return sup(this, resp, _.extend({}, options, {onlyDefined: true})) }, }, })
Here is how this method in conjunction with _respToOpt can be used to blend some JSON backend response into the sqimitive:
var MyModel = Sqimitive.Sqimitive.extend({ _opt: { date: new Date(0), id_key: 0, }, _respToOpt: { // This will transform incoming value into a Date object. date: function (value, key) { return [key, new Date(value)] }, // Pass through the value as it is. id_key: true, }, events: { change_id_key: function () { ... }, normalize_id_key: function (value) { value = parseInt(value) if (isNaN(value)) { throw 'What kind of ID is that?' } return value }, }, }) var sqim = new MyModel // Since regular set() is used to assign new values both normalize_id_key // and then change_id_key are called. As a result we get clean _opt. sqim.assignResp({date: '2000-01-01', id_key: '123', bazzz: 'Ouch!'}) sqim.get() == {date: Date('2000-01-01'), id_key: '123'} // date was turned to Date with the transformation function in _respToOpt. // id_key was turned to number thanks to normalize function we defined. // bazzz was ignored because onlyDefined wasn't set.
This method is named "assign" to emphasize that data may undergo transformations before being assigned by the sqimitive.
- bubble (event, args, fireSelf)
Sends event with arguments args upstream – triggers it on this._parent, then on that parent's parent and so on. If fireSelf is true fires event on this beforehand (by default it isn't). Since it calls methods, not necessary events, you you "recursively" call methods as well. This is very much like DOM's event bubbling except that it happens on sqimitives, not their el's. Returns this. See also sink() that works the opposite direction.
// Causes self and all parent sqimitives to be rendered: sqim.bubble('render', [], true) // Recursively call invoke('render') on all parents. sqim.bubble('invoke', ['render']) // We can use it to obtain data from owning sqimities too: var out = {} sqim.bubble('giveMeData', [out]) alert(out.data) // The above will work if any parent has a handler like this: parent.on('giveMeData', function (out) { out.data = 'here goes' })
- sink (event, args, fireSelf)
Propagates event with arguments args to all nested sqimitives, to all nested sqimitives of those sqimitives, to their children and so on. If fireSelf is true also fires event on this beforehand. Note that it might get quite intense with heavy nesting. Returns this. Since it calls methods, not necessary events, you you "recursively" See also bubble() that works the opposite direction.
// Recursively causes all nested views and self to be rendered. sqim.sink('render', []. true) // Recursively call remove() on self, all children and their children, removing // every single sqimitive from its parent (not necessary practical, just an // illustration). sqim.sink('invoke', ['remove'], true) var serialized = []; sqim.sink('saveTo', serialized) // Now if children implement something like this serialized will contain // a portable representation of the current hierarchy: child.saveTo = function (ser) { ser.push(this.get()) }
- $ (path)
Similar to this.el.find(path) but returns el if path is empty or is a dot (.). If this.el is null always returns an empty jQuery collection. If path is a jQuery object or a DOM node – returns $(path) (note that it may be outside of this.el or have length == 0).
sqim.$() //=> $(this.el) sqim.$('.') //=> $(this.el) sqim.$('a[href]') //=> $([A, A, ...]) sqim.$(document.body) //=> $('body') sqim.el = null sqim.$('') //=> $()
Underscore.js functions
Each Sqimitive.Sqimitive instance inherits a bunch of Underscore.js functions – those used to sort, filter, transform, count, locate, cook, boil, slice up and serve nested children into an object or array: sqim.invoke('render').
The following methods are available (links will take you to Underscore.js docs in a new tab):
- chain() – wrap list of children into an Underscore object on which you can call other Underscore methods that will modify that list, which you can then retrieve with sqim.chain()....().value()
- contains(sqim) – return true if there's a nested sqim instance
- countBy(iterator[, cx]) – same as groupBy() but returns count of children instead of lists: {gr1: 3, seco: 1, ...}
- difference([sqim, sqim, ...]) – same as without() but accepts one or more arrays
- each(iterator[, cx]) – call iterator(sqim, parentKey) on each children
- every(predicate[, cx]) – return true if for every child predicate(sqim, parentKey) was positive
- filter(predicate[, cx]) – return an array of all children for which predicate(sqim, parentKey) was positive (similar to pick() that returns an object)
- find(predicate[, cx]) – return the first child for which predicate(sqim, parentKey) was positive or undefined if none matched
- findWhere(props) – return the first child that have equal properties; don't use this to compare _opt for the same reason as with pluck() above
- first() – first nested child or undefined
- groupBy(iterator[, cx]) – put each child under the object's key named after iterator(sqim, parentKey) (iterator can be a string – property name) and return that object: {gr1: [Sqimitive], seco: [...]}
- indexBy(iterator[, cx]) – same as groupBy() but for unique groups; returns {gr1: Sqimitive, seco: Sqimitive, ...}
- indexOf(sqim) – determine the ordinal number of sqim or -1 if it's not nested
- initial([n]) – all but last n (1) children
- invoke(method[, arg1, arg2, ...]) – call method on each child, giving it passed arguments, put each result into an array and return it (similar to map())
- keys() – array of all childrens' _parentKey's
- last() – last nested child or undefined
- lastIndexOf(sqim) – same as indexOf() but counts from the end
- map(iterator[, cx]) – call iterator(sqim, parentKey) on each children, push result onto an array and return them
- max(iterator[, cx]) – rank all children by calling iterator(sqim, parentKey) and return the "highest" one
- min(iterator[, cx]) – same as max() but returns the "lowest" child
- omit(key[, key, ...]) – the opposite of pick()
- pairs() – array of [parentKey, sqim] items for all children
- partition(predicate) – split all children into two groups (first matching predicate, second – not); return both as an array
- pick(key[, key, ...]) – all children with _parentKey's matching any given argument; key can also be a single array of keys or a function (sqim, parentKey) returning true for items to be put into result (this form is similar to filter() that returns an array). Attention: parent keys are always string; doing sqim.pick(1) will never return results – do sqim.pick('1') instead
- pluck(prop) – picks prop property out of every child, push it onto an array and return them. You shouldn't use this to get _opt – use picker('get', 'opt') and map(), or get('opt') and invoke() instead
- reduce(iterator, memo[, cx]) – reduce the list of children to a single value by calling iterator(memo, sqim, parentKey)
- reduceRight(iterator, memo[, cx]) – same as reduceRight() but goes from the end
- reject(predicate[, cx]) – the opposite of filter()
- rest([n]) – all but first n (1) children
- sample([n]) – return random n children as an array or just one as it is if n isn't given
- shuffle() – get shuffled array of children instances
- some(predicate[, cx]) – return true if for any child predicate(sqim, parentKey) was positive
- sortBy(iterator[, cx]) – sort and return all children by ranking them with iterator(sqim, parentKey) from lowest to highest
- sortedIndex(sqim[, iterator[, cx]]) – determine index for item insertion that will maintain certain list sorting
- toArray() – return all children as an array copy – similar to nested() with no arguments but without keys
- where(props) – return only children that have equal properties; don't use this to compare _opt for the same reason as with pluck() above
- without(sqim[, sqim, ...]) – all children except those given as arguments