• log out

Tools

Tools are reusable components that you place on pages. They are designed to be self-contained and "just work", making use of PHP actions and streams to communicate with the server.

Someone with very little programming experience can construct web apps out of existing tools. Developers can construct their own tools and package them into plugins for others to install and use.

Rendering tools

To render a "Streams/chat" tool in PHP, you simply do

<?php echo Q::tool("Streams/chat", $options) ?>

and it is rendered as a <div> in the appropriate place on the page. The $options are exported to the client side as JSON in a data- attribute. When Q.activate() traverses the page, it finds this div and runs the tool's constructor, passing the options to it.

For pages with relatively static layouts, this can be enough. It also has the advantage of automatically preloading all the necessary js and css files, to avoid a flash of unstyled content. However, there are times you will want to activate tools dynamically in Javascript. This is done as follows:

Q.activate(
  container.appendChild(
    Q.Tool.setUpElement(
      "div", // or pass an existing element
      "Streams/chat",
      options
    )
  )
);

Tools are similar to "components" in React or "directives" in Angular. You can activate more than one tool on an element, simply by running the above JS code on an existing element. Or, in PHP, you would do

<?php echo Q::tool(array(
  "Q/tabs" => $options1,
  "Streams/related" => $options2
)); ?>

In this case, tools can be thought of as "adding behaviors" to existing elements. You can even add them similarly to directives in Angular, and they will be activated when Q.activate() is called on their container:

<div class="Q_tool Q_clickable_tool Streams_chat_tool">
The Q/clickable behavior is activated on this element,
and then a Streams/chat tool is constructed in it.
</div>

Referencing tools

Given an element, you can reference its associated tools by name. (If exactly one tool has been activated on this element, you can omit the tool name.)

var chat = Q.Tool.from(elem, "Streams/chat");

Each rendered tool gets a unique id, which can be used to refer to it like this:

Q.Tool.byId(toolId)

When tools are rendered with PHP, the parent tool's id serves as a prefix to the ids of child tools. This enables functions such as tool.children() to work without traversing the DOM.

However, when rendering two or more tools of the same type with PHP, one after the other, it's up to you to provide an additional piece of data to tell Qbix how you want to name the tools. For example:

<?php foreach ($notes as $note) {
  echo Q::tool(
    "First/note", $note, $note['id']
  );
} ?>

For tools created dynamically with JS, you can either let Qbix specify the id or provide extra information:

container.appendChild(
  Q.Tool.setUpElement(
    "div",
    "First/note",
    note,
    note.id, // optional
    parentTool.prefix // optional
  )
);

Replacing HTML content

To replace content of elements in a way that properly removes all activated tools, you should use Q.replace(existing, source). In fact, when new pages are loaded, such as with Q.handle(url), the Q.loadUrl.defaultHandler uses this function to replace the content of the slots.

If a tool's element has a data-Q-retain attribute, it will be retained instead of being replaced. This will preserve all the DOM elements inside it, and any associated objects such as jQuery event listeners, data and other things.

Once you've replaced the content of an element, it is likely that the new HTML has tools that need to be activated. So, most calls to Q.replace should be followed by Q.activate, like so:

var elem = Q.replace(container, source);
Q.activate(elem, function () {
  // all the new tools have now been
  // activated, except the ones that
  // were retained from before.
});
or using jQuery:
$(foo).tool(toolName, options).activate();

Tool state

A tool's state starts out as the options passed to its constructor (merged on top of the default options). From there, the tool may modify the state from time to time.

Unlike other MVC frameworks like Angular and Ember, Qbix provides an explicit method for signaling that a tool's state has changed:

tool.state.pubId = 'foo';
tool.state.streamName = 'bar';
tool.stateChanged(['pubId', 'streamName']);

Rather than traversing the entire state during a "digest" cycle, or giving you special set/get methods, Qbix expects the code which changes a tool's state to call stateChanged when it's done making an atomic change. This works together with the rendering method of tools, to efficiently update the DOM.

