Skip to content

Instantly share code, notes, and snippets.

@ialexi
Forked from jtaby/Proposed View changes.md
Created October 25, 2010 19:30
Show Gist options
  • Save ialexi/645566 to your computer and use it in GitHub Desktop.
Save ialexi/645566 to your computer and use it in GitHub Desktop.

Proposed View-layer changes

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:

Overview

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.

Name Changes

  • Renderer => RenderDelegate

Views

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:

  1. 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.

  1. 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>');
  }

})
  1. 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.

RenderDelegates

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 null. In the base render function, SC.View will call the render method if possible.

This is the only way to ensure backwards compatibility, since views do frequently extend from other views (such as SC.ButtonView) and override the render() method, yet call sc_super()—they do this, for instance, if they only want to modify certain styles rather than change everything.

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',

  mouseUp: function(evt) {
    ...
  },

  ...
  
  /* SC.View's init method: */
  init: function() {
    ...
    if (typeof this.renderDelegate === "string") {
      this.renderDelegate = this._renderDelegateLookupFor(this.renderDelegate);
    }
  },
  
  /* SC.View's render method: */
  render: function(context, firstTime) {
    var del = this.get('renderDelegate');
    if (del && firstTime) del.render(context);
    else if (del) del.update(context);
    
    if (!del) this.renderChildViews(context, firstTime);
  }
});

// Example 1, using an alternate style of button from the current theme.
myRoundedButtonView = SC.ButtonView.extend({
  // you should NOT have to have a different renderDelegate just because
  // you are changing appearance. The theme will decide if that is needed.
  theme: 'rounded'
});


// Example 2, dynamically choosing a render delegate at runtime depending on
// the browser's capabilities.
myCanvasButtonView = SC.ButtonView.extend({
  // changing the theme will vary the renderDelegate as the theme feels is needed.
  theme: SC.platform.supportsCanvas ? 'mytheme-canvas' : 'mytheme'
});

// 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>');
  }
});

What RenderDelegates Look Like

Theme.Button = SC.RenderDelegate.extend({
  render: function(context) {
    context.text(this.get('title'));
  },
  update: function($) {
    $.text(this.get('title'));
  }
}

The .get() method will look at the RenderDelegate itself, and if the property on the renderDelegate is "undefined", will look up the property on the theme's dataDelegate if present (which would, if instantiated by an SC.View, be the view itself).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment