Full Source Hide Source

BINDster
 

Bidrectional binding of data to the DOM

BINDster is an alternative to using HTML templates to create dynamic web applications. BINDster creates a real-time bi-directional link between the DOM and your Javascript object. By using iteration and conditional constructs all of the flexibility of templating is preserved. Because the linkage is bi-directional there is no nead for events to capture changes to form elements.

Model View controller

The best way to think of BINDster is in the context of a model, view, controller. BINDster is the glue that "binds" the model to the view.

As the user enters data into form elements in the view, the model is automatically updated. Your controller responds to events like a user clicking on a action. After processing these events BINDster compares the model to the DOM and any changes are reflected in the DOM.

The key features include:

Binding, conditional constructs and iteration are declared directly in the HTML using BINDster-specific attributes in a custom name space so the binding is always clear. If you prefer not to adorn your HTML with BINDster-specific attributes all functions may be applied in Javascript using selectors.

Hello World

What would the world be like without a "Hello". For BINDster's Hello World add this to your header to include bindster:

Create your model. The model can either be a prototyped object or a plain old object. Here we show a simple object created from JSON. It has a single property called world:

Create your view. This view has two elements, each of which is bound to the world property in the model. The immediate parameter forces the model to be updated immediatly as you type rather than when the input loses focus:

Bind the model to the view by creating a new instance of Bindster and passing it a new instance of your model. This goes just before </BODY>

And here it is. Enter text in the control to see how it works

For those that prefer not to introduce name spaces all bindster attributes are also available using data-xxx where xxx is the specific bindster attribute.

Iteration

Binding individual variables is usefull but binding repeating data is a lot more interesting. Let's say we want a simple address book with a name and email address. Here is the model:

The iterate tag will clone all of it's child nodes for each element in the array specified by the on attribute. The index attribute causes it to set a property to the ordinal position of the array element being iterated. This let's you bind to the specific array element. In this case the array is addresses, the index property ax

And the results look like this

For those that prefer not to introduce name spaces simply apply data-iterate-on and data-iterate-index directy on the <tr>:

Events

To do anything useful we need events. With BINDster you need events only for real-world actions that the user can take such as clicking on a button. We will extend the iteration example to allow the array of addresses to be added and deleted.

To do this we need a controller that will handle the add and delete actions

The Controller class is where you put functions that can be referred to in event handlers. When referencing data in the model you must refer to the model properties as this.model.propname or this.m.propname

The event tags must be prefixed with b: (or data-) so that BINDster has the opportunity to refresh the DOM after the event is processed. See the b:onclick events on line 34 and line 39.

You must also tell BINDster about your controller:

We also opted to specify the view. If you don't specify the DOM element id for the view it defaults to the entire document body.

And the results look like this

Conditional Expressions

BINDSter uses conditional constructs similar to those found in templating systems to control what is displayed. The conditions are usually based on testing properties in the model that serve as states for the applications.

We will extend the previous sample application to include a popup that allows the address to be edited. This popup is only displayed when the popup property is true. The popup binds directly to edit_address which is a temporary reference to the current address.

Our main iteration of the addresses has a couple of new elements

First you can see that we introduced the b:with on the iteration. This is simply a short cut for using indexes when referencing elements of an array being iterated. On line 54 you see a link that sets popup to true and also sets the temporary current_address property.

One other type of conditional expression is found on line 42. Any HTML attribute that is prefixed with a b: (or data-) can have a JavaScript expression embedded within {}. The expression returns a value that will be substituted into the attribute any time any of the components of the expression change. Here we are setting the class to 'active' when the current index is equal to the index being iterated.

Variables being referred to in the BINDSter tags must be defined in the model. popup and current_ax have been added. edit_address are also added. Any property referred to in BINDster attributes must be defined in either the model or the controller.

Finaly some tweaks are needed to the controller to setup edit_address when a new address is added:

Here is what the application looks like:

Other Goodies in BINDster

Custom Tags (Mappers)

BINDSter lets you map custom tags that effectively extends HTML. You can define your own custom tag and define how it's attributes will be mapped to attributes in an HTML template. Combining this with conditional expressions and iteration this is equivalent to having HTML macros.

Wrapping HTML

You can define an HTML template that will be used to wrap elements based on a selector expression. For example you could define a "wrapper" that took all anchor elements and wrapped them in a three part structure that would create rounded corners

