SproutCore 2.0 Beta 3 Released

written by TomHuda

4 Comments

UPDATE:

The following post refers to SproutCore 2.0, which has split off as a separate project, Ember.js. SproutCore continues with the latest release, SproutCore 1.8.


Three weeks ago, we released the second beta of SproutCore 2.0. We got a ton of great feedback, and today we’re happy to announce the release of SproutCore 2.0 beta 3, which incorporates solutions to the highest priority feedback we got.

As always, we also fixed a number of bugs. If you find a bug, please report it right away on our GitHub Issues tracker.

Because of your feedback, beta 3 comes with a number of significant semantic changes. If you have an existing SproutCore 2.0 app, please read these release notes as virtually every application will be affected.

Contexts in Handlebars

Until now, SproutCore templates had a few different contexts you needed to be aware of. Here’s what an app on beta 2 might look like:

{{#view}}
  {{name.first}}
  {{! name.first is looked up on
      the current view, as expected }}
 
  {{#if name}}
    {{#with name}}
      {{first}}
      {{! `first` is looked up on the
          current context, also as expected }}
 
      {{view
         firstNameBinding="parentView.parentView.name.first"}}
      {{! However, bindings ignore the Handlebars
          context and must be specified with
          knowledge of the view hierarchy }}
    {{/with}}
  {{/if}}
{{/view}}

Keeping track of all of these rules got pretty complicated, especially given that they didn’t map well onto Handlebars or Mustache semantics. Not only was this confusing to beta developers of SproutCore 2.0, we found it to be particularly confusing in our own apps. This problem was compounded by the fact that certain constructs (#if, #each) create new views in the view hierarchy.

In SproutCore 2.0 beta 3, we have smoothed out the experience significantly. We have made binding lookup both consistent with standalone Handlebars usage and also easier to understand in the context of SproutCore bindings:

{{#view}}
  {{name.first}}
  {{! name.first is looked up on the current view }}
  {{#if name}}
    {{#with name}}
      {{first}}
      {{! `first` is looked up on the current context }}
 
      {{#view firstNameBinding="first"}}
      {{! Bindings are now specified relative
          to the context, `name` }}
    {{/with}}
  {{/if}}
{{/view}}

New Ancestor Helpers

Along the same lines, every view now also has the ability to bind to the nearest ancestor that is an item view, collection view, or content view:

{{#view contentBinding="MyApp.dataController.content"}}
  {{#view MyApp.FormView}}
    {{view SC.TextField
           valueBinding="contentView.content.name"}}
    {{view SC.TextField
           valueBinding="contentView.content.title"}}
  {{/#view}}
{{/view}}
 
{{#collection contentBinding="MyApp.peopleController"}}
  {{itemView.content.name}}
  {{! itemView points at the view created for each person}}
 
  {{collectionView.content.peopleCount}}
  {{! collectionView points at the closest #collection}}
 
  {{contentView.content.name}}
  {{! in this case, the closest `content` view is the
      same as the `itemView`}}
{{/collection}}

The contentView property will find the nearest ancestor that has a content property. The collectionView property will find the nearest ancestor that is an instance of SC.CollectionView. Finally, the itemView property will find the nearest ancestor that is a direct child of an instance of SC.CollectionView.

Both of these new features mean that you should be able to eliminate almost all paths in your Handlebars templates that have to traverse the view hierarchy with the parentView property.

Two-Way Transforms

It is now possible to create a two-way transform when using bindings. This can be useful if you are using a binding to serialize and deserialize data:

var NUMBER_TRANSFORM = {
  to: function(string) {
    return parseInt(string);
  },
  from: function(value) {
    return String(value);
  }
};
 
App = SC.Namespace.create();
 
App.serialized = SC.Object.create({
  name: "Tom Dale",
  age: "25"
});
 
App.tomController = SC.Object.create({
  // this is identical to
  // SC.Binding.from(App.serialized, "name")
  nameBinding: "App.serialized.name",
  ageBinding: SC.Binding.from(App.serialized, "age")
                .transform(NUMBER_BINDING)
});
 
App.tomController.get('age') //=> 25
App.tomController.set('age', 26)
App.serialized.get('age') //=> "26"

This is a new feature and may have bugs. In particular, it is possible to get into an infinite loop state if transforms are not reversible. This can rear its ugly head if you don’t properly account for unexpected values. For instance, in the above case, if you try to set App.serialized.age to a non-numeric String, parseInt() will return NaN. Because NaN !== NaN in JavaScript, this can cause problems. The correct way to write the above transform is:

var NUMBER_TRANSFORM = {
  to: function(string) {
    // Because:
    // String(null) === "null"
    // parseInt("null") || null === null
    //
    // this transform is reversible
    return parseInt(string) || null;
  },
  from: function(value) {
    return String(value);
  }
};

In general, you should use SproutCore’s built-in transforms, and only write your own transforms if you’re sure you know what you’re doing. If you want a specific transform in the framework, please let us know. We are already planning to add built-in transforms for dates and numbers.

SC.Run, Observers & Additional Parameters

If you manually add an observer using SC.addObserver(), you can now include additional parameters that will be included as arguments when the observer is invoked:

var observedObject = SC.Object.create({
  foo: 'bar'
});
 
var observerObject = SC.Object.create({
  fooDidChange: function(object, key, value, context) {
    console.log(context);
    // prints "extra data" when invoked
  }
});
 
SC.addObserver(observedObject, 'foo', observerObject,
  observerObject.fooDidChange, "extra data");

You can similarly pass additional parameters to SC.run():

var myObject = SC.Object.create({
  doRuntimeStuff: function(a, b) {
    // prints "extra data" when invoked
    console.log(a + ' ' + b);
  }
});
 
SC.run(myObject, myObject.doRuntimeStuff, "extra", "data");

Template Debugging

In the past several betas, we received a lot of feedback about the difficulty in debugging Handlebars templates. We have a three-step plan to significantly improve the debugging situation:

  • In beta 2, we changed the rendering pipeline to guarantee that bindings to properties that already existed would always be synchronized before the template was rendered.
  • In beta 3, we have introduced two new helpers to make it possible to use tried-and-true JavaScript debugging techniques in helpers:
    • {{log path.to.property}}: because bindings are guaranteed to be synchronized, you can use this to log a value to the console.
    • {{debugger}}: this will drop a debugger during the execution of a template. Because templates are compiled into JavaScript functions, this may only be useful if you’re a JavaScript pro or familiar with Handlebars’ compilation output.
  • In the next release, we plan to attach the raw template to each compiled template function (including functions passed to block helpers), so you can see the source of a template in helpers.

In general, we are planning to add a debugging mode that has significantly more information available, at the cost of some performance.

Precompilation Status

On a related note, a number of people have requested the ability to precompile handlebars templates to improve production-time performance. Thanks to Kevin Decker, the master branch of Handlebars now has support for precompilation. This support is not yet included in SproutCore’s version of Handlebars, which will require a bit of additional work, but we expect to have support for precompilation in the next release.

Kevin’s work also speeds up compiled Handlebars templates and significantly reduces them in size. A win all around.

Assert Statements

Because SproutCore provides a high-level abstraction, it is sometimes possible to get very cryptic error messages. In the past few months, we have heard over and over again that people lose a lot of time to trying to track down errors in the internals of SproutCore. In the latest release, we have added a large number of assertions to the codebase that should improve the error output.

Some examples (among many, many more):

  • Handlebars helpers warn if you pass the wrong number of arguments to a helper: For instance:
    • instead of a cryptic error when calling {{#with foo bar}}, you now get You must pass exactly one argument to the with helper
    • if you call bindAttr without a hash, you get You must specify at least one hash argument to bindAttr
    • if you call bindAttr with a non-String value, you get You must provide a String for a bound attribute, not #<SC.View:>
    • if you call the #view helper without a View class, you get You must pass a view class to the #view helper, not Path.To.view (#<MyApp.View:sc205>)
  • If you try to call SC.set with an undefined object or key, you get You need to provide an object and key to get. Previously, you would have gotten something like Cannot find sc_meta_54378954378534 on #<object>

This does not eliminate all cryptic errors, but we now treat cryptic errors as bugs in general, and are committed to finding ways to provide more useful messages where possible. If you get a cryptic error and can reproduce it in a jsfiddle (or otherwise), please submit it as a bug.

It’s also worth noting that these sc_asserts can be stripped in production builds. The minified version of SproutCore that we ship comes with the asserts removed, while the unminified version comes with the asserts in.

Fix unknownProperty

In beta 2, there were cases where a get to an unknown property would not invoke the unknownProperty method. This problem is fixed in beta 3, so you can feel free to use unknownProperty for general-purpose proxy semantics.

Bug Fixes

We’ve also fixed many bugs. In particular, we fixed an infinite loop when binding attributes in Internet Explorer 8 and lower.

Coming Soon: Span Elimination

One of the largest remaining issues in beta 3 is the creation of a number of elements that we use to track the location of bound elements. This allows us to automatically update the DOM when the underlying property changes. However, this has caused problems, especially when using the collection helper in Handlebars templates. It also can create styling issues if using a strict stylesheet provided by a designer.

We have a number of ideas of how to address this, and hope to include a fix in the next release. The requirements for a fix:

  • eliminate elements that can affect the styles applied by CSS
  • eliminate superfluous views in the view hierarchy generated by #if and #with nodes
  • not require manually scanning the DOM for sigils that cannot be retrieved using getElementById (in other words, DOM replacement should be fast)
  • ideally, improve control over the item view (and its element) in a collection
  • ideally, improve control over the wrapper node for a view

Note that these changes, like the “Contexts in Handlebars” change above, would be breaking changes for existing SproutCore 2.0 applications. We have received a lot of feedback during the beta process, and want to make sure that we are proud of the API that we ship once we release SproutCore 2.0 RC1. We think you’ll agree that these changes are significant improvements, and hope you won’t hold these breaking changes during the beta period against us. We are big fans and adherents of Semantic Versioning, and will not intentionally violate it in final releases of SproutCore 2.0.

Getting It

As always, the easiest way to get started with SproutCore 2.0 is to download the starter kit, based on HTML5 Boilerplate.

You can also get the debug build (with comments) or the minified production build from the downloads section of the SproutCore repository on Github.