Examples, tips & tricks

This section is a collection of practical snippets and recipes demonstrating how Sqimitive addresses real-world, day-to-day problems.

Figuring “class name”

With its concept of “functions as first-class citizens”, JavaScript lacks any kind of “class name” references in instantiated objects. The best we can afford is duck-typing: if we see an object X having properties Y and Z the we suspect it’s that kind of object (i.e. of that “class”).

Quite often, while debugging we need to figure what’s the object we see, reliably. 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 some property, let’s say “sqClassName”, holding string reference to that prototype, like MyApp.Collections.FooBar.

First, let’s override Core::extend() with our own version that will mark each produced prototype so we know we’re looking at something we have created:

var BaseSqimitive = Sqimitive.jQuery.extend()

BaseSqimitive.extend = function () {
  var child = Sqimitive.jQuery.extend.apply(this, arguments)
  // If you're strictly ES6+ then use Symbols.
  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 can 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:

This technique allows attaching custom info to every class (prototype) but is fairly slow to enable in production. It’s recommended that you pass your “class name” (or other string) directly to Core::extend() – it’s zero-overhead and will appear in debugger too:

MyAppRoot.BaseView.SubView = MyAppRoot.BaseView.extend('BaseView.SubView', {
  // ...
})

// MyAppRoot.BaseView.SubView is a constructor, i.e. a function, and so has a
// name property:
MyAppRoot.BaseView.SubView.name     //=> 'BaseView.SubView'
  // some browsers even show the function's name when you hover or print such
  // an object

var sqim = new MyAppRoot.BaseView.SubView
sqim.constructor.name               //=> 'BaseView.SubView'

Countdown class

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. You can use native Promise objects or their “fractal” form (Sqimitive\Async), or you can write a simple class for counter-based synchronization:

// We need no associated el object so we extend Base and don't depend on
// jQuery.
var Countdown = Sqimitive.Base.extend('Countdown', {
  _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.Base.stub,
  error: Sqimitive.Base.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
  },
})

Its usage is straightforward:

var images = ['pic1.jpg', 'pic2.jpg']

var countdown = new Countdown({count: images.length}, function () {
  // Executed 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 an arbitrary
  // context.
  img.onload = countdown.dec
  img.src = path
})

Activity pipeline class

At other times, you might need complex synchronization, like Sqimitive\Async or Promises but with multiple stages that should be easy to override, e.g. in a subclass or by outside listeners.

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: the process begins with prereq(), proceeds to passthru(), then to transition() and finishes with done(). If the action has been stopped then cancel() occurs at any point. They are all methods but can be turned into events by overriding them with on() according to Sqimitive’s event model (evt).

An instance of this class can have only one stage active at a given time and so it ignores multiple start() calls until done() or cancel() are reached. done callbacks given during a single run are retained and called upon completion.

var Activity = Sqimitive.Base.extend('Activity', {
  _done: [],

  _opt: {
    cx: null,
    active: false,
  },

  // (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.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) {
    func && 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
  // currently active or calls func right away if not, without starting up.
  enqueue: function (func, cx, always) {
    cx = cx || this.get('cx') || this
    ;(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.jQuery.extend({
  _activity: null,

  elEvents: {
    'click .close': function () {
      this._activity.start(function () {
        alert('Completely went away...')
      })
    },
  },

  events: {
    init: function () {
      this._activity = new Activity({
        cx: this,
      })

      // You don't have to implement every method down here, this is just an
      // example.
      this._activity.on({
        passthru: function () {
          if (this.el(':visible')) {
            this.transition()
          } else {
            this.done()
          }
        },

        transition: function () {
          // Since Activity's event handlers are called with the activity's
          // context we can use this.get('cx') to access this page instance.
          this.get('cx').el.fadeOut(_.bind(this.done, this))
        },

        done: function () {
          this.get('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 starting the activity and postpone it if it is active – use enqueue():

this._activity.enqueue(function () {
  // At this point it's guaranteed that the activity is no more/was not
  // running.
})

Defined in: HELP.chem, lines 1090-1372 (283 lines)