API Docs for: v2.11.0
Show:

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);
}