Friday, Juan, Tom, Yehuda and I had a meeting where we discussed the renderer APIs and some ideas we had for the view layer. Here is what we came up with:
The purpose of these proposed changes is to lower the learning curve for users entering the SproutCore world and who are looking for quick feedback and an easy way to create custom views and a facility to create complex, themable views.
RenderContext => DOMBuilder Renderer => RenderDelegate layer => element
Our goal for the next version of SproutCore is to streamline the process of creating custom views. We've received a lot of feedback from the community regarding this process and we believe that simplifying it will be a huge benefit for people starting out with SproutCore.
For the purpose of this discussion, we will talk about 3 levels of custom view generation from the most basic, to the most complex:
- The one-off view:
This kind of custom view is the most basic, has some displayProperties associated with it, and has a custom render() implementation. The user doesn't have to implement firstTime, and doesn't have to worry about updating the view when the displayProperty changes. each displayProperty will have an observer on it, and when it changes, we will update that DOM element's val with the new value of the displayProperty. The mapping between selector and displayProperty is an API that still has to be worked out, but the idea is that you can create a custom view like this:
// THIS SAMPLE IS A WORK IN PROGRESS, THE API IS NOT FINAL
myView: SC.View.design({
displayProperties: 'fullName'.w(),
render: function(context){
context.push('<div class="fullName">Default Value</div>');
}
})
As I mentioned, the API for mapping '.fullName' to the innerHTML is still an API that has to be worked out, it could be a simple mapping between a css selector and a displayProperty. We could use WebKit DOM bindings to create the mapping between displayProperty and DOM element, or it could use the classname of the div to associate it with a displayProperty.
- A basic view, but with more complex handling
In this case, the user is still building a basic view, but he wants to more closely manage how the view behaves when one of its displayProperties change.
myView: SC.View.design({
displayProperties: 'fullName'.w(),
fullNameDidChange: function(newValue){
this.$('.fullName').val(newValue);
// THIS IS WHERE EXTRA PROCESSING WOULD GO
}.observes('fullName'),
render: function(context){
context.push('<div class="fullName">Default Value</div>');
}
})
- A complex, themable view
When the user wants to make a re-usalbe, and/or themable view, then they start have to take more control over its generation and updates. This is where RenderDelegates come into play, discussed in the next section.
At a macro level, RenderDelegates separate the generation of a view from the business logic of the view. This allows us to modify how views are being generated based on the theme. By default, a view's renderDelegate would be itself, allowing the API to be backwards-compatible. When the renderDelegate is set to another object, that object will be issued the render() method, and it will update the SC.RenderContext (which we propose to rename to SC.DOMBuilder).
This is how we would implement SC.ButtonView in SproutCore, as a themeable view.
SC.ButtonView = SC.View.extend({
// Render delegate can be a string, object, or computed property.
renderDelegate: 'button',
init: function() {
var renderDelegate = this.get('renderDelegate');
if (SC.typeOf(renderDelegate) === SC.T_STRING) {
renderDelegate = this.get('theme').renderDelegateFor(renderDelegate);
this.set('renderDelegate', renderDelegate);
}
if (renderDelegate) renderDelegate.dataProvider = this; // renderDelegate.set?
},
render: function(context, firstTime) {
var renderDelegate = this.get('renderDelegate');
if (renderDelegate) {
if (firstTime) renderDelegate.render(context);
else renderDelegate.update();
}
},
mouseUp: function(evt) {
...
},
...
});
// Example 1, using an alternate style of button from the current theme.
myRoundedButtonView = SC.ButtonView.extend({
renderDelegate: 'rounded-button'
});
// Example 2, dynamically choosing a render delegate at runtime depending on
// the browser's capabilities.
myCanvasButtonView = SC.ButtonView.extend({
renderDelegate: function() {
var theme = this.get('theme');
if (SC.browser.supportsCanvas) {
return theme.renderDelegateForKey('canvas-button');
}
return theme.renderDelegateForKey('button');
}.property().cacheable()
});
// Example 3, the user just wants a quick override of SC.ButtonView without
// implementing their own render delegate.
myCustomButtonView = SC.ButtonView.extend({
render: function(context, firstTime) {
context.push('<div class="my-sweet-button">Click!</div>');
}
});