Shopify/batman — pull request #750 by nickjs (2013-06-11 17:35:00+00:00)
Title: New View API

### WIP

This has been an ongoing effort for the last two weeks, and as it approaches a "ready to merge" state, we thought it prudent to start the code review process.

This pull request encapsulates a new view API which includes a Coca-like view hierarchy and lifecycle, synchronous rendering, and lots of code clean up. In the new hierarchy, the app contains a main layout view and every other view is a descendant of it.

This pull request description is very long but will temporarily serve as documentation for the new API. Eventually I'll move it into the actual docs. Hooray!

### Subviews and the View Hierarchy

Every view contains a `Set` of subviews. Adding a view to this superview is done by adding the view to the set, `superview.subviews.add(view)`.

Views are added to the DOM by adding them to a superview which is already included in the DOM. The app's layout view defaults to being the document body, so it is already in the DOM.

You still, however, need to specify where exactly in the superview's DOM the subview should be appended. Instead of Cocoa's `frame` property (an absolute positioning rectangle), you can set a `parentNode` property on a subview. This can either be a node contained in the superview's DOM already or a string selector in order to find one.

Alternatively, you may set the `contentFor` property on a subview. This uses batman.js' traditional `yield` system and will replace the yield node's content with the subview's `node`. You should set `contentFor` to a string matching the name of the `yield` in the subview.

### Render Context

Death to render contexts!

There are no longer render contexts associated with views. Instead, relevant data is set on the view itself and can be accessed with `view.get(property)`.

Previously running `get` on a render context would by default lookup the contexts descendants. This functionality can be reproduced with `view.lookupKeypath(property)`. The lookup chain is as follows:

**this view :arrow_right: all superviews :arrow_right: layout view :arrow_right: current controller :arrow_right: app :arrow_right: window**

### View Backed Bindings

We've made a better distinction between two types of bindings. Most bindings are still just typical value bindings of some sort. However there are also a view which are "view backed". This means that a view will either implicitly or explicitly be created for this particular node and binding and assigned this node.

For example, `IteratorBinding` is a view backed binding. It will still create a comment node and insert it into the DOM, and then create an `IteratorView` with the comment node as its `node`. For every iteration, an instance of `IterationView` is created, added as a subview of the `IteratorView`, and has its `parentNode` set to the comment node.

View backed bindings: **View binding, iterator binding, context binding, form binding, insertion binding.**

### Synchronous Rendering

The Renderer is finally synchronous! When you request `view.get('node')` for the first time, the view will call `loadView` on itself. This will fetch the view's `html`, create a new `div`, and populate it with that HTML. Assuming the HTML is already in memory somewhere (either as a string property set on the view or cached in the `HTMLStore`), this process will be synchronous.

The view will not be bound, however, until you append it to a superview. As soon as this happens for the first time, `initializeBindings` will be implicitly called if it hasn't been called already. When `initializeBindings` returns synchronously, every child node of the view's node will be bound and populated with all of the model data currently in memory.

There are two exceptions. If the view specifies a `source` property and that source is not already cached in `HTMLStore`, the store will end up sending an XHR to try to load the source. In this case, `view.get('node')` will return `undefined`. When this XHR completes, however, the `node` accessor will automatically be recalculated and will now return a populated (but unbound) node. If the view already has a superview, the new node will be automatically added to the parent. Magic!

The other exception is the iterator binding. In order to avoid completely blocking the UI thread on large iterations, all add operations are batched into one `nextTick`. This includes the initial adding of all items currently in the iteration set when the iterator is first bound. By `nextTick`ing only the iterator as a whole, we avoid the 4ms+ `nextTick` penalty on every iteration. Because both the iterator and iteration nodes are view backed now, you can listen for `viewWillAppear` on the iteration view to do any initial setup you would previously do in `childBindingsAdded`.

### Lifecycle

A view's lifecycle is now more well-defined. Here are the events that will be fired on a view instance:

- `viewWillAppear`: Every time the view is about to be added to the DOM. It will always have a superview.
- `viewDidAppear`: Every time the view has been inserted into the DOM. It is on the page, and could be selected with `document.querySelector`.
- `viewWillDisappear`: Every time the view is going to be removed from the DOM, but it is currently still there. It will still have a superview set.
- `viewDidDisappear`: The view is no longer in the DOM and may not be selected from the document. It may or may not have a superview, if it still belongs to a superview but that superview was removed from the DOM.
- `viewDidMoveToSuperview`: Every time the superview property is set to non-null, whether or not that superview is in the DOM.
- `viewWillRemoveFromSuperview`: Every time the subview is going to be removed from a superview, regardless of whether or not that superview is in the DOM.
- `viewDidLoad`: After `loadView` has been successfully called, the div has been created and populated with HTML from the `HTMLStore`.
- `ready`: All bindings have been initialized. One shot. **(Should it be?)**

### Subclassing

- Define either `html` or `source` to tell a view how to populate its node.
- Alternatively, override `loadView` in order to create a node from scratch.
- Never call `loadView` explicitly.
- You can override `addToParentNode` if you have to, but make sure you call the appropriate lifecycle events. For example, look at `IterationView`.

Designed by @nickjs in Ottawa.
Paired with @SoapyIllusions.

/cc @kristianpd @pushrax @hornairs @cjoudrey @matthelm