UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

246 lines (236 loc) 8.53 kB
import './plugins/allowed-globals.js'; import templateOnly from '../../component/template-only.js'; import '../../../shared-chunks/fragment-EpVz5Xuc.js'; import '../../../@glimmer/wire-format/index.js'; import { p as precompile } from '../../../shared-chunks/compiler-Ddfo5StE.js'; import '../../../@glimmer/validator/index.js'; import '../../../shared-chunks/reference-BNqcwZWH.js'; import '../../../shared-chunks/capabilities-DGmQ_mz4.js'; import { s as setComponentTemplate } from '../../../shared-chunks/template-Dc_cBOoX.js'; import { t as templateFactory } from '../../../shared-chunks/index-CSVCFS_p.js'; import compileOptions from './compile-options.js'; /** * All possible options passed to `template()` may specify a `moduleName`. */ /** * When using `template` in a class, you call it in a `static` block and pass * the class as the `component` option. * * ```ts * class MyComponent extends Component { * static { * template('{{this.greeting}}, {{@place}}!', * { component: this }, * // explicit or implicit option goes here * ); * } * } * ``` * * For the full explicit form, see {@linkcode ExplicitClassOptions}. For the * full implicit form, see {@linkcode ImplicitClassOptions}. */ /** * When using `template` outside of a class (i.e. a "template-only component"), you can pass * a `scope` option that explicitly provides the lexical scope for the template. * * This is called the "explicit form". * * ```ts * const greeting = 'Hello'; * const HelloWorld = template('{{greeting}} World!', { scope: () => ({ greeting }) }); * ``` */ /** * When using `template` *inside* a class (see * {@linkcode BaseClassTemplateOptions}), you can pass a `scope` option that * explicitly provides the lexical scope for the template, just like a template-only * component (see {@linkcode ExplicitTemplateOnlyOptions}). * * ```ts * class MyComponent extends Component { * static { * template('{{this.greeting}}, {{@place}}!', * { component: this }, * // explicit or implicit option goes here * ); * } * } * ``` * * ## The Scope Function's `instance` Parameter * * However, the explicit `scope` function in a *class* also takes an `instance` option * that provides access to the component's instance. * * Once it's supported in Handlebars, this will make it possible to represent private * fields when using the explicit form. * * ```ts * class MyComponent extends Component { * static { * template('{{this.#greeting}}, {{@place}}!', * { component: this }, * scope: (instance) => ({ '#greeting': instance.#greeting }), * ); * } * } * ``` */ /** * The *implicit* form of the `template` function takes an `eval` option that * allows the runtime compiler to evaluate local template variables without * needing to maintain an explicit list of the local variables used in the * template scope. * * The eval options *must* be passed in the following form: * * ```ts * { * eval() { return eval(arguments[0]) } * } * ``` * * ## Requirements of the `eval` Option * * **The syntactic form presented above is the only form you should use when * passing an `eval` option.** * * This is _required_ if you want your code to be compatible with the * compile-time implementation of `@ember/template-compiler`. While the runtime * compiler offers a tiny bit of additional wiggle room, you still need to follow * very strict rules. * * We don't recommend trying to memorize the rules. Instead, we recommend using * the snippet presented above and supported by the compile-time implementation. * * ### The Technical Requirements of the `eval` Option * * The `eval` function is passed a single parameter that is a JavaScript * identifier. This will be extended in the future to support private fields. * * Since keywords in JavaScript are contextual (e.g. `await` and `yield`), the * parameter might be a keyword. The `@ember/template-compiler/runtime` expects * the function to throw a `SyntaxError` if the identifier name is not valid in * the current scope. (The direct `eval` function takes care of this out of the * box.) * * Requirements: * * 1. The `eval` method must receive its parameter as `arguments[0]`, which * ensures that the variable name passed to `eval()` is not shadowed by the * function's parameter name. * 2. The `eval` option must be a function or concise method, and not an arrow. * This is because arrows do not have their own `arguments`, which breaks * (1). * 3. The `eval` method must call "*direct* `eval`", and not an alias of `eval`. * Direct `eval` evaluates the code in the scope it was called from, while * aliased versions of `eval` (including `new Function`) evaluate the code in * the global scope. * 4. The `eval` method must return the result of calling "direct `eval`". * * The easiest way to achieve these requirements is to use the exact syntax * presented above. This is *also* the only way to be compatible * * ## Rationale * * This is useful for two reasons: * * 1. This form is a useful _intermediate_ form for the compile-time toolchain. * It allows the content-tag preprocessor to convert the `<template>` syntax * into valid JavaScript without needing to involve full-fledged lexical * analysis. * 2. This form is a convenient form for manual prototyping when using the * runtime compiler directly. While it requires some extra typing relative to * `<template>`, it's a mechanical 1:1 transformation of the syntax. * * In practice, implementations that use a runtime compiler (for example, a * playground running completely in the browser) should probably use the * `content-tag` preprocessor to convert the template into the implicit form, * and then rely on `@ember/template-compiler/runtime` to evaluate the template. */ /** * When using `template` outside of a class (i.e. a "template-only component"), you can pass * an `eval` option that _implicitly_ provides the lexical scope for the template. * * This is called the "implicit form". * * ```ts * const greeting = 'Hello'; * const HelloWorld = template('{{greeting}} World!', { * eval() { return arguments[0] } * }); * ``` * * For more details on the requirements of the `eval` option, see {@linkcode ImplicitEvalOption}. */ /** * When using `template` inside of a class, you can pass an `eval` option that * _implicitly_ provides the lexical scope for the template, just as you can * with a {@linkcode ImplicitTemplateOnlyOptions | template-only component}. * * This is called the "implicit form". * * ```ts * class MyComponent extends Component { * static { * template('{{this.greeting}}, {{@place}}!', * { component: this }, * eval() { return arguments[0] } * ); * } * } * ``` * * ## Note on Private Fields * * The current implementation of `@ember/template-compiler` does not support * private fields, but once the Handlebars parser adds support for private field * syntax and it's implemented in the Glimmer compiler, the implicit form should * be able to support them. */ function template(templateString, providedOptions) { const options = { strictMode: true, ...providedOptions }; const evaluate = buildEvaluator(options); const normalizedOptions = compileOptions(options); const component = normalizedOptions.component ?? templateOnly(); const source = precompile(templateString, normalizedOptions); const template = templateFactory(evaluate(`(${source})`)); setComponentTemplate(template, component); return component; } const evaluator = source => { return new Function(`return ${source}`)(); }; function buildEvaluator(options) { if (options === undefined) { return evaluator; } if (options.eval) { return options.eval; } else { const scope = options.scope?.(); if (!scope) { return evaluator; } return source => { let hasThis = Object.prototype.hasOwnProperty.call(scope, 'this'); let thisValue = hasThis ? scope.this : undefined; let argNames = []; let argValues = []; for (let [name, value] of Object.entries(scope)) { if (name === 'this') { continue; } argNames.push(name); argValues.push(value); } let fn = new Function(...argNames, `return (${source})`); return hasThis ? fn.call(thisValue, ...argValues) : fn(...argValues); }; } } export { template };