Designing your own tools

Typically, the way you'd design a tool is to design its front-end first. This will allow developers to instantiate it dynamically on the client, e.g. $(foo).tool(toolName, options).activate(); as well as render it on the server, e.g. echo Q::tool($toolName, $options). You can then implement your own server rendering of the tool with PHP.

A tool has a constructor that takes options. As with other functions in Qbix, you can specify default options for the tool constructor. Typically, you would define each tool in its own file, named after the tool, like so:

First/web/js/tools/photo.js
========================

(function ($, window, undefined) {

/**
 * First/photo tool.
 * Put the description here
 * @module First
 * @class First photo
 * @constructor
 * @param {Object} options
 *  Hash of possible options
 * @param {Boolean} [options.editable]
 *  If the user can edit the photo
 * @param {Object} [options.editable]
 *  Options for Q/imagepicker
 * @param {Boolean} [options.showFile]
 *  Optional. Blabla.
 * @param {String} [options.throbber]
 *  The url of the "loading" image
 * @param {Object} [options.templates]
 *  Information for various templates
 * @param {Object} [options.onRefresh]
 *  Tool refreshed event
 * @param {Q.Event} [options.onCreate]
 *  New photo created event
 * @param {Object} [options.onUpdate]
 *  Existing photo updated event
 */
Q.Tool.define("First/photo", function(options) {
  
  if (!options.something) {
    throw "First/photo tool missing something";
  }

  // proceed to construct the tool
  var tool = this;
  var state = tool.state;
  
  // draw the tool, see method below
  this.refresh();

},

{ // DEFAULT OPTIONS
  editable: false,
  imagepicker: {
    showSize: "x200",
    fullSize: "x"
  },
  showFile: null,
  throbber: "Q/plugins/Q/img/throbbers/bars32.gif",
  templates: {
    view: {
      dir: 'views',
      name: 'First/photo/view',
      fields: { 
        alt: 'image', 
        titleClass: '', 
        titleTag: 'h2'
      }
    }
  },
  onCreate: new Q.Event(),
  onUpdate: new Q.Event(),
  onRefresh: new Q.Event()
},

// OPTIONAL: LIMIT WHAT OPTIONS
// SHOULD BE COPIED TO TOOL STATE
// (OTHERWISE ALL ARE)
["editable", "onCreate"],

{ // YOUR TOOL'S METHODS
  refresh: function (callback) {
    var tool = this;
    var state = tool.state;
    // code to refresh the whole tool
    // then e.g. trigger an event
    Q.Template.render(
      state.templates.view.name,
      function (err, html) {
        tool.element.innerHTML = html;
        Q.activate(tool.element,
        function () {
          // save some references for later
          tool.$abc = tool.$('.First_abc');
          // trigger event for others to hook
          tool.onRefresh.handle.call(tool);    
          // also see tool.rendering()
        });
      }
    );
  },
  
  // another example method:
  abcScroll: function (x, y) {
    tool.$abc.scrollTo(x, y);
  },
  
  // optional methods for your tool
  // that would be called by Qbix
  Q: {
    onInit: function () {
      // occurs after onInit
      // of all child tools
    },
    beforeRemove: function () {
      // clean up anything you've attached
      // to the elements, such as
      // jQuery event handlers, data, etc.
    },
    onRetain: function (newOptions) {
      // compare newOptions to this.state
      // and update the tool's appearance.
      // after this event, the tool's
      // state will be extended with
      // the new options.
    },
    onLayout: function (elem, container) {
      // Occurs if the layout is being
      // updated for this tool's element.
      // If you want more fine-grained control
      // then use Q.onLayout(element) instead.
    }
  }
}

);

Q.Template.set(
  'First/photo',
  '<img src="{{& src}}" alt="{{alt}}">'
  + '<div class="{{class}}">{{& title}}</div>'
);

})(window.jQuery, window);

