Programmatically rendering ember components


#1

There are quite a few posts out there about this topic. I wrote one myself a while back.

There is also another topic on this forum that goes over this topic as well. The solution in that thread was to do the following:

import Ember from 'ember';

function renderComponent(componentPath, options) {
  var helper = Ember.Handlebars.resolveHelper(options.data.view.container, componentPath);
  return helper.call(this, options);
}

export {
  renderComponent
};
export default Ember.Handlebars.makeBoundHelper(renderComponent);

The issue with this is that options.data.view.container is no longer available as of Ember 1.9 (possibly 1.8, I haven’t checked that though). As a temporary stop-gap, you can avoid creating a helper via makeBoundHelper and do something like this:

export default function(componentBinding, options) {  
    var componentPath = Ember.Handlebars.get(this, componentBinding, options),
        helper = Ember.Handlebars.resolveHelper(options.data.view.container, componentPath);

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

Here we’re simply accounting for the fact that our first argument is no longer a bound value, but the binding path. To get the actual value, we must call Ember.Handlebars.get (makeBoundHelper takes care of this for us, hence the name bound.). However, Ember.Handlebars.get is deprecated in favor of makeBoundHelper, so this is not an ideal solution.

The ideal solution, in my opinion, is to figure out where options.data.view.container went when using makeBoundHelper. Does anyone have any insight into this issue?


#2

It seems that the container is available in the context of the helper function:

import Ember from 'ember';

function renderComponent(componentPath, options) {
  var helper = Ember.Handlebars.resolveHelper(this.container, componentPath);
  return helper.call(this, options);
}

export {
  renderComponent
};
export default Ember.Handlebars.makeBoundHelper(renderComponent);

However, when I run this code I get a downstream error because the options don’t contain a view. I think losing options.data.view.container is just the tip of the iceberg here. It looks like the streams work has changed a lot of how the internal rendering works.

The only way something like this is going to continue working across versions of ember is if someone creates an official public helper for it. I found this github issue that you might want to resurrect.

cc @mmun @rwjblue


#3

Thanks @mitchlloyd, I’ve commented on that github issue. We’ll see where it goes from here.

I don’t know exactly how I feel about an official public helper for this. Sometimes, there are things I like to do in the helper that deviate from the standard “take this bound value and render a component based on it.”. I guess I could always take that additional logic and put it into a different helper that then calls into the official public helper, but I don’t really like how that feels.


#4

I don’t know a ton about this issue but I have used ember-dynamic-component before. Just wanted to make sure you’ve seen it.


How to dynamically load ember components by name in a template?
#5

I’ll be posting this on that Github issue, but it looks like it would be fairly simple to add a built-in component that does this. Following is the code I placed in ember-handlebars/lib/helpers/component.js:

import Ember from "ember-metal/core"; // Ember.assert
import lookupHelper from "ember-htmlbars/system/lookup-helper";

/**
 @module ember
 @submodule ember-htmlbars
 */

/**
 @method component
 @for Ember.Handlebars.helpers
 @param {String} componentName the name of the component to render
 */
export function componentHelper(params, hash, options, env) {
  Ember.assert('You can only one argument to the `component` helper, which should be ' +
               'a bound property whose value is the component to render.', params.length === 1);

  var helper = lookupHelper('loading-animation', env.data.view, env);
  return helper.helperFunction.call(this, [], hash, options, env);
}

I did a quick test in my application and it seemed to work correctly.


#6

It looks like the github issue that @mitchlloyd mentioned is getting some attention and will be worked on in the near future.

@samselikoff - I am currently using the add on you suggested in the interim. Thanks for the suggestion!


#7

Luke Melia’s {{component}} helper patch has landed on master.


#8

Sorry to revive this, but I’ve been looking around for a while with no luck.

How do we then use this to actually render a component programatically like the topic title suggests? With JS only?

I know how to add a component using the helper {{component componentName}}, but I don’t have the luxury of doing this in a template with what I’m working on. I’m creating an easy to use modal library where I can show a variety of modals from anywhere in the app.

I have a bunch of modals as components. But obviously I don’t want to go to each template and add all modal components in a hidden state, then just show each one as needed.


{{view}} migration when using with ContainerView
#9

@amk showed me a solid implementation for this:


#10

I think this has become an issue now that {{view}} is going away.

Edit: @CaC That was a while ago… containerView probably won’t be around for much longer IMO


#11

I am wrestling with this exact same problem. I currently use the pattern showed by JSBin linked by @CaC. However Ember.ContainerView is deprecated and will be removed.

So I looked around and found both the issue @amk linked and a comment where @ef4 had suggested to use {{each}} and {{component}} as a replacement for Ember.ContainerView on the 1.13 release blog post. At first that seemed to cover the use case, but the challenge is there is no real easy way to dynamically populate different attrs when using the {{component}} helper. Every single component class in the list would have to have the same attrs.

Example:

{{#each myComponentModals as |componentDetails|}}
  {{component componentDetails.name value=componentDetails.value checked=componentDetails.checked}}
{{/each}}

I think this pattern falls short of my needs currently. I don’t think it’s a fringe scenario for people to have dynamic components like this. Modals, dashboard widgets, reporting, customizable forms are all relatively common use cases IMHO.

Any advice would be greatly appreciated.


#12

@workmanw Yeah, that’s the crux of it. Worth filing an issue?

I did read somewhere about a new syntax for the {{component}} helper to do just that, but struggling to find it.


#13

I think the best and simplest way would be:

controller.js

import Component from '../components/my-component';
comp: function(){ // because 'component' is a taken keyword
  return  Component.create({
    some: 'properties'
  })
}

template.hbs

{{comp}}

I don’t know which problems would it cause, or against which Ember dogma does it go.

Question: How would it be possible to solve? I guess there is a template parser, which identify mustaches, and if it a keyword, then it process it, if it a property, then prints it. It could check if it an instaceof a component.

I started a naive workaround but i got stuck: http://emberjs.jsbin.com/lirawekapu/1/edit?html,js,output

Ok, so whats going on here?

On initialization i register a keyword component-2

Ember.Application.initializer({
  name: 'register-keywords',
  initialize: function(){
    var registerKeyword =  Ember.__loader.require("emberhtmlbars/keywords").registerKeyword;
    registerKeyword("component-2", component);
  }
});

component-2 takes two parameters, the first is the name of the component, 2nd is a hash of properties.

controller.js

obj: {foo: 'bar', baz: false}

template.hbs

{{component-2 ‘my-foo’ obj}}

is equivalent to

template.hbs

{{‘my-foo’ foo=“bar” baz=false}}

But it wont update if I change obj. Although rerender runs and hash has the new value. Anyway I like my first proposal better, but maybe I gave some idea how it should work.


#14

@amk I’m not sure if it qualifies as opening an issue. I feel as though they would direct me to stackoverflow or here to ask my question.

I was really hoping to maybe see core team member weigh in here, but I know they’re very busy.

@CaC I worry about the later solution being too brittle. Ultimately feels like Ember should have a native way to do this. :frowning:


#15

@CaC @amk This looks really promising: https://github.com/wycats/handlebars.js/issues/1050 .


#16

Great found @workmanw! This looks really great, indeed. I hope it will be implemented soon!


#17

To be absolutely clear: the only public API for instantiating components is from within templates. This is intentional.

I realize that experienced Ember devs are accustomed to the old view hierarchy model, and so they tend to reach for that when they hit a blocker. But that model doesn’t actually exist anymore, and in the places where it still works, you’re actually using backward-compatibility shims provided by Glimmer that incur performance cost.

If you find something that’s not possible to wire up correctly in a template, that’s a bug we need to solve. It sounds like the only gap mentioned in this thread so far is the need for a splat operator, which is something I want too, and I’m sure we can add without much difficulty. Another relevant discussion is the contextual component lookup RFC.

Liquid Fire has the same issue with modals that you’re describing, but I’m convinced that’s actually because it’s not a great API in the first place. Any time people are trying to describe how to render a component, but they’re not doing so from a template, you’re forced to reimplement a bunch of nice things that templates do automatically. My plan for liquid-fire is to deprecate the existing modals and instead make it easy to compose liquid-fire plus ember-wormhole, so that addons like ember-modal-dialog can easily animate.


#18

@ef4 and everyone. Thanks for your help.

I see now it’s more complex than I assumed it would be with how great the rest of Ember is.

For now I’m just loading a component in the application template, and then am using a service and event bus to show and hide things from there.