Live UI Blade Plugin

Blake Miner edited this page Jul 24, 2013 · 39 revisions

What is the Live UI Plugin?

Live UI is a Blade plugin that provides "live binding" support for client-side Blade templates. It is completely optional; you are not required to use it, and the implementation tries to be as modular as possible.

What is "live binding?"

Live binding essentially refers to the concept that your views (Blade templates in this case) are automatically updated when the data in your model changes. That is, when you change your data, your view automatically updates. This is surprisingly convenient; rather than writing jQuery selectors to find the element that needs to be changed, or hard-coding HTML strings into your client-side code, you simply change your model, and your view auto-updates. I'll show you what I mean shortly...

By the way... "live binding" is nothing new; I think ASP.net used live binding, and live binding has recently become popular in the JavaScript/Node.js world thanks to projects like Meteor and CanJS. I digress.

Getting Started

Using the Live UI plugin is as simple as adding a <script> tag to an HTML document. The Blade middleware automatically serves Blade plugins for you, so you can just setup the middleware and add this script tag:

<script type="text/javascript" src="/blade/plugins/liveui.js"></script>

Although since you're probably using Blade for your templates, here's the corresponding Blade code... how silly of me to actually write HTML in the first place.

script(type="text/javascript" src="/blade/plugins/liveui.js")

Live UI depends on Spark. Be sure to add this dependency before you use the Live UI plugin. If you clone the Meteor repository and run this script, it should output everything you need to use Spark.

An Example

Let's assume you were writing a web application that counted how many times you press a button. The view in this case might contain a button and a line of text that told the user the number of times they have pressed the button. An example of this view written is written in Blade. Our file shown below contains the stuff that goes in the <body> tag of a HTML document:

buttonClick.blade

div
  input(type="button" value="Click Me")
- var plural = clicks == 1 ? "" : "s"
p You clicked the button #{clicks} time#{plural}

That's great so far. You can now pass an Object like {"clicks": 23} into the view, and it will render some HTML. Now instead of rendering on the server, you want very responsive client-side templates. Fortunately, Blade makes that very easy; just include <script type="text/javascript" src="/blade/blade.js"></script> in your initial page load. See the browser usage section for more details.

Now... what's next? If you're like me, you are now tempted to include jQuery, give the button an 'id' attribute, add a 'span' tag within the 'p' tag so you can easily change the click count, bind event handlers, etc. Something like this might work, as well:

client.js

//Assuming jQuery is included...
$("input").click(function() {
  var clicks = $(this).val();
  var plural = clicks == 1 ? "" : "s";
  $("p").text("You clicked the button " + clicks + " time" + plural);
});

Well, that's okay, but consider this: you have already written the view, but it seems like you just duplicated a lot of your efforts, right? Plus, eventually, you might have more <input> tags and <p> tags, so you're going to need to give each of these an 'id' attribute, which can junk up the namespace. Instead, wouldn't it be nice to write almost no code?!?!?

How about this?

client.js

//Define a model
var model = new blade.Model({
  "clicks": 0
});
//Render the view, passing the model along
$("body").render("buttonClick", model);

And, then change your view to use Blade's event handlers syntax:

buttonClick.blade

div
  input(type="button" value="Click Me")
    {click}
      clicks++;
- var plural = clicks == 1 ? "" : "s"
p You clicked the button #{clicks} time#{plural}

Done! Okay, now... this is obviously a contrived example, but notice that you didn't duplicate your efforts writing jQuery selectors, event handlers, or view logic in your client.js JavaScript file. All of that stuff is right where it belongs -- in the view.

In our example above, we used Blade's built-in event handler syntax in combination with our Live UI plugin. By using the event handler syntax, our event handlers we defined in the view actually have access to the model (because we passed the model to our view). That means that all we write in our event handler is clicks++. Our model is then automatically updated any time the button is clicked. And, since our views are automatically updated when our model is updated, the view is updated automatically, as well! It's two-way model-view synchronization!

API