Before the constructor runs, the passed options are merged on top of the default ones, and a deep copy is made and stored in tool.state, so the tool can use and modify its state after it's constructed. The "STATE KEYS" array is optional — if it is omitted, then all the options are copied to the tool state.

Your app typically comes with a main module javascript file, which among other things defines where to find the tools:

First/web/First.js
==================

// among other things...

Q.Tool.define({
	"First/photo": "js/tools/photo.js",
	"First/album": "js/tools/album.js"
});

That's all you need! Now when Qbix is asked to render the "First/photo" tool, it loads the correct file and runs the constructor.

Your tools can also require other tools to be activated on the same element. This is how "base class behaviors" are implemented on Qbix. All you have to do is pass the name(s) of the required tools as a string or array:

Q.Tool.define(
"First/photo/preview", ["Streams/preview"], 
function () {
	// your constructor
}, ...);

Updating the DOM in your own tools

Qbix apps are encouraged to be efficient, and updating the DOM is no exception. Unlike libraries like React and Mithril, which expect you to regenerate the entire "virtual DOM" every time something changes, Qbix lets you explicitly declare what to do after changes have been signaled by calls to tool.stateChanged('a,b,c'). The tool.rendering(...) function is designed for updating the DOM as a function of the tool's state. Here is what you should do in the tool's constructor:

tool.rendering(['base','exponent'],
function (changed, previous, timestamp) {
  // changed - fields that really changed
  // previous - their previous values
  // timestamp - from requestAnimationFrame.
  
  // Go ahead and update the DOM!  
  // It's a good idea to store references
  // to DOM elements instead of searching for
  // them every time.
  
  // DO NOT READ from the DOM here,
  // so as to avoid layout thrashing.
  // DO NOT modify tool state here.
  
  this.someElement.innerHTML = "Result: " +
  Math.pow(changed.base, changed.exponent);
});

It's that easy. By default, Qbix defers the execution of your rendering handler until the next animation frame after the change occurred. If several more calls to tool.stateChanged happened in the meantime, Qbix aggregates all the changes and reports the end result to the rendering handler.

Sometimes, if you want to use libraries such as FastDOM or GSAP in your rendering handlers, you may want to skip waiting for the animation frame:

tool.rendering(fields, callback, null, true);

To summarize, tool.refresh() is typically the name of the method to refresh the entire tool, and is usually called by its constructor to render the tool in the first place. But for efficient updates when the tool's state changes, use tool.rendering(). To be even more thorough, you should implement tool.Q.onRetain to handle when a tool is being retained while HTML around it is being replaced. Implement tool.Q.onLayout to handle times when the layout might need to be updated (e.g. browser window has been resized).

Handling events in your own tools

Just as with pages, you often add event handlers which you want to remove when a tool is removed. To do this, simply pass the tool object instead of the key, like this:

function handler() { }
event.set(handler, tool);
event.add(handler, tool);
$('a').on('click', tool, handler);
$('a').bind('click', tool, handler);

This way you don't have to explitly remove the handler. Currently this only works with tools defined with Q.Tool.define().

Retaining tools

From time to time, Q.replace() may be called (e.g. when the page changes), and it will replace the content of some DOM elements with new HTML. This new HTML may contain instructions to activate a tool with the same id as the one that was just yanked out of the DOM, but different options. In this case, it will trigger an event: tool.Q.onRetain(newOptions). This is your chance to compare the new options to the tool's current state and make whatever updates you want. After this event, the tool's state will be extended with the new options.

Rendering tools in PHP

We have seen that tools in Qbix have a Javascript constructor used to activate them. But you might also want to provide a PHP handler to render the tools on the server side, for web crawlers and other cases where Javascript might not be available.

Best practices

Here is how you would refer to tools on the page, in your code:

tool = Q.Tool.byId(id);
tool = Q.Tool.from(element, toolName);
tool = Q.Tool.from(elementId, toolName);
tool = Q.Tool.from($jQuery, toolName);