Bookmarks and the Back Button

Single page applications need to support the back button and also to be able to define their state using unique URLs. BINDster uses hashmarks to accomplish this. Just define named anchors define what state properties are to be set when that anchor is linked to. Then if all internal links in the application use the hash marks the back button will work properly. This document is an example of navigating with hash marks.

Bind to Selects

Asside from simple bindings to input fields, BINDster can map to select inputs (dropdowns), automatically filling them with name/value pairs. It can also map to sets of radio buttons and check boxes.

Binding to Complex Controls

A controler plug-in structure allows binding to complex UI controls such as sliders.

Formatting and Parsing

Binding would not be very useful if there was no formatting or parsing. Formatters and parsers can be used to translate data as it is marshalled back and forth between form fields and the model. This transforms the model from just being a view model into being a richer object model

Validation and Error Handling

You can ensure that your model never gets bad data through parsing. BINDster is smart enough to keep a shadow copy of "bad" values so the user will always see the bad data until it is corrected but it is never marshalled into the model.

No Injection Risks

Most templating systems use innerHTML which means that malicious data input by users could potentially end up in HTML and causing script to be executed. BINDster does not use innerHTML and only sets the value of DOM elements.

About BINDster

Hosted on github under the MIT license.

Namespaces and data-xxx

BINDster's preferred method of declaring binding is to use namespaces for BINDSter specific tags and attributes. HTML 5 does not specifically support namespaces but tollerates them mainly for the benefit of popular namespaces such as SVG and MathML XFBML. However, other binding frameworks have chosen to use the data-xxx attributes of HTML 5 and this method is fully supported by BINDster as well. To use the data-xxx style do the following:

We feel that that the use of namespaces makes the HTML very clear and given the fact that this works with all modern browsers we feel it is worthwhile. Still it is your code and you need to decide which way is best for your project. If you use namespaces you should be aware of several issues:

Additionally all binding information can be specified by code in the controller and/or in the model. We don't like to prescribe any particular design pattern on you and in fact go out of our way to be flexible in terms of specifying binding information in the model, view and controller.

Binding

Binding connects data to a form field or other DOM element. The connection can be single directional which simply evaluates an expression and populates the value of a DOM element with the result of an expression or it can be bi-directional such that it links a form field with JavaScript variable or propery.

The b:bind Attribute

The b:bind attribute contains a Javascript expression the result of which will be used to set the value of the element containing the attribute. If the expression is not a string constant (e.g. does not begin with a quote) and the element containing the attribute is a form field then the binding is bi-directional. In that case BINDster will create an event that can update a property or JavaScript variable. Anytime an event is processed BINDster checks to see if the model is still in sync with the DOM and will update the DOM if needed. Bi-directional binding can be performed on these types of form fields:

All other elements have single directional binding. This also means that they can use expressions

How expressions are evaluted

b:bind expressions are evaluated in the context of the model, the controller and the global scope in that order. This means properties in the model need no qualification. Sometimes people prefer to put variables that are really releated to rendering (e.g. indexes, states etc) in the controller. You can put variables in the controller by simply declaring them in the controller and not in the model or you can explicityly qualify them by prefixing them with c. or controller.

When is data updated?

The update expression is executed from events in the element containing the bind attribute. The event depends on the type of element and on whether the b:when attribute is also present. In general updates occur when the form element loses focus, it's value is changed by clicking on it (radio buttons and checkboxes) or when it's value is changed by dropping down and selecting a value (select drop-downs). In the case of input text and password fields b:when can be used to specify the number milliseconds before BINDster will decide to pass through the DOM and determine whether updates need to be made.

The entire view is traversed everytime there is an event that could potentially change data in the model. As the view is traversed a cached value of the DOM element is compared with result of evaluating the bind expression and if there is a change the DOM element is updated. This provides adaquate speed such that the developer never needs to explicitly force the updating of the DOM when bound elements change.

There are times when the model may change other than as a result of binding. For example when server requests are made or when web workers complete a task. In that case the controller has a mehod injected called refresh which will schedule a resync of the DOM and the model.

Binding to checkboxes

When binding to a check box you need to specify the values when the box is checked or unchecked

Binding to radio buttons

When binding to a set of radio buttons you need to specify the value that determines whether the ratio button is checked.

As with all radio buttons the name attribute makes them into a group

