#

API Docs for: v2.11.1
Show: Inherited Protected Private Deprecated

# File: packages/ember-glimmer/lib/helpers/component.js

    /**
      @module ember
      @submodule ember-glimmer
    */
    import { assign } from 'ember-utils';
    import { CachedReference } from '../utils/references';
    import {
      CurlyComponentDefinition,
      validatePositionalParameters
    } from '../syntax/curly-component';
    import {
      Blocks,
      EvaluatedArgs,
      EvaluatedNamedArgs,
      EvaluatedPositionalArgs,
      isComponentDefinition
    } from 'glimmer-runtime';
    import { assert } from 'ember-metal';

    /**
      The `{{component}}` helper lets you add instances of `Ember.Component` to a
      template. See [Ember.Component](/api/classes/Ember.Component.html) for
      additional information on how a `Component` functions.
      `{{component}}`'s primary use is for cases where you want to dynamically
      change which type of component is rendered as the state of your application
      changes. This helper has three modes: inline, block, and nested.

      ### Inline Form

      Given the following template:

      ```app/application.hbs
      {{component infographicComponentName}}
      ```

      And the following application code:

      ```app/controllers/application.js
      export default Ember.Controller.extend({
        infographicComponentName: computed('isMarketOpen', {
          get() {
            if (this.get('isMarketOpen')) {
              return 'live-updating-chart';
            } else {
              return 'market-close-summary';
            }
          }
        })
      });
      ```

      The `live-updating-chart` component will be appended when `isMarketOpen` is
      `true`, and the `market-close-summary` component will be appended when
      `isMarketOpen` is `false`. If the value changes while the app is running,
      the component will be automatically swapped out accordingly.
      Note: You should not use this helper when you are consistently rendering the same
      component. In that case, use standard component syntax, for example:

      ```app/templates/application.hbs
      {{live-updating-chart}}
      ```

      ### Block Form

      Using the block form of this helper is similar to using the block form
      of a component. Given the following application template:

      ```app/templates/application.hbs
      {{#component infographicComponentName}}
        Last update: {{lastUpdateTimestamp}}
      {{/component}}
      ```

      The following controller code:

      ```app/controllers/application.js
      export default Ember.Controller.extend({
        lastUpdateTimestamp: computed(function() {
          return new Date();
        }),

        infographicComponentName: computed('isMarketOpen', {
          get() {
            if (this.get('isMarketOpen')) {
              return 'live-updating-chart';
            } else {
              return 'market-close-summary';
            }
          }
        })
      });
      ```

      And the following component template:

      ```app/templates/components/live-updating-chart.hbs
      {{! chart }}
      {{yield}}
      ```

      The `Last Update: {{lastUpdateTimestamp}}` will be rendered in place of the `{{yield}}`.

      ### Nested Usage

      The `component` helper can be used to package a component path with initial attrs.
      The included attrs can then be merged during the final invocation.
      For example, given a `person-form` component with the following template:

      ```app/templates/components/person-form.hbs
      {{yield (hash
        nameInput=(component "my-input-component" value=model.name placeholder="First Name")
      )}}
      ```

      When yielding the component via the `hash` helper, the component is invocked directly.
      See the following snippet:

      ```
      {{#person-form as |form|}}
        {{form.nameInput placeholder="Username"}}
      {{/person-form}}
      ```

      Which outputs an input whose value is already bound to `model.name` and `placeholder`
      is "Username".

      When yielding the component without the hash helper use the `component` helper.
      For example, below is a `full-name` component template:

      ```handlebars
      {{yield (component "my-input-component" value=model.name placeholder="Name")}}
      ```

      ```
      {{#full-name as |field|}}
        {{component field placeholder="Full name"}}
      {{/full-name}}
      ```

      @method component
      @since 1.11.0
      @for Ember.Templates.helpers
      @public
    */
    export class ClosureComponentReference extends CachedReference {
      static create(args, symbolTable, env) {
        return new ClosureComponentReference(args, symbolTable, env);
      }

      constructor(args, symbolTable, env) {
        super();
        this.defRef = args.positional.at(0);
        this.env = env;
        this.tag = args.positional.at(0).tag;
        this.symbolTable = symbolTable;
        this.args = args;
        this.lastDefinition = undefined;
        this.lastName = undefined;
      }

      compute() {
        // TODO: Figure out how to extract this because it's nearly identical to
        // DynamicComponentReference::compute(). The only differences besides
        // currying are in the assertion messages.
        let { args, defRef, env, symbolTable, lastDefinition, lastName } = this;
        let nameOrDef = defRef.value();
        let definition = null;

        if (nameOrDef && nameOrDef === lastName) {
          return lastDefinition;
        }

        this.lastName = nameOrDef;

        if (typeof nameOrDef === 'string') {
          definition = env.getComponentDefinition([nameOrDef], symbolTable);
          assert(`The component helper cannot be used without a valid component name. You used "${nameOrDef}" via (component "${nameOrDef}")`, definition);
        } else if (isComponentDefinition(nameOrDef)) {
          definition = nameOrDef;
        } else {
          assert(
            `You cannot create a component from ${nameOrDef} using the {{component}} helper`,
            nameOrDef
          );
          return null;
        }

        let newDef = createCurriedDefinition(definition, args);

        this.lastDefinition = newDef;

        return newDef;
      }
    }

    function createCurriedDefinition(definition, args) {
      let curriedArgs = curryArgs(definition, args);

      return new CurlyComponentDefinition(
        definition.name,
        definition.ComponentClass,
        definition.template,
        curriedArgs
      );
    }

    function curryArgs(definition, newArgs) {
      let { args, ComponentClass } = definition;
      let { positionalParams } = ComponentClass;

      // The args being passed in are from the (component ...) invocation,
      // so the first positional argument is actually the name or component
      // definition. It needs to be dropped in order for any actual positional
      // args to coincide with the ComponentClass's positionParams.

      // For "normal" curly components this slicing is done at the syntax layer,
      // but we don't have that luxury here.

      let [, ...slicedPositionalArgs] = newArgs.positional.values;

      if (positionalParams && slicedPositionalArgs.length) {
        validatePositionalParameters(newArgs.named, slicedPositionalArgs, positionalParams);
      }

      let isRest = typeof positionalParams === 'string';

      // For non-rest position params, we need to perform the position -> name mapping
      // at each layer to avoid a collision later when the args are used to construct
      // the component instance (inside of processArgs(), inside of create()).
      let positionalToNamedParams = {};

      if (!isRest && positionalParams && positionalParams.length > 0) {
        let limit = Math.min(positionalParams.length, slicedPositionalArgs.length);

        for (let i = 0; i < limit; i++) {
          let name = positionalParams[i];
          positionalToNamedParams[name] = slicedPositionalArgs[i];
        }

        slicedPositionalArgs.length = 0; // Throw them away since you're merged in.
      }

      // args (aka 'oldArgs') may be undefined or simply be empty args, so
      // we need to fall back to an empty array or object.
      let oldNamed = args && args.named && args.named.map || {};
      let oldPositional = args && args.positional && args.positional.values || [];

      // Merge positional arrays
      let mergedPositional = new Array(Math.max(oldPositional.length, slicedPositionalArgs.length));
      mergedPositional.splice(0, oldPositional.length, ...oldPositional);
      mergedPositional.splice(0, slicedPositionalArgs.length, ...slicedPositionalArgs);

      // Merge named maps
      let mergedNamed = assign({}, oldNamed, positionalToNamedParams, newArgs.named.map);

      let mergedArgs = EvaluatedArgs.create(
        EvaluatedPositionalArgs.create(mergedPositional),
        EvaluatedNamedArgs.create(mergedNamed),
        Blocks.empty()
      );

      return mergedArgs;
    }

    export default function(vm, args, symbolTable) {
      return ClosureComponentReference.create(args, symbolTable, vm.env);
    }