Dynamically render polymorphic component


#1

I’ve got a list of polymorphic items, each of which corresponds to a different component which needs to be displayed.

The type of the item can change at any time, at which point the component needs to change.

Initially I just had a bunch of {{#if...}} statements like:

{{#if isBlue}} {{blue-item item=item ...}} {{/if}}
{{#if isRed}} {{red-item item=item ...}} {{/if}}
{{#if isGreen}} {{green-item item=item ...}} {{/if}}

This works, but is rather unwieldy especially as each one has a bunch of attributes & event bindings, all of which are the same for each component type. There’s currently 7 different types and this will only grow over time, with the {{if}}'s that means there’s 7 lots of metamorph tags and associated overhead. There will be quite a few items in this list so I’d like the overhead to be as little as possible.

I’ve tried to tidy this by creating a helper which renders the right component type:

Ember.Handlebars.registerBoundHelper 'component-for-type', (type, options) ->
  name = switch type
    when 'red'    then "red-item"
    when 'blue'   then "blue-item"
    when 'green'  then "green-item"
    else throw new Ember.Error("no component found for '#{type}'")

  helper = Ember.Handlebars.resolveHelper(options.data.view.container, name)
  helper.call(this, options)

That means I can replace all the {{if}}'s with one:

{{component-for-type item.type 
    item=item 
    selectedItem=selectedItem
    onSelect="select" 
    onChange="change" 
    ... 
}}

This works for the initial render, but when the items type changes it blows up because the helper is trying to call view.appendChild on an already rendered view which isn’t allowed.

Another approach I’ve thought of is to render a partial based on the item type, I think that’ll get rid of all the ifs, but leaves me with 7 almost identical templates to maintain.

Anyone any thoughts?


Dynamic View Rendering In Ember 1.9
#2

Fantastic question. Sorry I don’t have an answer. I love the way you think!


#3

This looks like it will solve your problem: http://stackoverflow.com/questions/18972202/how-can-i-invoke-an-ember-component-dynamically-via-a-variable


#4

Thanks @davidjnelson, that’s doing basically the same as my original example except I’m using the new API for component lookup. It works fine for the initial render, but you can’t then change the component type and re-render without it exploding.

At the moment I’m using a ContainerView and replacing the childViews with the new component when the type changes. It works, but it’s a little clunky, especially as I’m using the private _Metamorph helper. Would be nice to have a baked in way of doing this - I’ll dig deeper and see about tidying it up into something I can turn into a pull request.

ComponentForTypeView = Ember.ContainerView.extend Ember._Metamorph,
  item: null
  onDelete: "onDelete"
  onSelect: "onSelect"

  childViews: (->
    [@componentForType()]
  ).property()

  changeChild: (->
    @clear()
    @pushObject(@componentForType())
  ).observes("item.type")

  componentForType: ->
    type = @get("item.type")

    container       = @get('container')
    componentLookup = container.lookup('component-lookup:main')
    itemComponent   = componentLookup.lookupFactory(type, container)

    itemComponent.create
      item:     @get("item")
      onDelete: @get("onDelete")
      onSelect: @get("onSelect")

Ember.Handlebars.helper('component-for-type', ComponentForTypeView)

#5

Any update on this? I am currentlyrunning into the same issue in which I’d like to dynamically render components based off of some item value.


#6

I’m attempting to use this stackoverflow solution as mentioned by @davidjnelson, however I am unsure on how to modify the options passed to the resolved component, in order to set some properties that the component is expecting, as well as how to set that content that will be injected in the component’s {{yield}}.

I’n my case, I am dynamically constructing a web form by iterating over an array of field definition objects, that contain the field name, field type (e.g. text, select, checkbox, ect), its label, whether its required, ect. The field’s type is what determines what component should be resolved, but then I must modify the options (I think?) in order to pass it things like the field value. Also, I must be able to set what gets rendered in the component’s {{yield}}, as my form field components utilize it for the label text.

Here is a simplified example of one of my form field components to better illustrate my scenario:

templates/components/form-input.hbs

<label {{bind-attr for=inputId}} class="control-label">{{yield}}</label>
{{input value=value type=type id=inputId class="form-control"}}
<div class="form-error-text">{{errorMsg}}</div>

Here’s how I render it regularly, in a non-dynamic way:

{{#form-input value=firstName required=true}}{{_ "First Name"}}{{/form-input}}

Here’s how I need to render it dynamically:

{{#each field in editableFields}}
    {{renderComponent content field}}
{{/each}}

Where content is the controller’s associated model that contains attributes that map directly to the names of the fields in editableFields, i.e. model.get(field.get('name')) would get the value from the model that needs to be rendered for that field.

So essentially I need to be able to set the value and required properties and the content between the {{form-input}} block tags dynamically from the renderComponent helper in the stackoverflow answer.


#7

So after some trial and error, I implemented the pieces I was missing in the renderComponent (or renderField rather in my case) helper and they seem to be working, however I just wanted to post my solution here, just to see if anyone could give some feedback, in case I am overlooking anything or not following best practices, ect.

Ember.Handlebars.registerBoundHelper('renderField', function(record, fieldCfg, options) {
    var helper;

    options.hash.value = record.get(fieldCfg.get('name')); 
    options.hash.required = fieldCfg.get('required');
    
    options.fn = function() {
        return fieldCfg.get('label');
    };        

    helper = Em.Handlebars.resolveHelper(
        options.data.view.container, 
        fieldCfg.get('componentName')
    );

    helper.call(this, options);
});

#8

Hi @billdami I’ve tried something very similar, it works on the first render but not when the type changes.

When the component type changes (assuming that’s bound), you get an error saying you can’t appendChild outside of render.

I’m currently still using my ContainerView approach above, but I’m sure there’s a better way. I’m holding off any further investigation until HTMLbars comes in because I think a fair bit is changing in the view side of things!


#9

@rlivsey yeah I saw that you mentioned that limitation in your previous posts, but luckily in my case the types are static, and won’t ever change. I just mainly wanted to make sure the way I had implemented the other parts of my renderField helper was done correctly, namely, setting of additional option parameters and the content to output in the resolved component’s {{yield}} via options.fn, as I couldnt really find any hard documentation on those aspects.


#10

Components with dynamic layout can be used for this. For example:

App.EditFieldComponent = Ember.Component.extend
model: {}
field: {}

layoutName: (->
    field = 'text'
    switch @field?.type
        when 'longtext' then field = 'textarea'
        when 'enum' then field = 'select'

    "components/edit-field/" + field + "-field"
).property('field')

name: (-> @field?.name).property('field')
value: (-> @get('model')?.get(@field?.name)).property('model', 'field')
bind: (->
    @model.set @field.name, @value
).observes('value')

When “field” changes, layout also changes and component updates live. Templates with inputs should contain input helpers or use attr bindings, then full two-way binding will be supported due to observer in component


#11

@Terion I’m still trying to wrap my head around the example you provided. Could you post an example of the HTML that would use your component?


#12

I’ve made an ember-cli addon which might be useful:

eg:

{{dynamic-component type=type onClick="thingClicked" value=something}}

hth


#13

thank you for sharing!


#14

This is quite cool dude! I’ve been waiting for something like this. Will keep an eye on development.


#15

Awesome component thank you. However it no longer works in Ember 1.9

I resolved by removing childViews and adding :

setChild: function(){
  this.clear();
  this.pushObject(this.componentForType());
}.on('init'),

#16

I released v0.0.2 which fixes this a few days ago, so you should just need to update it via npm.


#17

@rlivsey I know Ember 2.0 is moving towards components, and that they’re taking a bigger and bigger role. So, ember-dynamic-component is great. However, I can’t wrap my head around my use case.

I have a section where I want to display a list of tasks with their own type. Each type has it’s own template which allows the user edit the task.

I could create a component for each task type, but in the templates I need access to the store, for example. The “Repair Task” type needs a list of repair types from the store, to let the user select one of them.

I don’t feel comfortable accessing the store from a component. It is clearly not idiomatic and not the goal of a component. Also, i don’t feel comfortable doing {{dynamic-component type=t.taskType value=t repairTypes=repairTypes}}, i.e passing repairTypes to all of our task components. Just because all the other task types don’t require the repair types at all!

My intuition tells me that the ideal approach would be to use the render helper dynamically. Then, in each controller, I could access the store only if that template was rendered. However, even in this issue @rwjblue and @mixonic encourage moving towards components.

I feel like we could loose flexibility in that way.


#18

For the record here, this issue (https://github.com/emberjs/ember.js/pull/10093) will end up building a component helper that does what we’re after here.

Looks like it’s supposed to hit in Ember 1.11. Thanks for the work on the component @rlivsey!


#19

I would like to understand how is this tackled with the {{component}} helper? All components having the same API sounds limiting.


#20

A single API interface across various types is polymorphism. In your case, you would be dealing with types that are not polymorphic so some conditional switch or factory-like behavior would be involved I am guessing.