Binding Selects

When binding to selects these attributes are used:

Here is an example of two select statements working together to create categories of products for a shopping cart.

The data is normalized and consists of an assocative array of products, the key for which is a category code

The category codes are mapped via an associative array to their values

The HTML for the shopping cart looks like this

The binding of the select on line 131 fills the category select from an associative array of strings. The onchange simply clears the product selection when a category is selected. The binding of the second select fills from the product associative array. Because it is an array of objects fill-value returns the name of the product. It also "filters" the array to just those that are for the current category.

The controller is fairly simple and manages adding and deleting as well as computing the prices and totals:

The true flexibility of filling a select is demonstrated by showing alternative object model designs. What if instead of a single array of products that is filtered by a category the model was organized as a list of categories and each category had a list of products. This would be a more typical arrangement when using an ORM or noSQL database on the back-end. Here is the alternative structure:

and here is how the two selects would be filled

While it may not be realistic it is possible that your data is not coded at all. This might be useful when building a demo or protype. This structure is borrowed from one used in a knockout.js example:

And here is the way the selects would look

Note the use of the index reference in the fill-key which returns the array index. This means that the actual binding to the category and product variables in the model is the ordinal position of the category or product. This can then be used to retrieve the correct string values.

Iteration

Iteration is the process of repeating nodes in the DOM for each row in a corresponding array. Practically this is equivalent to iteration in a templating system. The difference is that iteration is achieved by cloning nodes. Each time the DOM is rendered it is synchronized with the current contents of the corresponding array. This means that the iteration is dynamically kept in sync with changing values in the array. It is highly optimized so that nodes are re-used when possible rather than re-generating each time. Each iteration may reference either through binding or in conditional expressings a specific row in the array

Iteration can be applied using the <b:iterate> tag.

Consider this simple example of iteration:

and note the following:

Here is the working example:

A few important nodes on iteration:

Conditionals

Conditional expressions allow DOM elements to be appear and disappear based on the value of properties in the model. Just like with binding, the behaviour is immediate when the properties in the model are changed by the controller. There attributes which control the hiding and showing of elements:

This sample shows how elements are shown and hidden

And here it is. Enter text in the control to see how it works

Binding Attributes

BINDSter also can bind values to most attributes. To do this:

The expression is evaluated in the context of the model so references to the model need no qualification. t is re-evaluated any time any of the component variables change value. You can use this on all attributes except for events (onxxxxx) because events are already interpreted as JS expressions

Parsers, Formatters, Validators and Errors

The data in the model should be in a format appropriate for computing rather than being formatted for display (think dollar signs, commas etc). This means it needs to be translated as it is marshalled back and forth between the model and the DOM. To that end BINDster provides:

Parsers

Parsers should do the following:

Here is an example of a parser:

Validators

Validators are similar to parsers but generally are not expected to have to parse the values. The validators are called after the parser and may also throw an error object. They do not need to return a value.

Here is an example of a validator:

Message Tags

A special tag that allows the text of errors to be defined directly in the view to keep text out of the controller. This makes multi-lingual applications easier to manage since only the view need be modified. They are defined as:

       <message name="error name" value="error message"></message>

        or if you don't want to use namespaces

       <div data-tag="message" data-name="error name" data-value="error message"></div>

The "error name" is a name that is referred to by the parser or validator when they throw an error. It is the message property of the object that they throw. The "error name" is the error string and may include {property references} within curly braces that inject values from other properties that are in the object being thrown by a parser or validator. Here is an example

Error Binding

It is customary to place an error message in view, often next to the field that is in error. BINDster has two attributes that help with this:

Here is a complete example:

Formatters

Formatters should do the following:

Here is an example of a formatter:

Wrappers

Wrappers allow DOM element to be "wrapped" within a document fragment. The wrapping is done based on selectors so any element of a particular class/id/tag specification can be wrapped. The wrappers are defined by applying the b:wrappername="wrapper name" to a container that then becomes a document fragement that will be used to wrap elements that reference it. The fragment should contain <insert></insert> to indicate where the referenced element should be copied. The DOM element that matches the selector and all it's descendands will be inserted into the document fragment and then document fragment replaces the original element.

This example will wrap any element with a class of super_rounded with a table structure tha can produce rounded corners.

Mappers (Custom Tags)