In order to keep your web apps efficient, you will want to access the DOM as little as possible. All the following, except the last one, are designed to let you work entirely with Tools in javascript without needing the DOM:

tool.id // the id of the tool
tool.prefix // used in some cases
tool.state // the state of the tool
tool.element // the root element of the tool
tool.remove() // removes a particular tool
tool.Q.beforeRemove // Q.Event
tool.children() // array of child tools
tool.child(append) // particular child
tool.parent() // immediate parent
tool.parents() // chain of parents
tool.$('a') // jQuery selector in tool element

Use the following to automatically remove and replace tools inside HTML:

Q.Tool.remove(container); // remove element
Q.Tool.clear(container); // remove only contents
Q.replace(
  existing, // HTMLElement in the DOM
  source // String or HTMLElement
);

Use the following to construct tools on elements at runtime:

Q.activate(container); // activates tools inside
$someJQuery.activate(); // same
Q.Tool.setUpElement(...); // returns HTMLElement
Q.Tool.setUpElementHTML(...); // returns String

For example, you could do this:

$(Q.Tool.setUpElement(...)) // set up element
.appendTo(container) // insert it in DOM
.activate(); // activate all the tools inside

These events let you take additional actions when things happen to tools:

// referring to a particular tool
tool.Q.onActivate // before children activated
tool.Q.onInit // after children & siblings inited
tool.Q.forEachChild(name, levels, callback) 

// by name or id of future or existing tool
Q.Tool.onActivate(nameOrId) // tool activated
Q.Tool.onInit(nameOrId) // tool initialized
Q.Tool.beforeRemove(nameOrId) // before removal

// Related to loading constructors
Q.Tool.onMissingConstructor
Q.Tool.onLoadedConstructor(toolName)

In the methods above, you can provide the name of the tools (such as "Streams/chat") or the id of a particular tool (such as "id: Streams_chat_foo").

As with the rest of Qbix, these functions are designed to help you express exactly what you want to do and do it efficiently. For example, instead of traversing the DOM, you can reference tools by their ids, get their children() or parents(), modify their state, call their methods, or attach event handlers.

(A small note: for efficiency reasons, the various Q.Tool.onActivate, onConstruct, onInit and beforeRemove events are not created unless you call the event factory yourself. This means that you should set your handlers with event.set() before the tools are constructed, and not rely on event.add() like you normally would be able to.)

Tools are like "ViewModels" and streams are like "Models" in MVC. Whenever you are building a self-contained block of functionality that can be "activated" on HTML elements, you define a tool. Whenever you want to build a "data model" without a visual interface, you define a stream. Tools often use the streams API to listen for events in their associated streams. A tool can pay attention to one or more streams, and a stream can have one or more tools listening to it.

Currently, Qbix does not provide a declarative DSL for two-way binding between tools and streams. A tool is responsible for setting up its own event listeners and updating its own state when the stream changes.

jQuery plugins

Qbix provides a convenience method for loading jQuery plugins on demand:

// Early on, tell Qbix where to find the plugin
Q.Tool.jQuery(name, 'path/to/file.js');

...

// And then, somewhere in your code:
$(elements).plugin(name, options, callback);

Like Angular, Qbix discourages you from directly applying jQuery plugins to elements, and wants you to apply them via tools instead. This way, they are automatically removed at the right time, their event handlers are removed, etc. You can do it very simply, like this:

$jquery.tool(toolName, options)
.activate(function (element) {
  // called for each element in the $jquery
  // this, here, refers to the tool
});

Then in your module's javascript file, you would just indicate the location of the tool, alongside the others:

First/web/First.js
==================

// among other things...
Q.Tool.define("First/photo","tools/photo.js");

// or activate it as a tool
$('<div />')
.tool('First/someTool', options)
.appendTo(container)
.activate(function () {
  // called after tool was activated
});