When you add Blade's Live UI plugin to your project, you get a few additional methods and objects:

  • blade.Context (see explanation of a Context below)
    • Instance methods
      • context.run(func) - Runs the specified function in this Context.
      • context.onInvalidate(func) - Calls func immediately if the Context is invalidated; otherwise, call it when this Context is invalidated and Context.flush() is called. When the Context is invalidated, func will be passed one argument: this Context.
      • context.invalidate() - Invalidates this Context and schedules a call to Context.flush()
    • Static methods
      • Context.flush() - For each invalidated Context, call all "on_invalidate" callbacks.
  • new blade.Model([initialData]) -- every Model instance has these methods:
    • model.add(key, value) - Adds key to the Model (if necessary) and calls model.set(key, value). Returns true if the value was changed; false, otherwise.
    • model.remove(key) - Removes key from the Model and returns its value without invalidating any Contexts.
    • model.get(key) - Returns the value of key and captures the current Context, so that when model.set(key, value) is called for this key, the Context will be invalidated.
    • model.set(key, value) - Sets the value of key to value and invalidates any Context that has called model.get(key) for this key. If the key has not yet been added, it is added first. Returns true if the value was changed; false, otherwise.
    • model.peek(key) - Returns the value of key, but does not capture the current Context.
    • model.put(key, value) - Sets the value of key to value without invalidating any Contexts.
    • model.invalidate([key]) - Invalidates any Contexts associated with the key or all keys if key is not specified.
    • model.observable - An Object that contains all of the keys of the Model as properties. Each property has a getter and setter method that automatically calls model.get(key) and model.set(key, value), respectively. If you add or remove properties from the "observable" Object, you will need to synchronized this Object with the Model by calling model.sync(); however, when adding or removing properties directly on the view using model.add or model.remove, the "observable" Object is automatically updated. Finally, it should also be noted that every "observable" Object also has a hidden _model property that refers back to the Model object.
    • model.sync() - Synchronizes the contents of the Model with the Model's "observable" Object. This can be useful if you add or remove properties to/from the "observable" Object and then want to synchronize with the actual Model. It should be noted that if the Model and "observable" Object are out-of-sync, all Contexts for all keys in the Model are invalidated. Returns true if the Model and "observable" Object were out-of-sync; false, otherwise.
    • model.serialize() - Serializes the Model using JSON.stringify and returns the serialized string.
    • model.validate([key]) - Validates a key, or all keys if unspecified, using the validation function(s) defined at model.validation[key]. See Model Validation section below for more details.
  • blade.runtime.renderTo(el, viewName, locals, cb) - Renders the specified view using the specified locals and injects the generated DOM into the specified element. In addition, any event handlers created by the view are bound. Finally, the element in focus is "preserved" and if the element either has an 'id' attribute or has a 'name' attribute and a parent who has an 'id' attribute. Views are rendered within the context specific to the element, as expected. That is, running renderTo against the same element will destroy all registered Contexts and their callbacks.