Mappers allow you to create your own tags that take will insert a document fragment in place of the tag while mapping attributes in the custom tag into the document fragment. The custom tags and document fragment are defined using

<tag name="tag name">Document framgement (HTML)</tag> Within the document fragment you can use __attribute name__ to include the value of an attribute when the fragement is copied upon usage. They are useful both for creating re-usable components. Here is simple mapper that defines a graphic button.

A couple of things to note:

You can use conditionals in a mapper though they are evaluated after the document fragment is inserted in the DOM. Here is an example of a mapper that generates standard HTML for form fields using the common <dt></dt><dd></dt></dl> pattern

Bookmarks

Dyanamic applications will often present multiple steps or states. Rather than each one being a seperate page that is loaded by the browser this often achieved by simply showing and hiding portions of the view. There are two problems with this

Bookmarks (named anchors) solve both of these issues and BINDster makes it easy to incorporate them in an application.

To use this technique place <a name="hash value url suffix" b:onarrival="code to establish state"></a> in front of each state. The "code to establish state" will generally be code that sets everything needed to put your application in the state it needs to be in. Then when you want to provide a link that takes you to that particular state you simply go to the "#hash value url suffix". BINDster will detect that a new URL has is present and execute the "code to establish state".

This documentation uses named anchors to navigate through the document.

A few things to note:

Forms

Forms leverage all of the features of binding as well as parsers, formatters and validators. Putting all of these things together you can create very robust forms with flexible and intuitive validation. This example shows all the features for form handling. Try entering a badly formatted email address and hit the tab key

In this example mappers have been defined for each input field so you can refer to the input field using the formfield tag:

The custom formfield tag is defined using a mapper which will include the correct form field based on the parameters

The mapper also contains an error field at the end that is bound to any errors assocatiated with the binding using b:binderror and b:binderrordata. The latter supplies a field name (reusing the field label) for the benefit of any error declartions which can now include {field}

Here are the messages

And here is the controller which contains all of the formatters, parsers and validators. It has a submit function at line 14 that might submit the model to the back-end. The submit function:

Binding from the Object Model and Controller

One of the principal design philosopies of BINDster is that it should support rich functional object models rather than simply a view model. This principal is supported by a validation framework that keeps bad data out of the model and by the ability to incorporate validation into the model in a way that can be reflected in the UI.

BINDSter also strives to be pattern agonostic and provide flexibility in terms of where you supply binding criteria so that it will fit best with your own patterns. Binding can be specified in:

Binding in the Controller

In the preRenderInitialize of your controller you may call the attr method to assign any BINDster attribute to a DOM element using a selector:

where ...

Here is an onclick event defined within the controller for a button with an id of "submit"

Attribute selectors have one additional feature which is that they can select DOM objects that are bound to a particular property. This lets you specify only the data binding in the view and have it double as identifier by which other more verbose binding attributes (e.g. formatters, validators, parsers) and be assigned. Here is an input field defined using a mapper that is bound to verifyEmail:

and here is the validator defined within the controller

Binding in the Object Model

Defining binding attributes in the object model is done by injecting attribute information into the objects by way of the the __props__ function. It can be provided in a prototye or directly as a property if using prototypical object styles.

The __props__ function returns an array of properties each of which may specify bindster attributes that will apply when that property is bound. The attributes may be assigned directly or indirectly by specifying a rule. BINDster attributes may be assigned to rules so that all properties that share the same characteristics (e.g. currency or dates) can have validators, parsers and formatters assigned once.

Consider the __props__ definition for a Pledge object:

In the preRenderInitialize method of the controller in this example we define the rules:

Here we see formatters and parsers being defined for the various rules. Note that the text rule defines a standard DOM attribute maxlength which will force the UI to limit the length of an input field. As with any normal DOM attribute defined via BINDster (e.g. b:maxlength) you can use a JavaScript expression. The prop is injected into the scope of evaluation and prop is a reference to the __props__ definition for the particular attribute so that props.length will represent the length that was defined in the object model.

Rich Object Models and AJAX

Using rich object models in the browser when much of the data comes from the server can require a lot of glue code. BINDster provides a method to take POJO objects that may be be retrieved from AJAX requests and "enrich" them by instantiating objects for them. It can handle references to other objects and arrays of other objects. It is used like this:

   var person = Bindster.fromPOJO(JSON.parse(data.person), Person)