-
Notifications
You must be signed in to change notification settings - Fork 781
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compiler, runtime): add support for form-associated elements (#4784
) This adds support for building form-associated custom elements in Stencil components, allowing Stencil components to participate in HTML forms in a rich manner. This is a popular request in the Stencil community (see #2284). The new form-association technology is exposed to the component author via the following changes: - A new option called `formAssociated` has been added to the [`ComponentOptions`](https://github.com/ionic-team/stencil/blob/06f6fad174c32b270ce239afab5002c23d30ccbc/src/declarations/stencil-public-runtime.ts#L10-L55) interface. - A new `@AttachInternals()` decorator can be used to indicate a property on a Stencil component to which an [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) object will be bound at runtime. - Using `@AttachInternals()` is supported both for lazy builds ([`www`](https://stenciljs.com/docs/www), [`dist`](https://stenciljs.com/docs/distribution)) as well as for [`dist-custom-elements`](https://stenciljs.com/docs/custom-elements). The new behavior is implemented at compile-time, and so should result in only very minimal increases in code / bundle size. Support exists for using form-associated components in both the lazy and the CE output targets, as well as some extremely minimal provisions for testing. Documentation for this feature was added to the Stencil site here: ionic-team/stencil-site#1247
- Loading branch information
1 parent
b1dd4ac
commit 5976c9b
Showing
38 changed files
with
794 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
src/compiler/transformers/component-lazy/attach-internals.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import type * as d from '@stencil/core/declarations'; | ||
import ts from 'typescript'; | ||
|
||
import { HOST_REF_ARG } from './constants'; | ||
|
||
/** | ||
* Create a binding for an `ElementInternals` object compatible with a | ||
* lazy-load ready Stencil component. | ||
* | ||
* In order to create a lazy-loaded form-associated component we need to access | ||
* the underlying host element (via the "$hostElement$" prop on {@link d.HostRef}) | ||
* to make the `attachInternals` call on the right element. This means that the | ||
* code generated by this function depends on there being a variable in scope | ||
* called {@link HOST_REF_ARG} with type {@link HTMLElement}. | ||
* | ||
* If an `@AttachInternals` decorator is present on a component like this: | ||
* | ||
* ```ts | ||
* @AttachInternals() | ||
* internals: ElementInternals; | ||
* ``` | ||
* | ||
* then this transformer will create syntax nodes which represent the | ||
* following TypeScript source: | ||
* | ||
* ```ts | ||
* if (hostRef.$hostElement$["s-ei"]) { | ||
* this.internals = hostRef.$hostElement$["s-ei"]; | ||
* } else { | ||
* this.internals = hostRef.$hostElement$.attachInternals(); | ||
* hostRef.$hostElement$["s-ei"] = this.internals; | ||
* } | ||
* ``` | ||
* | ||
* The `"s-ei"` prop on a {@link d.HostElement} may hold a reference to the | ||
* `ElementInternals` instance for that host. We store a reference to it | ||
* there in order to support HMR because `.attachInternals` may only be | ||
* called on an `HTMLElement` one time, so we need to store a reference to | ||
* the returned value across HMR updates. | ||
* | ||
* @param cmp metadata about the component of interest, gathered during compilation | ||
* @returns a list of expression statements | ||
*/ | ||
export function createLazyAttachInternalsBinding(cmp: d.ComponentCompilerMeta): ts.Statement[] { | ||
if (cmp.formAssociated && cmp.attachInternalsMemberName) { | ||
return [ | ||
ts.factory.createIfStatement( | ||
// the condition for the `if` statement here is just whether the | ||
// following is defined: | ||
// | ||
// ```ts | ||
// hostRef.$hostElement$["s-ei"] | ||
// ``` | ||
hostRefElementInternalsPropAccess(), | ||
ts.factory.createBlock( | ||
[ | ||
// this `ts.factory` call creates the following statement: | ||
// | ||
// ```ts | ||
// this.${ cmp.formInternalsMemberName } = hostRef.$hostElement$['s-ei']; | ||
// ``` | ||
ts.factory.createExpressionStatement( | ||
ts.factory.createBinaryExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createThis(), | ||
// use the name set on the {@link d.ComponentCompilerMeta} | ||
ts.factory.createIdentifier(cmp.attachInternalsMemberName), | ||
), | ||
ts.factory.createToken(ts.SyntaxKind.EqualsToken), | ||
hostRefElementInternalsPropAccess(), | ||
), | ||
), | ||
], | ||
true, | ||
), | ||
ts.factory.createBlock( | ||
[ | ||
// this `ts.factory` call creates the following statement: | ||
// | ||
// ```ts | ||
// this.${ cmp.attachInternalsMemberName } = hostRef.$hostElement$.attachInternals(); | ||
// ``` | ||
ts.factory.createExpressionStatement( | ||
ts.factory.createBinaryExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createThis(), | ||
// use the name set on the {@link d.ComponentCompilerMeta} | ||
ts.factory.createIdentifier(cmp.attachInternalsMemberName), | ||
), | ||
ts.factory.createToken(ts.SyntaxKind.EqualsToken), | ||
ts.factory.createCallExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createIdentifier(HOST_REF_ARG), | ||
ts.factory.createIdentifier('$hostElement$'), | ||
), | ||
ts.factory.createIdentifier('attachInternals'), | ||
), | ||
undefined, | ||
[], | ||
), | ||
), | ||
), | ||
// this `ts.factory` call produces the following: | ||
// | ||
// ```ts | ||
// hostRef.$hostElement$['s-ei'] = this.${ cmp.attachInternalsMemberName }; | ||
// ``` | ||
ts.factory.createExpressionStatement( | ||
ts.factory.createBinaryExpression( | ||
hostRefElementInternalsPropAccess(), | ||
ts.factory.createToken(ts.SyntaxKind.EqualsToken), | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createThis(), | ||
// use the name set on the {@link d.ComponentCompilerMeta} | ||
ts.factory.createIdentifier(cmp.attachInternalsMemberName), | ||
), | ||
), | ||
), | ||
], | ||
true, | ||
), | ||
), | ||
]; | ||
} else { | ||
return []; | ||
} | ||
} | ||
|
||
/** | ||
* Create TS syntax nodes which represent accessing the `"s-ei"` (stencil | ||
* element internals) property on `$hostElement$` (a {@link d.HostElement}) on a | ||
* {@link d.HostRef} element which is called {@link HOST_REF_ARG}. | ||
* | ||
* The corresponding TypeScript source will look like: | ||
* | ||
* ```ts | ||
* hostRef.$hostElement$["s-ei"] | ||
* ``` | ||
* | ||
* @returns TS syntax nodes | ||
*/ | ||
function hostRefElementInternalsPropAccess(): ts.ElementAccessExpression { | ||
return ts.factory.createElementAccessExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createIdentifier(HOST_REF_ARG), | ||
ts.factory.createIdentifier('$hostElement$'), | ||
), | ||
ts.factory.createStringLiteral('s-ei'), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** | ||
* Used to create an identifier for an argument to the constructor of a | ||
* transformed, lazy-build specific class. | ||
*/ | ||
export const HOST_REF_ARG = 'hostRef'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
src/compiler/transformers/component-native/attach-internals.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import type * as d from '@stencil/core/declarations'; | ||
import ts from 'typescript'; | ||
|
||
/** | ||
* Create a binding for an `ElementInternals` object compatible with a 'native' | ||
* component (i.e. one which extends `HTMLElement` and is distributed as a | ||
* standalone custom element). | ||
* | ||
* Since a 'native' custom element will extend `HTMLElement` we can call | ||
* `this.attachInternals` directly, binding it to the name annotated by the | ||
* developer with the `@AttachInternals` decorator. | ||
* | ||
* Thus if an `@AttachInternals` decorator is present on a component like | ||
* this: | ||
* | ||
* ```ts | ||
* @AttachInternals() | ||
* internals: ElementInternals; | ||
* ``` | ||
* | ||
* then this transformer will emit TS syntax nodes representing the | ||
* following TypeScript source code: | ||
* | ||
* ```ts | ||
* this.internals = this.attachInternals(); | ||
* ``` | ||
* | ||
* @param cmp metadata about the component of interest, gathered during | ||
* compilation | ||
* @returns an expression statement syntax tree node | ||
*/ | ||
export function createNativeAttachInternalsBinding(cmp: d.ComponentCompilerMeta): ts.ExpressionStatement[] { | ||
if (cmp.formAssociated && cmp.attachInternalsMemberName) { | ||
return [ | ||
ts.factory.createExpressionStatement( | ||
ts.factory.createBinaryExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createThis(), | ||
// use the name set on the {@link d.ComponentCompilerMeta} | ||
ts.factory.createIdentifier(cmp.attachInternalsMemberName), | ||
), | ||
ts.factory.createToken(ts.SyntaxKind.EqualsToken), | ||
ts.factory.createCallExpression( | ||
ts.factory.createPropertyAccessExpression( | ||
ts.factory.createThis(), | ||
ts.factory.createIdentifier('attachInternals'), | ||
), | ||
undefined, | ||
[], | ||
), | ||
), | ||
), | ||
]; | ||
} else { | ||
return []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.