Robert Mosolgo

To my knowledge, batman.js is not maintained. For that reason, I don't suggest that you use it for a new project!

Render Scope in AngularJS and Batman.js

Batman.js and AngularJS both create hierarchical view scopes, but their implementations are very different.

(please forgive my inconsistent rendering of AngularJS/Angular/Angular.js/angular, I don’t know which one is right!)

In batman.js and Angular, there is a view scope hierarchy which mirrors the DOM. In this heirarchy, objects may:

  • add values into the render scope
  • access their own values
  • belong to a parent object
  • access values from their parents
  • have child objects of their own

Consider a page like this:

Batman.js view hierarchy

The HouseholdView has many child views. The PersonViews belong to their parent, HouseholdView. They may access values from HouseholdView (such as the shared householdName).

Note: The batman.js view hierarchy includes a few other objects as well – see below.

Finding Values in the Hierarchy

To answer the question “how can child views access data from their parents”, Batman.js and Angular take different approaches.

$scope & Prototypal Inheritance

In Angular, data bindings are evaluated against a magical $scope object. The scope object has key-value pairs which correspond to values in the data bindings. When $scopes are created, Angular massages (tampers with?) the prototypal inheritance chain so that a child scope’s prototype is its parent scope.

(Usually, an object’s prototype is another “pristine” object of its same type. It’s generally treated as the “perfect instance” of the type. Other instances delegate to the prototype for properties that aren’t defined explicitly on themselves.)

In this case, a child $scope’s prototype is not a “pristine instance”, but instead it’s the parent $scope object. That way, if a value isn’t found in a child scope, it is looked up in the prototype chain. This is brilliant. Angular delegates value lookup to built-in JavaScript features. (There is one gotcha described below.)

When a parent $scope has many children, all children have the same parent $scope object as their prototype.

Batman.View & View::lookupKeypath

Batman.js builds a tree of Batman.View objects. The root of the tree is called the LayoutView and it is created automatically by batman.js. Each view keeps track of its children in its subviews, which is a Batman.Set containing views that are rendered inside it. Each view also keeps track of its superview, which is its parent Batman.View.

To evaluate data bindings, batman.js uses lookupKeypath on the view in question. This function climbs the “view hierarchy”, which actually includes a few extra objects:

  • The Batman.Controller instance which rendered the view
  • Batman.currentApp, which is the Batman.App subclass that you defined (the clas, not an instance)
  • Batman.container, which is usually window

Here’s the whole view hierarchy from the previous example:

The whole batman.js view hierarchy

Since Batman.currentApp is in the view hierarchy, any @classAccessors you define there are accessible in view bindings, akin to global scope in JavaScript.

Automatically-Created Scopes

In batman.js and Angular, there are data bindings that create child scopes of their own. For example, ng-repeat and data-foreach both create a collection of child scopes with the same parent.

Angular does this by creating many child $scopes with the same parent $scope as their prototype.

Batman.js does this by automatically adding nodes to the view hierarchy. One downside of batman.js is that creating lots and lots of new views is CPU-intensive. I don’t know whether the same is true for creating $scopes.

How Does It Know Which Scope to Bind To?

When I was reading about $scope, I learned that some new Angular users hit a snag when they try to set values on a parent scope from within a child scope. As JavaScript should, it updates the child $scope with the new value, not the parent $scope, which is the child’s prototype.

That’s how prototypal inheritance works: It looks up missing values on the prototype, but it sets new values on the instance. Then, it stops “falling back” to the prototype for the property that was set on the instance.

To work around this, it’s recommended to “always use a . in your ng-models”. (ng-model is a binding that creates a child scope.)

Batman.js doesn’t have this problem because, when uses lookupKeypath, it remembers which View object was the target for that keypath, then updates that object whenever the keypath changes.

However, Batman.js is prone to a different gotcha. If you leave an accessor unset (ie, returns undefined), then set it after a view has rendered, it’s possible that View::lookupKeypath won’t find it correctly. To avoid this, set defaults (or null) before bindings are evaluated:

  • before @render in controller actions
  • in the constructor for view instances

Or, make sure @accessors return null instead of undefined.

Batman.js treats undefined as the signal that an object doesn’t have an accessor for a keypath, so be careful when setting keys undefined!

How Does It Know When to Update the DOM?

When these scope objects (View or $scope) change, the framework must update the DOM accordingly.

Angular has a “digest cycle” where it checks for changes in the $scope since last run, then updates the DOM if necessary. It automatically tracks any values that are put into templates. You can also watch other keys on $scope with $scope.$watch. If you modify $scope from outside Angular.js code, you must manually trigger the digest cycle with $scope.$apply.

Batman.View uses the Batman.Property system to automatically track dependencies and changes. Any keypath that is passed to a data- binding is automatically observed. DOM updates are triggered when:

  • A keypath is updated with set. Doesn’t matter whether it’s inside batman.js code or inside an AJAX callback – batman.js will recognize the update either way.
  • A property’s dependencies change. When you declare a computed property with @accessor and bind it to a view, the view will update the DOM whenever that property’s dependencies cause it to change.

(In fact, those two cases are the heart of observability in batman.js: assign a value with set or delegate to batman.js’s source tracking.)

To force an update, use set to update a bound property or one of its dependencies.

Other Random Points

  • Angular’s “evalute an expression” is like batman.js’s “lookup a keypath”
  • Batman.View::propagateToSubviews is like $scope.$broadcast: it sends messages down the view tree.
  • As of batman.js almost-v0.17, there is no analog for $scope.$emit (which sends events up the view chain)
  • batman.js exports global function $context($0) which is just like angular.element($0).scope() (where $0 is the highlighted element in the Chrome inspector).

My Opinion

I think Angular’s $scope is brilliant. I imagine it’s performant as well, although I don’t know (and I’m currently writing without internet access). It introduces few gotchas. In general, it seems like it Just WorksTM.

I’m in the market to improve the performance of Batman.View, but I’m not sure I can take anything from $scope. All of batman.js depends on playing by the observability rules. I don’t see any way I can get native JS prototypal inheritance to participate in that.

I also like sticking with the “It’s just batman.js” in the view layer. If you can write good @accessors, then you’ve mastered Batman.View, too.