Stage | Start Date | Release Date | Release Versions | Relevant Team(s) | RFC PR | ||||
---|---|---|---|---|---|---|---|---|---|
Accepted |
2022-04-16 |
Unreleased |
|
Ember.js |
This RFC propose to introduce a template()
function as an substitution for
the <template>
syntax extension defined in RFC #779 using
standard JavaScript, for use in environments where the custom syntax extension
is not available, and also as a publishing format for addons on NPM:
import { template } from "@ember/template-compilation";
import Hello from "my-app/components/hello";
// <template><Hello /></template> becomes...
export default template(`<Hello />`, () => ({ Hello }));
// export const Foo = <template><Hello /></template> becomes...
export const Foo = template(`<Hello />`, () => ({ Hello }));
// export class Bar {
// <template><Hello /></template>
// }
export class Bar {
static {
template(`<Hello />`, () => ({ Hello }), this);
}
}
Idiomatic usages of the template()
function can be pre-compiled by a build
step if desired, but a runtime implementation can also be used via an opt-in
described below.
We also propose an alternative form that uses the template()
function as a
tag function for JavaScript tagged template strings. However, this variant does
not have a runtime implementation and must be handled by a build step:
import { template } from "@ember/template-compilation";
import Hello from "my-app/components/hello";
// <template><Hello /></template> becomes...
export default template`<Hello />`;
// export const Foo = <template><Hello /></template> becomes...
export const Foo = template`<Hello />`;
// export class Bar {
// <template><Hello /></template>
// }
export class Bar {
static {
template`<Hello />`;
}
}
In addition, we also propose a new @ember/template-compilation/runtime
module as an opt-in to enable runtime template compilation.
RFC #779 introduced a first-class component syntax feature that has numerous benefits, among which are:
- Opt-in to strict mode (RFC #496)
- Eliminating the need for name-based runtime resolutions
- Ability to use imported template constructs
- Ability to interact with the surrounding JavaScript scope
However, one of the drawbacks is that it requires a custom syntax extension
(<template>
) and a custom file format (.gjs/.gts
), which requires a build
step and other custom tooling.
RFC #779 makes a good case for why this is necessary and preferable to the alternatives. We stand by the rationales given in RFC #779, and this RFC does not intend to revisit its recommendations. However, there remain cases where this drawback is highly undesirable or simply not acceptable:
-
Until we have implemented good editor integration, the developer experience of using
.gjs
/.gts
could be abysmal (without any syntax highlighting or completion at all, etc). Even after we ship good editor integration for the mainstream editors, this will likely remain the case for a subset of less used editors that we did not or could not prioritize supporting. -
There may be editors and environments that are impossible for us to support, either because they don't have an extension system at all, or those systems do not expose the capabilities we need. For example, CodeSandbox, CodePen, TypeScript playground, etc.
-
When publishing packages to NPM, it is important and desirable to publish only standard
.js
files that does not require further processing by the consumer (see v2 Addon Format). -
There are use cases that prefer or require components that are directly runnable in the browser without a build step, such as bug report templates, runnable code samples, generating components dynamically at runtime, etc.
From here on, we will refer to users and use cases collectively as "standard
JavaScript environments". Under these circumstances, users would be unable to
adopt the <template>
feature, which means they cannot take advantage of the
numerous benefits unlocked by the feature, such as the strict mode opt-in.
Also, if an addon chose to only provide template constructs (e.g. helpers and components) exclusively via importable modules (as opposed to merging them into "App Javascript"), then there will be no easy way to access these template constructs from those environments.
It is not technically impossible to accomplish these same goals in standard
JavaScript environments. After all, the <template>
feature is specified using
primitives that already exist. Users in standard JavaScript environments can
just use those primitives directly.
However, that is not an ideal outcome. <template>
is a user-facing feature,
and is poised to (or at least well-positioned to) become the main way Ember
users read, write and reason about components in the next edition of Ember when
the feature is fully rolled out.
On the other hand, the primitives it is based on are very low-level, verbose,
error-prone and were more intended as a compilation target than an user-facing
authoring format. Their low-level nature and flexibility also makes it easy to
get the details wrong, such as forgetting to pass the strictMode: true
flag
and or deviate from the normal lexical scope capturing rules. It would be
unfortunate if being restricted to the standard JavaScript environment also
means dropping down to a completely different programming model for components.
Currently, the addon ember-template-imports provides
an alternative that does work in regular .js
/.ts
files – the "hbs backtick"
format. However, it is not a good candidate for solving these problems for a
few reasons, some of which were the same reasons RFC #779 chose not to go that
route:
-
The feature was never specified in RFC #779 or any other RFC. It was added to the addon when its purpose was to be a sandbox for experimentation. With the acceptance of RFC #779, the experimentation phase is over and the addon should be transitioned into a compliant polyfill, which means removing the alternative
hbs
form. -
The naming conflicts with the "official"
hbs
fromember-cli-htmlbars
. This would have been fine if we chose to go that route in RFC #779 and have plans to evolve the official version to have the same feature. With RFC #779 favoring the<template>
approach, this is not going to happen and this conflict will now be quite confusing, especially since they actually do very different things (e.g. the "official"hbs
does not return a component). -
It uses a standard JavaScript syntax in a non-standard way, semantics-wise. It looks like an inert JavaScript string but has access to the lexical scope around it (without using the
${ ... }
syntax), which can be confusing. It also means that it is impossible to provide a runtime implementation using the same syntax that works in environments that does not permit a build step. -
It uses
static template =
to associate templates to classes. This implies runtime behavior that is actually not true (e.g. TypeScript will believe thetemplate
property exists on the class). -
Values consumed by the template but is otherwise unused in the rest of the file may generate errors/warnings from linters or language servers.
To address these issues, this RFC propose that we introduce a new template()
function that servers as a middle ground between the <template>
language
extension and the low-level primitives.
-
It is designed to have the same semantics as the
<template>
feature (such as the strict-mode opt-in, returning a template-only component when not attached to a class), providing the same high-level programming model for authoring components in standard JavaScript environments. -
It requires manually supplying the lexical scope variable bindings (the "scope function").
-
It uses a static initializer block to associate the template.
-
While it is more cumbersome to use and have a degraded experience (e.g. possibly lacking hbs syntax highlighting) compared to first-class component templates, it is still designed to be ergonomic to use, to the extent possible in standard JavaScript environments.
-
It is designed to be pre-processed at build time where possible, just like the
<template>
feature and today's "official"hbs
tag. However, in cases where this is not possible, there will be a runtime implementation available provided the template compiler is also available at runtime. -
For environments where it is possible to configure the development and build tools to recognize the format, the tagged template literal form can be used, which automatically captures the lexical scope variable bindings. This directly replaces ember-template-imports's "hbs backtick" format but brings its naming into alignment with the
<template>
feature.This form will produce an error at runtime if not pre-processed out as a correct runtime implementation is impossible.
Despite the drawbacks mentioned above, this format may still be preferred as a transitional tool for early adopters while editor integrations are being worked on. It may also be useful for communicating with code snippets in platforms where syntax highlighting is not yet available for the custom
.gjs
format (such as Discord and GitHub today), but where it's desirable to still retain the highlighting for the JavaScript/TypeScript portions.
This RFC also recommends that the addon spec (v2.1?) to be updated to
consider using template()
(the function call version, not the tagged template
literal form) as the primary publishing format for component templates and
consider reducing and deprecating the need for the alternative mechanisms (such
as shipping .hbs
modules). However, this is only a general recommendation,
and a future RFC or amendment is needed to explore that further.
That being said, such an update is only needed for the purpose of clarifying
the recommended way of shipping templates in addons. Because template()
is
just standard JavaScript, there is nothing stopping addons from adopting the
format as long as their target Ember versions support the feature, or that they
depend on a suitable polyfill.
This RFC proposes to re-specify the <template>
language extension into a
de-sugaring into template()
calls:
-
Top-level declaration
import Foo from "somewhere"; <template> <Foo /> <template>
becomes
import { template } from "@ember/template-compilation"; import Foo from "somewhere"; export default template( ` <Foo /> `, () => ({ Foo }) );
-
Expression
import Foo from "somewhere"; export const Bar = <template><Foo /><template>;
becomes
import { template } from "@ember/template-compilation"; import Foo from "somewhere"; export const Bar = template(`<Foo />`, () => ({ Foo }));
-
Class
import Foo from "somewhere"; export default class Bar { <template><Foo /><template> }
becomes
import { template } from "@ember/template-compilation"; import Foo from "somewhere"; export default class Bar { static { template(`<Foo />`, () => ({ Foo }), this); } }
When used in this form, the function returns/passes through the target supplied in the third argument (the component class).
Note that the use of a static initializer block here is important for capturing access to private fields. This will be discussed in a subsequent section.
In these small snippets, the scope bindings may look very verbose compared to the size of the templates, but in real-world templates, the ratio will improve.
This de-sugaring specified in this section is intended to subsume the parts
of RFC #779 which references its compilation output, and where it specified the
feature in terms of the low-level primitives such as precompileTemplate()
. If
this RFC is accepted, the <template>
feature should only be understood in
terms of template()
, which will be a stable and public API that stands on its
own.
Any build-time optimization that pre-compiles the template()
calls at build
time are required to ensure their output to have equivalent semantics as the
runtime template()
calls, assuming the same set of AST plugins are applied.
Their exact compilation output should be considered private implementation
details which may change over time and should not be relied upon.
Once this RFC is accepted, any future changes (including deprecations) to the
primitive APIs referenced in RFC #779 should have no direct bearing on the
<template>
or template()
features.
Alternatively, the template()
function can also be used as a tag function for
a JavaScript tagged template string. This alternate form automatically captures
the lexical scope variable bindings. However, this form does not have a runtime
implementation and must be processed by a build step.
-
Top-level declaration
import Foo from "somewhere"; <template> <Foo /> <template>
becomes
import { template } from "@ember/template-compilation"; import Foo from "somewhere"; export default template`<Foo />`;
-
Expression
import Foo from "somewhere"; export const Bar = template`<Foo />`;
becomes
import { template } from "@ember/template-compilation"; import Foo from "somewhere"; export const Bar = template`<Foo />`;
-
Class
import Foo from "somewhere"; export default class Bar { <template><Foo /><template> }
becomes
import { template } from "@ember/template-compilation"; import Foo from "somewhere"; export default class Bar { static { template`<Foo />`; } }
Again, the use of a static initializer block is important for capturing access to private fields.
In this form, the tagged template string literal must be the only statement in the static initializer block. However, the JavaScript standard permits multiple static initializer blocks in the same class body, so any other initialization logic can be placed in separate blocks as needed. It is an error for a class body to contain multiple templates associated this way, just as it is for a class body to contain multiple
<template>
blocks.
The <template>
feature proposed by RFC #779 explicitly chose not to provide
access to private fields when a <template>
is associated with a class, citing
limitations to the compilation output, specifically that the compiled template,
as proposed, would be placed outside of the class body, rendering it impossible
to access any private fields. RFC #779 acknowledges that is a gap that should
be addressed in a future RFC.
With the advancement of static initialization blocks, it is
now possible to place the associated template directly inside the class body.
This RFC takes advantage of this and propose an adornment to allow access to
private fields inside <template>
tags and the proposed template()
:
import { tracked } from "@glimmer/tracking";
export default class Foo {
@tracked #value;
<template>
My value is {{this.#value}}.
The other value is {{@other.#value}}.
</template>
}
import { template } from "@ember/template-compilation";
import { tracked } from "@glimmer/tracking";
export default class Foo {
@tracked #value;
static {
template`
My value is {{this.#value}}.
The other value is {{@other.#value}}.
`;
}
}
import { template } from "@ember/template-compilation";
import { tracked } from "@glimmer/tracking";
export default class Foo {
@tracked #value;
static {
template(`
My value is {{this.#value}}.
The other value is {{@other.#value}}.
`, () => ({ '#value': (_) => _.#value }, this);
}
}
As shown in this last example, it requires a small amendment to the "scope function". The scope function was first introduced in RFC #496, and then subsequently modified slightly from returning an array to an object literal.
The purpose of the scope function is to provide the rendering layer with the needed lexical scope variable bindings that are referenced in the template, in the form of key-value pairs in the returned object literal, where the key is the identifier (variable) as referenced in the template, and the value is the value referenced by the identifier.
To support private field access, we propose to extend this format to allow
keys that starts with a leading #
character. For each unique private field
reference in the template (any .#
access), a corresponding entry must be
added to the object literal where the key matches the name of the private
field in string form. This is not a legal JavaScript identifier thus it does
not generate a conflict with the current format, and will require quoting. The
value of the entry is expected to be a JavaScript "accessor function" that
takes a single argument – an object instance of the same class – and returns
the current value of the private field at the time of the call.
This will require an update to the glimmer-vm rendering engine to support this
format. Internally, it will compile the template in such a way that any access
to a private field in the template will go through the corresponding accessor
function. Since the accessor functions are embedded inside the class body, they
will be permitted to access the private fields declared for the class. It is
therefore crucial that the template()
call is embedded inside a static
initializer block in the class body.
With this change, we can fully support private fields in templates. As shown in
the examples, this applies to both direct access (this.#field
) as well as
indirect access (some.nested.object.of.the.same.kind.#field
), as long as it
is of the same instance type, fully matching the JavaScript semantics.
Typically, it is desirable to for Ember apps to precompile all templates at build time. By default, Ember does not include a template compiler in the bundle as it adds significant amount of code but most apps do not have a use for it.
The template()
function proposed in this RFC is designed so that idiomatic
usages (defined in the next section) can be processed by a built step, which
is enabled by default.
Non-idiomatic usages are not guaranteed to be transformable at build time. If a transformation cannot be applied, the call will be left as-is for runtime processing. Note that since the runtime template compiler is not available by default (see below for how to opt-in), this default behavior will result in a runtime error.
Because the rules for determining idiomatic usages are purely syntactical, they can also be checked by a linter. We propose to include a enabled-by-default lint rule to warn against accidental non-idiomatic usages.
The purpose of specifying an idiomatic subset of the possible syntaxes and
compositions is to reflect that this feature is intended to be an equivalent
of the <template>
feature in environments where the custom extensions are not
viable. Therefore, developers should try to stick to the same set of semantics
and affordances provided by <template>
. It also defines the common subset of
features that tools like codemods and linters must be able to handle.
Deviating outside of this subset of idiomatic uses may result in degraded performance (due to requiring runtime template compilations) and development experience (due to tools not able to make static analysis on the templates), so developers should be very aware of these tradeoffs and should be a deliberate choice.
That being said, we acknowledge that there are legitimate use cases for wanting the extra flexibility or needing runtime behavior. By including a runtime implementation of the feature (again, see below), we get handling for all the remaining edge cases "for free".
In either form, the template()
function must be imported directly from the
@ember/template-compilation
module for the invocation to be considered
idiomatic. Renaming the import binding is permitted. The import binding must
be used directly in the invocations/call-sites. For example, if the imported
value is stored into another constant storing the imported into another
variable, then any indirect invocations are not considered an idiomatic use.
For the function call form of the template()
function, idiomatic usages are
defined as:
-
Unless otherwise specified:
- All string literals inside the call (arguments, property keys, etc) must:
- Be quoted with single quotes, or
- Be quoted with double quotes, or
- Be quoted with backticks but without a tag and without interpolations.
- All object literals ("POJOs") inside the call must:
- Not contain duplicate property names, and
- Not contain computed property names, even if the computed name is just a string literal, and
- Not contain any getters, setters or methods, and
- Not use the spread operator.
- All function expressions must:
- Be specified with either the
function
keyword or arrows, and - Be anonymous, and
- Not have any modifiers (e.g.
async
,function*
, etc), and - Have exactly one statement in the function body, which must be a
return
statement. For arrow function expressions, this implies the single-expression implicit return shorthand syntax can be used, which is encouraged but optional.
- Be specified with either the
- All string literals inside the call (arguments, property keys, etc) must:
-
The first argument to the call must be a string literal containing the template source.
-
The second argument to the call is optional. If provided, it must be a function expression that:
- Has no parameters, and
- Returns an object literal where every name-value pair must be in the form
of:
- The property name must be a valid JavaScript identifier and its value must be a reference to the variable with the same name. This implies that the JavaScript shorthand property syntax can be used, which is encouraged but optional.
-
In order to associate a template with a component class:
- The
template()
function must be called from within astatic
block on the class, where it must be the only statement in the block, and - A third argument must be supplied to the call, and it must be the
this
keyword, and - The specifications for the object literal described in 3.2. is expanded
to permit the following:
- The property name can also be a valid JavaScript private field name
(i.e. a valid JavaScript identifier prefixed by the
#
character), which must be quoted. Its value must be a function expression that:- Has exactly one parameter, and
- When called with an object instance of the same class as parameter, it must return the value of the corresponding private field on the object.
- The property name can also be a valid JavaScript private field name
(i.e. a valid JavaScript identifier prefixed by the
- The
-
No additional arguments can be passed.
-
Comments are permitted in any positions allowable by JavaScript, and any conforming tool must be configured to ignore comments when applying these rules. However, there is no guarantee that the comments will be preserved in the output, and even if they are preserved, there are no guarantee around the placements of these comments in the output.
For the tagged template string literal form, idiomatic usages are defined as:
- The string literal must not contain any interpolations.
- For class association, the tagged template string must be placed inside a
static
block on the class, where it must be the only statement in the block.
Some examples of idiomatic usages:
template("Hello world!");
template(`Hello world!`);
template("Hello world!", () => ({}));
template("Hello world!", () => {
return {};
});
template("Hello world!", function () {
return {};
});
template("<Hello />", () => ({ Hello }));
template("<Hello />", () => {
return { Hello };
});
template("<Hello />", function () {
return { Hello };
});
// Quoting the property names are also allowed
template("<Hello />", () => ({ Hello: Hello }));
template("<Hello />", () => {
return { Hello: Hello };
});
template("<Hello />", function () {
return { Hello: Hello };
});
class Foo {
static {
template("<Hello />", () => ({ Hello }), this);
}
}
class Bar {
static {
// Note: this does meet the requirements for associating the template with
// the surrounding `Bar` class (rule 4). However, it is otherwise perfectly
// legal to have an *unassociated* template() call inside a static block.
this.bar = template("<Hello />");
}
}
Some examples of non-idiomatic or incorrect usages:
template();
let t = template;
t("Hello world!");
let source = "Hello world!";
template(source);
template("Hello" + " " + "world!");
template(`Hello ${audience}!`);
template(stripIndent`
Hello
`);
template("Hello world!", () => {});
const EMPTY_OBJECT = {};
template("Hello world!", () => EMPTY_OBJECT);
template("Hello world!", function scope() {
return {};
});
template("Hello world!", /* this is a comment */ () => {});
template("<Hello />", () => ({ ...scope }));
template("<Hello />", () => ({ ["Hello"]: Hello }));
template("<Hello />", () => {
let scope = {};
scope["Hello"] = Hello;
return scope;
});
template("<Hello />", () => ({ Hello }), class Foo {});
template("<Hello />", () => ({ Hello }), Foo);
class Foo {
static template = template("<Hello />", () => ({ Hello }));
}
class Bar {
static {
debugger;
template`<Hello />`;
}
}
As pointed out above, the philosophy around these restrictions is to maintain
parity between the possible semantics with the <template>
and restricting the
syntactic variations to make it feasible for tools to perform the static
analysis reliably.
On the other hand, at least in some of the use cases we are targeting, these would be hand-written in hand-maintained source code files (as opposed to only being a compilation target). Therefore, it is also an important balancing act to not add gratuitous restrictions that prescribe a "canonical syntax" that is easy to violate by accident. For example, while would could mandate, say, that the scope function must be specified using arrow functions, it could be easily forgotten and get missed in code-reviews, which could subtly cause the template to fallback to runtime compilation. Formatting tools like prettier may also automatically rewrite the code between different equivalent styles.
If the idiomatic usage rules are followed, a build-time processor will be able to provide warnings or errors for common mistakes, such as syntax errors within the Handlebars source code, missing or unused scope variables, etc. It may also be possible to provide some limited editor integration, though as pointed out by RFC #779, there are some inherit challenges with this approach.
Ember has always had the ability to compile template at runtime. However, since this incur significant costs and most apps do not benefit from it, this feature is disabled by default and requires and explicit opt-in to include the runtime template compiler into the app's bundle. The mechanism of the opt-in will be discussed later in this section.
If the template compiler is available at runtime, then it will be possible to
use the template()
function at runtime as well. However, as mentioned above,
this does not extend to the "tagged template string" format. Attempting to use
the template()
function as a tag at runtime will result in a helpful error in
debug builds and undefined behavior on production builds.
If the template compiler is not available at runtime, then attempts to call the
template()
function at runtime will result in a helpful error in debug builds
and undefined behavior on production builds. On production builds, it is not
guaranteed that the template
named export may be undefined (i.e. it may not
even be a function), but developers must not rely on this to "feature-detect"
the availability of runtime compilation.
Notably, even when template compilation is available at runtime, the result of the compilations may be subtly different. This is because that applications may have custom glimmer/handlebars AST plugins in their build, and these plugins will not be available at runtime.
This may change in the future, but it has always been true for runtime template compilation and will likely remain to be the case for the foreseeable future, as these plugins are often authored or packaged in ways that are inherently not browser-friendly. This usually only used for optional syntax extensions such as "inline {{let}}" so in practice it is not an issue. However, some polyfills for template features also uses the AST plugin API and those polyfilled features will not be available to templates compiled at runtime.
Today, applications can opt-in to bundling the runtime compiler with something
along these lines in their ember-cli-build.js
:
app.import("node_modules/ember-source/dist/ember-template-compiler.js");
The exact details depend on the version of ember-source
and is not very well
documented. It also does not align with the direction we are headed, where we
encourage using the modules system and imports to express dependencies. This
configuration is also not friendly to tree-shaking/code-splitting, as it forces
the template compiler to be included into the main/initial bundle, when it may
only be needed in one of the infrequently used routes.
This RFC takes the opportunity to improve on this by proposing a new module –
@ember/template-compilation/runtime
. When the template()
module is imported
from this location, it guarantees that those calls will not be pre-processed
at build time. It will also ensure the runtime template compiler is included in
the bundle to support the runtime calls.
In addition, this module can also be imported for side-effect. Like the above,
this again ensures the runtime template compiler is included in the build. It
essentially serves as an alternative to the app.import()
opt-in today. Note
that merely including this side-effect import does not preclude build-time
pre-processing from happening, which is controlled by other build config such
as the inclusion of the relevant Babel plugins. It simply ensures that if, for
any reason, there were unprocessed template()
calls left in the build, they
will work correctly at runtime without producing any errors.
The general recommendation is that the @ember/template-compilation
module
should be used (and the idiomatic usage guidelines are followed) in most
situations and whenever possible, so that the code can be agnostic against how
and where the compilation happens. Whether the application or environment is
set up for build-time pre-processing,
either as a "side-effect" import statement, or if its exported values are imported, then it serves as an opt-in for making runtime template compilation available.
For the time being, we propose that this module should have a single named
export – template
, which is a re-export of the template()
function we
described above. It has the same semantics as template()
from the main module
except it is always invalid to use this imported function as a tag, so linters
and TypeScript can trivial flag this invalid usage.
The purpose of the re-exported template()
from the runtime module is to
signal to both human readers and build tools that the template compilation is
deliberately deferred to runtime. These usages must not be processed at build
time and linters must not warn about non-idiomatic usages.
Typically, developers should favor importing template()
from the main module
so that their code can be agonistic against where the compilation is happening.
However, since the intent is that build-time compilation should be possible,
they should also take care to ensure they stay within the set of idiomatic set
of usages to guarantee that is the case, and this re-export from the runtime
module offers a way to signal that the deviations are deliberate.
This RFC does not propose to add re-exports for other features found in the
@ember/template-compilation
package. Conceptually, it makes sense to provide
the same ex-export for at least the compile()
function. However, as we move
to a world where components are used everywhere and the need for "standalone
templates" dwindles, it is unclear that the compile()
function is needed in
the long run. In any case, if that turns out to be necessary, a future RFC can
propose to add support for that and the precedence set forth in this RFC should
make that a straightforward process.
We don't expect this feature to pose any significant challenges in terms of
TypeScript support, other than having a few overloaded signatures/purpose on
the same template()
function may make things a little cumbersome, but not
prohibitively so. The signature of the function can be described as such:
// These types are defined in other RFCs
interface Component<Signature> {
/* ... */
}
type Scope = () => Record<string, unknown>;
function template<Signature>(source: string): Component<Signature>;
function template<Signature>(
source: string,
scope: Scope
): Component<Signature>;
function template<Signature, C extends Component<Signature>>(
source: string,
scope: Scope,
target: C
): C;
// For the build-time tagged template literal extension. By not specifying
// additional arguments here, TypeScript actually enforces that the tagged
// string literal cannot have any interpolation expressions.
function template<Signature>(
source: TemplateStringsArray
): Component<Signature>;
One nice thing about this design is that there is a natural place for the type
(generic) argument for the template's signature, negating the need for the
special <template[Signature]>
syntax.
The teaching recommendations in RFC #779 shall remain in full force.
We continue to recommend teaching materials to be focused around <template>
and .gjs
. The template()
feature is recommended only in standard JavaScript
environments where adoption of <template>
and .gjs
is not feasible.
Because template()
is a standard JavaScript API that has a "real" import
location, there is a natural place to document and describe its behavior in the
API docs. This should be the primary reference of the feature, and should also
where we document the idiomatic subset as well.
In the section of the guides where we teach <template>
, there should also be
a small subsection discussing using template()
as an alternative, including
what the appropriate use cases and its potential drawbacks. There may also be
value in teaching <template>
in terms of its desugaring into template()
in
the guides. Some developers may find it easier to appreciate what <template>
is or does and its benefits by comparing it to the equivalent standard
JavaScript desugaring.
Exposing the ability to manually pass a scope function can expose flexibility that we don't intend on providing and opening it up to misuse. For example, the closure could do more than just simply capturing the surrounding lexical scope, impart side-effects, observe the timing of the call, etc.
However, this is not a new problem in a sense, as the primitives already exists
and the current status quo is that users who cannot use <template>
must use
those primitives directly. With good linting and the ability to precompile and
analyze templates statically, it provides good incentives for developers to
stick with the idiomatic subset, which is carefully defined to have the same
semantics and power as <template>
.
In any case, a lot of these "dangerous" patterns are already possible outside of the scope function, especially after the default helper manager making it easy to define inline helpers.
With the ability to fallback to runtime compilation, the existence of these edge cases do not create a lot of additional implementation complexity.
We could use class decorators to associate templates to classes instead of the
static initializer block approach. The biggest drawback is that we would not be
able to access private fields with this design. In addition, in practice it
feels pretty awkward to use due to the length of the source text and the amount
of arguments needed, even for trivial examples. It also doesn't help this case
that stage 3 decorators require that class decorators come after the export
and default
keywords:
export @template(`<Hello />`, () => ({ Hello }) class Bar {
}
It seems likely that the <template>
tag will eventually gain some kind of
"attributes" syntax for configuring things like whitespace handling (see the
next section).
One option is to "reserve space" for that now, but it can also be done later by
optionally allowing the current "scope closure" argument to be a options object
where the scope closure would be passed as { scope: () => ({ ... }) }
.
One thing that RFC #779 did not specify is that how whitespace
should be handled. The default conclusion is probably that the whitespaces
should be preserved exactly the way they appear in the .gjs
source text.
While this technically works, it is a conclusion that pleases and benefits no
one, and will likely be revised/refined during the "spec work" process before
the feature is shipped.
On the one hand, there is the argument that whitespaces are typically not significant in almost all HTML contexts templates. Shipping all these useless whitespace just to have the browser fold them away when rendered into the DOM is a not insignificant amount of wasted space in app bundles today. It also just creates unnecessary work for the browser and the rendering layer.
On the other hand, there is the argument that whitespaces can be significant.
Sometimes, this can be observed from uses of the <pre>
element, but really,
the whitespace handling for any element (including <pre>
) can be changed
using the CSS white-space
property, so it is not necessarily safe to assume
that we can fold/collapse whitespaces in templates just because that's the
default behavior of the browser.
However, even with that in mind, the default conclusion for the whitespace
handling in <template>
tags is still not useful, especially when the
author is trying to preserve significant whitespace for something like code
samples. This is because <template>
tags often appears in JavaScript
contexts that already has leading indentation (such as inside a class body), so
in practice, if we preserve the whitespace exactly as it appears inside the
<template>
tag, it still would not do what the author was trying to
accomplish.
It seems likely that we would need to define some default (perhaps globally
configurable) whitespace handling rules, along with some mechanisms for local
overrides. Perhaps something like <template white-space="pre">
.
Once we decide that for <template>
, then we have to make the additional
choice of what to do about template()
. Because they are the same feature,
they face the same challenges. Again, the default course of action would be to
do nothing/preserve the whitespaces exactly, but it's a solution that serves no
one for the same reasons.
It is the same challenge that multi-line backtick string literals faces in
normal JavaScript. Perhaps it would have been ideal if the language specified
backticks to have stripIndent
semantics, but that ship has sailed now.
In the build time version of template()
, we could certainly apply the same
whitespace handling semantics as we have the full source text available to us
for the analysis. Depends on what those semantics are, we may or may not be
able to do the same in the runtime version, and it may be a divergence that we
have to accept.
Of course, in those cases, since the compilation runs at runtime anyway,
developers can apply arbitrary whitespace normalization on the source text
themselves before passing them into the template()
function, such as using
the popular strip-indent NPM package, so it does not create any
functional limitations per-se. The main issue here is whether we end up with
different semantics in this area between the build-time and runtime versions,
and whether it is acceptable to require the few developers needing runtime
compilation to know that the difference exists. Then again, because of the lack
of AST plugins at runtime, this may not be the only or even the most notable
difference in a given application anyway.
In any case, we don't necessarily have to solve this problem as part of this
RFC, as we would still have to do the same amendment on <template>
, with that
conclusion being the primary driver for the decision in template()
. However,
it is import to come to some conclusion before either of these features is
officially shipped as it would be hard to change after.