Arguments:

  • element - the DOM element into which the generated HTML code will be injected
  • viewName - the view template to be loaded and rendered
  • locals - the locals to be passed to the view. If a Model object is passed to this method, the Model's observable Object will be passed to the view.
  • [landmarkOptions] - the options passed to the created Landmark (see https://github.com/meteor/meteor/wiki/Spark)
  • [cb] - a callback of the form cb(err) where err is the Error object thrown when the template was loaded (or null if no error occurred). This callback is called exactly once, when the template is loaded.

It should also be noted that changing the contents of el or removing el from the DOM may confuse Spark and cause errors. To remove el from the DOM or to delete its child nodes, for example, it is best to call Spark.finalize(el) first.

  • jQueryObject.render(viewName, locals, landmarkOptions, cb) - Calls blade.Runtime.renderTo for this element.

In the next section, I'll show you how Blade's Live UI works under the hood and how you can use it to make some really cool stuff.

How it works

Blade's event handlers work like this:

  1. The event handler function is defined in the view, and due to closures, it has access to the locals (i.e. the model) passed to the view.
  2. The event handler function is turned into a string of JavaScript and inserted into an HTML comment directly before the element to which it shall be bound. For example, if you add an event handler to an <input> tag, an HTML comment is inserted right before the <input> tag.
  3. The appropriate HTML attribute is added to the bound element to register the event handler. For example, if your event handler is for a 'change' event, then the 'onchange' attribute is added to the element. The code placed into the 'onchange' attribute is something like this: return blade.runtime.trigger(this, arguments). The runtime.trigger function will, by default, grab (and delete) the HTML comment right before the element, run it through eval(), wire up the generated event handler function, and handle the event. Therefore, the runtime.trigger function should only run once for each particular event handler. Using this method, even if the template was rendered on the server, the appropriate event handlers will still be setup when the browser renders the HTML; unfortunately, since the event handler functions are generated with eval() (outside the scope of the view), they will no longer have access to the view's locals.
  4. If, on the other hand, templates are rendered on the client using the Live UI plugin's blade.Runtime.renderTo function, the view template will be passed through Spark, allowing the event handlers to be properly wired up.

Live UI:

Live UI "borrows" some ideas from Meteor when it comes to model-to-view synchronization. Anytime Model.get(key) is called on a Model, the current "Context" (I'll explain later) is recorded and associated with the key. Then, anytime Model.set(key, value) is called on a Model, all Contexts associated with key are invalidated. A Context is nothing more than a list of callbacks to be called when the Context is invalidated. For example, when you run a function in a certain Context, that function can be automatically re-run when the Context is invalidated.

Suppose you run a function that renders a view, for example, in a new Context. Anytime you call Model.get(key) within that Context, you associate the Context with that particular key in the Model, and when Model.set(key, value) is called for that same key (even outside the Context), the Context associated with the key is invalidated, allowing the function that called Model.get(key) to be re-evaluated. It all sounds weird at first, but it's really quite simple; the implementation of reactive Contexts is short and sweet. You can read a slightly better explanation on this StackOverflow page - How does Meteor's reactivity work behind the scenes?.

Now that we have reactive Contexts, we can re-render our view templates whenever our Model changes. In addition, every Model has an 'observable' property, which is an Object that contains getter and setter properties for each key in the Model. For example, you can do this:

model.add("clicks", 0);
var x = model.observable;
console.log(x.clicks); //calls model.get("clicks") and prints 0
x.clicks = 20; //calls model.set("clicks", 20)

When you pass a Model object to a view, the model's 'observable' property is passed to the view instead. This means any time the view references a key of the Model, Model.get(key) is called, associating the current Context with the key and allowing the view to be automatically re-rendered when Model.set(key, value) is called for that same key. Neat!

Element Preservation

Re-rendering a view any time that the Model changes can have some unintended side-effects for your view. For example, consider an <input type="text"> element with a 'keyup' event handler that updates the Model with the current value of the text box. On each 'keyup' event, the Model is updated, which possibly causes the view to be re-rendered. In this case, the user's focus, cursor position, and text selection is lost because the element was removed from the DOM and replaced with the new re-rendered element. Blade's Live UI plugin (well... actually Spark) solves this problem for you using "element preservation" techniques. You can enable this feature on a per-element basis by giving the focusable element a unique 'id' attribute, or a 'name' attribute that is unique within the closest parent that has an 'id'. Blade's Live UI plugin will automatically look at the focused element and, if applicable, record the partially entered text, cursor position, and text selection. Once the view is re-rendered, the Live UI plugin hunts for the same element and resets the focus along with any other applicable state information.

Models

You can add any type of data to a Model Object: booleans, numbers, strings, Objects, or functions. When adding Objects or arrays to a Model, changes to those Objects are not tracked without calling Model.invalidate. Check out the following example:

var users = [ ...data goes here... ];
var m = new blade.Model({"users": users});
//... Then later...
users[2].firstName = "Ben"; //This does not trigger `Model.set` or invalidate any contexts
//Nor does this...
m.observable.users[2].firstName = "Ben"; //This only calls `Model.get`
//To actually invalidate Contexts, you have to invoke `Model.set`... but wait...
m.set("users", users); //Oh, crap!  Even this didn't work because the value was unchanged!
m.invalidate("users"); //Finally, it works! (invalidate() function was added in Blade 2.6.0)

Also, when calling functions from the observable property, a convenient side-effect is that this refers back to the observable property:

For example:

var m = new blade.Model({
    "count": 0,
    "countDesc": function() {
        if(this.count > 0)
            return "Positive";
        else if(this.count == 0)
            return "Zero";
        else
            return "Negative";
    }
});
m.observable.countDesc(); //returns "Zero"
m.set("count", 23);
m.observable.countDesc(); //returns "Positive"
m.get("countDesc")(); //does not work

Also note that since countDesc() implicitly calls Model.get("count"), it is also reactive.

Model Validation

You can validate a Model's value by associating a validation function with the Model's key. For example:

model.validation.firstName = function(input) {
    //Return true to be valid; false to be invalid
    //return input.length > 0;
    //Can also return an Object for more options
    return {
        "valid": input.length > 0,
        "value": input.trim()
    };
};
//Now set firstName
model.set("firstName", "Blake  ");
//Now check to see if the input was valid
model.valid("firstName"); //true
//Now let's check what value was set...
model.peek("firstName") // "Blake"

As shown above, the validation function may return true (indicating that the value is valid), false (indicating that the valid is invalid), or an Object containing value and valid properties. The value property corresponds to the "sanitized" value after validation, and the valid property is a boolean indicating whether or not the value is valid.

Please feel free to edit this wiki page and add more content!