Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically choosing a view at runtime with Ember + Handlebars

I am using Ember, Ember Data, and Handlebars to display a timeline with a number of different types of models. My current implementation, though functioning properly, seems like it could be drastically improved with a convention and a helper. However, I can't figure out how to use already defined templates.

This is what I have:

{{#view App.AccountSelectedView contentBinding="App.selectedAccountController.everythingSorted"}}
  {{#with content}}
    <ol class="timeline">
      {{#each this}}
        {{#is constructor="App.Design"}}
        ... stuff about the design
        {{/is}}
        {{#is constructor="App.Order"}}
        ... stuff about the order
        {{/is}}
        {{#is constructor="App.Message"}}
        ... stuff about the message
        {{/is}}
      {{/each}}
    </ol>
  {{/with}}
{{/view}}

...along with a helper...

Handlebars.registerHelper('is', function(options) {
  if (this.constructor == options.hash["constructor"]) {
    return options.fn(this);
  }
});

I would rather rely on some convention to figure out what view to render. For example:

<script type="text/x-handlebars-template" data-model="App.Design" id="design-view">
... stuff about the design
</script>

<script type="text/x-handlebars-template" data-model="App.Order" id="order-view">
... stuff about the order
</script>

Perhaps the data-model attribute could be used to determine how an object is rendered.

{{#view App.SelectedAccountView contentBinding="App.selectedAccountController.everythingSorted"}}
  {{#with content}}
    <ol class="timeline">
      {{#each this}}
        {{viewish this}}
      {{/each}}
    </ol>
  {{/with}}
{{/view}}

Alas, I can't figure out how to access templates from a helper.

Handlebars.registerHelper('viewish', function(options) {
   // Were I able to access the templates this question
   // would be unnecessary.
   // Handlebars.TEMPLATES is undefined...
});

Also, is this something I should want to do with Handlebars?

like image 487
Jon M. Avatar asked Jan 11 '12 18:01

Jon M.


4 Answers

use ViewStates, see examples at:

http://jsfiddle.net/rsaccon/AD2RY/

like image 90
Roberto Avatar answered Nov 02 '22 16:11

Roberto


I solved this by establishing my own convention using a mixin. A model corresponds to a view with a similar name. For example, an App.Design model instance corresponds to the view App.DesignView.

App.ViewTypeConvention = Ember.Mixin.create({
  viewType: function() {
    return Em.getPath(this.get('constructor') + 'View');
  }.property().cacheable()
});

I mix this into my models...

App.Design.reopen(App.ViewTypeConvention);
App.Order.reopen(App.ViewTypeConvention);

...and iterate over a mixed collection like this:

{{#each content}}
  {{view item.viewType tagName="li" contentBinding="this"}}
{{/each}}

This way, I avoid defining the convention explicitly in my models. Thanks to Gordon, I realized that the view could by specified using a property on an object. I would still really like to hear about the 'right' way to solve this problem.

like image 32
Jon M. Avatar answered Nov 02 '22 18:11

Jon M.


This is just off the top of my head: I would create a separate template/view for each model type. E.g. there would be a DesignView, OrderView, etc. Each of these would specify the template to use with templateName (all code coffeescript):

App.DesignView = Em.View.extend
  templateName: 'design'

App.OrderView = Em.View.extend
  templateName: 'order'

All of the custom rendering for each type would be done inside the view/template.

At this point we need to have some template logic to decide which view to show for each item. The easiest thing to do would be to store the viewType on the model.

App.Design = Em.Model.extend
  viewType: App.DesignView

App.Order = Em.Model.extend
  viewType: App.OrderView

Then the template could look like:

{{#collection contentBinding="App.selectedAccountController.everythingSorted"}}
  {{view content.viewType contentBinding="content"}}
{{/collection}}

This is not ideal, however, since we don't want the model to know about the view layer. Instead, we could create some factory logic to create a view for a model. Then we could create a computed property on the controller which contains an array of the models and their corresponding views:

App.selectedAccountController = Em.ArrayController.create
  ..
  viewForModel: (model) ->
    # if model is instance of Design return DesignView, Order return OrderView etc.
  everythingSortedWithViews: ( ->
    everythingSorted.map (model) ->
      {model: model, viewType: @viewForModel(model)}
  ).property('everythingSorted')

The template would then look like this:

{{#collection contentBinding="App.selectedAccountController.everythingSortedWithView"}}
  {{view content.viewType contentBinding="content.model"}}
{{/collection}}

There are probably better ways to do this. I would love to hear someone who is closer to the core of Ember give a solution.

like image 1
ghempton Avatar answered Nov 02 '22 18:11

ghempton


This is what I used for a similar scenario.

Model 'page' hasMany 'activity'.

// App.PageModel
export default DS.Model.extend({
    index     : DS.attr('number'),
    activity  : DS.hasMany('activity',  { async: true })
});

Model 'activity' has property 'type' that references which template to use for the content in another property 'configuration'.

// App.ActivityModel
export default DS.Model.extend({
    activityId    : DS.attr('string'),
    type          : DS.attr('string'),
    page          : DS.belongsTo('page', { async: true }),
    configuration : DS.attr()
});

Note the lack of attribute type for configuration. This offers the means of storing a collection of randomly structured objects. For consistently structured objects, I suggest using Ember-Data.Model-Fragments.

Main template:

{{! page.hbs }}
{{#with activity}}
    {{#each}}
        {{partial type}}
    {{/each}}
{{/with}}

For type: 'static', it uses the {{{3 mustache option}}} to render a html string.

{{! static.hbs }}
{{{configuration.content}}}

The other options are far more complex, yet still simplified using a 'with'. ie: for type: 'multiplechoice',

{{! multiplechoice.hbs }}
{{#with configuration}}
    {{#each options}}
    <label {{bind-attr class=":label selected:checked:unchecked"}}>
        {{view Ember.Checkbox checkedBinding="selected" }}
        {{#if text.content}}
            {{{text.content}}}
        {{else}}
            {{text}}
        {{/if}}
    </label>
    {{/each}}
    {{ ...etc... }}
{{/with}}

With the partials, remember to consider nomenclature and/or folder structure depending on your environment, ie '_partialname.hbs' or 'viewname/partialname.hbs'

like image 1
greg.arnott Avatar answered Nov 02 '22 16:11

greg.arnott