ember.jsember-cliember-i18n

Dynamically render components in Ember for ember-i18n


First of all, ember's component helper doesn't help in this case. That would only help if I knew how many components I needed to render, and in what order.

I need to be able to render components based on a string like:
{{user}} has made a bid of {{bid}}, where:

  1. {{user}} and {{bid}} will be replaced with components.
  2. the given string is unknown, with an unknown number of dynamic sections representing components (The components would be passed in with the given string).

If these dynamic sections were helpers, this would be easy - but helpers just don't cut it for certain items in my game.

Ideally I could do something like this:

{{translated-content
  content='{{user}} has made a bid of {{bid}}'
  user=(component 'user-ui')
  bid=(component 'bid-ui') }}

Is this possible with ember?


Solution

  • With some help, I've come up with the following component which works with ember-i18n, and ember 1.11 or later.

    It could likely be optimised further, but it works nice and fast the way it is.

    Create a new component

    ember g component t-t

    template.hbs

    {{#each parts as |part|}}
    
      {{#if part.isComponent}}
        {{component part.content}}
      {{else}}
        {{part.content}}
      {{/if}}
    
    {{/each}}
    

    component.js

    import Ember from 'ember';
    const { $ } = Ember;
    
    export default Ember.Component.extend({
    
      tagName: 'span',
    
      updateComponents: Ember.on('didReceiveAttrs',function(opts){
    
        let newAttrs = opts.newAttrs;
        let components = {};
    
        $.each(newAttrs,(key,val)=>{
    
          if( key !== 't' && typeof val === 'object' ){
            let keys = Object.keys(val);
            if(keys.length && keys[0].indexOf('COMPONENT_')>=0){
              components[key] = val;
            }
          }
    
        });
    
        this.set('_components',components);
    
      }),
    
      parts: Ember.computed('_components','t','i18n.locale',function(){
    
        let attrs = [];
        let components = this.get('_components');
        let componentKeys = Object.keys(components);
    
        $.each(this.attrs,(key,val)=>{
          if( key !== 't'){
            if( componentKeys.indexOf(key)<0 ){
              attrs[key] = val;
            } else {
              attrs[key] = `{{${key}}}`;
            }
          }
        });
    
        let content = this.get('i18n').t(this.get('t'),attrs).toString();
        content = content.replace(/\{\{(\w+?)\}\}/g,(fullMatch)=>{
          return `{{split}}${fullMatch}{{split}}`;
        });
    
        let parts = content.split('{{split}}');
    
        parts.forEach((val,i)=>{
          let isComponent;
          let key = val.replace(/\{\{(\w+?)\}\}/g,(fullMatch,key)=>{
            isComponent = true;
            return key;
          });
    
          if(isComponent && components[key]){
            parts[i] = {
              isComponent: true,
              content: components[key]
            };
          } else {
            parts[i] = {
              content: Ember.String.htmlSafe(val)
            };
          }
    
        });
    
        return parts;
    
      }),
    
    }).reopenClass({
      positionalParams: ['t']
    });
    

    Usage

    {{t-t
      'your-ember-i18n-path'
      key1='Normal Content (example)'
      key2=(component 'your-component') }}