diff --git a/versioned_docs/version-v4.22/build-variables.md b/versioned_docs/version-v4.22/build-variables.md new file mode 100644 index 000000000..30cc41d9e --- /dev/null +++ b/versioned_docs/version-v4.22/build-variables.md @@ -0,0 +1,48 @@ +--- +title: Build Constants +description: Stencil has a number of add-ons that you can use with the build process. +slug: /build-variables +--- + +# Build Constants + +Build Constants in Stencil allow you to run specific code only when Stencil is running in development mode. This code is stripped from your bundles when doing a production build, therefore keeping your bundles as small as possible. + +### Using Build Constants + +Lets dive in and look at an example of how to use our build constants: + +```tsx +import { Component, Build } from '@stencil/core'; + +@Component({ + tag: 'stencil-app', + styleUrl: 'stencil-app.scss' +}) +export class StencilApp { + + componentDidLoad() { + if (Build.isDev) { + console.log('im in dev mode'); + } else { + console.log('im running in production'); + } + + if (Build.isBrowser) { + console.log('im in the browser'); + } else { + console.log('im in prerendering (server)'); + } + } +} +``` + +As you can see from this example, we just need to import `Build` from `@stencil/core` and then we can use the `isDev` constant to detect when we are running in dev mode or production mode. + +### Use Cases + +Some use cases we have come up with are: + +- Diagnostics code that runs in dev to make sure logic is working like you would expect +- `console.log()`'s that may be useful for debugging in dev mode but that you don't want to ship +- Disabling auth checks when in dev mode diff --git a/versioned_docs/version-v4.22/components/_category_.json b/versioned_docs/version-v4.22/components/_category_.json new file mode 100644 index 000000000..57bbce615 --- /dev/null +++ b/versioned_docs/version-v4.22/components/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Components", + "position": 2 +} diff --git a/versioned_docs/version-v4.22/components/api.md b/versioned_docs/version-v4.22/components/api.md new file mode 100644 index 000000000..6051e207b --- /dev/null +++ b/versioned_docs/version-v4.22/components/api.md @@ -0,0 +1,197 @@ +--- +title: Component API +sidebar_label: API +description: Component API +slug: /api +--- + +# Component API + +The whole API provided by stencil can be condensed in a set of decorators, lifecycles hooks and rendering methods. + + +## Decorators + +Decorators are a pure compiler-time construction used by stencil to collect all the metadata about a component, the properties, attributes and methods it might expose, the events it might emit or even the associated stylesheets. +Once all the metadata has been collected, all the decorators are removed from the output, so they don't incur any runtime overhead. + +- [@Component()](./component.md) declares a new web component +- [@Prop()](./properties.md#the-prop-decorator-prop) declares an exposed property/attribute +- [@State()](./state.md#the-state-decorator-state) declares an internal state of the component +- [@Watch()](./reactive-data.md#the-watch-decorator-watch) declares a hook that runs when a property or state changes +- [@Element()](./host-element.md#element-decorator) declares a reference to the host element +- [@Method()](./methods.md) declares an exposed public method +- [@Event()](./events.md#event-decorator) declares a DOM event the component might emit +- [@Listen()](./events.md#listen-decorator) listens for DOM events + + +## Lifecycle hooks + +- [connectedCallback()](./component-lifecycle.md#connectedcallback) +- [disconnectedCallback()](./component-lifecycle.md#disconnectedcallback) +- [componentWillLoad()](./component-lifecycle.md#componentwillload) +- [componentDidLoad()](./component-lifecycle.md#componentdidload) +- [componentShouldUpdate(newValue, oldValue, propName): boolean](./component-lifecycle.md#componentshouldupdate) +- [componentWillRender()](./component-lifecycle.md#componentwillrender) +- [componentDidRender()](./component-lifecycle.md#componentdidrender) +- [componentWillUpdate()](./component-lifecycle.md#componentwillupdate) +- [componentDidUpdate()](./component-lifecycle.md#componentdidupdate) +- **[render()](./templating-and-jsx.md)** + +## componentOnReady() + +This isn't a true "lifecycle" method that would be declared on the component class definition, but instead is a utility method that +can be used by an implementation consuming your Stencil component to detect when a component has finished its first render cycle. + +This method returns a promise which resolves after `componentDidRender()` on the _first_ render cycle. + +:::note +`componentOnReady()` only resolves once per component lifetime. If you need to hook into subsequent render cycle, use +`componentDidRender()` or `componentDidUpdate()`. +::: + +Executing code after `componentOnReady()` resolves could look something like this: + +```ts +// Get a reference to the element +const el = document.querySelector('my-component'); + +el.componentOnReady().then(() => { + // Place any code in here you want to execute when the component is ready + console.log('my-component is ready'); +}); +``` + +The availability of `componentOnReady()` depends on the component's compiled output type. This method is only available for lazy-loaded +distribution types ([`dist`](../output-targets/dist.md) and [`www`](../output-targets/www.md)) and, as such, is not available for +[`dist-custom-elements`](../output-targets/custom-elements.md) output. If you want to simulate the behavior of `componentOnReady()` for non-lazy builds, +you can implement a helper method to wrap the functionality similar to what the Ionic Framework does [here](https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/helpers.ts#L60-L79). + +## The `appload` event + +In addition to component-specific lifecycle hooks, a special event called `appload` will be emitted when the app and all of its child components have finished loading. You can listen for it on the `window` object. + +If you have multiple apps on the same page, you can determine which app emitted the event by checking `event.detail.namespace`. This will be the value of the [namespace config option](../config/01-overview.md#namespace) you've set in your Stencil config. + +```tsx +window.addEventListener('appload', (event) => { + console.log(event.detail.namespace); +}); +``` + +## Other + +The following primitives can be imported from the `@stencil/core` package and used within the lifecycle of a component: + +- [**Host**](./host-element.md): `<Host>`, is a functional component that can be used at the root of the render function to set attributes and event listeners to the host element itself. Refer to the [Host Element](./host-element.md) page for usage info. + +- **Fragment**: `<Fragment>`, often used via `<>...</>` syntax, lets you group elements without a wrapper node. + + To use this feature, ensure that the following TypeScript compiler options are set: + - [`jsxFragmentFactory` is set](https://www.typescriptlang.org/tsconfig#jsxFragmentFactory) to "Fragment" + - [`jsxFactory` is set](https://www.typescriptlang.org/tsconfig#jsxFactory) to "h" + + __Type:__ `FunctionalComponent`<br /> + __Example:__ + ```tsx + import { Component, Fragment, h } from '@stencil/core' + @Component({ + tag: 'cmp-fragment', + }) + export class CmpFragment { + render() { + return ( + <> + <div>...</div> + <div>...</div> + <div>...</div> + </> + ); + } + } + ``` + +- [**h()**](./templating-and-jsx.md): It's used within the `render()` to turn the JSX into Virtual DOM elements. + +- [**readTask()**](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing): Schedules a DOM-read task. The provided callback will be executed in the best moment to perform DOM reads without causing layout thrashing. + + __Type:__ `(task: Function) => void` + +- [**writeTask()**](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing): Schedules a DOM-write task. The provided callback will be executed in the best moment to perform DOM mutations without causing layout thrashing. + + __Type:__ `(task: Function) => void` + +- **forceUpdate()**: Schedules a new render of the given instance or element even if no state changed. Notice `forceUpdate()` is not synchronous and might perform the DOM render in the next frame. + + __Type:__ `(ref: HTMLElement) => void`<br /> + __Example:__ + ```ts + import { forceUpdate } from '@stencil/core' + + // inside a class component function + forceUpdate(this); + ``` + +- **getAssetPath()**: Gets the path to local assets. Refer to the [Assets](../guides/assets.md#getassetpath) page for usage info. + + __Type:__ `(path: string) => string`<br /> + __Example:__ + ```tsx + import { Component, Prop, getAssetPath, h } from '@stencil/core' + @Component({ + tag: 'cmp-asset', + }) + export class CmpAsset { + @Prop() icon: string; + + render() { + return ( + <img src={getAssetPath(`assets/icons/${this.icon}.png`)} /> + ); + } + } + ``` + +- **setAssetPath()**: Sets the path for Stencil to resolve local assets. Refer to the [Assets](../guides/assets.md#setassetpath) page for usage info. + + __Type:__ `(path: string) => string`<br /> + __Example:__ + ```ts + import { setAssetPath } from '@stencil/core'; + setAssetPath(`{window.location.origin}/`); + ``` + +- **setMode()**: Sets the style mode of a component. Refer to the [Styling](./styling.md#style-modes) page for usage info. + + __Type:__ `(ref: HTMLElement) => string`<br /> + __Example:__ + ```ts + import { setMode } from '@stencil/core' + + // set mode based on a property + setMode((el) => el.getAttribute('mode')); + ``` + +- **getMode()**: Get the current style mode of your application. Refer to the [Styling](./styling.md#style-modes) page for usage info. + + __Type:__ `(ref: HTMLElement) => string`<br /> + __Example:__ + ```ts + import { getMode } from '@stencil/core' + + getMode(this); + ``` + +- **getElement()**: Retrieve a Stencil element for a given reference. + + __Type:__ `(ref: getElement) => string`<br /> + __Example:__ + ```ts + import { getElement } from '@stencil/core' + + const stencilComponent = getElement(document.querySelector('my-cmp')) + if (stencilComponent) { + stencilComponent.componentOnReady().then(() => { ... }) + } + ``` + diff --git a/versioned_docs/version-v4.22/components/component-lifecycle.md b/versioned_docs/version-v4.22/components/component-lifecycle.md new file mode 100644 index 000000000..72609e476 --- /dev/null +++ b/versioned_docs/version-v4.22/components/component-lifecycle.md @@ -0,0 +1,182 @@ +--- +title: Component Lifecycle Methods +sidebar_label: Lifecycle Methods +description: Component Lifecycle Methods +slug: /component-lifecycle +--- + +# Component Lifecycle Methods + +Components have numerous lifecycle methods which can be used to know when the component "will" and "did" load, update, and render. These methods can be added to a component to hook into operations at the right time. + +Implement one of the following methods within a component class and Stencil will automatically call them in the right order: + +import LifecycleMethodsChart from '@site/src/components/LifecycleMethodsChart'; + +<LifecycleMethodsChart /> + +## connectedCallback() + +Called every time the component is connected to the DOM. +When the component is first connected, this method is called before `componentWillLoad`. + +It's important to note that this method can be called more than once, every time, the element is **attached** or **moved** in the DOM. For logic that needs to run every time the element is attached or moved in the DOM, it is considered a best practice to use this lifecycle method. + +```tsx +const el = document.createElement('my-cmp'); +document.body.appendChild(el); +// connectedCallback() called +// componentWillLoad() called (first time) + +el.remove(); +// disconnectedCallback() + +document.body.appendChild(el); +// connectedCallback() called again, but `componentWillLoad()` is not. +``` + + +This `lifecycle` hook follows the same semantics as the one described by the [Custom Elements Spec](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) + +## disconnectedCallback() + +Called every time the component is disconnected from the DOM, ie, it can be dispatched more than once, DO not confuse with a "onDestroy" kind of event. + +This `lifecycle` hook follows the same semantics as the one described by the [Custom Elements Spec](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). + +## componentWillLoad() + +Called once just after the component is first connected to the DOM. Since this method is only called once, it's a good place to load data asynchronously and to setup the state without triggering extra re-renders. + +A promise can be returned, that can be used to wait for the first `render()`. + +## componentDidLoad() + +Called once just after the component is fully loaded and the first `render()` occurs. + +## componentShouldUpdate() + +This hook is called when a component's [`Prop`](./properties.md) or [`State`](./state.md) property changes and a rerender is about to be requested. This hook receives three arguments: the new value, the old value and the name of the changed state. It should return a boolean to indicate if the component should rerender (`true`) or not (`false`). + +A couple of things to notice is that this method will not be executed before the initial render, that is, when the component is first attached to the dom, nor when a rerender is already scheduled in the next frame. + +Let’s say the following two props of a component change synchronously: + +```tsx +component.somePropA = 42; +component.somePropB = 88; +``` + +The `componentShouldUpdate` will be first called with arguments: `42`, `undefined` and `somePropA`. If it does return `true`, the hook will not be called again since the rerender is already scheduled to happen. Instead, if the first hook returned `false`, then `componentShouldUpdate` will be called again with `88`, `undefined` and `somePropB` as arguments, triggered by the `component.somePropB = 88` mutation. + +Since the execution of this hook might be conditioned, it's not good to rely on it to watch for prop changes, instead use the `@Watch` decorator for that. + +## componentWillRender() + +Called before every `render()`. + +A promise can be returned, that can be used to wait for the upcoming render. + +## componentDidRender() + +Called after every `render()`. + + +## componentWillUpdate() + +Called when the component is about to be updated because some `Prop()` or `State()` changed. +It's never called during the first `render()`. + +A promise can be returned, that can be used to wait for the next render. + + +## componentDidUpdate() + +Called just after the component updates. +It's never called during the first `render()`. + + +## Rendering State + +It's always recommended to make any rendered state updates within `componentWillRender()`, since this is the method which get called _before_ the `render()` method. Alternatively, updating rendered state with the `componentDidLoad()`, `componentDidUpdate()` and `componentDidRender()` methods will cause another rerender, which isn't ideal for performance. + +If state _must_ be updated in `componentDidUpdate()` or `componentDidRender()`, it has the potential of getting components stuck in an infinite loop. If updating state within `componentDidUpdate()` is unavoidable, then the method should also come with a way to detect if the props or state is "dirty" or not (is the data actually different or is it the same as before). By doing a dirty check, `componentDidUpdate()` is able to avoid rendering the same data, and which in turn calls `componentDidUpdate()` again. + + +## Lifecycle Hierarchy + +A useful feature of lifecycle methods is that they take their child component's lifecycle into consideration too. For example, if the parent component, `cmp-a`, has a child component, `cmp-b`, then `cmp-a` isn't considered "loaded" until `cmp-b` has finished loading. Another way to put it is that the deepest components finish loading first, then the `componentDidLoad()` calls bubble up. + +It's also important to note that even though Stencil can lazy-load components, and has asynchronous rendering, the lifecycle methods are still called in the correct order. So while the top-level component could have already been loaded, all of its lifecycle methods are still called in the correct order, which means it'll wait for a child components to finish loading. The same goes for the exact opposite, where the child components may already be ready while the parent isn't. + +In the example below we have a simple hierarchy of components. The numbered list shows the order of which the lifecycle methods will fire. + +```markup + <cmp-a> + <cmp-b> + <cmp-c></cmp-c> + </cmp-b> + </cmp-a> +``` + +1. `cmp-a` - `componentWillLoad()` +2. `cmp-b` - `componentWillLoad()` +3. `cmp-c` - `componentWillLoad()` +4. `cmp-c` - `componentDidLoad()` +5. `cmp-b` - `componentDidLoad()` +6. `cmp-a` - `componentDidLoad()` + +Even if some components may or may not be already loaded, the entire component hierarchy waits on its child components to finish loading and rendering. + + +## Async Lifecycle Methods + +Some lifecycle methods, e.g. `componentWillRender`, `componentWillLoad` and `componentWillUpdate`, can also return promises which allows the method to asynchronously retrieve data or perform any async tasks. A great example of this is fetching data to be rendered in a component. For example, this very site you're reading first fetches content data before rendering. But because `fetch()` is async, it's important that `componentWillLoad()` returns a `Promise` to ensure its parent component isn't considered "loaded" until all of its content has rendered. + +Below is a quick example showing how `componentWillLoad()` is able to have its parent component wait on it to finish loading its data. + +```tsx +componentWillLoad() { + return fetch('/some-data.json') + .then(response => response.json()) + .then(data => { + this.content = data; + }); +} +``` + +## Example + +This simple example shows a clock and updates the current time every second. The timer is started when the component is added to the DOM. Once it's removed from the DOM, the timer is stopped. + +```tsx +import { Component, State, h } from '@stencil/core'; + +@Component({ + tag: 'custom-clock' +}) +export class CustomClock { + + timer: number; + + @State() time: number = Date.now(); + + connectedCallback() { + this.timer = window.setInterval(() => { + this.time = Date.now(); + }, 1000); + } + + disconnectedCallback() { + window.clearInterval(this.timer); + } + + render() { + const time = new Date(this.time).toLocaleTimeString(); + + return ( + <span>{ time }</span> + ); + } +} +``` diff --git a/versioned_docs/version-v4.22/components/component.md b/versioned_docs/version-v4.22/components/component.md new file mode 100644 index 000000000..a45fd06fe --- /dev/null +++ b/versioned_docs/version-v4.22/components/component.md @@ -0,0 +1,392 @@ +--- +title: Component Decorator +sidebar_label: Component +description: Documentation for the @Component decorator +slug: /component +--- + +# Component Decorator + +`@Component()` is a decorator that designates a TypeScript class as a Stencil component. +Every Stencil component gets transformed into a web component at build time. + +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + // additional options +}) +export class TodoList { + // implementation omitted +} +``` + +## Component Options + +The `@Component()` decorator takes one argument, an object literal containing configuration options for the component. +This allows each component to be individually configured to suit the unique needs of each project. + +Each option, its type, and whether it's required is described below. + +### tag + +**Required** + +**Type: `string`** + +**Details:**<br/> +This value sets the name of the custom element that Stencil will generate. +To adhere to the [HTML spec](https://html.spec.whatwg.org/#valid-custom-element-name), the tag name must contain a dash ('-'). + +Ideally, the tag name is a globally unique value. +Having a globally unique value helps prevent naming collisions with the global `CustomElementsRegistry`, where all custom elements are defined. +It's recommended to choose a unique prefix for all your components within the same collection. + +**Example**:<br/> +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', +}) +export class TodoList { + // implementation omitted +} +``` +After compilation, the component defined in `TodoList` can be used in HTML or another TSX file: +```html +<!-- Here we use the component in an HTML file --> +<todo-list></todo-list> +``` +```tsx +{/* Here we use the component in a TSX file */} +<todo-list></todo-list> +``` + +### assetsDirs + +**Optional** + +**Type: `string[]`** + +**Details:**<br/> +`assetsDirs` is an array of relative paths from the component to a directory containing the static files (assets) the component requires. + +**Example**:<br/> +Below is an example project's directory structure containing an example component and assets directory. + +``` +src/ +└── components/ + ├── assets/ + │ └── sunset.jpg + └── todo-list.tsx +``` + +Below, the `todo-list` component will correctly load the `sunset.jpg` image from the `assets/` directory, using Stencil's [`getAssetPath()`](../guides/assets.md#getassetpath). + +```tsx +import { Component, Prop, getAssetPath, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + // 1. assetsDirs lists the 'assets' directory as a relative (sibling) + // directory + assetsDirs: ['assets'] +}) +export class TodoList { + image = "sunset.jpg"; + + render() { + // 2. the asset path is retrieved relative to the asset base path to use in + // the <img> tag + const imageSrc = getAssetPath(`./assets/${this.image}`); + return <img src={imageSrc} /> + } +} +``` + +In the example above, the following allows `todo-list` to display the provided asset: +1. The `TodoList`'s `@Component()` decorator has the `assetsDirs` property, and lists the file's sibling directory, `assets/`. + This will copy the `assets` directory over to the distribution directory. +2. Stencil's [`getAssetPath()`](../guides/assets.md#getassetpath) is used to retrieve the path to the image to be used in the `<img>` tag + +For more information on configuring assets, please see Stencil's [Assets Guide](../guides/assets.md) + + +### formAssociated + +**Optional** + +**Type: `boolean`** + +**Default: `false`** + +If `true` the component will be +[form-associated](https://html.spec.whatwg.org/dev/custom-elements.html#form-associated-custom-element), +allowing you to take advantage of the +[`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals) +API to enable your Stencil component to participate in forms. + +A minimal form-associated Stencil component could look like this: + +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'form-associated', + formAssociated: true +}) +export class FormAssociated { + render() { + return <span>form associated!</span> + } +} +``` + +See the documentation for [form-associated components](./form-associated.md) +for more info and examples. + +### scoped + +**Optional** + +**Type: `boolean`** + +**Default: `false`** + +**Details:**<br/> +If `true`, the component will use [scoped stylesheets](./styling.md#scoped-css). + +Scoped CSS is an alternative to using the native [shadow DOM](./styling.md#shadow-dom) style encapsulation. +It appends a data attribute to your styles to make them unique and thereby scope them to your component. +It does not, however, prevent styles from the light DOM from seeping into your component. + +To use the native [shadow DOM](./styling.md#shadow-dom), see the configuration for [`shadow`](#shadow). + +This option cannot be set to `true` if `shadow` is enabled. + +**Example**:<br/> +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + scoped: true +}) +export class TodoList { + // implementation omitted +} +``` + +### shadow + +**Optional** + +**Type: `boolean | { delegatesFocus: boolean }`** + +**Default: `false`** + +**Details:**<br/> +If `true`, the component will use [native Shadow DOM encapsulation](./styling.md#shadow-dom). +It will fall back to `scoped` if the browser does not support shadow-dom natively. + +`delegatesFocus` is a property that [provides focus](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus) to the first focusable entry in a component using Shadow DOM. +If an object literal containing `delegatesFocus` is provided, the component will use [native Shadow DOM encapsulation](./styling.md#shadow-dom), regardless of the value assigned to `delegatesFocus`. + +When `delegatesFocus` is set to `true`, the component will have `delegatesFocus: true` added to its shadow DOM. + +When `delegatesFocus` is `true` and a non-focusable part of the component is clicked: +- the first focusable part of the component is given focus +- the component receives any available `focus` styling + +If `shadow` is set to `false`, the component will not use native shadow DOM encapsulation. + +This option cannot be set to enabled if `scoped` is enabled. + +**Example 1**:<br/> +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + shadow: true +}) +export class TodoList { + // implementation omitted +} +``` + +**Example 2**:<br/> +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + shadow: { + delegatesFocus: true, + } +}) +export class TodoList { + // implementation omitted +} +``` + +### styleUrl + +**Optional** + +**Type: `string`** + +**Details:**<br/> +Relative URL to an external stylesheet containing styles to apply to your component. +Out of the box, Stencil will only process CSS files (files ending with `.css`). +Support for additional CSS variants, like Sass, can be added via [a plugin](https://stenciljs.com/docs/plugins#related-plugins). + +**Example**:<br/> +Below is an example project's directory structure containing an example component and stylesheet. +``` +src/ +└── components/ + ├── todo-list.css + └── todo-list.tsx +``` + +By setting `styleUrl`, Stencil will apply the `todo-list.css` stylesheet to the `todo-list` component: + +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + styleUrl: './todo-list.css', +}) +export class TodoList { + // implementation omitted +} +``` + +### styleUrls + +**Optional** + +**Type: `string[] | { [modeName: string]: string | string[]; }`** + +**Details:**<br/> +A list of relative URLs to external stylesheets containing styles to apply to your component. + +Alternatively, an object can be provided that maps a named "mode" to one or more stylesheets. + +Out of the box, Stencil will only process CSS files (ending with `.css`). +Support for additional CSS variants, like Sass, can be added via [a plugin](https://stenciljs.com/docs/plugins#related-plugins). + +**Example**:<br/> +Below is an example project's directory structure containing an example component and stylesheet. +``` +src/ +└── components/ + ├── todo-list-1.css + ├── todo-list-2.css + └── todo-list.tsx +``` + +By setting `styleUrls`, Stencil will apply both stylesheets to the `todo-list` component: + +```tsx title="Using an array of styles" +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + styleUrls: ['./todo-list-1.css', './todo-list-2.css'] +}) +export class TodoList { + // implementation omitted +} +``` + +```tsx title="Using modes" +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + styleUrls: { + ios: 'todo-list-1.ios.scss', + md: 'todo-list-2.md.scss', + } +}) +export class TodoList { + // implementation omitted +} +``` + +Read more on styling modes in the Components [Styling](./styling.md#style-modes) section. + +### styles + +**Optional** + +**Type: `string | { [modeName: string]: any }`** + +**Details:**<br/> +A string that contains inlined CSS instead of using an external stylesheet. +The performance characteristics of this feature are the same as using an external stylesheet. + +When using `styles`, only CSS is permitted. +See [`styleUrl`](#styleurl) if you need more advanced features. + +**Example**:<br/> +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'todo-list', + styles: 'div { background-color: #fff }' +}) +export class TodoList { + // implementation omitted +} +``` + +## Embedding or Nesting Components + +Components can be composed easily by adding the HTML tag to the JSX code. Since the components are just HTML tags, nothing needs to be imported to use a Stencil component within another Stencil component. + +Here's an example of using a component within another component: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'my-embedded-component' +}) +export class MyEmbeddedComponent { + @Prop() color: string = 'blue'; + + render() { + return ( + <div>My favorite color is {this.color}</div> + ); + } +} +``` + +```tsx +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'my-parent-component' +}) +export class MyParentComponent { + + render() { + return ( + <div> + <my-embedded-component color="red"></my-embedded-component> + </div> + ); + } +} +``` + +The `my-parent-component` includes a reference to the `my-embedded-component` in the `render()` function. diff --git a/versioned_docs/version-v4.22/components/events.md b/versioned_docs/version-v4.22/components/events.md new file mode 100644 index 000000000..96ed92f07 --- /dev/null +++ b/versioned_docs/version-v4.22/components/events.md @@ -0,0 +1,225 @@ +--- +title: Events +sidebar_label: Events +description: Events +slug: /events +--- + +# Events + +There is **NOT** such a thing as *stencil events*, instead, Stencil encourages the use of [DOM events](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events). +However, Stencil does provide an API to specify the events a component can emit, and the events a component listens to. It does so with the `Event()` and `Listen()` decorators. + +## Event Decorator + +Components can emit data and events using the Event Emitter decorator. + +To dispatch [Custom DOM events](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events) for other components to handle, use the `@Event()` decorator. + +```tsx +import { Event, EventEmitter } from '@stencil/core'; + +... +export class TodoList { + + @Event() todoCompleted: EventEmitter<Todo>; + + todoCompletedHandler(todo: Todo) { + this.todoCompleted.emit(todo); + } +} +``` + +The code above will dispatch a custom DOM event called `todoCompleted`. + +The `Event(opts: EventOptions)` decorator optionally accepts an options object to shape the behavior of dispatched events. The options and defaults are described below. + +```tsx +export interface EventOptions { + /** + * A string custom event name to override the default. + */ + eventName?: string; + /** + * A Boolean indicating whether the event bubbles up through the DOM or not. + */ + bubbles?: boolean; + + /** + * A Boolean indicating whether the event is cancelable. + */ + cancelable?: boolean; + + /** + * A Boolean value indicating whether or not the event can bubble across the boundary between the shadow DOM and the regular DOM. + */ + composed?: boolean; +} +``` + +Example: + +```tsx +import { Event, EventEmitter } from '@stencil/core'; + +... +export class TodoList { + + // Event called 'todoCompleted' that is "composed", "cancellable" and it will bubble up! + @Event({ + eventName: 'todoCompleted', + composed: true, + cancelable: true, + bubbles: true, + }) todoCompleted: EventEmitter<Todo>; + + todoCompletedHandler(todo: Todo) { + const event = this.todoCompleted.emit(todo); + if(!event.defaultPrevented) { + // if not prevented, do some default handling code + } + } +} +``` + +:::note +In the case where the Stencil `Event` type conflicts with the native web `Event` type, there are two possible solutions: + +1. Import aliasing: +```tsx +import { Event as StencilEvent, EventEmitter } from '@stencil/core'; + +@StencilEvent() myEvent: EventEmitter<{value: string, ev: Event}>; +``` + +2. Namespace the native web `Event` type with `globalThis`: +```tsx +@Event() myEvent: EventEmitter<{value: string, ev: globalThis.Event}>; +``` +::: + +## Listen Decorator + +The `Listen()` decorator is for listening to DOM events, including the ones dispatched from `@Events`. The event listeners are automatically added and removed when the component gets added or removed from the DOM. + +In the example below, assume that a child component, `TodoList`, emits a `todoCompleted` event using the `EventEmitter`. + +```tsx +import { Listen } from '@stencil/core'; + +... +export class TodoApp { + + @Listen('todoCompleted') + todoCompletedHandler(event: CustomEvent<Todo>) { + console.log('Received the custom todoCompleted event: ', event.detail); + } +} +``` + +### Listen's options + +The `@Listen(eventName, opts?: ListenOptions)` includes a second optional argument that can be used to configure how the DOM event listener is attached. + +```tsx +export interface ListenOptions { + target?: 'body' | 'document' | 'window'; + capture?: boolean; + passive?: boolean; +} +``` + +The available options are `target`, `capture` and `passive`: + + +#### target + +Handlers can also be registered for an event other than the host itself. +The `target` option can be used to change where the event listener is attached, this is useful for listening to application-wide events. + +In the example below, we're going to listen for the scroll event, emitted from `window`: + +```tsx + @Listen('scroll', { target: 'window' }) + handleScroll(ev) { + console.log('the body was scrolled', ev); + } +``` + +#### passive + +By default, Stencil uses several heuristics to determine if it must attach a `passive` event listener or not. The `passive` option can be used to change the default behavior. + +Please check out [https://developers.google.com/web/updates/2016/06/passive-event-listeners](https://developers.google.com/web/updates/2016/06/passive-event-listeners) for further information. + + +#### capture + +Event listener attached with `@Listen` does not "capture" by default. +When a event listener is set to "capture", it means the event will be dispatched during the "capture phase". +Check out [https://www.quirksmode.org/js/events_order.html](https://www.quirksmode.org/js/events_order.html) for further information. + + +```tsx + @Listen('click', { capture: true }) + handleClick(ev) { + console.log('click'); + } +``` + +## Keyboard events + +For keyboard events, you can use the standard `keydown` event in `@Listen()` and use `event.keyCode` or `event.which` to get the key code, or `event.key` for the string representation of the key. + +```tsx +@Listen('keydown') +handleKeyDown(ev: KeyboardEvent){ + if (ev.key === 'ArrowDown'){ + console.log('down arrow pressed') + } +} +``` +More info on event key strings can be found in the [w3c spec](https://www.w3.org/TR/uievents-key/#named-key-attribute-values). + + +## Using events in JSX + +Within a stencil compiled application or component you can also bind listeners to events directly in JSX. This works very similar to normal DOM events such as `onClick`. + +Let's use our TodoList component from above: + +```tsx +import { Event, EventEmitter } from '@stencil/core'; + +... +export class TodoList { + + @Event() todoCompleted: EventEmitter<Todo>; + + todoCompletedHandler(todo: Todo) { + this.todoCompleted.emit(todo); + } +} +``` + +We can now listen to this event directly on the component in our JSX using the following syntax: + +```tsx +<todo-list onTodoCompleted={ev => this.someMethod(ev)} /> +``` + +This property is generated automatically and is prefixed with "on". For example, if the event emitted is called `todoDeleted` the property will be called `onTodoDeleted`: + +```tsx +<todo-list onTodoDeleted={ev => this.someOtherMethod(ev)} /> +``` + +## Listening to events from a non-JSX element + +```tsx +<todo-list></todo-list> +<script> + const todoListElement = document.querySelector('todo-list'); + todoListElement.addEventListener('todoCompleted', event => { /* your listener */ }) +</script> +``` diff --git a/versioned_docs/version-v4.22/components/form-associated.md b/versioned_docs/version-v4.22/components/form-associated.md new file mode 100644 index 000000000..f434a78ed --- /dev/null +++ b/versioned_docs/version-v4.22/components/form-associated.md @@ -0,0 +1,298 @@ +--- +title: Form-Associated Components +sidebar_label: Form-Associated Components +description: Form-Associated Stencil Components +slug: /form-associated +--- + +# Building Form-Associated Components in Stencil + +As of v4.5.0, Stencil has support for form-associated custom elements. This +allows Stencil components to participate in a rich way in HTML forms, +integrating with native browser features for validation and accessibility while +maintaining encapsulation and control over their styling and presentation. + +:::caution +Browser support for the APIs that this feature depends on is still not +universal[^1] and the Stencil team has no plans at present to support or +incorporate any polyfills for the browser functionality. Before you ship +form-associated Stencil components make sure that the browsers you need to +support have shipped the necessary APIs. +::: + +## Creating a Form-Associated Component + +A form-associated Stencil component is one which sets the new [`formAssociated`](./component.md#formassociated) +option in the argument to the `@Component` +decorator to `true`, like so: + +```tsx +import { Component } from '@stencil/core'; + +@Component({ + tag: 'my-face', + formAssociated: true, +}) +export class MyFACE { +} +``` + +This element will now be marked as a form-associated custom element via the +[`formAssociated`](https://html.spec.whatwg.org/#custom-elements-face-example) +static property, but by itself this is not terribly useful. + +In order to meaningfully interact with a `<form>` element that is an ancestor +of our custom element we'll need to get access to an +[`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) +object corresponding to our element instance. Stencil provides a decorator, +`@AttachInternals`, which does just this, allowing you to decorate a property on +your component and bind an `ElementInternals` object to that property which you +can then use to interact with the surrounding form. + +:::info +Under the hood the `AttachInternals` decorator makes use of the very similarly +named +[`attachInternals`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals) +method on `HTMLElement` to associate your Stencil component with an ancestor +`<form>` element. During compilation, Stencil will generate code that calls +this method at an appropriate point in the component lifecycle for both +[lazy](../output-targets/dist.md) and [custom elements](../output-targets/custom-elements.md) builds. +::: + +A Stencil component using this API to implement a custom text input could look +like this: + +```tsx title="src/components/custom-text-input.tsx" +import { Component, h, AttachInternals, State } from '@stencil/core'; + +@Component({ + tag: 'custom-text-input', + shadow: true, + formAssociated: true +}) +export class CustomTextInput { + @State() value: string; + + @AttachInternals() internals: ElementInternals; + + handleChange(event) { + this.value = event.target.value; + this.internals.setFormValue(event.target.value); + } + + componentWillLoad() { + this.internals.setFormValue("a default value"); + } + + render() { + return ( + <input + type="text" + value={this.value} + onInput={(event) => this.handleChange(event)} + /> + ) + } +} +``` + +If this component is rendered within a `<form>` element like so: + + +```html +<form> + <custom-text-input name="my-custom-input"></custom-text-input> +</form> +``` + +then it will automatically be linked up to the surrounding form. The +`ElementInternals` object found at `this.internals` will have a bunch of +methods on it for interacting with that form and getting key information out of +it. + +In our `<custom-text-input>` example above we use the +[`setFormValue`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue) +method to set a value in the surrounding form. This will read the `name` +attribute off of the element and use it when setting the value, so the value +typed by a user into the `input` will added to the form under the +`"my-custom-input"` name. + +This example just scratches the surface, and a great deal more is possible with +the `ElementInternals` API, including [setting the element's +validity](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity), +reading the validity state of the form, reading other form values, and more. + +## Lifecycle Callbacks + +Stencil allows developers building form-associated custom elements to define a +[standard series of lifecycle +callbacks](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions) +which enable their components to react dynamically to events in their +lifecycle. These could allow fetching data when a form loads, changing styles +when a form's `disabled` state is toggled, resetting form data cleanly, and more. + +### `formAssociatedCallback` + +This callback is called when the browser both associates the element with and +disassociates the element from a form element. The function is called with the +form element as an argument. This could be used to set an `ariaLabel` when the +form is ready to use, like so: + +```tsx title='src/components/form-associated-cb.tsx' +import { Component, h, AttachInternals } from '@stencil/core'; + +@Component({ + tag: 'form-associated', + formAssociated: true, +}) +export class FormAssociatedCmp { + @AttachInternals() + internals: ElementInternals; + + formAssociatedCallback(form) { + form.ariaLabel = 'formAssociated called'; + } + + render() { + return <input type="text" />; + } +} +``` + +### `formDisabledCallback` + +This is called whenever the `disabled` state on the element _changes_. This +could be used to keep a CSS class in sync with the disabled state, like so: + +```tsx title='src/components/form-disabled-cb.tsx' +import { Component, h, State } from '@stencil/core'; + +@Component({ + tag: 'form-disabled-cb', + formAssociated: true, +}) +export class MyComponent { + @State() cssClass: string = ""; + + formDisabledCallback(disabled: boolean) { + if (disabled) { + this.cssClass = "background-mode"; + } else { + this.cssClass = ""; + } + } + + render() { + return <input type="text" class={this.cssClass}></input> + } +} +``` + +### `formResetCallback` + +This is called when the form is reset, and should be used to reset the +form-associated component's internal state and validation. For example, you +could do something like the following: + +```tsx title="src/components/form-reset-cb.tsx" +import { Component, h, AttachInternals } from '@stencil/core'; + +@Component({ + tag: 'form-reset-cb', + formAssociated: true, +}) +export class MyComponent { + @AttachInternals() + internals: ElementInternals; + + formResetCallback() { + this.internals.setValidity({}); + this.internals.setFormValue(""); + } + + render() { + return <input type="text"></input> + } +} +``` + +### `formStateRestoreCallback` + +This method will be called in the event that the browser automatically fills +out your form element, an event that could take place in two different +scenarios. The first is that the browser can restore the state of an element +after navigating or restarting, and the second is that an input was made using a +form auto-filling feature. + +In either case, in order to correctly reset itself your form-associated component +will need the previously selected value, but other state may also be necessary. +For instance, the form value to be submitted for a date picker component would +be a specific date, but in order to correctly restore the component's visual +state it might also be necessary to know whether the picker should display a +week or month view. + +The +[`setFormValue`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue) +method on `ElementInternals` provides some support for this use-case, allowing +you to submit both a _value_ and a _state_, where the _state_ is not added to +the form data sent to the server but could be used for storing some +client-specific state. For instance, a pseudocode sketch of a date picker +component that correctly restores whether the 'week' or 'month' view is active +could look like: + +```tsx title="src/components/fa-date-picker.tsx" +import { Component, h, State, AttachInternals } from '@stencil/core'; + +@Component({ + tag: 'fa-date-picker', + formAssociated: true, +}) +export class MyDatePicker { + @State() value: string = ""; + @State() view: "weeks" | "months" = "weeks"; + + @AttachInternals() + internals: ElementInternals; + + onInputChange(e) { + e.preventDefault(); + const date = e.target.value; + this.setValue(date); + } + + setValue(date: string) { + // second 'state' parameter is used to store both + // the input value (`date`) _and_ the current view + this.internals.setFormValue(date, `${date}#${this.view}`); + } + + formStateRestoreCallback(state, _mode) { + const [date, view] = state.split("#"); + this.view = view; + this.setValue(date); + } + + render() { + return <div> + Mock Date Picker, mode: {this.view} + <input class="date-picker" onChange={e => this.onInputChange(e)}></input> + </div> + } +} +``` + +Note that the `formStateRestoreCallback` also receives a second argument, +`mode`, which can be either `"restore"` or `"autocomplete"`, indicating the +reason for the form restoration. + +For more on form restoration, including a complete example, check out [this +great blog post on the +subject](https://web.dev/articles/more-capable-form-controls#restoring-form-state). + +## Resources + +- [WHATWG specification for form-associated custom elements](https://html.spec.whatwg.org/dev/custom-elements.html#form-associated-custom-elements) +- [ElementInternals and Form-Associated Custom Elements](https://webkit.org/blog/13711/elementinternals-and-form-associated-custom-elements/) from the WebKit blog +- [Web.dev post detailing how form-associated lifecycle callbacks work](https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks) + +[^1]: See https://caniuse.com/?search=attachInternals for up-to-date adoption estimates. diff --git a/versioned_docs/version-v4.22/components/functional-components.md b/versioned_docs/version-v4.22/components/functional-components.md new file mode 100644 index 000000000..cd3b27af2 --- /dev/null +++ b/versioned_docs/version-v4.22/components/functional-components.md @@ -0,0 +1,108 @@ +--- +title: Functional Components +sidebar_label: Functional Components +description: Functional Components +slug: /functional-components +--- + +# Working with Functional Components + +Functional components are quite different to normal Stencil web components because they are a part of Stencil's JSX compiler. A functional component is basically a function that takes an object of props and turns it into JSX. + +```tsx +const Hello = props => <h1>Hello, {props.name}!</h1>; +``` + +When the JSX transpiler encounters such a component, it will take its attributes, pass them into the function as the `props` object, and replace the component with the JSX that is returned by the function. + +```tsx +<Hello name="World" /> +``` + +Functional components also accept a second argument `children`. + +```tsx +const Hello = (props, children) => [ + <h1>Hello, {props.name}</h1>, + children +]; +``` + +The JSX transpiler passes all child elements of the component as an array into the function's `children` argument. + +```tsx +<Hello name="World"> + <p>I'm a child element.</p> +</Hello> +``` + +Stencil provides a `FunctionalComponent` generic type that allows to specify an interface for the component's properties. + +```tsx +// Hello.tsx + +import { FunctionalComponent, h } from '@stencil/core'; + +interface HelloProps { + name: string; +} + +export const Hello: FunctionalComponent<HelloProps> = ({ name }) => ( + <h1>Hello, {name}!</h1> +); +``` + +## Working with children + +The second argument of a functional component receives the passed children, but in order to work with them, `FunctionalComponent` provides a utils object that exposes a `map()` method to transform the children, and a `forEach()` method to read them. Reading the `children` array is not recommended since the stencil compiler can rename the vNode properties in prod mode. + +```tsx +export interface FunctionalUtilities { + forEach: (children: VNode[], cb: (vnode: ChildNode, index: number, array: ChildNode[]) => void) => void; + map: (children: VNode[], cb: (vnode: ChildNode, index: number, array: ChildNode[]) => ChildNode) => VNode[]; +} +export interface ChildNode { + vtag?: string | number | Function; + vkey?: string | number; + vtext?: string; + vchildren?: VNode[]; + vattrs?: any; + vname?: string; +} +``` + +**Example:** + +```tsx +export const AddClass: FunctionalComponent = (_, children, utils) => ( + utils.map(children, child => ({ + ...child, + vattrs: { + ...child.vattrs, + class: `${child.vattrs.class} add-class` + } + } + )) +); +``` + +:::note +When using a functional component in JSX, its name must start with a capital letter. Therefore it makes sense to export it as such. +::: + + +## Disclaimer + +There are a few major differences between functional components and class components. Since functional components are just syntactic sugar within JSX, they... + +* aren't compiled into web components, +* don't create a DOM node, +* don't have a Shadow DOM or scoped styles, +* don't have lifecycle hooks, +* are stateless. + +When deciding whether to use functional components, one concept to keep in mind is that often the UI of your application can be a function of its state, i. e., given the same state, it always renders the same UI. If a component has to hold state, deal with events, etc, it should probably be a class component. If a component's purpose is to simply encapsulate some markup so it can be reused across your app, it can probably be a functional component (especially if you're using a component library and thus don't need to style it). + +:::caution +Stencil does not support re-exporting a functional component from a "barrel file" and dynamically rendering it in another component. This is a [known limitation](https://github.com/ionic-team/stencil/issues/5246) within Stencil. Instead, either use class components and remove the import or import the functional component directly. +::: \ No newline at end of file diff --git a/versioned_docs/version-v4.22/components/host-element.md b/versioned_docs/version-v4.22/components/host-element.md new file mode 100644 index 000000000..c391fbe35 --- /dev/null +++ b/versioned_docs/version-v4.22/components/host-element.md @@ -0,0 +1,159 @@ +--- +title: Working with host elements +sidebar_label: Host Element +description: Working with host elements +slug: /host-element +--- + +# Working with host elements + +Stencil components render their children declaratively in their `render` method [using JSX](./templating-and-jsx.md). Most of the time, the `render()` function describes the children elements that are about to be rendered, but it can also be used to render attributes of the host element itself. + + +## `<Host>` + +The `Host` functional component can be used at the root of the render function to set attributes and event listeners to the host element itself. This works just like any other JSX: + +```tsx +// Host is imported from '@stencil/core' +import { Component, Host, h } from '@stencil/core'; + +@Component({tag: 'todo-list'}) +export class TodoList { + @Prop() open = false; + render() { + return ( + <Host + aria-hidden={this.open ? 'false' : 'true'} + class={{ + 'todo-list': true, + 'is-open': this.open + }} + /> + ) + } +} +``` + +If `this.open === true`, it will render: +```tsx +<todo-list class="todo-list is-open" aria-hidden="false"></todo-list> +``` + +similarly, if `this.open === false`: + +```tsx +<todo-list class="todo-list" aria-hidden="true"></todo-list> +``` + +`<Host>` is a virtual component, a virtual API exposed by stencil to declaratively set the attributes of the host element, it will never be rendered in the DOM, i.e. you will never see `<Host>` in Chrome Dev Tools for instance. + + +### `<Host>` can work as a `<Fragment>` + +`<Host>` can also be used when more than one component needs to be rendered at the root level for example: + +It could be achieved by a `render()` method like this: + +```tsx +@Component({tag: 'my-cmp'}) +export class MyCmp { + render() { + return ( + <Host> + <h1>Title</h1> + <p>Message</p> + </Host> + ); + } +} +``` + +This JSX would render the following HTML: + +```markup +<my-cmp> + <h1>Title</h1> + <p>Message</p> +</my-cmp> +``` + +Even if we don't use `<Host>` to render any attribute in the host element, it's a useful API to render many elements at the root level. + +## Element Decorator + +The `@Element()` decorator is how to get access to the host element within the class instance. This returns an instance of an `HTMLElement`, so standard DOM methods/events can be used here. + +```tsx +import { Element } from '@stencil/core'; + +... +export class TodoList { + + @Element() el: HTMLElement; + + getListHeight(): number { + return this.el.getBoundingClientRect().height; + } +} +``` + +In order to reference the host element when initializing a class member you'll need to use TypeScript's definite assignment assertion modifier to avoid a +type error: + +```tsx +import { Element } from '@stencil/core'; + +... +export class TodoList { + + @Element() el!: HTMLElement; + + private listHeight = this.el.getBoundingClientRect().height; +} +``` + +If you need to update the host element in response to prop or state changes, you should do so in the `render()` method using the `<Host>` element. + +## Styling + +See full information about styling on the [Styling page](./styling.md#shadow-dom-in-stencil). + +CSS can be applied to the `<Host>` element by using its component tag defined in the `@Component` decorator. + +```tsx +@Component({ + tag: 'my-cmp', + styleUrl: 'my-cmp.css' +}) +... +``` + +my-cmp.css: + +```css +my-cmp { + width: 100px; +} +``` + +### Shadow DOM + +Something to beware of is that Styling the `<Host>` element when using shadow DOM does not work quite the same. Instead of using the `my-cmp` element selector you must use `:host`. + +```tsx +@Component({ + tag: 'my-cmp', + styleUrl: 'my-cmp.css', + shadow: true +}) +... +``` + +my-cmp.css: + +```css +:host { + width: 100px; +} +``` diff --git a/versioned_docs/version-v4.22/components/methods.md b/versioned_docs/version-v4.22/components/methods.md new file mode 100644 index 000000000..931839b91 --- /dev/null +++ b/versioned_docs/version-v4.22/components/methods.md @@ -0,0 +1,98 @@ +--- +title: Methods +sidebar_label: Methods +description: methods +slug: /methods +--- + +# Method Decorator + +The `@Method()` decorator is used to expose methods on the public API. Functions decorated with the `@Method()` decorator can be called directly from the element, i.e. they are intended to be callable from the outside! + +:::note +Developers should try to rely on publicly exposed methods as little as possible, and instead default to using properties and events as much as possible. As an app scales, we've found it's easier to manage and pass data through @Prop rather than public methods. +::: + +```tsx +import { Method } from '@stencil/core'; + +export class TodoList { + + @Method() + async showPrompt() { + // show a prompt + } +} +``` + +Call the method like this: + +:::note +Developers should ensure that the component is defined by using the whenDefined method of the custom element registry before attempting to call public methods. +::: + +```tsx +(async () => { + await customElements.whenDefined('todo-list'); + const todoListElement = document.querySelector('todo-list'); + await todoListElement.showPrompt(); +})(); +``` + +## Public methods must be async + +Stencil's architecture is async at all levels which allows for many performance benefits and ease of use. By ensuring publicly exposed methods using the `@Method` decorator return a promise: + +- Developers can call methods before the implementation was downloaded without componentOnReady(), which queues the method calls and resolves after the component has finished loading. + +- Interaction with the component is the same whether it still needs to be lazy-loaded, or is already fully hydrated. + +- By keeping a component's public API async, apps could move the components transparently to [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and the API would still be the same. + +- Returning a promise is only required for publicly exposed methods which have the `@Method` decorator. All other component methods are private to the component and are not required to be async. + + +```tsx +// VALID: using async +@Method() +async myMethod() { + return 42; +} + +// VALID: using Promise.resolve() +@Method() +myMethod2() { + return Promise.resolve(42); +} + +// VALID: even if it returns nothing, it needs to be async +@Method() +async myMethod3() { + console.log(42); +} + +// INVALID +@Method() +notOk() { + return 42; +} +``` + +## Private methods + +Non-public methods can still be used to organize the business logic of your component and they do NOT have to return a Promise. + +```tsx +class Component { + // Since `getData` is not a public method exposed with @Method + // it does not need to be async + getData() { + return this.someData; + } + render() { + return ( + <div>{this.getData()}</div> + ); + } +} +``` diff --git a/versioned_docs/version-v4.22/components/properties.md b/versioned_docs/version-v4.22/components/properties.md new file mode 100644 index 000000000..25205e11a --- /dev/null +++ b/versioned_docs/version-v4.22/components/properties.md @@ -0,0 +1,974 @@ +--- +title: Properties +sidebar_label: Properties +description: Properties +slug: /properties +--- + +# Properties + +Props are custom attributes/properties exposed publicly on an HTML element. They allow developers to pass data to a +component to render or otherwise use. + +## The Prop Decorator (`@Prop()`) + +Props are declared on a component using Stencil's `@Prop()` decorator, like so: + +```tsx +// First, we import Prop from '@stencil/core' +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list', +}) +export class TodoList { + // Second, we decorate a class member with @Prop() + @Prop() name: string; + + render() { + // Within the component's class, its props are + // accessed via `this`. This allows us to render + // the value passed to `todo-list` + return <div>To-Do List Name: {this.name}</div> + } +} +``` + +In the example above, `@Prop()` is placed before (decorates) the `name` class member, which is a string. By adding +`@Prop()` to `name`, Stencil will expose `name` as an attribute on the element, which can be set wherever the component +is used: + +```tsx +{/* Here we use the component in a TSX file */} +<todo-list name={"Tuesday's To-Do List"}></todo-list> +``` +```html +<!-- Here we use the component in an HTML file --> +<todo-list name="Tuesday's To-Do List"></todo-list> +``` + +In the example above the `todo-list` component is used almost identically in TSX and HTML. The only difference between +the two is that in TSX, the value assigned to a prop (in this case, `name`) is wrapped in curly braces. In some cases +however, the way props are passed to a component differs slightly between HTML and TSX. + +## Variable Casing + +In the JavaScript ecosystem, it's common to use 'camelCase' when naming variables. The example component below has a +class member, `thingToDo` that is camelCased. + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + // thingToDo is 'camelCased' + @Prop() thingToDo: string; + + render() { + return <div>{this.thingToDo}</div>; + } +} +``` + +Since `thingToDo` is a prop, we can provide a value for it when we use our `todo-list-item` component. Providing a +value to a camelCased prop like `thingToDo` is nearly identical in TSX and HTML. + +When we use our component in a TSX file, an attribute uses camelCase: + +```tsx +<todo-list-item thingToDo={"Learn about Stencil Props"}></todo-list-item> +``` + +In HTML, the attribute must use 'dash-case' like so: + +```html +<todo-list-item thing-to-do="Learn about Stencil Props"></todo-list-item> +``` + +## Data Flow + +Props should be used to pass data down from a parent component to its child component(s). + +The example below shows how a `todo-list` component uses three `todo-list-item` child components to render a ToDo list. + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list', +}) +export class TodoList { + render() { + return ( + <div> + <h1>To-Do List Name: Stencil To Do List</h1> + <ul> + {/* Below are three Stencil components that are children of `todo-list`, each representing an item on our list */} + <todo-list-item thingToDo={"Learn about Stencil Props"}></todo-list-item> + <todo-list-item thingToDo={"Write some Stencil Code with Props"}></todo-list-item> + <todo-list-item thingToDo={"Dance Party"}></todo-list-item> + </ul> + </div> + ) + } +} +``` +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() thingToDo: string; + + render() { + return <li>{this.thingToDo}</li>; + } +} +``` + +:::note +Children components should not know about or reference their parent components. This allows Stencil to +efficiently re-render your components. Passing a reference to a component as a prop may cause unintended side effects. +::: + +## Mutability + +A Prop is by default immutable from inside the component logic. Once a value is set by a user, the component cannot +update it internally. For more advanced control over the mutability of a prop, please see the +[mutable option](#prop-mutability-mutable) section of this document. + +## Types + +Props can be a `boolean`, `number`, `string`, or even an `Object` or `Array`. The example below expands the +`todo-list-item` to add a few more props with different types. + +```tsx +import { Component, Prop, h } from '@stencil/core'; +// `MyHttpService` is an `Object` in this example +import { MyHttpService } from '../some/local/directory/MyHttpService'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() isComplete: boolean; + @Prop() timesCompletedInPast: number; + @Prop() thingToDo: string; + @Prop() myHttpService: MyHttpService; +} +``` + +### Boolean Props + +A property on a Stencil component that has a type of `boolean` may be declared as: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() isComplete: boolean; +} +``` + +To use this version of `todo-list-item` in HTML, we pass the string `"true"`/`"false"` to the component: +```html +<!-- Set isComplete to 'true' --> +<todo-list-item is-complete="true"></todo-list-item> +<!-- Set isComplete to 'false' --> +<todo-list-item is-complete="false"></todo-list-item> +``` + +To use this version of `todo-list-item` in TSX, `true`/`false` is used, surrounded by curly braces: +```tsx +// Set isComplete to 'true' +<todo-list-item isComplete={true}></todo-list-item> +// Set isComplete to 'false' +<todo-list-item isComplete={false}></todo-list-item> +``` + +There are a few ways in which Stencil treats props that are of type `boolean` that are worth noting: + +1. The value of a boolean prop will be `false` if provided the string `"false"` in HTML + +```html +<!-- The 'todo-list-item' component will have an isComplete value of `false` --> +<todo-list-item is-complete="false"></todo-list-item> +``` +2. The value of a boolean prop will be `true` if provided a string that is not `"false"` in HTML + +```html +<!-- The 'todo-list-item' component will have an isComplete value of --> +<!-- `true` for each of the following examples --> +<todo-list-item is-complete=""></todo-list-item> +<todo-list-item is-complete="0"></todo-list-item> +<todo-list-item is-complete="False"></todo-list-item> +``` +3. The value of a boolean prop will be `undefined` if it has no [default value](#default-values) and one of +the following applies: + 1. the prop is not included when using the component + 2. the prop is included when using the component, but is not given a value + +```html +<!-- Both examples using the 'todo-list-item' component will have an --> +<!-- isComplete value of `undefined` --> +<todo-list-item></todo-list-item> +<todo-list-item is-complete></todo-list-item> +``` + +### Number Props + +A property on a Stencil component that has a type of `number` may be declared as: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() timesCompletedInPast: number; +} +``` + +To use this version of `todo-list-item` in HTML, we pass the numeric value as a string to the component: +```html +<!-- Set timesCompletedInPast to '0' --> +<todo-list-item times-completed-in-past="0"></todo-list-item> +<!-- Set timesCompletedInPast to '23' --> +<todo-list-item times-completed-in-past="23"></todo-list-item> +``` + +To use this version of `todo-list-item` in TSX, a number surrounded by curly braces is passed to the component: +```tsx +// Set timesCompletedInPast to '0' +<todo-list-item timesCompletedInPast={0}></todo-list-item> +// Set timesCompletedInPast to '23' +<todo-list-item timesCompletedInPast={23}></todo-list-item> +``` + +### String Props + +A property on a Stencil component that has a type of `string` may be declared as: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() thingToDo: string; +} +``` + +To use this version of `todo-list-item` in HTML, we pass the value as a string to the component: +```html +<!-- Set thingToDo to 'Learn about Stencil Props' --> +<todo-list-item thing-to-do="Learn about Stencil Props"></todo-list-item> +<!-- Set thingToDo to 'Write some Stencil Code with Props' --> +<todo-list-item thing-to-do="Write some Stencil Code with Props"></todo-list-item> +``` + +To use this version of `todo-list-item` in TSX, we pass the value as a string to the component. Curly braces aren't +required when providing string values to props in TSX, but are permitted: +```tsx +// Set thingToDo to 'Learn about Stencil Props' +<todo-list-item thingToDo="Learn about Stencil Props"></todo-list-item> +// Set thingToDo to 'Write some Stencil Code with Props' +<todo-list-item thingToDo="Write some Stencil Code with Props"></todo-list-item> +// Set thingToDo to 'Write some Stencil Code with Props' with curly braces +<todo-list-item thingToDo={"Learn about Stencil Props"}></todo-list-item> +``` + +### Object Props + +A property on a Stencil component that has a type of `Object` may be declared as: + +```tsx +// TodoListItem.tsx +import { Component, Prop, h } from '@stencil/core'; +import { MyHttpService } from '../path/to/MyHttpService'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + // Use `@Prop()` to declare the `httpService` class member + @Prop() httpService: MyHttpService; +} +``` +```tsx +// MyHttpService.ts +export class MyHttpService { + // This implementation intentionally left blank +} +``` + +In TypeScript, `MyHttpService` is both an `Object` and a 'type'. When using user-defined types like `MyHttpService`, the +type must always be exported using the `export` keyword where it is declared. The reason for this is Stencil needs to +know what type the prop `httpService` is when passing an instance of `MyHttpService` to `TodoListItem` from a parent +component. + +To set `httpService` in TSX, assign the property name in the custom element's tag to the desired value like so: +```tsx +// TodoList.tsx +import { Component, h } from '@stencil/core'; +import { MyHttpService } from '../MyHttpService'; + +@Component({ + tag: 'todo-list', + styleUrl: 'todo-list.css', + shadow: true, +}) +export class ToDoList { + private httpService = new MyHttpService(); + + render() { + return <todo-list-item httpService={this.httpService}></todo-list-item>; + } +} +``` +Note that the prop name is using `camelCase`, and the value is surrounded by curly braces. + +It is not possible to set `Object` props via an HTML attribute like so: +```html +<!-- this will not work --> +<todo-list-item http-service="{ /* implementation omitted */ }"></todo-list-item> +``` +The reason for this is that Stencil will not attempt to serialize object-like strings written in HTML into a JavaScript object. +Similarly, Stencil does not have any support for deserializing objects from JSON. +Doing either can be expensive at runtime, and runs the risk of losing references to other nested JavaScript objects. + +Instead, properties may be set via `<script>` tags in a project's HTML: +```html +<script> + document.querySelector('todo-list-item').httpService = { /* implementation omitted */ }; +</script> +``` + + +### Array Props + +A property on a Stencil component that is an Array may be declared as: + +```tsx +// TodoList.tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() itemLabels: string[]; +} +``` + +To set `itemLabels` in TSX, assign the prop name in the custom element's tag to the desired value like so: +```tsx +// TodoList.tsx +import { Component, h } from '@stencil/core'; +import { MyHttpService } from '../MyHttpService'; + +@Component({ + tag: 'todo-list', + styleUrl: 'todo-list.css', + shadow: true, +}) +export class ToDoList { + private labels = ['non-urgent', 'weekend-only']; + + render() { + return <todo-list-item itemLabels={this.labels}></todo-list-item>; + } +} +``` +Note that the prop name is using `camelCase`, and the value is surrounded by curly braces. + +It is not possible to set `Array` props via an HTML attribute like so: +```html +<!-- this will not work --> +<todo-list-item item-labels="['non-urgent', 'weekend-only']"></todo-list-item> +``` +The reason for this is that Stencil will not attempt to serialize array-like strings written in HTML into a JavaScript object. +Doing so can be expensive at runtime, and runs the risk of losing references to other nested JavaScript objects. + +Instead, properties may be set via `<script>` tags in a project's HTML: +```html +<script> + document.querySelector('todo-list-item').itemLabels = ['non-urgent', 'weekend-only']; +</script> +``` + +### Advanced Prop Types + +#### `any` Type + +TypeScript's [`any` type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#any) is a special type +that may be used to prevent type checking of a specific value. Because `any` is a valid type in TypeScript, Stencil +props can also be given a type of `any`. The example below demonstrates three different ways of using props with type +`any`: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + // isComplete has an explicit type annotation + // of `any`, and no default value + @Prop() isComplete: any; + // label has an explicit type annotation of + // `any` with a default value of 'urgent', + // which is a string + @Prop() label: any = 'urgent'; + // thingToDo has no type and no default value, + // and will be considered to be type `any` by + // TypeScript + @Prop() thingToDo; + + render() { + return ( + <ul> + <li>isComplete has a value of - {this.isComplete} - and a typeof value of "{typeof this.isComplete}"</li> + <li>label has a value of - {this.label} - and a typeof value of "{typeof this.label}"</li> + <li>thingToDo has a value of - {this.thingToDo} - and a typeof value of "{typeof this.thingToDo}"</li> + </ul> + ); + } +} +``` + +When using a Stencil prop typed as `any` (implicitly or explicitly), the value that is provided to a prop retains its +own type information. Neither Stencil nor TypeScript will try to change the type of the prop. To demonstrate, let's use +`todo-list-item` twice, each with different prop values: + +```tsx +{/* Using todo-list-item in TSX using differnt values each time */} +<todo-list-item isComplete={42} label={null} thingToDo={"Learn about any-typed props"}></todo-list-item> +<todo-list-item isComplete={"42"} label={1} thingToDo={"Learn about any-typed props"}></todo-list-item> +``` + +The following will rendered from the usage example above: +```md +- isComplete has a value of - 42 - and a typeof value of "number" +- label has a value of - - and a typeof value of "object" +- thingToDo has a value of - Learn about any-typed props - and a typeof value of "string" + +- isComplete has a value of - 42 - and a typeof value of "string" +- label has a value of - 1 - and a typeof value of "number" +- thingToDo has a value of - Learn about any-typed props - and a typeof value of "string" +``` + +In the first usage of `todo-list-item`, `isComplete` is provided a number value of 42, whereas in the second usage it +receives a string containing "42". The types on `isComplete` reflect the type of the value it was provided, 'number' and +'string', respectively. + +Looking at `label`, it is worth noting that although the prop has a [default value](#default-values), it does +not narrow the type of `label` to be of type 'string'. In the first usage of `todo-list-item`, `label` is provided a +value of null, whereas in the second usage it receives a number value of 1. The types of the values stored in `label` +are correctly reported as 'object' and 'number', respectively. + +#### Optional Types + +TypeScript allows members to be marked optional by appending a `?` at the end of the member's name. The example below +demonstrates making each a component's props optional: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + // completeMsg is optional, has an explicit type + // annotation of `string`, and no default value + @Prop() completeMsg?: string; + // label is optional, has no explicit type + // annotation, but does have a default value + // of 'urgent' + @Prop() label? = 'urgent'; + // thingToDo has no type annotation and no + // default value + @Prop() thingToDo?; + + render() { + return ( + <ul> + <li>completeMsg has a value of - {this.completeMsg} - and a typeof value of "{typeof this.completeMsg}"</li> + <li>label has a value of - {this.label} - and a typeof value of "{typeof this.label}"</li> + <li>thingToDo has a value of - {this.thingToDo} - and a typeof value of "{typeof this.thingToDo}"</li> + </ul> + ); + } +} +``` + +When using a Stencil prop that is marked as optional, Stencil will try to infer the type of the prop if a type is +not explicitly given. In the example above, Stencil is able to understand that: + +- `completeMsg` is of type string, because it has an explicit type annotation +- `label` is of type string, because it has a [default value](#default-values) that is of type string +- `thingToDo` [is of type `any`](#any-type), because it has no explicit type annotation, nor default value + +Because Stencil can infer the type of `label`, the following will fail to compile due to a type mismatch: + +```tsx +{/* This fails to compile with the error "Type 'number' is not assignable to type 'string'" for the label prop. */} +<todo-list-item completeMsg={"true"} label={42} thingToDo={"Learn about any-typed props"}></todo-list-item> +``` + +It is worth noting that when using a component in an HTML file, such type checking is unavailable. This is a constraint +on HTML, where all values provided to attributes are of type string: + +```html +<!-- using todo-list-item in HTML --> +<todo-list-item complete-msg="42" label="null" thing-to-do="Learn about any-typed props"></todo-list-item> +``` +renders: +```md +- completeMsg has a value of - 42 - and a typeof value of "string" +- label has a value of - null - and a typeof value of "string" +- thingToDo has a value of - Learn about any-typed props - and a typeof value of "string" +``` + +#### Union Types + +Stencil allows props types be [union types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types), +which allows you as the developer to combine two or more pre-existing types to create a new one. The example below shows +a `todo-list-item` who accepts a `isComplete` prop that can be either a string or boolean. + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() isComplete: string | boolean; +} +``` + +This component can be used in both HTML: +```html +<todo-list-item is-complete="true"></todo-list-item> +<todo-list-item is-complete="false"></todo-list-item> +<todo-list-item is-complete></todo-list-item> +<todo-list-item></todo-list-item> +``` +and TSX: +```tsx +<todo-list-item isComplete={true}></todo-list-item> +<todo-list-item isComplete={false}></todo-list-item> +``` + +When using union types, the type of a component's `@Prop()` value can be ambiguous at runtime. +In the provided example, under what circumstances does `@Prop() isComplete` function as a `string`, and when does it serve as a `boolean`? + +When using a component in HTML, the runtime value of a `@Prop()` is a string whenever an attribute is set. +This is a result of setting the HTML attribute for the custom element. +```html +<!-- Since this is HTML, the value of `isComplete` in `ToDoListItem` will be a string --> + +<!-- Set isComplete to "true". --> +<todo-list-item is-complete="true"></todo-list-item> +<!-- Set isComplete to "false" --> +<todo-list-item is-complete="false"></todo-list-item> +<!-- Set isComplete to "" --> +<todo-list-item is-complete></todo-list-item> +``` +However, if an attribute is not specified, the runtime value of the property will be `undefined`: +```html +<!-- Since `is-complete` is omitted, the value of `isComplete` in `ToDoListItem` will be `undefined` --> +<todo-list-item></todo-list-item> +``` + +When the attribute on a component is set using `setAttribute`, the runtime value of a `@Prop()` is always [coerced to a string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion). +```html +<script> + // both of these `setAttribute` calls set the property `isComplete` to "true" (string) + document.querySelector('todo-list-item').setAttribute('is-complete', true); + document.querySelector('todo-list-item').setAttribute('is-complete', "true"); + // both of these `setAttribute` calls set the property `isComplete` to "false" (string) + document.querySelector('todo-list-item').setAttribute('is-complete', false); + document.querySelector('todo-list-item').setAttribute('is-complete', "false"); +</script> +``` + +However, if the property of a custom element is directly changed, its type will match the value that was provided. +```html +<script> + // Set the property `isComplete` to `true` (boolean) + document.querySelector('todo-list-item').isComplete = true; + // Set the property `isComplete` to "true" (string) + document.querySelector('todo-list-item').isComplete = "true"; + // Set the property `isComplete` to `false` (boolean) + document.querySelector('todo-list-item').isComplete = false; + // Set the property `isComplete` to "false" (string) + document.querySelector('todo-list-item').isComplete = "false"; +</script> +``` + +When using a component in TSX, a `@Prop()`'s type will match the value that was provided. +```tsx +// Since this is TSX, the value of `isComplete` in `ToDoListItem` +// depends on the type of the value passed to the component. +// +// Set the property `isComplete` to `true` (boolean) +<todo-list-item isComplete={true}></todo-list-item> +// Set the property `isComplete` to "true" (string) +<todo-list-item isComplete={"true"}></todo-list-item> +// Set the property `isComplete` to `false` (boolean) +<todo-list-item isComplete={false}></todo-list-item> +// Set the property `isComplete` to "false" (string) +<todo-list-item isComplete={"false"}></todo-list-item> +``` + +## Default Values + +Stencil props can be given a default value as a fallback in the event a prop is not provided: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'component-with-some-props', +}) +export class ComponentWithSomeProps { + @Prop() aNumber = 42; + @Prop() aString = 'defaultValue'; + + render() { + return <div>The number is {this.aNumber} and the string is {this.aString}</div> + } +} +``` +Regardless of if we use this component in HTML or TSX, "The number is 42 and the string is defaultValue" is displayed +when no values are passed to our component: +```html +<component-with-some-props></component-with-some-props> +``` + +The default values on a component can be overridden by specifying a value for a prop with a default value. For the +example below, "The number is 7 and the string is defaultValue" is rendered. Note how the value provided to `aNumber` +overrides the default value, but the default value of `aString` remains the same: +```html +<component-with-some-props a-number="7"></component-with-some-props> +``` + +### Inferring Types from Default Values + +When a default value is provided, Stencil is able to infer the type of the prop from the default value: + +```tsx +import { Component, Prop, h } from '@stencil/core'; +@Component({ + tag: 'component-with-many-props', +}) +export class ComponentWithManyProps { + // both props below are of type 'boolean' + @Prop() boolean1: boolean; + @Prop() boolean2 = true; + + // both props below are of type 'number' + @Prop() number1: number; + @Prop() number2 = 42; + + // both props below are of type 'string' + @Prop() string1: string; + @Prop() string2 = 'defaultValue'; +} +``` + +## Required Properties + +By placing a `!` after a prop name, Stencil mark that the attribute/property as required. This ensures that when the +component is used in TSX, the property is used: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + // Note the '!' after the variable name. + @Prop() thingToDo!: string; +} +``` + +## Prop Validation + +To do validation of a Prop, you can use the [@Watch()](./reactive-data.md#the-watch-decorator-watch) decorator: + +```tsx +import { Component, Prop, Watch, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class TodoList { + // Mark the prop as required, to make sure it is provided when we use `todo-list-item`. + // We want stricter guarantees around the contents of the string, so we'll use `@Watch` to perform additional validation. + @Prop() thingToDo!: string; + + @Watch('thingToDo') + validateName(newValue: string, _oldValue: string) { + // don't allow `thingToDo` to be the empty string + const isBlank = typeof newValue !== 'string' || newValue === ''; + if (isBlank) { + throw new Error('thingToDo is a required property and cannot be empty') + }; + // don't allow `thingToDo` to be a string with a length of 1 + const has2chars = typeof newValue === 'string' && newValue.length >= 2; + if (!has2chars) { + throw new Error('thingToDo must have a length of more than 1 character') + }; + } +} +``` + +## @Prop() Options + +The `@Prop()` decorator accepts an optional argument to specify certain options to modify how a prop on a component +behaves. `@Prop()`'s optional argument is an object literal containing one or more of the following fields: + +```tsx +export interface PropOptions { + attribute?: string; + mutable?: boolean; + reflect?: boolean; +} +``` + +### Attribute Name (`attribute`) + +Properties and component attributes are strongly connected but not necessarily the same thing. While attributes are an +HTML concept, properties are a JavaScript concept inherent to Object-Oriented Programming. + +In Stencil, the `@Prop()` decorator applied to a **property** will instruct the Stencil compiler to also listen for +changes in a DOM attribute. + +Usually, the name of a property is the same as the attribute, but this is not always the case. Take the following +component as example: + +```tsx +import { Component, Prop, h } from '@stencil/core'; +// `MyHttpService` is an `Object` in this example +import { MyHttpService } from '../some/local/directory/MyHttpService'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop() isComplete: boolean; + @Prop() thingToDo: string; + @Prop() httpService: MyHttpService; +} +``` + +This component has **3 properties**, but the compiler will create **only 2 attributes**: `is-complete` and +`thing-to-do`. + +```html +<todo-list-item is-complete="false" thing-to-do="Read Attribute Naming Section of Stencil Docs"></todo-list-item> +``` + +Notice that the `httpService` type is not a primitive (e.g. not a `number`, `boolean`, or `string`). Since DOM +attributes can only be strings, it does not make sense to have an associated DOM attribute called `"http-service"`. +Stencil will not attempt to serialize object-like strings written in HTML into a JavaScript object. +See [Object Props](#object-props) for guidance as to how to configure `httpService`. + +At the same time, the `isComplete` & `thingToDo` properties follow 'camelCase' naming, but attributes are +case-insensitive, so the attribute names will be `is-complete` & `thing-to-do` by default. + +Fortunately, this "default" behavior can be changed using the `attribute` option of the `@Prop()` decorator: + +```tsx +import { Component, Prop, h } from '@stencil/core'; +// `MyHttpService` is an `Object` in this example +import { MyHttpService } from '../some/local/directory/MyHttpService'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop({ attribute: 'complete' }) isComplete: boolean; + @Prop({ attribute: 'thing' }) thingToDo: string; + @Prop({ attribute: 'my-service' }) httpService: MyHttpService; +} +``` + +By using this option, we are being explicit about which properties have an associated DOM attribute and the name of it +when using the component in HTML. + +```html +<todo-list-item complete="false" thing="Read Attribute Naming Section of Stencil Docs" my-service="{}"></todo-list-item> +``` + +### Prop Mutability (`mutable`) + +A Prop is by default immutable from inside the component logic. +However, it's possible to explicitly allow a Prop to be mutated from inside the component, by declaring it as mutable, as in the example below: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop({ mutable: true }) thingToDo: string; + + componentDidLoad() { + this.thingToDo = 'Ah! A new value!'; + } +} +``` + +#### Mutable Arrays and Objects + +Stencil compares Props by reference in order to efficiently rerender components. +Setting `mutable: true` on a Prop that is an object or array allows the _reference_ to the Prop to change inside the component and trigger a render. +It does not allow a mutable change to an existing object or array to trigger a render. + +For example, to update an array Prop: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'my-component', +}) +export class MyComponent { + @Prop({mutable: true}) contents: string[] = []; + timer: NodeJS.Timer; + + connectedCallback() { + this.timer = setTimeout(() => { + // this does not create a new array. when stencil + // attempts to see if any of its Props have changed, + // it sees the reference to its `contents` Prop is + // the same, and will not trigger a render + + // this.contents.push('Stencil') + + // this does create a new array, and therefore a + // new reference to the Prop. Stencil will pick up + // this change and rerender + this.contents = [...this.contents, 'Stencil']; + // after 3 seconds, the component will re-render due + // to the reference change in `this.contents` + }, 3000); + } + + disconnectedCallback() { + if (this.timer) { + clearTimeout(this.timer); + } + } + + render() { + return <div>Hello, World! I'm {this.contents[0]}</div>; + } +} +``` + +In the example above, updating the Prop in place using `this.contents.push('Stencil')` would have no effect. +Stencil does not see the change to `this.contents`, since it looks at the _reference_ of the Prop, and sees that it has not changed. +This is done for performance reasons. +If Stencil had to walk every slot of the array to determine if it changed, it would incur a performance hit. +Rather, it is considered better for performance and more idiomatic to re-assign the Prop (in the example above, we use the spread operator). + +The same holds for objects as well. +Rather than mutating an existing object in-place, a new object should be created using the spread operator. This object will be different-by-reference and therefore will trigger a re-render: + + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +export type MyContents = {name: string}; + +@Component({ + tag: 'my-component', +}) +export class MyComponent { + @Prop({mutable: true}) contents: MyContents; + timer: NodeJS.Timer; + + connectedCallback() { + this.timer = setTimeout(() => { + // this does not create a new object. when stencil + // attempts to see if any of its Props have changed, + // it sees the reference to its `contents` Prop is + // the same, and will not trigger a render + + // this.contents.name = 'Stencil'; + + // this does create a new object, and therefore a + // new reference to the Prop. Stencil will pick up + // this change and rerender + this.contents = {...this.contents, name: 'Stencil'}; + // after 3 seconds, the component will re-render due + // to the reference change in `this.contents` + }, 3000); + } + + disconnectedCallback() { + if (this.timer) { + clearTimeout(this.timer); + } + } + + render() { + return <div>Hello, World! I'm {this.contents.name}</div>; + } +} +``` + +### Reflect Properties Values to Attributes (`reflect`) + +In some cases it may be useful to keep a Prop in sync with an attribute. In this case you can set the `reflect` option +in the `@Prop()` decorator to `true`. When a prop is reflected, it will be rendered in the DOM as an HTML attribute. + +Take the following component as example: + +```tsx +import { Component, Prop, h } from '@stencil/core'; + +@Component({ + tag: 'todo-list-item', +}) +export class ToDoListItem { + @Prop({ reflect: false }) isComplete: boolean = false; + @Prop({ reflect: true }) timesCompletedInPast: number = 2; + @Prop({ reflect: true }) thingToDo: string = "Read Reflect Section of Stencil Docs"; +} +``` + +The component in the example above uses [default values](#default-values), and can be used in HTML like so: +```html +<!-- Example of using todo-list-item in HTML --> +<todo-list-item></todo-list-item> +``` + +When rendered in the DOM, the properties configured with `reflect: true` will be reflected in the DOM: + +```html +<todo-list-item times-completed-in-past="2" thing-to-do="Read Reflect Section of Stencil Docs" ></todo-list-item> +``` + +While the properties not set to "reflect", such as `isComplete`, are not rendered as attributes, it does not mean it's +not there - the `isComplete` property still contains the `false` value as assigned: + +```tsx +const cmp = document.querySelector('todo-list-item'); +console.log(cmp.isComplete); // it prints 'false' +``` diff --git a/versioned_docs/version-v4.22/components/reactive-data.md b/versioned_docs/version-v4.22/components/reactive-data.md new file mode 100644 index 000000000..b41db4c12 --- /dev/null +++ b/versioned_docs/version-v4.22/components/reactive-data.md @@ -0,0 +1,248 @@ +--- +title: Reactive Data, Handling arrays and objects +sidebar_label: Reactive Data +description: Reactive Data, Handling arrays and objects +slug: /reactive-data +--- + +# Reactive Data + +Stencil components update when props or state on a component change. + +## Rendering methods + +When props or state change on a component, the [`render()` method](./templating-and-jsx.md) is scheduled to run. + +## The Watch Decorator (`@Watch()`) + +`@Watch()` is a decorator that is applied to a method of a Stencil component. +The decorator accepts a single argument, the name of a class member that is decorated with `@Prop()` or `@State()`, or +a host attribute. A method decorated with `@Watch()` will automatically run when its associated class member or attribute changes. + +```tsx +// We import Prop & State to show how `@Watch()` can be used on +// class members decorated with either `@Prop()` or `@State()` +import { Component, Prop, State, Watch } from '@stencil/core'; + +@Component({ + tag: 'loading-indicator' +}) +export class LoadingIndicator { + // We decorate a class member with @Prop() so that we + // can apply @Watch() + @Prop() activated: boolean; + // We decorate a class member with @State() so that we + // can apply @Watch() + @State() busy: boolean; + + // Apply @Watch() for the component's `activated` member. + // Whenever `activated` changes, this method will fire. + @Watch('activated') + watchPropHandler(newValue: boolean, oldValue: boolean) { + console.log('The old value of activated is: ', oldValue); + console.log('The new value of activated is: ', newValue); + } + + // Apply @Watch() for the component's `busy` member. + // Whenever `busy` changes, this method will fire. + @Watch('busy') + watchStateHandler(newValue: boolean, oldValue: boolean) { + console.log('The old value of busy is: ', oldValue); + console.log('The new value of busy is: ', newValue); + } + + @Watch('activated') + @Watch('busy') + watchMultiple(newValue: boolean, oldValue: boolean, propName:string) { + console.log(`The new value of ${propName} is: `, newValue); + } +} +``` + +In the example above, there are two `@Watch()` decorators. +One decorates `watchPropHandler`, which will fire when the class member `activated` changes. +The other decorates `watchStateHandler`, which will fire when the class member `busy` changes. + +When fired, the decorated method will receive the old and new values of the prop/state. +This is useful for validation or the handling of side effects. + +:::info +The `@Watch()` decorator does not fire when a component initially loads. +::: + +### Watching Native HTML Attributes + +Stencil's `@Watch()` decorator also allows you to watch native HTML attributes on the constructed host element. Simply +include the attribute name as the argument to the decorator (this is case-sensitive): + +```tsx +@Watch('aria-label') +onAriaLabelChange(newVal: string, oldVal: string) { + console.log('Label changed:', newVal, oldVal); +} +``` + +:::note +Since native attributes are not `@Prop()` or `State()` members of the Stencil component, they will not automatically trigger a +re-render when changed. If you wish to re-render a component in this instance, you can leverage the `forceUpdate()` method: + +```tsx +import { Component, forceUpdate, h } from '@stencil/core'; + +@Watch('aria-label') +onAriaLabelChange() { + forceUpdate(this); // Forces a re-render +} +``` +::: + +## Handling Arrays and Objects + +When Stencil checks if a class member decorated with `@Prop()` or `@State()` has changed, it checks if the reference to the class member has changed. +When a class member is an object or array, and is marked with `@Prop()` or `@State`, in-place mutation of an existing entity will _not_ cause `@Watch()` to fire, as it does not change the _reference_ to the class member. + +### Updating Arrays + +For arrays, the standard mutable array operations such as `push()` and `unshift()` won't trigger a component update. +These functions will change the content of the array, but won't change the reference to the array itself. + +In order to make changes to an array, non-mutable array operators should be used. +Non-mutable array operators return a copy of a new array that can be detected in a performant manner. +These include `map()` and `filter()`, and the [spread operator syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator). +The value returned by `map()`, `filter()`, etc., should be assigned to the `@Prop()` or `@State()` class member being watched. + +For example, to push a new item to an array, create a new array with the existing values and the new value at the end: + +```tsx +import { Component, State, Watch, h } from '@stencil/core'; + +@Component({ + tag: 'rand-numbers' +}) +export class RandomNumbers { + // We decorate a class member with @State() so that we + // can apply @Watch(). This will hold a list of randomly + // generated numbers + @State() randNumbers: number[] = []; + + private timer: NodeJS.Timer; + + // Apply @Watch() for the component's `randNumbers` member. + // Whenever `randNumbers` changes, this method will fire. + @Watch('randNumbers') + watchStateHandler(newValue: number[], oldValue: number[]) { + console.log('The old value of randNumbers is: ', oldValue); + console.log('The new value of randNumbers is: ', newValue); + } + + connectedCallback() { + this.timer = setInterval(() => { + // generate a random whole number + const newVal = Math.ceil(Math.random() * 100); + + /** + * This does not create a new array. When stencil + * attempts to see if any Watched members have changed, + * it sees the reference to its `randNumbers` State is + * the same, and will not trigger `@Watch` or a re-render + */ + // this.randNumbers.push(newVal) + + /** + * Using the spread operator, on the other hand, does + * create a new array. `randNumbers` is reassigned + * using the value returned by the spread operator. + * The reference to `randNumbers` has changed, which + * will trigger `@Watch` and a re-render + */ + this.randNumbers = [...this.randNumbers, newVal] + }, 1000) + } + + disconnectedCallback() { + if (this.timer) { + clearInterval(this.timer) + } + } + + render() { + return( + <div> + randNumbers contains: + <ol> + {this.randNumbers.map((num) => <li>{num}</li>)} + </ol> + </div> + ) + } +} +``` + +### Updating an object + +The spread operator should be used to update objects. +As with arrays, mutating an object will not trigger a view update in Stencil. +However, using the spread operator and assigning its return value to the `@Prop()` or `@State()` class member being watched will. +Below is an example: + +```tsx +import { Component, State, Watch, h } from '@stencil/core'; + +export type NumberContainer = { + val: number, +} + +@Component({ + tag: 'rand-numbers' +}) +export class RandomNumbers { + // We decorate a class member with @State() so that we + // can apply @Watch(). This will hold a randomly generated + // number. + @State() numberContainer: NumberContainer = { val: 0 }; + + private timer: NodeJS.Timer; + + // Apply @Watch() for the component's `numberContainer` member. + // Whenever `numberContainer` changes, this method will fire. + @Watch('numberContainer') + watchStateHandler(newValue: NumberContainer, oldValue: NumberContainer) { + console.log('The old value of numberContainer is: ', oldValue); + console.log('The new value of numberContainer is: ', newValue); + } + + connectedCallback() { + this.timer = setInterval(() => { + // generate a random whole number + const newVal = Math.ceil(Math.random() * 100); + + /** + * This does not create a new object. When stencil + * attempts to see if any Watched members have changed, + * it sees the reference to its `numberContainer` State is + * the same, and will not trigger `@Watch` or are-render + */ + // this.numberContainer.val = newVal; + + /** + * Using the spread operator, on the other hand, does + * create a new object. `numberContainer` is reassigned + * using the value returned by the spread operator. + * The reference to `numberContainer` has changed, which + * will trigger `@Watch` and a re-render + */ + this.numberContainer = {...this.numberContainer, val: newVal}; + }, 1000) + } + + disconnectedCallback() { + if (this.timer) { + clearInterval(this.timer) + } + } + + render() { + return <div>numberContainer contains: {this.numberContainer.val}</div>; + } +} +``` diff --git a/versioned_docs/version-v4.22/components/state.md b/versioned_docs/version-v4.22/components/state.md new file mode 100644 index 000000000..5a78b0447 --- /dev/null +++ b/versioned_docs/version-v4.22/components/state.md @@ -0,0 +1,265 @@ +--- +title: Internal state +sidebar_label: Internal State +description: Use the State() for component's internal state +slug: /state +--- + +# State + +'State' is a general term that refers to the values and objects that are stored on a class or an instance of a class for +use now or in the future. + +Like a regular TypeScript class, a Stencil component may have one or more internal class members for holding value(s) +that make up the component's state. Stencil allows developers to optionally mark class members holding some part of the +class's state with the `@State()` decorator to trigger a rerender when the state changes. + +## The State Decorator (`@State`) + +Stencil provides a decorator to trigger a rerender when certain class members change. A component's class members that +should trigger a rerender must be decorated using Stencil's `@State()` decorator, like so: +```tsx +// First, we import State from '@stencil/core' +import { Component, State, h } from '@stencil/core'; + +@Component({ + tag: 'current-time', +}) +export class CurrentTime { + // Second, we decorate a class member with @State() + // When `currentTime` changes, a rerender will be + // triggered + @State() currentTime: number = Date.now(); + + render() { + // Within the component's class, its members are + // accessed via `this`. This allows us to render + // the value stored in `currentTime` + const time = new Date(this.currentTime).toLocaleTimeString(); + + return ( + <span>{time}</span> + ); + } +} +``` + +In the example above, `@State()` is placed before (decorates) the `currentTime` class member, which is a number. This +marks `currentTime` so that any time its value changes, the component rerenders. + +However, the example above doesn't demonstrate the real power of using `@State`. `@State` members are meant to only be +updated within a class, which the example above never does after the initial assignment of `currentTime`. This means +that our `current-time` component will never rerender! We fix that in the example below to update `current-time` every +1000 milliseconds (1 second): + +```tsx +import { Component, State, h } from '@stencil/core'; + +@Component({ + tag: 'current-time', +}) +export class CurrentTime { + timer: number; + + // `currentTime` is decorated with `@State()`, + // as we need to trigger a rerender when its + // value changes to show the latest time + @State() currentTime: number = Date.now(); + + connectedCallback() { + this.timer = window.setInterval(() => { + // the assignment to `this.currentTime` + // will trigger a re-render + this.currentTime = Date.now(); + }, 1000); + } + + disconnectedCallback() { + window.clearInterval(this.timer); + } + + render() { + const time = new Date(this.currentTime).toLocaleTimeString(); + + return ( + <span>{time}</span> + ); + } +} +``` + +The example above makes use of the [connectedCallback() lifecycle method](./component-lifecycle.md#connectedcallback) +to set `currentTime` to the value of `Date.now()` every 1000 milliseconds (or, every one second). Because the value of +`currentTime` changes every second, Stencil calls the `render` function on `current-time`, which pretty-prints the +current time. + +The example above also makes use of the +[disconnectedCallback() lifecycle method](./component-lifecycle.md#disconnectedcallback) to properly clean up the timer +that was created using `setInterval` in `connectedCallback()`. This isn't necessary for using `@State`, but is a general +good practice when using `setInterval`. + +## When to Use `@State()`? + +`@State()` should be used for all class members that should trigger a rerender when they change. However, not all +internal state might need to be decorated with `@State()`. If you know for sure that the value will either not change or +that it does not need to trigger a re-rendering, `@State()` is not necessary. It is considered a 'best practice' to +only use `@State()` when absolutely necessary. Revisiting our `current-time` component: + +```tsx +import { Component, State, h } from '@stencil/core'; + +@Component({ + tag: 'current-time', +}) +export class CurrentTime { + // `timer` is not decorated with `@State()`, as + // we do not wish to trigger a rerender when its + // value changes + timer: number; + + // `currentTime` is decorated with `@State()`, + // as we need to trigger a rerender when its + // value changes to show the latest time + @State() currentTime: number = Date.now(); + + connectedCallback() { + // the assignment to `this.timer` will not + // trigger a re-render + this.timer = window.setInterval(() => { + // the assignment to `this.currentTime` + // will trigger a re-render + this.currentTime = Date.now(); + }, 1000); + } + + disconnectedCallback() { + window.clearInterval(this.timer); + } + + render() { + const time = new Date(this.currentTime).toLocaleTimeString(); + + return ( + <span>{time}</span> + ); + } +} +``` + +## Examples + +### Using `@State()` with `@Listen()` + +This example makes use of `@State` and [`@Listen`](./events.md#listen-decorator) decorators. We define a class member +called `isOpen` and decorate it with `@State()`. With the use of `@Listen()`, we respond to click events toggling the +value of `isOpen`. + +```tsx +import { Component, Listen, State, h } from '@stencil/core'; + +@Component({ + tag: 'my-toggle-button' +}) +export class MyToggleButton { + // `isOpen` is decorated with `@State()`, + // changes to it will trigger a rerender + @State() isOpen: boolean = true; + + @Listen('click', { capture: true }) + handleClick() { + // whenever a click event occurs on + // the component, update `isOpen`, + // triggering the rerender + this.isOpen = !this.isOpen; + } + + render() { + return <button> + {this.isOpen ? "Open" : "Closed"} + </button>; + } +} +``` + +### Complex Types + +For more advanced use cases, `@State()` can be used with a complex type. In the example below, we print a list of `Item` +entries. Although we start with zero `Item`s initially, we use the same pattern as we did before to add a new `Item` to +`ItemList`'s `items` array once every 2000 milliseconds (2 seconds). Every time a new entry is added to `items`, a +rerender occurs: + +```tsx +import { Component, State, h } from '@stencil/core'; + +// a user defined, complex type describing an 'Item' +type Item = { + id: number; + description: string, +} + +@Component({ + tag: 'item-list', +}) +export class ItemList { + // `timer` is not decorated with `@State()`, as + // we do not wish to trigger a rerender when its + // value changes + timer: number; + + // `items` will trigger a rerender if + // the value assigned to the variable changes + @State() items: Item[] = []; + + connectedCallback() { + // the assignment to `this.timer` will not + // trigger a re-render + this.timer = window.setInterval(() => { + const newTodo: Item = { + description: "Item", + id: this.items.length + 1 + }; + // the assignment to `this.items` will + // trigger a re-render. the assignment + // using '=' is important here, as we + // need that to make sure the rerender + // occurs + this.items = [...this.items, newTodo]; + }, 2000); + } + + disconnectedCallback() { + window.clearInterval(this.timer); + } + + render() { + return ( + <div> + <h1>To-Do List</h1> + <ul> + {this.items.map((todo) => <li>{todo.description} #{todo.id}</li>)} + </ul> + </div> + ); + } +} +``` + +It's important to note that it's the reassignment of `this.items` that is causing the rerender in `connectedCallback()`: +```ts +this.items = [...this.items, newTodo]; +``` + +Mutating the existing reference to `this.items` like in the examples below will not cause a rerender, as Stencil will +not know that the contents of the array has changed: +```ts +// updating `items` either of these ways will not +// cause a rerender +this.items.push(newTodo); +this.items[this.items.length - 1] = newTodo; +``` + +Similar to the examples above, this code sample makes use of the +[connectedCallback() lifecycle method](./component-lifecycle.md#connectedcallback) to create a new `Item` and add +it to `items` every 2000 milliseconds (every two seconds). The example above also makes use of the +[disconnectedCallback() lifecycle method](./component-lifecycle.md#disconnectedcallback) to properly clean up the timer +that was created using `setInterval` in `connectedCallback()`. diff --git a/versioned_docs/version-v4.22/components/styling.md b/versioned_docs/version-v4.22/components/styling.md new file mode 100644 index 000000000..b8e4408be --- /dev/null +++ b/versioned_docs/version-v4.22/components/styling.md @@ -0,0 +1,338 @@ +--- +title: Styling Components +sidebar_label: Styling +description: Styling Components +slug: /styling +--- + +# Styling Components + +## Shadow DOM + +### What is the Shadow DOM? + +The [shadow DOM](https://developers.google.com/web/fundamentals/web-components/shadowdom) is an API built into the browser that allows for DOM encapsulation and style encapsulation. It is a core aspect of the Web Component standards. The shadow DOM shields a component’s styles, markup, and behavior from its surrounding environment. This means that we do not need to be concerned about scoping our CSS to our component, nor worry about a component’s internal DOM being interfered with by anything outside the component. + +When talking about the shadow DOM, we use the term "light DOM" to refer to the “regular” DOM. The light DOM encompasses any part of the DOM that does not use the shadow DOM. + +### Shadow DOM in Stencil + +The shadow DOM hides and separates the DOM of a component in order to prevent clashing styles or unwanted side effects. We can use the shadow DOM in our Stencil components to ensure our components won’t be affected by the applications in which they are used. + +To use the Shadow DOM in a Stencil component, you can set the `shadow` option to `true` in the component decorator. + +```tsx +@Component({ + tag: 'shadow-component', + styleUrl: 'shadow-component.css', + shadow: true, +}) +export class ShadowComponent {} +``` + +If you'd like to learn more about enabling and configuring the shadow DOM, see the [shadow field of the component api](./component.md#component-options). + +By default, components created with the [`stencil generate` command](../config/cli.md#stencil-generate) use the shadow DOM. + +### Styling with the Shadow DOM + +With the shadow DOM enabled, elements within the shadow root are scoped, and styles outside of the component do not apply. As a result, CSS selectors inside the component can be simplified, as they will only apply to elements within the component. We do not have to include any specific selectors to scope styles to the component. + +```css +:host { + color: black; +} + +div { + background: blue; +} +``` + +:::note +The `:host` pseudo-class selector is used to select the [`Host` element](./host-element.md) of the component +::: + +With the shadow DOM enabled, only these styles will be applied to the component. Even if a style in the light DOM uses a selector that matches an element in the component, those styles will not be applied. + +### Shadow DOM QuerySelector + +When using Shadow DOM and you want to query an element inside your web component, you must first use the [`@Element` decorator](./host-element.md#element-decorator) to gain access to the host element, and then you can use the `shadowRoot` property to perform the query. This is because all of your DOM inside your web component is in a shadowRoot that Shadow DOM creates. For example: + +```tsx +import { Component, Element } from '@stencil/core'; + +@Component({ + tag: 'shadow-component', + styleUrl: 'shadow-component.css', + shadow: true +}) +export class ShadowComponent { + + @Element() el: HTMLElement; + + componentDidLoad() { + const elementInShadowDom = this.el.shadowRoot.querySelector('.a-class-selector'); + + ... + } + +} +``` + +### Shadow DOM Browser Support + +The shadow DOM is currently natively supported in the following browsers: + +- Chrome +- Firefox +- Safari +- Edge (v79+) +- Opera + +In browsers which do not support the shadow DOM we fall back to scoped CSS. This gives you the style encapsulation that comes along with the shadow DOM but without loading in a huge shadow DOM polyfill. + +### Scoped CSS + +An alternative to using the shadow DOM is using scoped components. You can use scoped components by setting the `scoped` option to `true` in the component decorator. + +```tsx +@Component({ + tag: 'scoped-component', + styleUrl: 'scoped-component.css', + scoped: true, +}) +export class ScopedComponent {} +``` + +Scoped CSS is a proxy for style encapsulation. It works by appending a data attribute to your styles to make them unique and thereby scope them to your component. It does not, however, prevent styles from the light DOM from seeping into your component. + +## CSS Custom Properties + +CSS custom properties, also often referred to as CSS variables, are used to contain values that can then be used in multiple CSS declarations. For example, we can create a custom property called `--color-primary` and assign it a value of `blue`. + +```css +:host { + --color-primary: blue; +} +``` + +And then we can use that custom property to style different parts of our component + +```css +h1 { + color: var(--color-primary); +} +``` + +### Customizing Components with Custom Properties + +CSS custom properties can allow the consumers of a component to customize a component’s styles from the light DOM. Consider a `shadow-card` component that uses a custom property for the color of the card heading. + +```css +:host { + --heading-color: black; +} + +.heading { + color: var(--heading-color); +} +``` + +:::note +CSS custom properties must be declared on the `Host` element (`:host`) in order for them to be exposed to the consuming application. +::: + +The `shadow-card` heading will have a default color of `black`, but this can now be changed in the light DOM by selecting the `shadow-card` and changing the value of the `--heading-color` custom property. + +```css +shadow-card { + --heading-color: blue; +} +``` + +## CSS Parts + +CSS custom properties can be helpful for customizing components from the light DOM, but they are still a little limiting as they only allow a user to modify specific properties. For situations where users require a higher degree of flexibility, we recommend using the [CSS `::part()` pseudo-element](https://developer.mozilla.org/en-US/docs/Web/CSS/::part). You can define parts on elements of your component with the “part” attribute. + +```tsx +@Component({ + tag: 'shadow-card', + styleUrl: 'shadow-card.css', + shadow: true, +}) +export class ShadowCard { + @Prop() heading: string; + + render() { + return ( + <Host> + <h1 part="heading">{this.heading}</h1> + <slot></slot> + </Host> + ); + } +} +``` + +Then you can use the `::part()` pseudo-class on the host element to give any styles you want to the element with the corresponding part. + +```css +shadow-card::part(heading) { + text-transform: uppercase; +} +``` + +This allows for greater flexibility in styling as any styles can now be added to this element. + +### Exportparts + +If you have a Stencil component nested within another component, any `part` specified on elements of the child component will not be exposed through the parent component. In order to expose the `part`s of the child component, you need to use the `exportparts` attribute. Consider this `OuterComponent` which contains the `InnerComponent`. + +```tsx +@Component({ + tag: 'outer-component', + styleUrl: 'outer-component.css', + shadow: true, +}) +export class OuterComponent { + render() { + return ( + <Host> + <h1>Outer Component</h1> + <inner-component exportparts="inner-text" /> + </Host> + ); + } +} + +@Component({ + tag: 'inner-component', + styleUrl: 'inner-component.css', + shadow: true, +}) +export class InnerComponent { + render() { + return ( + <Host> + <h1 part="inner-text">Inner Component</h1> + </Host> + ); + } +} +``` + +By specifying "inner-text" as the value of the `exportparts` attribute, elements of the `InnerComponent` with a `part` of "inner-text" can now be styled in the light DOM. Even though the `InnerComponent` is not used directly, we can style its parts through the `OuterComponent`. + +```html +<style> + outer-component::part(inner-text) { + color: blue; + } +</style> + +<outer-component /> +``` + +## Style Modes + +Component Style Modes enable you to create versatile designs for your components by utilizing different styling configurations. This is achieved by assigning the styleUrls property of a component to a collection of style mode names, each linked to their respective CSS files. + +### Example: Styling a Button Component + +Consider a basic button component that supports both iOS and Material Design aesthetics: + +```tsx title="Using style modes to style a component" +@Component({ + tag: 'simple-button', + styleUrls: { + md: './simple-button.md.css', // styles for Material Design + ios: './simple-button.ios.css' // styles for iOS + }, +}) +export class SimpleButton { + // ... +} +``` + +In the example above, two different modes are declared. One mode is named `md` (for 'Material Design') and refers back to a Material Design-specific stylesheet. Likewise, the other is named `ios` (for iOS) and references a different stylesheet for iOS-like styling. Both stylesheets are relative paths to the file that declares the component. While we have chosen short names in the above example, there's no limitation to the keys used in the `styleUrls` object. + +To dictate the style mode (Material Design or iOS) in which the button should be rendered, you must initialize the desired mode before any component rendering occurs. This can be done as follows: + +```ts +import { setMode } from '@stencil/core'; +setMode(() => 'ios'); // Setting iOS as the default mode for all components +``` + +The `setMode` function processes all elements, enabling the assignment of modes individually based on specific element attributes. For instance, by assigning the `mode` attribute to a component: + +```html +<simple-button mode="ios"></simple-button> +``` + +You can conditionally set the style mode based on the `mode` property: + +```ts +import { setMode } from '@stencil/core'; + +const defaultMode = 'md'; // Default to Material Design +setMode((el) => el.getAttribute('mode') || defaultMode); +``` + +The reason for deciding which mode to apply can be very arbitrary and based on your requirements, using an element property called `mode` is just one example. + +### Important Considerations + +- __Initialization:__ Style modes must be defined at the start of the component lifecycle and cannot be changed thereafter. If you like to change the components mode dynamically you will have to re-render it entirely. +- __Usage Requirement:__ A style mode must be set to ensure the component loads with styles. Without specifying a style mode, the component will not apply any styles. +- __Input Validation:__ Verify a style mode is supported by a component you are setting it for. Setting an un-supported style mode keeps the component unstyled. +- __Querying Style Mode:__ To check the current style mode and e.g. provide different functionality based on the mode, use the `getMode` function: + +```ts +import { getMode } from '@stencil/core'; + +const simpleButton = document.queryElement('simple-button') +console.log(getMode(simpleButton)); // Outputs the current style mode of component +``` + +This approach ensures your components are adaptable and can dynamically switch between different styles, enhancing the user experience across various platforms and design preferences. + +## Global styles + +While most styles are usually scoped to each component, sometimes it's useful to have styles that are available to all the components in your project. To create styles that are globally available, start by creating a global stylesheet. For example, you can create a folder in your `src` directory called `global` and create a file called `global.css` within that. Most commonly, this file is used to declare CSS custom properties on the root element via the `:root` pseudo-class. This is because styles provided via the `:root` pseudo-class can pass through the shadow boundary. For example, you can define a primary color that all your components can use. + +```css +:root { + --color-primary: blue; +} +``` + +In addition to CSS custom properties, other use cases for a global stylesheet include + +- Theming: defining CSS variables used across the app +- Load fonts with `@font-face` +- App wide font-family +- CSS resets + +To make the global styles available to all the components in your project, the `stencil.config.ts` file comes with an optional [`globalStyle` setting](../config/01-overview.md#globalstyle) that accepts the path to your global stylesheet. + +```tsx +export const config: Config = { + namespace: 'app', + globalStyle: 'src/global/global.css', + outputTarget: [ + { + type: 'www', + }, + ], +}; +``` + +The compiler will run the same minification, autoprefixing, and plugins over `global.css` and generate an output file for the [`www`](../output-targets/www.md) and [`dist`](../output-targets/dist.md) output targets. The generated file will always have the `.css` extension and be named as the specified `namespace`. + +In the example above, since the namespace is `app`, the generated global styles file will be located at: `./www/build/app.css`. + +This file must be manually imported in the `index.html` of your application. + +```html +<link rel="stylesheet" href="/build/app.css" /> +``` diff --git a/versioned_docs/version-v4.22/components/templating-and-jsx.md b/versioned_docs/version-v4.22/components/templating-and-jsx.md new file mode 100644 index 000000000..43847d481 --- /dev/null +++ b/versioned_docs/version-v4.22/components/templating-and-jsx.md @@ -0,0 +1,471 @@ +--- +title: Using JSX +sidebar_label: Using JSX +description: Using JSX +slug: /templating-jsx +--- + +# Using JSX + +Stencil components are rendered using JSX, a popular, declarative template syntax. Each component has a `render` function that returns a tree of components that are rendered to the DOM at runtime. + +## Basics + +The `render` function is used to output a tree of components that will be drawn to the screen. + +```tsx +class MyComponent { + render() { + return ( + <div> + <h1>Hello World</h1> + <p>This is JSX!</p> + </div> + ); + } +} +``` + +In this example we're returning the JSX representation of a `div`, with two child elements: an `h1` and a `p`. + +### Host Element + +If you want to modify the host element itself, such as adding a class or an attribute to the component itself, use the `<Host>` functional component. Check for more details [here](./host-element.md) + + +## Data Binding + +Components often need to render dynamic data. To do this in JSX, use `{ }` around a variable: + +```tsx +render() { + return ( + <div>Hello {this.name}</div> + ) +} +``` + +:::note +If you're familiar with ES6 template variables, JSX variables are very similar, just without the `$`: +::: + +```tsx +//ES6 +`Hello ${this.name}` + +//JSX +Hello {this.name} +``` + + +## Conditionals + +If we want to conditionally render different content, we can use JavaScript if/else statements: +Here, if `name` is not defined, we can just render a different element. + +```tsx +render() { + if (this.name) { + return ( <div>Hello {this.name}</div> ) + } else { + return ( <div>Hello, World</div> ) + } +} +``` + +Additionally, inline conditionals can be created using the JavaScript ternary operator: + +```tsx +render() { + return ( + <div> + {this.name + ? <p>Hello {this.name}</p> + : <p>Hello World</p> + } + </div> + ); +} +``` + +**Please note:** Stencil reuses DOM elements for better performance. Consider the following code: + +```tsx +{someCondition + ? <my-counter initialValue={2} /> + : <my-counter initialValue={5} /> +} +``` + +The above code behaves exactly the same as the following code: + +```tsx +<my-counter initialValue={someCondition ? 2 : 5} /> +``` + +Thus, if `someCondition` changes, the internal state of `<my-counter>` won't be reset and its lifecycle methods such as `componentWillLoad()` won't fire. Instead, the conditional merely triggers an update to the very same component. + +If you want to destroy and recreate a component in a conditional, you can assign the `key` attribute. This tells Stencil that the components are actually different siblings: + +```tsx +{someCondition + ? <my-counter key="a" initialValue={2} /> + : <my-counter key="b" initialValue={5} /> +} +``` + +This way, if `someCondition` changes, you get a new `<my-counter>` component with fresh internal state that also runs the lifecycle methods `componentWillLoad()` and `componentDidLoad()`. + + +## Slots + +Components often need to render dynamic children in specific locations in their component tree, allowing a developer to supply child content when using our component, with our component placing that child component in the proper location. + +To do this, you can use the [Slot](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) tag inside of your `my-component`. + +```tsx +// my-component.tsx + +render() { + return ( + <div> + <h2>A Component</h2> + <div><slot /></div> + </div> + ); +} + +``` + +Then, if a user passes child components when creating our component `my-component`, then `my-component` will place that +component inside of the second `<div>` above: + +```tsx +render(){ + return( + <my-component> + <p>Child Element</p> + </my-component> + ) +} +``` + +Slots can also have `name`s to allow for specifying slot output location: + +```tsx +// my-component.tsx + +render(){ + return [ + <slot name="item-start" />, + <h1>Here is my main content</h1>, + <slot name="item-end" /> + ] +} +``` + +```tsx +render(){ + return( + <my-component> + <p slot="item-start">I'll be placed before the h1</p> + <p slot="item-end">I'll be placed after the h1</p> + </my-component> + ) +} +``` + +### Slots Outside Shadow DOM + +:::caution +Slots are native to the [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), but Stencil polyfills +the behavior to work for non-shadow components as well. However, you may encounter issues using slots outside the Shadow DOM especially with +component trees mixing shadow and non-shadow components, or when passing a slot through many levels of components. In many cases, this behavior can +be remedied by wrapping the `slot` in an additional element (like a `div` or `span`) so the Stencil runtime can correctly "anchor" the relocated +content in its new location. +::: + +There are known use cases that the Stencil runtime is not able to support: + +- Forwarding slotted content to another slot with a different name:<br/> + It is recommended that slot names stay consistent when slotting content through multiple levels of components. **Avoid** defining slot tags like + `<slot name="start" slot="main" />`. + +## Dealing with Children + +The children of a node in JSX correspond at runtime to an array of nodes, +whether they are created by mapping across an array with +[`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) +or simply declared as siblings directly in JSX. This means that at runtime the +children of the two top-level divs below (`.todo-one` and `.todo-two`) will be +represented the same way: + + +```tsx +render() { + return ( + <> + <div class="todo-one"> + {this.todos.map((todo) => ( + <span>{ todo.taskName }</span> + )} + </div> + <div class="todo-two"> + <span>{ todos[0].taskName }</span> + <span>{ todos[1].taskName }</span> + </div> + </> + ) +} +``` + +If this array of children is dynamic, i.e., if any nodes may be added, +removed, or reordered, then it's a good idea to set a unique `key` attribute on +each element like so: + +```tsx +render() { + return ( + <div> + {this.todos.map((todo) => ( + <div key={todo.uid}> + <div>{todo.taskName}</div> + </div> + ))} + </div> + ) +} +``` + +When nodes in a children array are rearranged Stencil makes an effort to +preserve DOM nodes across renders but it isn't able to do so in all cases. +Setting a `key` attribute lets Stencil ensure it can match up new and old +children across renders and thereby avoid recreating DOM nodes unnecessarily. + +:::caution +Do not use an array index or some other non-unique value as a key. Try to +ensure that each child has a key which does not change and which is unique +among all its siblings. +::: + +### Automatic Key Insertion + +During compilation Stencil will automatically add key attributes to any JSX +nodes in your component's render method which are not nested within curly +braces. This allows Stencil’s runtime to accurately reconcile children when +their order changes or when a child is conditionally rendered. + +For instance, consider a render method looking something like this: + +```tsx + render() { + return ( + <div> + { this.disabled && <div id="no-key">no key!</div> } + <div id="slot-wrapper"> + <slot/> + </div> + </div> + ); + } +``` + +While it might seem like adding a key attribute to the `#slot-wrapper` div +could help ensure that elements will be matched up correctly when the component +re-renders, this is actually superfluous because Stencil will automatically add +a key to that element when it compiles your component. + +:::note +The Stencil compiler can only safely perform automatic key insertion in certain +scenarios where there is no danger of the keys accidentally causing elements to +be considered different when they should be treated the same (or vice versa). + +In particular, the compiler will not automatically insert `key` attributes if a +component's `render` method has more than one `return` statement or if it +returns a [conditional +expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator). +Additionally, the compiler will not add key attributes to any JSX which is +found within curly braces (`{ }`). +::: + +## Handling User Input + +Stencil uses native [DOM events](https://developer.mozilla.org/en-US/docs/Web/Events). + +Here's an example of handling a button click. Note the use of the [Arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). + +```tsx +... +export class MyComponent { + private handleClick = () => { + alert('Received the button click!'); + } + + render() { + return ( + <button onClick={this.handleClick}>Click Me!</button> + ); + } +} +``` + +Here's another example of listening to input `change`. Note the use of the [Arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). + +```tsx +... +export class MyComponent { + private inputChanged = (event: Event) => { + console.log('input changed: ', (event.target as HTMLInputElement).value); + } + + render() { + return ( + <input onChange={this.inputChanged}/> + ); + } +} +``` + + +## Complex Template Content + +So far we've seen examples of how to return only a single root element. We can also nest elements inside our root element + +In the case where a component has multiple "top level" elements, the `render` function can return an array. +Note the comma in between the `<div>` elements. + +```tsx +render() { + return ([ + // first top level element + <div class="container"> + <ul> + <li>Item 1</li> + <li>Item 2</li> + <li>Item 3</li> + </ul> + </div>, + + // second top level element, note the , above + <div class="another-container"> + ... more html content ... + </div> + ]); +} +``` + +Alternatively you can use the `Fragment` functional component, in which case you won't need to add commas: + +```tsx +import { Fragment } from '@stencil/core'; +... +render() { + return (<Fragment> + // first top level element + <div class="container"> + <ul> + <li>Item 1</li> + <li>Item 2</li> + <li>Item 3</li> + </ul> + </div> + + <div class="another-container"> + ... more html content ... + </div> + </Fragment>); +} +``` + +It is also possible to use `innerHTML` to inline content straight into an element. This can be helpful when, for example, loading an svg dynamically and then wanting to render that inside of a `div`. This works just like it does in normal HTML: + +```markup +<div innerHTML={svgContent}></div> +``` + +## Getting a reference to a DOM element + +In cases where you need to get a direct reference to an element, like you would normally do with `document.querySelector`, you might want to use a `ref` in JSX. Lets look at an example of using a `ref` in a form: + +```tsx +@Component({ + tag: 'app-home', +}) +export class AppHome { + + textInput!: HTMLInputElement; + + handleSubmit = (event: Event) => { + event.preventDefault(); + console.log(this.textInput.value); + } + + render() { + return ( + <form onSubmit={this.handleSubmit}> + <label> + Name: + <input type="text" ref={(el) => this.textInput = el as HTMLInputElement} /> + </label> + <input type="submit" value="Submit" /> + </form> + ); + } +} +``` + +In this example we are using `ref` to get a reference to our input `ref={(el) => this.textInput = el as HTMLInputElement}`. We can then use that ref to do things such as grab the value from the text input directly `this.textInput.value`. + + +## Avoid Shared JSX Nodes + +The renderer caches element lookups in order to improve performance. However, a side effect from this is that the exact same JSX node should not be shared within the same renderer. + +In the example below, the `sharedNode` variable is reused multiple times within the `render()` function. The renderer is able to optimize its DOM element lookups by caching the reference, however, this causes issues when nodes are reused. Instead, it's recommended to always generate unique nodes like the changed example below. + +```diff +@Component({ + tag: 'my-cmp', +}) +export class MyCmp { + + render() { +- const sharedNode = <div>Text</div>; + return ( + <div> +- {sharedNode} +- {sharedNode} ++ <div>Text</div> ++ <div>Text</div> + </div> + ); + } +} +``` + +Alternatively, creating a factory function to return a common JSX node could be used instead since the returned value would be a unique instance. For example: + +```tsx +@Component({ + tag: 'my-cmp', +}) +export class MyCmp { + + getText() { + return <div>Text</div>; + } + + render() { + return ( + <div> + {this.getText()} + {this.getText()} + </div> + ); + } +} +``` + +## Other Resources + +- [Understanding JSX for StencilJS Applications](https://www.joshmorony.com/understanding-jsx-for-stencil-js-applications/) diff --git a/versioned_docs/version-v4.22/config/01-overview.md b/versioned_docs/version-v4.22/config/01-overview.md new file mode 100644 index 000000000..bc6007c38 --- /dev/null +++ b/versioned_docs/version-v4.22/config/01-overview.md @@ -0,0 +1,551 @@ +--- +title: Config +sidebar_label: Overview +description: Config +slug: /config +--- + +# Stencil Config + +In most cases, the `stencil.config.ts` file does not require any customization since Stencil comes with great default values out-of-the-box. In general, it's preferred to keep the config as minimal as possible. In fact, you could even delete the `stencil.config.ts` file entirely and an app would compile just fine. But at the same time, the compiler can be configured at the lowest levels using this config. Below are the many *optional* config properties. + +Example `stencil.config.ts`: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + namespace: 'MyApp', + srcDir: 'src' +}; +``` + +## buildDist + +*default: true (prod), false (dev)* + +Sets whether or not Stencil will execute output targets and write output to +`dist/` when `stencil build` is called. Defaults to `false` when building for +development and `true` when building for production. If set to `true` then +Stencil will always build all output targets, regardless of whether the build +is in dev or prod mode or using watch mode. + +```tsx +buildDist: true +``` + +## buildEs5 + +Sets if the ES5 build should be generated or not. +It defaults to `false`. +Setting `buildEs5` to `true` will also create ES5 builds for both dev and prod modes. +Setting `buildEs5` to `prod` will only build ES5 in prod mode. + +```tsx +buildEs5: boolean | 'prod' +``` + +## bundles + +By default, Stencil will statically analyze the application and generate a component graph of how all the components are interconnected. From the component graph it is able to best decide how components should be grouped depending on their usage with one another within the app. By doing so it's able to bundle components together in order to reduce network requests. However, bundles can be manually generated using the `bundles` config. + +The `bundles` config is an array of objects that represent how components are grouped together in lazy-loaded bundles. This config is rarely needed as Stencil handles this automatically behind the scenes. + +```tsx +bundles: [ + { components: ['ion-button'] }, + { components: ['ion-card', 'ion-card-header'] } +] +``` + +## cacheDir + +*default: '.stencil'* + +The directory where sub-directories will be created for caching when [`enableCache`](#enablecache) is set `true` or if using +[Stencil's Screenshot Connector](../testing/stencil-testrunner/07-screenshot-connector.md). + +A Stencil config like the following: + +```ts title='stencil.config.ts' +import { Config } from '@stencil/core'; + +export const config: Config = { + ..., + enableCache: true, + cacheDir: '.cache', + testing: { + screenshotConnector: 'connector.js' + } +} +``` + +Will result in the following file structure: + +```tree +stencil-project-root +└── .cache + ├── .build <-- Where build related file caching is written + | + └── screenshot-cache.json <-- Where screenshot caching is written +``` + +## devServer + +Please see the [Dev-Server docs](./dev-server.md). + +## docs + +Please see the [docs config](./docs.md). + +## enableCache + +*default: `true`* + +Stencil will cache build results in order to speed up rebuilds. To disable this feature, set `enableCache` to `false`. + +```tsx +enableCache: true +``` + +## extras + +Please see the [Extras docs](./extras.md). + +## env + +*default: `{}`* + +An object that can hold environment variables for your components to import and use. These variables can hold data objects depending on the environment you compile the components for. For example, let's say we want to provide an URL to our API based on a specific environment, we could provide it as such: + +```ts title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + ..., + env: { + API_BASE_URL: process.env.API_BASE_URL + } +} +``` + +Now when you build your components with this environment variable set, you can import it in your component as follows: + +```ts +import { Component, h, Env, Host } from '@stencil/core'; + +@Component({ + tag: 'api-component', +}) +export class APIComponent { + async connectedCallback () { + const res = await fetch(Env.API_BASE_URL) + // ... + } +} +``` + +## generateExportMaps + +*default: `false`* + +Stencil will generate [export maps](https://nodejs.org/api/packages.html#packages_exports) that correspond with various output target outputs. This includes the root +entry point based on the [primary output target](../output-targets/01-overview.md#primary-package-output-target-validation) (or first eligible output target if not specified), +the entry point for the lazy-loader (if using the `dist` output target), and entry points for each component (if using `dist-custom-elements`). + +## globalScript + +The global script config option takes a file path as a string. + +The global script runs once before your library/app loads, so you can do things like setting up a connection to an external service or configuring a library you are using. + +The code to be executed should be placed within a default function that is exported by the global script. Ensure all of the code in the global script is wrapped in the function that is exported: + +```javascript +export default function() { // or export default async function() + initServerConnection(); +} +``` + +:::note +The exported function can also be `async` but be aware that this can have implications on the performance of your application as all rendering operations will be deferred until after the global script finishes. +::: + +## globalStyle + +Stencil is traditionally used to compile many components into an app, and each component comes with its own compartmentalized styles. However, it's still common to have styles which should be "global" across all components and the website. A global CSS file is often useful to set [CSS Variables](../components/styling.md). + +Additionally, the `globalStyle` config can be used to precompile styles with Sass, PostCSS, etc. + +Below is an example folder structure containing a webapp's global css file, named `app.css`. + +```bash +src/ + components/ + global/ + app.css +``` + +The global style config takes a file path as a string. The output from this build will go to the `buildDir`. In this example it would be saved to `www/build/app.css`. + +```tsx +globalStyle: 'src/global/app.css' +``` + +Check out the [styling docs](../components/styling.md#global-styles) of how to use global styles in your app. + +## hashedFileNameLength + +*default: `8`* + +When the `hashFileNames` config is set to `true`, and it is a production build, the `hashedFileNameLength` config is used to determine how many characters the file name's hash should be. + +```tsx +hashedFileNameLength: 8 +``` + +## hashFileNames + +*default: `true`* + +During production builds, the content of each generated file is hashed to represent the content, and the hashed value is used as the filename. If the content isn't updated between builds, then it receives the same filename. When the content is updated, then the filename is different. By doing this, deployed apps can "forever-cache" the build directory and take full advantage of content delivery networks (CDNs) and heavily caching files for faster apps. + +```tsx +hashFileNames: true +``` + +## hydratedFlag + +When using the [lazy build](https://stenciljs.com/docs/distribution) Stencil +has support for automatically applying a class or attribute to a component and +all of its child components when they have finished hydrating. This can be used +to prevent a [flash of unstyled content +(FOUC)](https://en.wikipedia.org/wiki/Flash_of_unstyled_content), a +typically-undesired 'flicker' of unstyled HTML that might otherwise occur +during component rendering while various components are asynchronously +downloaded and rendered. + +By default, Stencil will add the `hydrated` CSS class to elements to indicate +hydration. The `hydratedFlag` config field allows this behavior to be +customized, by changing the name of the applied CSS class, setting it to use an +attribute to indicate hydration, or changing which type of CSS properties and +values are assigned before and after hydrating. This config can also be used to +turn off this behavior by setting it to `null`. + +If a Stencil configuration does not supply a value for `hydratedFlag` then +Stencil will automatically generate the following default configuration: + +```ts +const defaultHydratedFlag: HydratedFlag = { + hydratedValue: 'inherit', + initialValue: 'hidden', + name: 'hydrated', + property: 'visibility', + selector: 'class', +}; +``` + +If `hydratedFlag` is explicitly set to `null`, Stencil will not set a default +configuration and the behavior of marking hydration with a class or attribute +will be disabled. + +```tsx +hydratedFlag: null | { + name?: string, + selector?: 'class' | 'attribute', + property?: string, + initialValue?: string, + hydratedValue?: string +} +``` + +The supported options are as follows: + +### name + +*default: 'hydrated'* + +The name which Stencil will use for the attribute or class that it sets on +elements to indicate that they are hydrated. + +```tsx +name: string +``` + +### selector + +*default: 'class'* + +The way that Stencil will indicate that a component has been hydrated. When +`'class'`, Stencil will set the `name` option on the element as a class, and +when `'attribute'`, Stencil will similarly set the `name` option as an +attribute. + +```tsx +selector: 'class' | 'attribute' +``` + +### property + +*default: 'visibility'* + +The CSS property used to show and hide components. This defaults to the CSS +`visibility` property. Other possible CSS properties might include `display` +with the `initialValue` setting as `none`, or `opacity` with the `initialValue` +as `0`. Defaults to `visibility`. + +```tsx +property: string +``` + +### initialValue + +*default: 'hidden'* + +This is the value which should be set for the property specified by `property` +on all components before hydration. + +```tsx +initialValue: string +``` + +### hydratedValue + +*default: 'inherit'* + +This is the value which should be set for the property specified by `property` +on all components once they've completed hydration. + +```tsx +hydratedValue: string +``` + +## invisiblePrehydration + +*default: `true`* + +When `true`, `invisiblePrehydration` will visually hide components before they are hydrated by adding an automatically injected style tag to the document's head. Setting `invisiblePrehydration` to `false` will not inject the style tag into the head, allowing you to style your web components pre-hydration. + +:::note +Setting `invisiblePrehydration` to `false` will cause everything to be visible when your page is loaded, causing a more prominent Flash of Unstyled Content (FOUC). However, you can style your web component's fallback content to your preference. +::: + +```tsx +invisiblePrehydration: true +``` + +## minifyCss + +_default: `true` in production_ + +When `true`, the browser CSS file will be minified. + +## minifyJs + +_default: `true` in production_ + +When `true`, the browser JS files will be minified. Stencil uses [Terser](https://terser.org/) under-the-hood for file minification. + +## namespace + +*default: `App`* + +The `namespace` config is a `string` representing a namespace for the app. For apps that are not meant to be a library of reusable components, the default of `App` is just fine. However, if the app is meant to be consumed as a third-party library, such as `Ionic`, a unique namespace is required. + +```tsx +namespace: "Ionic" +``` +## outputTargets + +Please see the [Output Target docs](../output-targets/01-overview.md). + +## plugins + +Please see the [Plugin docs](./plugins.md). + +## preamble + +*default: `undefined`* + +Used to help to persist a banner or add relevant information about the resulting build, the `preamble` configuration +field is a `string` that will be converted into a pinned comment and placed at the top of all emitted JavaScript files, +with the exception of any emitted polyfills. Escaped newlines may be placed in the provided value for this field and +will be honored by Stencil. + +Example: +```tsx +preamble: 'Built with Stencil\nCopyright (c) SomeCompanyInc.' +``` +Will generate the following comment: +```tsx +/*! + * Built with Stencil + * Copyright (c) SomeCompanyInc. + */ +``` + +## sourceMap + +*default: `true`* + +When omitted or set to `true`, sourcemaps will be generated for a project. +When set to `false`, sourcemaps will not be generated. + +```tsx +sourceMap: true | false +``` + +Sourcemaps create a translation between Stencil components that are written in TypeScript/JSX and the resulting +JavaScript that is output by Stencil. Enabling source maps in your project allows for an improved debugging experience +for Stencil components. For example, they allow external tools (such as an Integrated Development Environment) to add +breakpoints directly in the original source code, which allows you to 'step through' your code line-by-line, to inspect +the values held in variables, to observe logic flow, and more. + +Please note: Stencil will always attempt to minify a component's source code as much as possible during compilation. +When `sourceMap` is enabled, it is possible that a slightly different minified result will be produced by Stencil when +compared to the minified result produced when `sourceMap` is not enabled. + +Developers are responsible for determining whether or not they choose to serve sourcemaps in each environment their +components are served and implementing their decision accordingly. + +## srcDir + +*default: `src`* + +The `srcDir` config specifies the directory which should contain the source typescript files for each component. The standard for Stencil apps is to use `src`, which is the default. + +```tsx +srcDir: 'src' +``` + +## taskQueue + +*default: `async`* + +Sets the task queue used by stencil's runtime. The task queue schedules DOM read and writes +across the frames to efficiently render and reduce layout thrashing. By default, the +`async` is used. It's recommended to also try each setting to decide which works +best for your use-case. In all cases, if your app has many CPU intensive tasks causing the +main thread to periodically lock-up, it's always recommended to try +[Web Workers](../guides/workers.md) for those tasks. + +* `congestionAsync`: DOM reads and writes are scheduled in the next frame to prevent layout + thrashing. When the app is heavily tasked and the queue becomes congested it will then + split the work across multiple frames to prevent blocking the main thread. However, it can + also introduce unnecessary reflows in some cases, especially during startup. `congestionAsync` + is ideal for apps running animations while also simultaneously executing intensive tasks + which may lock-up the main thread. + +* `async`: DOM read and writes are scheduled in the next frame to prevent layout thrashing. + During intensive CPU tasks it will not reschedule rendering to happen in the next frame. + `async` is ideal for most apps, and if the app has many intensive tasks causing the main + thread to lock-up, it's recommended to try [Web Workers](../guides/workers.md) + rather than the congestion async queue. + +* `immediate`: Makes writeTask() and readTask() callbacks to be executed synchronously. Tasks + are not scheduled to run in the next frame, but do note there is at least one microtask. + The `immediate` setting is ideal for apps that do not provide long-running and smooth + animations. Like the async setting, if the app has intensive tasks causing the main thread + to lock-up, it's recommended to try [Web Workers](../guides/workers.md). + +```tsx +taskQueue: 'async' +``` + +## testing + +Please see the [testing config docs](../testing/stencil-testrunner/02-config.md). + +## transformAliasedImportPaths + +*default: `true`* + +This sets whether or not Stencil should transform [path aliases]( +https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) set +in a project's `tsconfig.json` from the assigned module aliases to resolved +relative paths. This will not transform external imports (like `@stencil/core`) or +relative imports (like `'../utils'`). + +This option applies globally and will affect all code processed by Stencil, +including `.d.ts` files and spec tests. + +An example of path transformation could look something like the following. + +First, a set of `paths` aliases in `tsconfig.json`: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "paths": { + "@utils": [ + "../path/to/utils" + ] + } + } +} +``` + +Then with the following input: + +```ts title="src/my-module.ts" +import { utilFunc, UtilInterface } from '@utils' + +export function util(arg: UtilInterface) { + utilFunc(arg) +} +``` + +Stencil will produce the following output: + +```js title="dist/my-module.js" +import { utilFunc } from '../path/to/utils'; +export function util(arg) { + utilFunc(arg); +} +``` + +```ts title="dist/my-module.d.ts" +import { UtilInterface } from '../path/to/utils'; +export declare function util(arg: UtilInterface): void; +``` + +## validatePrimaryPackageOutputTarget + +*default: `false`* + +When `true`, validation for common `package.json` fields will occur based on setting an output target's `isPrimaryPackageOutputTarget` flag. +For more information on package validation, please see the [output target docs](../output-targets/01-overview.md#primary-package-output-target-validation). + +## rollupConfig + +Passes custom configuration down to rollup itself. The following options can be overwritten: + +- `inputOptions`: [`context`](https://rollupjs.org/configuration-options/#context), [`external`](https://rollupjs.org/configuration-options/#external), [`moduleContext`](https://rollupjs.org/configuration-options/#modulecontext) [`treeshake`](https://rollupjs.org/configuration-options/#treeshake) +- `outputOptions`: [`globals`](https://rollupjs.org/configuration-options/#output-globals) + +*default: `{}`* + +## watchIgnoredRegex + +*default: `[]`* + +*type: `RegExp | RegExp[]`* + +A regular expression (or array of regular expressions) that can be used to omit files from triggering a rebuild in watch mode. During compile-time, each file in the Stencil +project will be tested against each regular expression to determine if changes to the file (or directory) should trigger a project rebuild. + +:::note +If you want to ignore TS files such as `.ts`/`.js` or `.tsx`/`.jsx` extensions, these files will also need to be specified in your project's tsconfig's +[`watchOptions`](https://www.typescriptlang.org/docs/handbook/configuring-watch.html#configuring-file-watching-using-a-tsconfigjson) _in addition_ to the +`watchIgnoredRegex` option. For instance, if we wanted to ignore the `my-component.tsx` file, we'd specify: + +```json title="tsconfig.json" +{ + ..., + "watchOptions": { + "excludeFiles": ["src/components/my-component/my-component.tsx"] + } +} +``` + +::: diff --git a/versioned_docs/version-v4.22/config/_category_.json b/versioned_docs/version-v4.22/config/_category_.json new file mode 100644 index 000000000..b9de453d1 --- /dev/null +++ b/versioned_docs/version-v4.22/config/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Config", + "position": 5 +} diff --git a/versioned_docs/version-v4.22/config/cli.md b/versioned_docs/version-v4.22/config/cli.md new file mode 100644 index 000000000..5ea10a3a8 --- /dev/null +++ b/versioned_docs/version-v4.22/config/cli.md @@ -0,0 +1,107 @@ +--- +title: Stencil CLI +sidebar_label: CLI +description: Stencil CLI +slug: /cli +--- + +# Command Line Interface (CLI) + +Stencil's command line interface (CLI) is how developers can build their projects, run tests, and more. +Stencil's CLI is included in the compiler, and can be invoked with the `stencil` command in a project where `@stencil/core` is installed. + +## `stencil build` + +Builds a Stencil project. The flags below are the available options for the `build` command. + +| Flag | Description | Alias | +|------|-------------|-------| +| `--ci` | Run a build using recommended settings for a Continuous Integration (CI) environment. Defaults the number of workers to 4, allows for extra time if taking screenshots via the tests and modifies the console logs. | | +| `--config` | Path to the `stencil.config.ts` file. This flag is not needed in most cases since Stencil will find the config. Additionally, a Stencil config is not required. | `-c` | +| `--debug` | Adds additional runtime code to help debug, and sets the log level for more verbose output. | | +| `--dev` | Runs a development build. | | +| `--docs` | Generate all docs based on the component types, properties, methods, events, JSDocs, CSS Custom Properties, etc. | | +| `--es5` | Creates an ES5 compatible build. By default ES5 builds are not created during development in order to improve build times. However, ES5 builds are always created during production builds. Use this flag to create ES5 builds during development. | | +| `--log` | Write logs for the `stencil build` into `stencil-build.log`. The log file is written in the same location as the config. | | +| `--prerender` | Prerender the application using the `www` output target after the build has completed. | | +| `--prod` | Runs a production build which will optimize each file, improve bundling, remove unused code, minify, etc. A production build is the default, this flag is only used to override the `--dev` flag. | | +| `--max-workers` | Max number of workers the compiler should use. Defaults to use the same number of CPUs the Operating System has available. | | +| `--next` | Opt-in to test the "next" Stencil compiler features. | | +| `--no-cache` | Disables using the cache. | | +| `--no-open` | By default the `--serve` command will open a browser window. Using the `--no-open` command will not automatically open a browser window. | | +| `--port` | Port for the [Integrated Dev Server](./dev-server.md). Defaults to `3333`. | `-p` | +| `--serve` | Starts the [Integrated Dev Server](./dev-server.md). | | +| `--stats` | Write stats about the project to `stencil-stats.json`. The stats file is written in the same location as the config. | | +| `--verbose` | Logs additional information about each step of the build. | | +| `--watch` | Watches files during development and triggers a rebuild when files are updated. | | + +## `stencil docs` + +Performs a one-time generation of documentation for your project. +For more information on documentation generation, please see the [Documentation Generation section](../documentation-generation/01-overview.md). + +## `stencil generate` + +Alias: `stencil g` + +Starts the interactive generator for a new Stencil component. +The generator will ask you for a name for your component, and whether any stylesheets or testing files should be generated. + +If you wish to skip the interactive generator, a component tag name may be provided on the command line: +```shell +stencil generate my-new-component +``` + +All components will be generated within the `src/components` folder. +Within `src/components`, a directory will be created with the same name as the component tag name you provided containing the generated files. +For example, if you specify `page-home` as the component tag name, the files will be generated in `src/components/page-home`: +```plain +src +└── components + └── page-home + ├── page-home.css + ├── page-home.e2e.ts + ├── page-home.spec.ts + └── page-home.tsx +``` + +It is also possible to specify one or more sub-folders to generate the component in. +For example, if you specify `pages/page-home` as the component tag name, the files will be generated in `src/components/pages/page-home`: +```shell +stencil generate pages/page-home +``` +The command above will result in the following directory structure: +```plain +src +└── components + └── pages + └── page-home + ├── page-home.css + ├── page-home.e2e.ts + ├── page-home.spec.ts + └── page-home.tsx +``` + +## `stencil help` + +Aliases: `stencil --help`, `stencil -h` + +Prints various tasks that can be run and their associated flags to the terminal. + +## `stencil test` + +Tests a Stencil project. The flags below are the available options for the `test` command. + +| Flag | Description | +|------|-------------| +| `--spec` | Tests `.spec.ts` files using [Jest](https://jestjs.io/). | +| `--e2e` | Tests `.e2e.ts` files using [Puppeteer](https://developers.google.com/web/tools/puppeteer) and [Jest](https://jestjs.io/). | +| `--no-build` | Skips the build process before running end-to-end tests. When using this flag, it is assumed that your Stencil project has been built prior to running `stencil test`. Unit tests do not require this flag. | +| `--devtools` | Opens the dev tools panel in Chrome for end-to-end tests. Setting this flag will disable `--headless` | +| `--headless` | Sets the headless mode to use in Chrome for end-to-end tests. `--headless` and `--headless=true` will enable the "old" headless mode in Chrome, that was used by default prior to Chrome v112. `--headless=new` will enable the new headless mode introduced in Chrome v112. See [this article](https://developer.chrome.com/articles/new-headless/) for more information on Chrome's new headless mode. | + +## `stencil version` + +Aliases: `stencil -v`, `stencil --version` + +Prints the version of Stencil to the terminal. diff --git a/versioned_docs/version-v4.22/config/dev-server.md b/versioned_docs/version-v4.22/config/dev-server.md new file mode 100644 index 000000000..f83c55a05 --- /dev/null +++ b/versioned_docs/version-v4.22/config/dev-server.md @@ -0,0 +1,152 @@ +--- +title: Integrated Dev Server Config +sidebar_label: Dev Server +description: Integrated Dev Server Config +slug: /dev-server +--- + +# Integrated Dev Server + +Stencil comes with an integrated dev server in order to simplify development. By integrating the build process and the dev server, Stencil is able to drastically improve the development experience without requiring complicated build scripts and configuration. As app builds and re-builds take place, the compiler is able to communicate with the dev server, and vice versa. + +## Hot Module Replacement + +The compiler already provides a watch mode, but coupled with the dev server it's able to go one step farther by reloading only what has changed within the browser. Hot Module Replacement allows the app to keep its state within the browser, while switching out individual components with their updated logic after file saves. + +## Style Replacement + +Web components can come with their own css, can use shadow dom, and can have individual style tags. Traditionally, live-reload external css links usually does the trick, however, updating components with inline styles within shadow roots has been a challenge. With the integrated dev server, Stencil is able to dynamically update styles for all components, whether they're using shadow dom or not, without requiring a page refresh. + +## Development Errors + +When errors happen during development, such as printing an error for invalid syntax, Stencil will not only log the error and the source of the error in the console, but also overlay the app with the error so it's easier to read. + +## Open In Editor + +When a development error is shown and overlays the project within the browser, line numbers pointing to the source text are clickable, +which will open the source file directly in your IDE. + +## Dev Server Config + +### `address` + +**Optional** + +**Type: `string`** + +**Default: `0.0.0.0`** + +IP address used by the dev server. The default is `0.0.0.0`, which points to all IPv4 addresses on the local machine, such as `localhost`. + +### `basePath` + +**Optional** + +**Type: `string`** + +**Default: `/`** + +Base path to be used by the server. Defaults to the root pathname. + +### `https` + +**Optional** + +**Type: `{ key: string; cert: string; } | false`** + +**Default: `false`** + +By default the dev server runs over the http protocol. Instead you can run it over https by providing your own SSL certificate and key (see example below). + +#### Example + +```tsx +import { readFileSync } from 'fs'; +import { Config } from '@stencil/core'; + +export const config: Config = { + devServer: { + reloadStrategy: 'pageReload', + port: 4444, + https: { + cert: readFileSync('cert.pem', 'utf8'), + key: readFileSync('key.pem', 'utf8'), + }, + }, +}; +``` + +### `initialLoadUrl` + +**Optional** + +**Type: `string`** + +**Default: `/`** + +The URL the dev server should first open to. + +### `logRequests` + +**Optional** + +**Type: `boolean`** + +**Default: `false`** + +Every request to the server will be logged within the terminal. + +### `openBrowser` + +**Optional** + +**Type: `boolean`** + +**Default: `true`** + +By default, when dev server is started the local dev URL is opened in your default browser. However, to prevent this URL to be opened change this value to `false`. + +### `pingRoute` + +**Optional** + +**Type: `string | null`** + +**Default: `/ping`** + +Route to be registered on the dev server that will respond with a 200 OK response once the Stencil build has completed. The Stencil dev server will "spin up" +before the build process has completed, which can cause issues with processes that rely on the compiled and served output (like E2E testing). This route provides +a way for these processes to know when the build has finished and is ready to be accessed. + +If set to `null`, no route will be registered. + +### `port` + +**Optional** + +**Type: `number`** + +**Default: `3333`** + +Sets the server's port. + +### `reloadStrategy` + +**Optional** + +**Type: `'hmr' | 'pageReload' | null`** + +**Default: `hmr`** + +When files are watched and updated, by default the dev server will use `hmr` (Hot Module Replacement) to update the page without a full page refresh. +To have the page do a full refresh use `pageReload`. To disable any reloading, use `null`. + +### `root` + +**Optional** + +**Type: `string`** + +**Default: `www` output directory if exists, project root otherwise** + +The directory to serve files from. diff --git a/versioned_docs/version-v4.22/config/docs.md b/versioned_docs/version-v4.22/config/docs.md new file mode 100644 index 000000000..2ceb4458c --- /dev/null +++ b/versioned_docs/version-v4.22/config/docs.md @@ -0,0 +1,44 @@ +--- +title: Documentation Generation Config +sidebar_label: Docs Config +description: Documentation Generation Config +slug: /docs-config +--- + +# Docs Config + +The `docs` config option allows global configuration of certain behaviors related to [documentation generation output targets](../documentation-generation/01-overview.md). + +:::note +These configurations are **global** and will be applied to all output target instances including those defined in the [`outputTargets`](../output-targets/01-overview.md) +configuration, as well as those injected by CLI flags (like `--docs`). +::: + +## markdown + +The `markdown` config object allows certain customizations for markdown files generated by the [`docs-readme` output target](../documentation-generation/docs-readme.md) or the +`--docs` CLI flag. + +### targetComponent + +**Optional** + +**Default: `{ textColor: '#333', background: '#f9f' }`** + +This option allows you to change the colors used when generating the dependency graph mermaid diagrams for components. Any hex color string is a valid +value. + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + docs: { + markdown: { + targetComponent: { + textColor: '#fff', + background: '#000', + }, + }, + }, +}; +``` diff --git a/versioned_docs/version-v4.22/config/extras.md b/versioned_docs/version-v4.22/config/extras.md new file mode 100644 index 000000000..bfed8bfac --- /dev/null +++ b/versioned_docs/version-v4.22/config/extras.md @@ -0,0 +1,141 @@ +--- +title: Extras Config +sidebar_label: Extras +description: Extras Config +slug: /config-extras +--- + +# Extras + +The `extras` config contains options to enable new/experimental features in +Stencil, add & remove runtime for DOM features that require manipulations to +polyfills, etc. For example, not all DOM APIs are fully polyfilled when using +the Slot polyfill. Most of these are opt-in, since not all users require the +additional runtime. + +### appendChildSlotFix + +By default, the slot polyfill does not update `appendChild()` so that it appends new child nodes into the correct child slot like how shadow dom works. This is an opt-in polyfill for those who need it. + +### cloneNodeFix + +By default, the runtime does not polyfill `cloneNode()` when cloning a component that uses the slot polyfill. This is an opt-in polyfill for those who need it. + +### enableImportInjection + +In some cases, it can be difficult to lazily load Stencil components in a separate project that uses a bundler such as +[Vite](https://vitejs.dev/). + +Enabling this flag will allow downstream projects that consume a Stencil library and use a bundler such as Vite to lazily load the Stencil library's components. + +In order for this flag to work: + +1. The Stencil library must expose lazy loadable components, such as those created with the + [`dist` output target](../output-targets/dist.md) +2. The Stencil library must be recompiled with this flag set to `true` + +This flag works by creating dynamic import statements for every lazily loadable component in a Stencil project. +Users of this flag should note that they may see an increase in their bundle size. + +Defaults to `false`. + +### experimentalImportInjection + +:::caution +This flag has been deprecated in favor of [`enableImportInjection`](#enableimportinjection), which provides the same +functionality. `experimentalImportInjection` will be removed in a future major version of Stencil. +::: + +In some cases, it can be difficult to lazily load Stencil components in a separate project that uses a bundler such as +[Vite](https://vitejs.dev/). + +This is an experimental flag that, when set to `true`, will allow downstream projects that consume a Stencil library +and use a bundler such as Vite to lazily load the Stencil library's components. + +In order for this flag to work: + +1. The Stencil library must expose lazy loadable components, such as those created with the + [`dist` output target](../output-targets/dist.md) +2. The Stencil library must be recompiled with this flag set to `true` + +This flag works by creating dynamic import statements for every lazily loadable component in a Stencil project. +Users of this flag should note that they may see an increase in their bundle size. + +Defaults to `false`. + +### experimentalScopedSlotChanges + +This option updates runtime behavior for Stencil's support of slots in **scoped** components to match more closely with +the native Shadow DOM behaviors. + +When set to `true`, the following behaviors will be applied: + +- Stencil will hide projected nodes that do not have a destination `slot` ([#2778](https://github.com/ionic-team/stencil/issues/2877)) (since v4.10.0) +- The `textContent` getter will return the text content of all nodes located in a slot (since v4.10.0) +- The `textContent` setter will overwrite all nodes located in a slot (since v4.10.0) + +Defaults to `false`. + +:::note +These behaviors only apply to components using scoped encapsulation! +::: + +### experimentalSlotFixes + +This option enables all current and future slot-related fixes. When enabled it +will enable the following options, overriding their values if they are +specified separately: + +- [`slotChildNodesFix`](#slotchildnodesfix) +- [`scopedSlotTextContentFix`](#scopedslottextcontentfix). +- [`appendChildSlotFix`](#appendchildslotfix) +- [`cloneNodeFix`](#clonenodefix) + +Slot-related fixes to the runtime will be added over the course of Stencil v4, +with the intent of making these the default behavior in Stencil v5. When set to +`true` fixes for the following issues will be applied: + +- Elements rendered outside of slot when shadow not enabled [(#2641)](https://github.com/ionic-team/stencil/issues/2641) (since v4.2.0) +- A slot gets the attribute hidden when it shouldn't [(#4523)](https://github.com/ionic-team/stencil/issues/4523) (since v4.7.0) +- Nested slots mis-ordered when not using Shadow DOM [(#2997)](https://github.com/ionic-team/stencil/issues/2997) (since v4.7.0) +- Inconsistent behavior: slot-fb breaks styling of default slot content in component with 'shadow: false' [(#2937)](https://github.com/ionic-team/stencil/issues/2937) (since v4.7.2) +- Slot content went missing within dynamic component [(#4284)](https://github.com/ionic-team/stencil/issues/4284) (since v4.8.2) +- Slot element loses its parent reference and disappears when its parent is rendered conditionally [(#3913)](https://github.com/ionic-team/stencil/issues/3913) (since v4.8.2) +- Failed to execute 'removeChild' on 'Node' [(#3278)](https://github.com/ionic-team/stencil/issues/3278) (since v4.9.0) +- React fails to manage children in Stencil slot [(#2259)](https://github.com/ionic-team/stencil/issues/2259) (since v4.9.0) +- Slot name is not updated when it is bind to a prop [(#2982)](https://github.com/ionic-team/stencil/issues/2982) (since 4.12.1) +- Conditionally rendered slots not working [(#5335)](https://github.com/ionic-team/stencil/issues/5335) (since 4.13.0) + +:::note +New fixes enabled by this experimental flag are not subject to Stencil's +[semantic versioning policy](../reference/versioning.md). +::: + +### lifecycleDOMEvents + +Dispatches component lifecycle events. By default these events are not dispatched, but by enabling this to `true` these events can be listened for on `window`. Mainly used for testing. + +| Event Name | Description | +| ----------------------------- | ------------------------------------------------------ | +| `stencil_componentWillLoad` | Dispatched for each component's `componentWillLoad`. | +| `stencil_componentWillUpdate` | Dispatched for each component's `componentWillUpdate`. | +| `stencil_componentWillRender` | Dispatched for each component's `componentWillRender`. | +| `stencil_componentDidLoad` | Dispatched for each component's `componentDidLoad`. | +| `stencil_componentDidUpdate` | Dispatched for each component's `componentDidUpdate`. | +| `stencil_componentDidRender` | Dispatched for each component's `componentDidRender`. | + +### scopedSlotTextContentFix + +An experimental flag that when set to `true`, aligns the behavior of invoking the `textContent` getter/setter on a scoped component to act more like a component that uses the shadow DOM. Specifically, invoking `textContent` on a component will adhere to the return values described in [MDN's article on textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#description). Defaults to `false`. + +### scriptDataOpts + +:::caution +This option has been deprecated and will be removed in the next major Stencil release. +::: + +It is possible to assign data to the actual `<script>` element's `data-opts` property, which then gets passed to Stencil's initial bootstrap. This feature is only required for very special cases and rarely needed. When set to `false` it will not read this data. Defaults to `false`. + +### slotChildNodesFix + +For browsers that do not support shadow dom (IE11 and Edge 18 and below), slot is polyfilled to simulate the same behavior. However, the host element's `childNodes` and `children` getters are not patched to only show the child nodes and elements of the default slot. Defaults to `false`. diff --git a/versioned_docs/version-v4.22/config/plugins.md b/versioned_docs/version-v4.22/config/plugins.md new file mode 100644 index 000000000..5ac6e4c8f --- /dev/null +++ b/versioned_docs/version-v4.22/config/plugins.md @@ -0,0 +1,55 @@ +--- +title: Plugin Config +sidebar_label: Plugins +description: Plugin Config +slug: /plugins +--- + +# Plugins + +## Stencil plugins + +By default, Stencil does not come with `Sass` or `PostCSS` support. However, either can be added using the `plugins` array. + +```tsx +import { Config } from '@stencil/core'; +import { sass } from '@stencil/sass'; + +export const config: Config = { + plugins: [ + sass() + ] +}; +``` + +## Rollup plugins + +The `rollupPlugins` config can be used to add your own [Rollup](https://rollupjs.org) plugins. +Under the hood, stencil ships with some built-in plugins including `node-resolve` and `commonjs`, since the execution order of rollup plugins is important, stencil provides an API to inject custom plugin **before node-resolve** and after **commonjs transform**: + + +```tsx +export const config = { + rollupPlugins: { + before: [ + // Plugins injected before rollupNodeResolve() + resolvePlugin() + ], + after: [ + // Plugins injected after commonjs() + nodePolyfills() + ] + } +} +``` + +### Related Plugins + +- [@stencil/sass](https://www.npmjs.com/package/@stencil/sass) +- [@stencil-community/postcss](https://www.npmjs.com/package/@stencil-community/postcss) +- [@stencil-community/less](https://www.npmjs.com/package/@stencil-community/less) +- (Deprecated) [@stencil/stylus](https://www.npmjs.com/package/@stencil/stylus) + + +## Node Polyfills +See the [Node Polyfills in Module bundling](../guides/module-bundling.md#node-polyfills) for other examples. diff --git a/versioned_docs/version-v4.22/core/_category_.json b/versioned_docs/version-v4.22/core/_category_.json new file mode 100644 index 000000000..677e2f463 --- /dev/null +++ b/versioned_docs/version-v4.22/core/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Core Compiler API", + "position": 9 +} diff --git a/versioned_docs/version-v4.22/core/cli-api.md b/versioned_docs/version-v4.22/core/cli-api.md new file mode 100644 index 000000000..c0c3ce4b3 --- /dev/null +++ b/versioned_docs/version-v4.22/core/cli-api.md @@ -0,0 +1,62 @@ +--- +title: Stencil Core CLI API +sidebar_label: CLI API +description: Stencil Core CLI API +slug: /cli-api +--- + +# Stencil Core CLI API + +The CLI API can be found at `@stencil/core/cli` and ran by `bin/stencil`. + + +## createNodeLogger() + +```tsx +createNodeLogger(process: any): Logger +``` + +Creates a "logger", based off of NodeJS APIs, that will be used by the compiler and dev-server. +By default the CLI uses this method to create the NodeJS logger. The NodeJS "process" +object should be provided as the first argument. + + +## createNodeSystem() + +```tsx +createNodeSystem(process: any): CompilerSystem +``` + +Creates the "system", based off of NodeJS APIs, used by the compiler. This includes any and +all file system reads and writes using NodeJS. The compiler itself is unaware of Node's +`fs` module. Other system APIs include any use of `crypto` to hash content. The NodeJS "process" +object should be provided as the first argument. + + +## parseFlags() + +```tsx +parseFlags(args: string[]): ConfigFlags +``` + +Used by the CLI to parse command-line arguments into a typed `ConfigFlags` object. +This is an example of how it's used internally: `parseFlags(process.argv.slice(2))`. + + +## run() + +```tsx +run(init: CliInitOptions): Promise<void> +``` + +Runs the CLI with the given options. This is used by Stencil's default `bin/stencil` file, +but can be used externally too. + + +## runTask() + +```tsx +runTask(process: any, config: Config, task: TaskCommand, sys?: CompilerSystem): Promise<void> +``` + +Runs individual tasks giving a NodeJS `process`, Stencil `config`, and task command. You can optionally pass in the `sys` that's used by the compiler. See [createNodeSystem()](#createnodesystem) for more details. diff --git a/versioned_docs/version-v4.22/core/compiler-api.md b/versioned_docs/version-v4.22/core/compiler-api.md new file mode 100644 index 000000000..2a876a43b --- /dev/null +++ b/versioned_docs/version-v4.22/core/compiler-api.md @@ -0,0 +1,156 @@ +--- +title: Stencil Core Compiler API +sidebar_label: Compiler API +description: Stencil Core Compiler API +slug: /compiler-api +--- + +# Stencil Core Compiler API + +The compiler API can be found at `@stencil/core/compiler/stencil.js`. This module can +work in a NodeJS environment. + + +```tsx +// NodeJS (commonjs) +const stencil = require('@stencil/core/compiler'); +``` + +## transpile() + +```tsx +transpile(code: string, opts?: TranspileOptions): Promise<TranspileResults> +``` + +The `transpile()` function inputs source code as a string, with various options +within the second argument. The function is stateless and returns a `Promise` of the +results, including diagnostics and the transpiled code. The `transpile()` function +does not handle any bundling, minifying, or precompiling any CSS preprocessing like +Sass or Less. + +The `transpileSync()` equivalent is available so the same function +it can be called synchronously. However, TypeScript must be already loaded within +the global for it to work, where as the async `transpile()` function will load +TypeScript automatically. + +Since TypeScript is used, the source code will transpile from TypeScript to JavaScript, +and does not require Babel presets. Additionally, the results includes an `imports` +array of all the import paths found in the source file. The transpile options can be +used to set the `module` format, such as `cjs`, and JavaScript `target` version, such +as `es2017`. + + +## transpileSync() + +```tsx +transpileSync(code: string, opts?: TranspileOptions): TranspileResults +``` + +Synchronous equivalent of the `transpile()` function. When used in a browser +environment, TypeScript must already be available globally, where as the async +`transpile()` function will load TypeScript automatically. + + +## createCompiler() + +```tsx +createCompiler(config: Config): Promise<Compiler> +``` + +The compiler is the utility that brings together many tools to build optimized components, such as a +transpiler, bundler and minifier. When using the CLI, the `stencil build` command uses the compiler for +the various builds, such as a production build, or watch mode during development. If only one file should +be transpiled (converting source code from TypeScript to JavaScript) then the `transpile()` function should be used instead. + +Given a Stencil config, this method asynchronously returns a `Compiler` instance. The config provided +should already be created using the `loadConfig({...})` method. + +Below is an example of a NodeJS environment running a full build. + +```tsx +import { createNodeLogger, createNodeSys } from '@stencil/core/sys/node'; +import { createCompiler, loadConfig } from '@stencil/core/compiler'; + +const logger = createNodeLogger(process); +const sys = createNodeSys(process); +const validated = await loadConfig({ + logger, + sys, + config: { + /* user config */ + }, +}); +const compiler = await createCompiler(validated.config); +const results = await compiler.build(); +``` + + +## createSystem() + +```tsx +createSystem(): CompilerSystem +``` + +The compiler uses a `CompilerSystem` instance to access any file system reads and writes. When used +from the CLI, the CLI will provide its own system based on NodeJS. This method provide a compiler +system is in-memory only and independent of any platform. + + +## dependencies + +```tsx +dependencies: CompilerDependency[] +``` + +The `dependencies` array is only informational and provided to state which versions of dependencies +the compiler was built and works with. For example, the version of TypeScript, Rollup and Terser used +for this version of Stencil are listed here. + + +## loadConfig() + +```tsx +loadConfig(init?: LoadConfigInit): Promise<LoadConfigResults> +``` + +The `loadConfig(init)` method is used to take raw config information and transform it into a +usable config object for the compiler and dev-server. The `init` argument should be given +an already created system and logger which can also be used by the compiler. + + +## optimizeCss() + +```tsx +optimizeCss(cssInput?: OptimizeCssInput): Promise<OptimizeCssOutput> +``` + +Utility function used by the compiler to optimize CSS. + + +## optimizeJs() + +```jsx +optimizeJs(jsInput?: OptimizeJsInput): Promise<OptimizeJsOutput> +``` + +Utility function used by the compiler to optimize JavaScript. Knowing the JavaScript target +will further apply minification optimizations beyond usual minification. + + +## path + +```tsx +path: PlatformPath +``` + +Utility of the `path` API provided by NodeJS, but capable of running in any environment. +This `path` API is only the POSIX version: https://nodejs.org/api/path.html + + +## version + +```tsx +version: string +``` + +Current version of `@stencil/core`. diff --git a/versioned_docs/version-v4.22/core/dev-server-api.md b/versioned_docs/version-v4.22/core/dev-server-api.md new file mode 100644 index 000000000..d2dc907db --- /dev/null +++ b/versioned_docs/version-v4.22/core/dev-server-api.md @@ -0,0 +1,16 @@ +--- +title: Stencil Core Dev Server API +sidebar_label: Dev Server API +description: Stencil Core Dev Server API +slug: /dev-server-api +--- + +# Stencil Core Dev Server API + +The CLI API can be found at `@stencil/core/dev-server`. + +## start() + +```tsx +start(stencilDevServerConfig: StencilDevServerConfig, logger: Logger, watcher?: CompilerWatcher): Promise<DevServer> +``` diff --git a/versioned_docs/version-v4.22/documentation-generation/01-overview.md b/versioned_docs/version-v4.22/documentation-generation/01-overview.md new file mode 100644 index 000000000..2ea2375f6 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/01-overview.md @@ -0,0 +1,43 @@ +--- +title: Stencil Documentation Generation +sidebar_label: Overview +description: Stencil Documentation Generation +slug: /doc-generation +--- + +# Documentation Generation + +- [`docs-readme`: Documentation readme files formatted in markdown](./docs-readme.md) +- [`docs-json`: Documentation data formatted in JSON](./docs-json.md) +- [`docs-custom`: Custom documentation generation](./docs-custom.md) +- [`docs-vscode`: Documentation generation for VS Code](./docs-vscode.md) +- [`stats`: Stats about the compiled files](./docs-stats.md) + +## Docs Auto-Generation + +As apps scale with more and more components, and team size and members continue to adjust over time, it's vital all components are well documented, and that the documentation itself is maintained. Maintaining documentation is right up there with some of the least interesting things developers must do, but that doesn't mean it can't be made easier. + +Throughout the build process, the compiler is able to extract documentation from each component, to include JSDocs comments, types of each member on the component (thanks TypeScript!) and CSS Variables (CSS Custom Properties). + + +### Component Property Docs Example: + +To add a description to a `@Prop`, simply add a comment on the previous line: + +```tsx +/** (optional) The icon to display */ +@Prop() iconType = ""; +``` + +### CSS Example: + +Stencil will also document CSS variables when you specify them via jsdoc-style comments inside your css or scss files: + +```css + :root { + /** + * @prop --primary: Primary header color. + */ + --primary: blue; + } +``` diff --git a/versioned_docs/version-v4.22/documentation-generation/_category_.json b/versioned_docs/version-v4.22/documentation-generation/_category_.json new file mode 100644 index 000000000..669450fc2 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Documentation Generation", + "position": 7 +} diff --git a/versioned_docs/version-v4.22/documentation-generation/docs-custom.md b/versioned_docs/version-v4.22/documentation-generation/docs-custom.md new file mode 100644 index 000000000..61da15c79 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/docs-custom.md @@ -0,0 +1,76 @@ +--- +title: Custom Docs Generation +sidebar_label: Custom Docs (docs-custom) +description: Custom Docs +slug: /docs-custom +--- + +# Custom Docs Generation + +Stencil exposes an output target titled `docs-custom` where users can access the generated docs json data. This feature can be used to generate custom markdown or to execute other logic on the json data during the build. As with other docs output targets, `strict` mode is supported. + +To make use of this output target, simply add the following to your Stencil configuration. + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-custom', + generator: (docs: JsonDocs) => { + // Custom logic goes here + } + } + ] +}; +``` + +## Config + +| Property | Description | Default | +|-------------|------------------------------------------------------------------------------------------|---------| +| `generator` | A function with the docs json data as argument. | | +| `strict` | If set to true, Stencil will output a warning whenever there is missing documentation. | `false` | + + + +# Custom Docs Data Model + +The generated docs JSON data will in the type of `JsonDocs` which consists of main `components` array which consists of components that stencil core found and meta information such as `timestamp` and `compiler` + +## JsonDocs + +| Property | Description | +|-------------|------------------------------------------------------------------------------------------| +| `components` | Array with type of `JsonDocsComponent[]` which consists component information| +| `timestamp` | `string` with timestamp | +| `compiler` | `Object` with `typescriptVersion`, `compiler`, and `version` | + +## JsonDocsComponent + +| Property | Description | +|-------------|------------------------------------------------------------------------------------------| +| `dirPath` | Component directory path | +| `fileName` | File name | +| `filePath` | File path | +| `readmePath` | Readme file path | +| `usagesDir` | Stencil looks in a directory named `usages/` in the same directory as your component to find usage examples. This holds the full path to that directory. | +| `encapsulation` | Component `encapsulation` type. Possible values are `shadow`, `scoped`, `none` | +| `tag` | Component tag described in `.tsx` file | +| `readme` | Component readme file first line content | +| `docs` | Description written in top of `@Component` e.g. /** Documentation Example */. If no JSDoc is present, default to any manually written text in the component's markdown file. Empty otherwise. | +| `docsTags` | Annotations (In the way of JSDoc ) written in `.tsx` file will be collected here | +| `overview` | Description written in top of `@Component` e.g. /** Documentation Example */ | +| `usage` | Array of [usage examples](./docs-json.md#usage), written in Markdown files in the `usages/` directory adjacent to the current component. | +| `props` | Array of metadata objects for each usage of the [`@Prop` decorator](../components/properties.md#the-prop-decorator-prop) on the current component. | +| `methods` | Array of metadata objects for each usage of the [`@Method` decorator](../components/methods.md) on the current component. | +| `events` | Array of metadata objects for each usage of the [`@Event` decorator](../components/events.md#event-decorator) on the current component. | +| `listeners` | Array of metadata objects for each usage of the [`@Listen` decorator](../components/events.md#listen-decorator) on the current component. | +| `styles` | Array of objects documenting annotated [CSS variables](./docs-json.md#css-variables) used in the current component's CSS. | +| `slots` | Array of objects documenting [slots](./docs-json.md#slots) which are tagged with `@slot` in the current component's JSDoc comment. | +| `parts` | Array of objects derived from `@part` tags in the current component's JSDoc comment. | +| `dependents` | Array of components where current component is used | +| `dependencies` | Array of components which is used in current component | +| `dependencyGraph` | Describes a tree of components coupling | + diff --git a/versioned_docs/version-v4.22/documentation-generation/docs-json.md b/versioned_docs/version-v4.22/documentation-generation/docs-json.md new file mode 100644 index 000000000..418c21399 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/docs-json.md @@ -0,0 +1,278 @@ +--- +title: Docs JSON Data Output Target +sidebar_label: JSON Docs (docs-json) +description: JSON Docs +slug: /docs-json +--- + +# Generating Documentation in JSON format + +Stencil supports automatically [generating `README` files](./docs-readme.md) in +your project which pull in [JSDoc comments](https://jsdoc.app/) and provide a +straightforward way to document your components. + +If you need more flexibility, Stencil can also write documentation to a JSON +file which you could use for a custom downstream documentation website. + +You can try this out is using the `--docs-json` CLI flag like so: + +```bash +stencil build --docs-json path/to/docs.json +``` + +You can also add the `docs-json` output target to your project's configuration +file in order to auto-generate this file every time you build: + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-json', + file: 'path/to/docs.json' + } + ] +}; +``` + +The JSON file output by Stencil conforms to the [`JsonDocs` interface in +Stencil's public TypeScript +declarations](https://github.com/ionic-team/stencil/blob/main/src/declarations/stencil-public-docs.ts). + +## `supplementalPublicTypes` + +As of Stencil v4 the JSON documentation generation functionality in Stencil +supports a new configuration option, `supplementalPublicTypes`. + +This functionality makes it easy to automatically document types and interfaces +which otherwise wouldn't be included in the documentation that Stencil +generates. By default, Stencil includes extensive information about the types +used in the public APIs of all your components, meaning the properties on your +components decorated with `@Watch`, `@Event`, `@Prop` and so on. This makes it +easy to document your components' APIs; however, if your project uses other +types which aren't found in the public API of a component then those types +won't be included. + +The new `supplementalPublicTypes` option fills in this gap by allowing you to +designate a file of types which should be included in the output of the +`docs-json` output target. + +This information will be found in a top-level property called `typeLibrary` on +the JSON output and will conform to the [`JsonDocsTypeLibrary` interface in +Stencil's public TypeScript +declarations](https://github.com/ionic-team/stencil/blob/main/src/declarations/stencil-public-docs.ts). + +Using this option could look something like this: + +```ts title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-json', + file: 'path/to/docs.json', + supplementalPublicTypes: 'src/public-interfaces.ts', + } + ] +}; +``` + +## CSS Variables + +Stencil can document CSS variables if you annotate them with JSDoc-style +comments in your CSS/SCSS files. If, for instance, you had a component with a +CSS file like the following: + +```css title="src/components/my-button/my-button.css" +:host { + /** + * @prop --background: Background of the button + * @prop --background-activated: Background of the button when activated + * @prop --background-focused: Background of the button when focused + */ + --background: pink; + --background-activated: aqua; + --background-focused: fuchsia; +} +``` + +Then you'd get the following in the JSON output: + +```json title="Example docs-json Output" +[ + { + "name": "--background", + "annotation": "prop", + "docs": "Background of the button" + }, + { + "name": "--background-activated", + "annotation": "prop", + "docs": "Background of the button when activated" + }, + { + "name": "--background-focused", + "annotation": "prop", + "docs": "Background of the button when focused" + } +] +``` + +If the style sheet is configured to be used with [a specific mode](../components/styling.md), the mode associated with +the CSS property will be provided as well: + +```diff title="Example docs-json Output with Mode" +[ + { + "name": "--background", + "annotation": "prop", + "docs": "Background of the button" ++ "mode": "ios", + }, + { + "name": "--background-activated", + "annotation": "prop", + "docs": "Background of the button when activated" ++ "mode": "ios", + }, + { + "name": "--background-focused", + "annotation": "prop", + "docs": "Background of the button when focused" ++ "mode": "ios", + } +] +``` + +:::note +This functionality works with both standard CSS and with Sass, although for the +latter you'll need to have the +[@stencil/sass](https://github.com/ionic-team/stencil-sass) plugin installed +and configured. +::: + +## Slots + +If one of your Stencil components makes use of +[slots](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) for +rendering children you can document them by using the `@slot` JSDoc tag in the +component's comment. + +For instance, if you had a `my-button` component with a slot you might document +it like so: + +```tsx title="src/components/my-button/my-button.tsx" +import { Component, h } from '@stencil/core'; + +/** + * @slot buttonContent - Slot for the content of the button + */ +@Component({ + tag: 'my-button', + styleUrl: 'my-button.css', + shadow: true, +}) +export class MyButton { + render() { + return <button><slot name="buttonContent"></slot></button> + } +} +``` + +This would show up in the generated JSON file like so: + +```json +"slots": { + "name": "buttonContent", + "docs": "Slot for the content of the button" +} +``` + +:::caution +Stencil does not check that the slots you document in a component's JSDoc +comment using the `@slot` tag are actually present in the JSX returned by the +component's `render` function. + +It is up to you as the component author to ensure the `@slot` tags on a +component are kept up to date. +::: + + +## Usage + +You can save usage examples for a component in the `usage/` subdirectory within +that component's directory. The content of these files will be added to the +`usage` property of the generated JSON. This allows you to keep examples right +next to the code, making it easy to include them in a documentation site or +other downstream consumer(s) of your docs. + +:::caution +Stencil doesn't check that your usage examples are up-to-date! If you make any +changes to your component's API you'll need to remember to update your usage +examples manually. +::: + +If, for instance, you had a usage example like this: + +````md title="src/components/my-button/usage/my-button-usage.md" +# How to use `my-button` + +A button is often a great help in adding interactivity to an app! + +You could use it like this: + +```html +<my-button>My Button!</my-button> +``` +```` + + +You'd get the following in the JSON output under the `"usage"` key: + +```json +"usage": { + "a-usage-example": "# How to use `my-button`\n\nA button is often a great help in adding interactivity to an app!\n\nYou could use it like this:\n\n```html\n<my-button>My Button!</my-button>\n```\n" +} +``` + + +## Custom JSDocs Tags + +In addition to reading the [standard JSDoc tags](https://jsdoc.app/), users can +use their own custom tags which will be included in the JSON data without any +configuration. + +This can be useful if your team has your own documentation conventions which you'd like to stick with. + +If, for example, we had a component with custom JSDoc tags like this: + +```tsx +import { Component, h } from '@stencil/core'; + +/** + * @customDescription This is just the best button around! + */ +@Component({ + tag: 'my-button', + styleUrl: 'my-button.css', + shadow: true, +}) +export class MyButton { + render() { + return <button><slot name="buttonContent"></slot></button> + } +} +``` + +It would end up in the JSON data like this: + +```json +"docsTags": [ + { + "name": "customDescription", + "text": "This is just the best button around!" + } +], +``` diff --git a/versioned_docs/version-v4.22/documentation-generation/docs-readme.md b/versioned_docs/version-v4.22/documentation-generation/docs-readme.md new file mode 100644 index 000000000..f7da50da9 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/docs-readme.md @@ -0,0 +1,565 @@ +--- +title: Docs Readme Auto-Generation +sidebar_label: README Docs (docs-readme) +description: README Docs +slug: /docs-readme +--- + +# Docs Readme Markdown File Auto-Generation + +Stencil is able to auto-generate `readme.md` files for your components. +This can help you to maintain consistently formatted documentation for your components which lives right next to them and renders in GitHub. + +## Setup + +To generate markdown files, it is recommended to add the `docs-readme` output target to your Stencil configuration file: + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-readme' + } + ] +}; +``` + +## Generating README Files + +### Using the Build Command + +If your project has a `docs-readme` output target configured in your Stencil configuration file, the Stencil [build command](../config/cli.md#stencil-build) is all that's needed to generate README docs: +```bash +npx stencil build +``` +If you're running the build command with the `--watch` flag, your project's README files will automatically update without requiring multiple explicit build commands: +```bash +npm stencil build --watch +``` + +:::info +When running the build command with the `--dev` flag, README files will not be generated. +This is to prevent unnecessary I/O operations during the development cycle. +::: + +If you choose not to include a `docs-readme` output target in your Stencil configuration file, use the `--docs` CLI flag as a part of the build command: +```bash +npx stencil build --docs +``` + +This will cause the Stencil compiler to perform a one-time build of your entire project, including README files. + +### Using the Docs Command + +As an alternative to the build command, the [docs command](../config/cli.md#stencil-docs) can be used to perform a one time generation of the documentation: +```bash +npx stencil docs +``` +Running `stencil docs` will generate documentation for [all documentation output targets](./01-overview.md), not just `docs-readme`. + +## README Sections + +Most generated markdown content will automatically be generated without requiring any additional configuration. +Content is generated based on its Stencil component, rather than requiring you to configure multiple flags. +Each section below describes the different types of content Stencil recognizes and will automatically generate. + +### Custom Markdown Content + +Once you've generated a `readme.md` file, you can add your own markdown content to the file. +You may add any content above the following comment in a component's `readme.md`: +``` +Custom content goes here! +<!-- Auto Generated Below --> +``` + +Any custom content placed above this comment will be persisted on subsequent builds of the README file. + +### Internal Components + +A Stencil component may be marked as internal to a library using the unofficial JSDoc `@internal` tag. +By placing `@internal` in a component's class-level JSDoc it will skip the generation of the README for the component. + +In the code block below, `@internal` is added to the JSDoc for `MyComponent`: + +```tsx title="A component with @internal in its JSDoc" +/** + * @internal + */ +@Component({ + tag: 'my-component', + shadow: true, +}) +export class MyComponent { /* omitted */ } +``` + +The usage of `@internal` causes no README to be generated for `MyComponent`. + +If a README already exists for the component, it will not be updated. + +### Deprecation Notices + +A Stencil component may be marked as deprecated using the [JSDoc `@deprecated` tag](https://jsdoc.app/tags-deprecated). +By placing `@deprecated` in a component's class-level JSDoc it will cause the generated README to denote the component as deprecated. + +For a component with the JSDoc: + +```tsx title="A component with @deprecated in its JSDoc" +/** + * @deprecated since v2.0.0 + */ +@Component({ + tag: 'my-component', + shadow: true, +}) +export class MyComponent { /* omitted */ } +``` + +In the code block above, `@deprecated` is added to the JSDoc for `MyComponent`. +This causes the generated README to contain: +``` +> **[DEPRECATED]** since v2.0.0 +``` + +The deprecation notice will always begin with `> **[DEPRECATED]**`, followed by the description provided in the JSDoc. +In this case, that description is "since v2.0.0". + +The deprecation notice will be placed after the [custom content](#custom-markdown-content) in the README. + +If a component is not marked as deprecated, this section will be omitted from the generated README. + +### Component Overview + +A Stencil component that has a JSDoc comment on its class component like so: + +```tsx title="A component with an overview in its JSDoc" +/** + * A simple component for formatting names + * + * This component will do some neat things! + */ +@Component({ + tag: 'my-component', + shadow: true, +}) +export class MyComponent { } +``` +will generate the following section in your component's README: + +``` +## Overview + +A simple component for formatting names + +This component will do some neat things! +``` + +The overview will be placed after the [deprecation notice](#deprecation-notices) section of the README. + +If a component's JSDoc does not contain an overview, this section will be omitted from the generated README. + +### Usage Examples + +Usage examples are user-generated markdown files that demonstrate how another developer might use a component. +These files are separate from a component's README file, and are placed in a `usage/` directory adjacent to a component's implementation. + +The content of these files will be added to a `Usage` section of the generated README. +This allows you to keep examples right next to the code, making it easy to include them in a documentation site or other downstream consumer(s) of your docs. + +The example usage file below describes how to use a component defined in `src/components/my-component/my-component.tsx`: + +````md title="src/components/my-component/usage/my-component-usage.md" +# How to Use `my-component` + +This component is used to provide a way to greet a user using their first, middle, and last name. +This component will properly format the provided name, even when all fields aren't provided: + +```html +<my-component first="Stencil"></my-component> +<my-component first="Stencil" last="JS"></my-component> +``` +```` + +When the README for `my-component` is regenerated, following will be added to the README: + +````md +## Usage + +### My-component-usage + +# How to Use `my-component` + +This component is used to provide a way to greet a user using their first, middle, and last name. +This component will properly format the provided name, even when all fields aren't provided: + +```html +<my-component first="Stencil"></my-component> +<my-component first="Stencil" last="JS"></my-component> +``` +```` + +:::caution +Stencil does not check that your usage examples are up-to-date. +If you make any changes to your component's API, you'll need to update your usage examples manually. +::: + +The usage section will be placed after the [overview section](#component-overview) of the README. + +If a component's directory does not contain any usage files, this section will be omitted from the generated README. + +### @Prop() Details + +Usages of Stencil's [`@Prop()` decorator](../components/properties.md) are described in a table containing the following information for each usage of `@Prop()`: +- **Property**: The name of the property on the TypeScript class. +- **Attribute**: The name of the attribute associated with the property name. +- **Description**: A description of the property, if one was given in a JSDoc comment for the property. +- **Type**: The TypeScript type of the property. +- **Default**: The default value of the property. + +For the following usages of `@Prop()` in a component: +```ts +export class MyComponent { + /** + * The first name + */ + @Prop() first!: string; // the '!' denotes a required property + /** + * @deprecated since v2.1.0 + */ + @Prop() middle: string; + @Prop() lastName = "Smith"; + + // ... +} +``` + +The following section will be generated: +```md +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------------- | ----------- | ---------------------------------------------------------------------- | -------- | ----------- | +| `first` _(required)_ | `first` | The first name | `string` | `undefined` | +| `lastName` | `last-name` | | `string` | `"Smith"` | +| `middle` | `middle` | <span style="color:red">**[DEPRECATED]**</span> since v2.1.0<br/><br/> | `string` | `undefined` | +``` + +The properties section will be placed after the [usage examples section](#usage-examples) of the README. + +If a component does not use the `@Prop()` decorator, this section will be omitted from the generated README. + +### @Event() Details + +Usages of Stencil's [`@Event()` decorator](../components/events.md) are described in a table containing the following information for each usage of `@Event()`: +- **Event**: The name of the property on the TypeScript class decorated with `@Event()`. +- **Description**: A description of the property, if one was given in a JSDoc comment for the property. +- **Type**: The TypeScript type of the property. + +For the following usages of `@Event()` in a component: +```tsx +export class MyComponent { + /** + * Emitted when an event is completed + */ + @Event() todoCompleted: EventEmitter<number>; + /** + * @deprecated + */ + @Event() todoUndo: EventEmitter<number>; + + // ... +} +``` + +The following section will be generated: + +```md +## Events + +| Event | Description | Type | +| --------------- | ---------------------------------------------------------- | --------------------- | +| `todoCompleted` | Emitted when an event is completed | `CustomEvent<number>` | +| `todoUndo` | <span style="color:red">**[DEPRECATED]**</span> <br/><br/> | `CustomEvent<number>` | +``` + +The events section will be placed after the [@Prop() section](#prop-details) of the README. + +If a component does not use the `@Event()` decorator, this section will be omitted from the generated README. + +### @Method() Details + +Components that use Stencil's [`@Method()` decorator](../components/methods.md) will have a section describing each usage `@Method`. + +Each usage of `@Method` will be documented with its own subsection containing the following: +- The method signature will be used as the heading for each subsection +- A description of the method will immediately follow, if one was provided in a JSDoc +- A 'Parameters' section that contains a table the describes the name, TypeScript type, and description of each parameter of the method +- A 'Returns' section that contains the return type of the method, along with a description of the returned value. + +For the following usages of `@Method()` in a component: + +```ts +export class MyComponent { + /** + * Scroll by a specified X/Y distance in the component. + * + * @param x The amount to scroll by on the horizontal axis. + * @param y The amount to scroll by on the vertical axis. + * @param duration The amount of time to take scrolling by that amount. + * @returns the total distance travelled + */ + @Method() + async scrollByPoint(x: number, y: number, duration: number): Promise<number> { /* omitted */ } + + // ... +} +``` + +The following section will be generated: + +```md +## Methods + +### `scrollByPoint(x: number, y: number, duration: number) => Promise<number>` + +Scroll by a specified X/Y distance in the component. + +#### Parameters + +| Name | Type | Description | +| ---------- | -------- | ---------------------------------------------------- | +| `x` | `number` | The amount to scroll by on the horizontal axis. | +| `y` | `number` | The amount to scroll by on the vertical axis. | +| `duration` | `number` | The amount of time to take scrolling by that amount. | + +#### Returns + +Type: `Promise<number>` + +the total distance travelled +``` + +The methods section will be placed after the [@Event section](#event-details) of the README. + +If a component does not use the `@Method()` decorator, this section will be omitted from the generated README. + +### @slot Details + +A component that uses [slots](../components/templating-and-jsx.md#slots) may describe its slots in the component's JSDoc using the Stencil-specific `@slot` JSDoc tag. +The `@slot` tag follows the following format: +``` +@slot [slot-name] - [description] +``` +where `slot-name` corresponds to the name of the slot in the markup, and `description` describes the usage of the slot. + +For this JSDoc tag to be read properly, the following is required: +1. Either `slot-name` or `description` must be included. Both may be included though. +2. The '-' separating the two is required. + +For the default slot, omit the `slot-name`. + +This information is presented in a table containing the following columns: +- **Slot**: The name of the slot. The default slot will have no name/be empty. +- **Description**: A description of the slot, if one was given. + +For the following usages of `@slot()` in a component: +```tsx +/** + * @slot - Content is placed between the named slots if provided without a slot. + * @slot primary - Content is placed to the left of the main slotted-in text. + * @slot secondary - Content is placed to the right of the main slotted-in text. + */ +@Component({ + tag: 'my-component', + shadow: true, +}) +export class MyComponent { + // ... + + render() { + return ( + <section> + <slot name="primary"></slot> + <div class="content"> + <slot></slot> + </div> + <slot name="secondary"></slot> + </section> + ); + } +} +``` + +The following table is generated: + +```md +## Slots + +| Slot | Description | +| ------------- | --------------------------------------------------------------------- | +| | Content is placed between the named slots if provided without a slot. | +| `"primary"` | Content is placed to the left of the main slotted-in text. | +| `"secondary"` | Content is placed to the right of the main slotted-in text. | + +``` + +The slots section will be placed after the [@Method section](#method-details) of the README. + +If a component's top-level JSDoc does not use `@slot` tags, this section will be omitted from the generated README. + +### Shadow Parts + +A component that uses [CSS shadow parts](../components/styling.md#css-parts) may describe the component's shadow parts in the component's JSDoc using the Stencil-specific `@part` JSDoc tag. +The `@part` tag follows the following format: +``` +@part [part-name] - [description] +``` +where `part-name` corresponds to the name of the shadow part in the markup, and `description` describes its usage. + +For this tag to be read properly, the following is required: +1. Either `part-name` or `description` must be included, although using both is strongly encouraged. +2. The '-' separating the two is required. + +This information is presented in a table containing the following columns: +- **Part**: The name of the shadow part. +- **Description**: A description of the shadow part, if one was given. + +For the following usages of `@part()` in a component: + +```tsx +/** + * @part label - The label text describing the component. + */ +@Component({ + tag: 'my-component', + styleUrl: 'my-component.css', + shadow: true, +}) +export class MyComponent { + // ... + + render() { + return ( + <div part="label"> + <slot></slot> + </div> + ); + } +} +``` + +The following table will be generated: +```md +## Shadow Parts + +| Part | Description | +| ----------------------------------------------------------- | ---------------------------------------- | +| `"label"` | The label text describing the component. | +``` + +The shadow parts section will be placed after the [@Slot Details](#slot-details) of the README. + +If a component's top-level JSDoc does not use `@part` tags, this section will be omitted from the generated README. + +### Styling Details + +Styling in CSS files can be documented in Stencil components as well. +One use case for documenting styles using Stencil is to note a CSS variable that a component's styling depends on. +Using the `@prop` JSDoc in a component's CSS file, Stencil can generate this documentation as well. + +This information is presented in a table containing the following columns: +- **Name**: The name of the custom property. +- **Description**: A description of the custom property, if one was given. + +For the following usages of `@prop` in a component's css file: + +```css +:host { + /** + * @prop --border-radius: Border radius of the avatar and inner image + */ + border-radius: var(--border-radius); +} +``` +The following table will be generated: + +```md +## CSS Custom Properties + +| Name | Description | +| ----------------- | ------------------------------------------- | +| `--border-radius` | Border radius of the avatar and inner image | +``` + +The styling details section will be placed after the [Shadow Parts Details](#shadow-parts) of the README. + +If a component's styles does not include styling details, this section will be omitted from the generated README. + +### Custom Footers + +Removing or customizing the footer can be done by adding a `footer` property to +the output target. This string is added to the generated Markdown files without +modification, so you can use Markdown syntax in it for rich formatting: + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-readme', + footer: '*Built with love!*', + } + ] +}; +``` + +The following footer will be placed at the bottom of your component's README file: +``` +*Built with love!* +``` + +## Configuration +### Specifying the Output Directory + +By default, a README file will be generated in the same directory as the +component it corresponds to. This behavior can be changed by setting the `dir` +property on the output target configuration. Specifying a directory will create +the structure `{dir}/{component}/readme.md`. + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-readme', + dir: 'output' + } + ] +}; +``` + +### Strict Mode + +Adding `strict: true` to the output target configuration will cause Stencil to output a warning whenever the project is built with missing documentation. + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-readme', + strict: true + } + ] +}; +``` + +When strict mode is enabled, the following items are checked: +1. `@Prop()` usages must be documented, unless the property is marked as `@deprecated` +2. `@Method()` usages must be documented, unless the method is marked as `@deprecated` +3. `@Event()` usages must be documented, unless the event is marked as `@deprecated` +4. CSS Part usages must be documented diff --git a/versioned_docs/version-v4.22/documentation-generation/docs-stats.md b/versioned_docs/version-v4.22/documentation-generation/docs-stats.md new file mode 100644 index 000000000..62fbf97a1 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/docs-stats.md @@ -0,0 +1,194 @@ +--- +title: Docs Stats Auto-Generation +sidebar_label: Stats (stats) +description: Stats Generation +slug: /stats +--- + +# Stats + +Often it's very helpful to understand the state of your libraries generated files in the form of json data. To build the docs as json, use the `--stats` flag, followed by a path on where to write the json file. + +```tsx + scripts: { + "docs.data": "stencil build --stats" + "docs.data-with-optional-file": "stencil build --stats path/to/stats.json" + } +``` + +Another option would be to add the `stats` output target to the `stencil.config.ts` in order to auto-generate this file with every build: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'stats', + file: 'path/to/stats.json' // optional + } + ] +}; +``` + +If you don't pass a file name to the `--stats` flag or the output target's `file` key, the file will be output to the root directory of your project as `stencil-stats.json` + +Check out the typescript declarations for the JSON output: https://github.com/ionic-team/stencil/blob/main/src/declarations/stencil-public-docs.ts + + +## Data Model + +The file that's generated will produce data similar to this: + +```json +{ + "timestamp": "2021-09-22T17:31:14", + "compiler": { + "name": "node", + "version": "16.9.1" + }, + "app": { + "namespace": "ExampleStencilLibrary", + "fsNamespace": "example-stencil-library", + "components": 1, + "entries": 1, + "bundles": 30, + "outputs": [ + { + "name": "dist-collection", + "files": 3, + "generatedFiles": [ + "./dist/collection/components/my-component/my-component.js", + // etc... + ] + }, + { + "name": "dist-lazy", + "files": 26, + "generatedFiles": [ + "./dist/cjs/example-stencil-library.cjs.js", + // etc... + ] + }, + { + "name": "dist-types", + "files": 1, + "generatedFiles": [ + "./dist/types/stencil-public-runtime.d.ts" + ] + } + ] + }, + "options": { + "minifyJs": true, + "minifyCss": true, + "hashFileNames": true, + "hashedFileNameLength": 8, + "buildEs5": true + }, + "formats": { + "esmBrowser": [ + { + "key": "my-component.entry", + "components": [ + "my-component" + ], + "bundleId": "p-12cc1edd", + "fileName": "p-12cc1edd.entry.js", + "imports": [ + "p-24af5948.js" + ], + "originalByteSize": 562 + } + ], + "esm": [ + // exact same model as the esmBrowser, but for esm files + ], + "es5": [ + // exact same model as the esmBrowser, but for es5 files + ], + "system": [ + // exact same model as the esmBrowser, but for system files + ], + "commonjs": [ + // exact same model as the esmBrowser, but for cjs files + ] + }, + "components": [ + { + "tag": "my-component", + "path": "./src/components/my-component/my-component.js", + "source": "./src/components/my-component/my-component.tsx", + "elementRef": null, + "componentClassName": "MyComponent", + "assetsDirs": [], + "dependencies": [], + "dependents": [], + "directDependencies": [], + "directDependents": [], + "docs": { + "tags": [], + "text": "" + }, + "encapsulation": "shadow", + "excludeFromCollection": false, + "events": [], + "internal": false, + "legacyConnect": [], + "legacyContext": [], + "listeners": [], + "methods": [], + "potentialCmpRefs": [], + "properties": [ + { + "name": "first", + "type": "string", + "attribute": "first", + "reflect": false, + "mutable": false, + "required": false, + "optional": false, + "complexType": { + "original": "string", + "resolved": "string", + "references": {} + }, + "docs": { + "tags": [], + "text": "The first name" + }, + "internal": false + }, + ], + }, + ], + "entries": [ + { + "cmps": [ + // Expanded component details are produced here + ], + "entryKey": "my-component.entry" + } + ], + "componentGraph": { + "sc-my-component": [ + "p-24af5948.js" + ] + }, + "sourceGraph": { + "./src/components/my-component/my-component.tsx": [ + "./src/utils/utils" + ], + "./src/index.ts": [], + "./src/utils/utils.ts": [] + }, + "collections": [] +} +``` + +## Usage + +### Preload tags + +One example of usage with this file is to automatically create preload tags automatically. [Here's a link to a gist](https://gist.github.com/splitinfinities/8dcd1b4acf315632cd1e1dd9891fe8f1) containing some code samples about how to set up preloading to improve performance + diff --git a/versioned_docs/version-v4.22/documentation-generation/docs-vscode.md b/versioned_docs/version-v4.22/documentation-generation/docs-vscode.md new file mode 100644 index 000000000..40db9a483 --- /dev/null +++ b/versioned_docs/version-v4.22/documentation-generation/docs-vscode.md @@ -0,0 +1,65 @@ +--- +title: VS Code Documentation +sidebar_label: VS Code (docs-vscode) +description: VS Code Documentation +slug: /docs-vscode +--- + +# VS Code Documentation + +One of the core features of web components is the ability to create [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), +which allow developers to reuse custom functionality defined in their components. +When Stencil compiles a project, it generates a custom element for each component in the project. +Each of these [custom elements has an associated `tag` name](../components/component.md#component-options) that allows the custom +element to be used in HTML files. + +By default, integrated development environments (IDEs) like VS Code are not aware of a project's custom elements when +authoring HTML. +In order to enable more intelligent features in VS Code, such as auto-completion, hover tooltips, etc., developers +need to inform it of their project's custom elements. + +The `docs-vscode` output target tells Stencil to generate a JSON file containing this information. + +This is an opt-in feature and will save a JSON file containing [custom data](https://github.com/microsoft/vscode-custom-data) +in a directory specified by the output target. +Once the feature is enabled and VS Code is informed of the JSON file's location, HTML files can gain code editing +features similar to TSX files. + +## Enabling + +To generate custom element information for VS Code, add the `docs-vscode` output target to your `stencil.config.ts`: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'docs-vscode', + file: 'vscode-data.json', + } + ] +}; +``` + +where `file` is the name & location of the file to be generated. +By default, Stencil assumes that the file will be generated in the project's root directory. + +To generate the JSON file, have Stencil build your project. + +## Configuring VS Code + +Once the `docs-vscode` output target has been enabled and the JSON file generated, VS Code needs to be informed of it. + +Recent versions of VS Code have a settings option named `html.customData`, which resolves to a list of JSON files to +use when augmenting the default list of HTML elements. +Add the path to the generated JSON file for your project's types to be added: + +```json +{ + "html.customData": [ + "./vscode-data.json" + ] +} +``` + diff --git a/versioned_docs/version-v4.22/framework-integration/01-overview.md b/versioned_docs/version-v4.22/framework-integration/01-overview.md new file mode 100644 index 000000000..5acb19728 --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/01-overview.md @@ -0,0 +1,25 @@ +--- +title: Framework Integration +sidebar_label: Overview +description: Framework Integration +slug: /overview +--- + +# Framework Integration + +Stencil's primary goal is to remove the need for components to be written using a specific framework's API. +It accomplishes this by using standardized web platform APIs that work across all modern browsers. +Using the low-level component model that is provided by the browser (which all frameworks are built on) allows Stencil components to work inside a framework or without one. + +The experience of integrating web components directly into existing applications can be tricky at times, as frameworks have varying support for vanilla web components. +In order to accommodate the various issues the Stencil team has created Framework Wrappers to make the process simpler. + +The Framework Wrappers are configured like output targets, and emit a native library, just like if your components were originally written using any of these frameworks: + +- [Angular](./angular.md) +- [React](./react.md) +- [Vue](./vue.md) +- [Ember (Community)](./ember.md) + +By using Stencil bindings, you can build your components once, and have Stencil emit Angular/React/Vue libraries. +This way, the consumers of your components can enjoy all the features of their framework of choice. diff --git a/versioned_docs/version-v4.22/framework-integration/_category_.json b/versioned_docs/version-v4.22/framework-integration/_category_.json new file mode 100644 index 000000000..9d1b1cade --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Framework Integrations", + "position": 3 +} diff --git a/versioned_docs/version-v4.22/framework-integration/angular.md b/versioned_docs/version-v4.22/framework-integration/angular.md new file mode 100644 index 000000000..689ab7a12 --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/angular.md @@ -0,0 +1,757 @@ +--- +title: Angular Integration with Stencil +sidebar_label: Angular +description: Learn how to wrap your components so that people can use them natively in Angular +slug: /angular +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Angular Integration + +**Supports: Angular 12+ • TypeScript 4.0+ • Stencil v2.9.0+** + +Stencil can generate Angular component wrappers for your web components. This allows your Stencil components to be used within +an Angular application. The benefits of using Stencil's component wrappers over the standard web components include: + +- Angular component wrappers will be detached from change detection, preventing unnecessary repaints of your web component. +- Web component events will be converted to RxJS observables to align with Angular's `@Output()` and will not emit across component boundaries. +- Optionally, form control web components can be used as control value accessors with Angular's reactive forms or `[ngModel]`. +- It is not necessary to include the [Angular `CUSTOM_ELEMENTS_SCHEMA`](https://angular.io/api/core/CUSTOM_ELEMENTS_SCHEMA) in all modules consuming your Stencil components. + +## Setup + +### Project Structure + +We recommend using a [monorepo](https://www.toptal.com/front-end/guide-to-monorepos) structure for your component library with component +wrappers. Your project workspace should contain your Stencil component library and the library for the generated Angular component wrappers. + +An example project set-up may look similar to: + +``` +top-most-directory/ +└── packages + ├── stencil-library/ + │ ├── stencil.config.js + │ └── src/components + └── angular-workspace/ + └── projects/ + └── component-library/ + └── src/ + ├── lib/ + └── public-api.ts +``` + +This guide uses Lerna for the monorepo, but you can use other solutions such as Nx, Turborepo, etc. + +To use Lerna with this walk through, globally install Lerna: + +```bash npm2yarn +npm install --global lerna +``` + +#### Creating a Monorepo + +:::note +If you already have a monorepo, skip this section. +::: + +```bash npm2yarn +# From your top-most-directory/, initialize a workspace +lerna init + +# install dependencies +npm install + +# install typescript and node types +npm install typescript @types/node --save-dev +``` + +#### Creating a Stencil Component Library + +:::note +If you already have a Stencil component library, skip this section. +::: + +In the `packages/` directory, run the following commands to generate a Stencil component library: + +```bash npm2yarn +npm init stencil components stencil-library +cd stencil-library +# Install dependencies +npm install +``` + +#### Creating an Angular Component Library + +:::note +If you already have an Angular component library, skip this section. +::: + +The first time you want to create the component wrappers, you will need to have an Angular library package to write to. + +In the `packages/` directory, use the Angular CLI to generate a workspace and a library for your Angular component wrappers: + +```bash +npx -p @angular/cli ng new angular-workspace --no-create-application +cd angular-workspace +npx -p @angular/cli ng generate library component-library +``` + +You can delete the `component-library.component.ts`, `component-library.service.ts`, and `*.spec.ts` files. + +You will also need to add your generated Stencil library as a peer-dependency so import references can be resolved correctly: + +```diff +// packages/angular-workspace/projects/component-library/package.json + +"peerDependencies": { + "@angular/common": "^15.1.0", +- "@angular/core": "^15.1.0" ++ "@angular/core": "^15.1.0", ++ "stencil-library": "*" +} +``` + +For more information, see the Lerna documentation on [package dependency management](https://lerna.js.org/docs/getting-started#package-dependency-management). + +:::note +The Angular CLI will install Jasmine as a dependency to your Angular workspace. However, Stencil uses Jest as it's unit testing solution. To avoid +type definition collisions when attempting to build your Stencil project, you can remove `jasmine-core` and `@types/jasmine` as dependencies in the Angular +workspace `package.json` file: + +```bash npm2yarn +# from `/packages/angular-workspace` +npm uninstall jasmine-core @types/jasmine +``` + +::: + +### Adding the Angular Output Target + +Install the `@stencil/angular-output-target` dependency to your Stencil component library package. + +```bash npm2yarn +# Install dependency +npm install @stencil/angular-output-target --save-dev +``` + +In your project's `stencil.config.ts`, add the `angularOutputTarget` configuration to the `outputTargets` array: + +```ts +import { angularOutputTarget } from '@stencil/angular-output-target'; + +export const config: Config = { + namespace: 'stencil-library', + outputTargets: [ + // By default, the generated proxy components will + // leverage the output from the `dist` target, so we + // need to explicitly define that output alongside the + // Angular target + { + type: 'dist', + }, + angularOutputTarget({ + componentCorePackage: 'stencil-library', + outputType: 'component', + directivesProxyFile: '../angular-workspace/projects/component-library/src/lib/stencil-generated/components.ts', + directivesArrayFile: '../angular-workspace/projects/component-library/src/lib/stencil-generated/index.ts', + }), + ], +}; +``` + +:::tip +The `componentCorePackage` should match the `name` field in your Stencil project's `package.json`. + +`outputType` should be set to `'component'` for Stencil projects using the `dist` output. Otherwise if using the custom elements output, `outputType` should be set to `'scam'` or `'standalone'`. + +The `directivesProxyFile` is the relative path to the file that will be generated with all of the Angular component wrappers. You will replace the +file path to match your project's structure and respective names. You can generate any file name instead of `components.ts`. + +The `directivesArrayFile` is the relative path to the file that will be generated with a constant of all the Angular component wrappers. This +constant can be used to easily declare and export all the wrappers. + +::: + +See the [API section below](#api) for details on each of the output target's options. + +You can now build your Stencil component library to generate the component wrappers. + +```bash npm2yarn +# Build the library and wrappers +npm run build +``` + +If the build is successful, you will now have contents in the file specified in `directivesProxyFile` and `directivesArrayFile`. + +You can now finally import and export the generated component wrappers for your component library. For example, in your library's main Angular module: + +```ts title="component-library.module.ts" +import { DIRECTIVES } from './stencil-generated'; + +@NgModule({ + declarations: [...DIRECTIVES], + exports: [...DIRECTIVES], +}) +export class ComponentLibraryModule {} +``` + +Any components that are included in the `exports` array should additionally be exported in your main entry point (either `public-api.ts` or +`index.ts`). Skipping this step will lead to Angular Ivy errors when building for production. For this guide, simply add the following line to the +automatically generated `public-api.ts` file: + +```ts title="public-api.ts" +export * from './lib/component-library.module'; +export { DIRECTIVES } from './lib/stencil-generated'; +export * from './lib/stencil-generated/components'; +``` + +### Registering Custom Elements + +The default behavior for this output target does not handle automatically defining/registering the custom elements. One strategy (and the approach +the [Ionic Framework](https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/src/app-initialize.ts#L21-L34) takes) is to use the loader to define all custom elements during app initialization: + +```ts title="component-library.module.ts" +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { defineCustomElements } from 'stencil-library/loader'; + +@NgModule({ + ..., + providers: [ + { + provide: APP_INITIALIZER, + useFactory: () => defineCustomElements, + multi: true + }, + ] +}) +export class ComponentLibraryModule {} +``` + +See the [documentation](../output-targets/dist.md) for more information on defining custom elements using the +`dist` output target, or [update the Angular output target](#do-i-have-to-use-the-dist-output-target) to use `dist-custom-elements`. + +### Link Your Packages (Optional) + +:::note +If you are using a monorepo tool (Lerna, Nx, etc.), skip this section. +::: + +Before you can successfully build a local version of your Angular component library, you will need to link the Stencil package to the Angular package. + +From your Stencil project's directory, run the following command: + +```bash npm2yarn +# Link the working directory +npm link +``` + +From your Angular component library's directory, run the following command: + +```bash npm2yarn +# Link the package name +npm link name-of-your-stencil-package +``` + +The name of your Stencil package should match the `name` property from the Stencil component library's `package.json`. + +Your component libraries are now linked together. You can make changes in the Stencil component library and run `npm run build` to propagate the +changes to the Angular component library. + +:::note +As an alternative to `npm link` , you can also run `npm install` with a relative path to your Stencil component library. This strategy, +however, will modify your `package.json` so it is important to make sure you do not commit those changes. +::: + +## Consumer Usage + +:::note + +If you already have an Angular app, skip this section. + +::: + +### Angular with Modules + +#### Creating a Consumer Angular App + +From your Angular workspace (`/packages/angular-workspace`), run the following command to generate an Angular application with modules: + +```bash +npx -p @angular/cli ng generate app my-app --standalone=false +``` + +#### Consuming the Angular Wrapper Components + +This section covers how developers consuming your Angular component wrappers will use your package and component wrappers in an Angular project using modules. + +In order to use the generated component wrappers in the Angular app, you'll first need to build your Angular component library. From the root +of your Angular workspace (`/packages/angular-workspace`), run the following command: + +```bash +npx -p @angular/cli ng build component-library +``` + +:::note +In the output of the `ng build` command you may see a warning that looks like this: + +``` +▲ [WARNING] The glob pattern import("./**/.entry.js") did not match any files [empty-glob] + +node_modules/@stencil/core/internal/client/index.js:3808:2: + 3808 │ `./${bundleId}.entry.js${BUILD.hotModuleReplacement && hmrVers... + ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +This is a known issue in esbuild (used under the hood by `ng build`) and should +not cause an issue, but at present there's unfortunately no way to suppress +this warning. +::: + +<Tabs + groupId="outputType" + defaultValue="component" + values={[ + { label: 'outputType: "component"', value: 'component' }, + { label: 'outputType: "scam"', value: 'scam' }, + { label: 'outputType: "standalone"', value: 'standalone' }, + ] +}> +<TabItem value="component"> + +Import your component library into your Angular app's module. If you distributed your components through a primary `NgModule`, you can simply import that module into an implementation to use your components. + +```ts title="app.module.ts" +import { ComponentLibraryModule } from 'component-library'; + +@NgModule({ + imports: [ComponentLibraryModule], +}) +export class AppModule {} +``` + +Otherwise you will need to add the components to your module's `declarations` and `exports` arrays. + +```ts title="app.module.ts" +import { MyComponent } from 'component-library'; + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], +}) +export class AppModule {} +``` + +You can now directly leverage your components in their template and take advantage of Angular template binding syntax. + +```html title="app.component.html" +<my-component first="Your" last="Name"></my-component> +``` + +</TabItem> + +<TabItem value="scam"> + +Now you can reference your component library as a standard import. Each component will be exported as a separate module. + +```ts title="app.module.ts" +import { MyComponentModule } from 'component-library'; + +@NgModule({ + imports: [MyComponentModule], +}) +export class AppModule {} +``` + +You can now directly leverage your components in their template and take advantage of Angular template binding syntax. + +```html title="app.component.html" +<my-component first="Your" last="Name"></my-component> +``` + +</TabItem> + +<TabItem value="standalone"> + +Now you can import and reference your components in your consuming application in the same way you would with any other Angular components: + +```ts title="app.component.ts" +import { Component } from '@angular/core'; +import { MyComponent } from 'component-library'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], + standalone: true, + imports: [MyComponent], +}) +export class AppComponent {} +``` + +You can now leverage your components in the template and take advantage of Angular template binding syntax. + +```html title="app.component.html" +<my-component first="Your" last="Name"></my-component> +``` + +</TabItem> + +</Tabs> + +From your Angular workspace (`/packages/angular-workspace`), run `npm start` and navigate to `localhost:4200`. You should see the +component rendered correctly. + +### Angular with Standalone Components + +In Angular CLI v17, the default behavior is to generate a new project with standalone components. + +From your Angular workspace (`/packages/angular-workspace`), run the following command to generate an Angular application: + +```bash +npx -p @angular/cli ng generate app my-app +``` + +#### Consuming the Angular Wrapper Components + +This section covers how developers consuming your Angular component wrappers will use your package and component wrappers. + +In order to use the generated component wrappers in the Angular app, you'll first need to build your Angular component library. From the root +of your Angular workspace (`/packages/angular-workspace`), run the following command: + +```bash +npx -p @angular/cli ng build component-library +``` + +:::note +In the output of the `ng build` command you may see a warning that looks like this: + +``` +▲ [WARNING] The glob pattern import("./**/.entry.js") did not match any files [empty-glob] + +node_modules/@stencil/core/internal/client/index.js:3808:2: + 3808 │ `./${bundleId}.entry.js${BUILD.hotModuleReplacement && hmrVers... + ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``` + +This is a known issue in esbuild (used under the hood by `ng build`) and should +not cause an issue, but at present there's unfortunately no way to suppress +this warning. +::: + +<Tabs + groupId="outputType" + defaultValue="component" + values={[ + { label: 'outputType: "component"', value: 'component' }, + { label: 'outputType: "scam"', value: 'scam' }, + { label: 'outputType: "standalone"', value: 'standalone' }, + ] +}> +<TabItem value="component"> + +Import your component library into your component. You must distribute your components through a primary `NgModule` to use your components in a standalone component. + +```ts title="app.component.ts" +import { Component } from '@angular/core'; +import { ComponentLibraryModule } from 'component-library'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ComponentLibraryModule], + templateUrl: './app.component.html', +}) +export class AppComponent {} +``` + +You can now directly leverage your components in their template and take advantage of Angular template binding syntax. + +```html title="app.component.html" +<my-component first="Your" last="Name"></my-component> +``` + +</TabItem> + +<TabItem value="scam"> + +Now you can reference your component library as a standard import. Each component will be exported as a separate module. + +```ts title="app.module.ts" +import { Component } from '@angular/core'; +import { MyComponentModule } from 'component-library'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [MyComponentModule], + templateUrl: './app.component.html', +}) +export class AppComponent {} +``` + +You can now directly leverage your components in their template and take advantage of Angular template binding syntax. + +```html title="app.component.html" +<my-component first="Your" last="Name"></my-component> +``` + +</TabItem> + +<TabItem value="standalone"> + +Now you can import and reference your components in your consuming application in the same way you would with any other standalone Angular components: + +```ts title="app.component.ts" +import { Component } from '@angular/core'; +import { MyComponent } from 'component-library'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [MyComponent], + templateUrl: './app.component.html', +}) +export class AppComponent {} +``` + +You can now leverage your components in the template and take advantage of Angular template binding syntax. + +```html title="app.component.html" +<my-component first="Your" last="Name"></my-component> +``` + +</TabItem> + +</Tabs> + +From your Angular workspace (`/packages/angular-workspace`), run `npm start` and navigate to `localhost:4200`. You should see the component rendered correctly. + +## API + +### componentCorePackage + +**Required** + +**Type: `string`** + +The name of the Stencil package where components are available for consumers (i.e. the value of the `name` property in your Stencil component library's `package.json`). +This is used during compilation to write the correct imports for components. + +For a starter Stencil project generated by running: + +```bash npm2yarn +npm init stencil component my-component-lib +``` + +The `componentCorePackage` would be set to: + +```ts title="stencil.config.ts" +export const config: Config = { + ..., + outputTargets: [ + angularOutputTarget({ + componentCorePackage: 'my-component-lib', + // ... additional config options + }) + ] +} +``` + +Which would result in an import path like: + +```js +import { MyComponent } from 'my-component-lib/components/my-component.js'; +``` + +### customElementsDir + +**Optional** + +**Default: `'components'`** + +**Type: `string`** + +This option can be used to specify the directory where the generated +custom elements live. + +### excludeComponents + +**Optional** + +**Default: `[]`** + +**Type: `string[]`** + +This lets you specify component tag names for which you don't want to generate Angular wrapper components. This is useful if you need to write framework-specific versions of components. For instance, in Ionic Framework, this is used for routing components - like tabs - so that +Ionic Framework can integrate better with Angular's Router. + +### directivesArrayFile + +**Optional** + +**Default: `null`** + +**Type: `string`** + +Used to provide a list of type Proxies to the Angular Component Library. +See [Ionic Framework](https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/src/directives/proxies-list.ts) for a sample. + +### directivesProxyFile + +**Required** + +**Type: `string`** + +This parameter allows you to name the file that contains all the component wrapper definitions produced during the compilation process. This is the +first file you should import in your Angular project. + +### outputType + +**Required** + +**Default: `'component'`** + +**Type: `'component' | 'scam' | 'standalone`** + +Specifies the type of output to be generated. It can take one of the following values: + +1. `component`: Generates all the component wrappers to be declared on an Angular module. This option is required for Stencil projects using the `dist` hydrated output. + +2. `scam`: Generates a separate Angular module for each component. + +3. `standalone`: Generates standalone component wrappers. + +Both `scam` and `standalone` options are compatible with the `dist-custom-elements` output. + +:::note +The configuration for the [Custom Elements](../output-targets/custom-elements.md) output target must set the +[export behavior](../output-targets/custom-elements.md#customelementsexportbehavior) to `single-export-module` for the wrappers to generate correctly +if using the `scam` or `standalone` output type. +::: + +Note: Please choose the appropriate `outputType` based on your project's requirements and the desired output structure. + +### valueAccessorConfigs + +**Optional** + +**Default: `[]`** + +**Type: `ValueAccessorConfig[]`** + +This lets you define which components should be integrated with `ngModel` (i.e. form components). It lets you set what the target prop is (i.e. `value`), +which event will cause the target prop to change, and more. + +```tsx title="stencil.config.ts" +const angularValueAccessorBindings: ValueAccessorConfig[] = [ + { + elementSelectors: ['my-input[type=text]'], + event: 'myChange', + targetAttr: 'value', + type: 'text', + }, +]; + +export const config: Config = { + namespace: 'stencil-library', + outputTargets: [ + angularOutputTarget({ + componentCorePackage: 'component-library', + directivesProxyFile: '{path to your proxy file}', + valueAccessorConfigs: angularValueAccessorBindings, + }), + { + type: 'dist', + esmLoaderPath: '../loader', + }, + ], +}; +``` + +## FAQs + +### Do I have to use the `dist` output target? + +No! By default, this output target will look to use the `dist` output, but the output from `dist-custom-elements` can be used alternatively. + +To do so, change the type `outputType` argument to either `scam` or `standalone`. For more information on both these options, see the [API section](#outputtype). + +```ts title="stencil.config.ts" +export const config: Config = { + ..., + outputTargets: [ + // Needs to be included + { + type: 'dist-custom-elements' + }, + angularOutputTarget({ + componentCorePackage: 'component-library', + directivesProxyFile: '{path to your proxy file}', + // This is what tells the target to use the custom elements output + outputType: 'standalone' // or 'scam' + }) + ] +} +``` + +Now, all generated imports will point to the default directory for the custom elements output. If you specified a different directory +using the `dir` property for `dist-custom-elements`, you need to also specify that directory for the Angular output target. See +[the API section](#customelementsdir) for more information. + +In addition, all the Web Component will be automatically defined as the generated component modules are bootstrapped. So, you do not need to implement +the Stencil loader for lazy-loading the custom elements (i.e. you can remove the `APP_INITIALIZER` logic introduced [in this section](#adding-the-angular-output-target)). +As such, the generated Angular components can now be directly imported and declared on any Angular module implementing them: + +```ts title="app.module.ts" +import { MyComponent } from 'component-library'; + +@NgModule({ + declarations: [MyComponent], +}) +export class AppModule {} +``` + +### What is the best format to write event names? + +Event names shouldn’t include special characters when initially written in Stencil, try to lean on using camelCased event names for interoperability +between frameworks. + +### How do I bind input events directly to a value accessor? + +You can configure how your input events can map directly to a value accessor, allowing two-way data-binding to be a built in feature of any of your +components. Take a look at [valueAccessorConfig's option above](#valueaccessorconfigs). + +### How do I access components with ViewChild or ViewChildren? + +Once included, components could be referenced in your code using `ViewChild` and `ViewChildren` as in the following example: + +```tsx +import { Component, ElementRef, ViewChild } from '@angular/core'; + +import { TestComponent } from 'test-components'; + +@Component({ + selector: 'app-home', + template: `<test-components #test></test-components>`, + styleUrls: ['./home.component.scss'], +}) +export class HomeComponent { + @ViewChild(TestComponent) myTestComponent: ElementRef<TestComponent>; + + async onAction() { + await this.myTestComponent.nativeElement.testComponentMethod(); + } +} +``` + +### Why aren't my custom interfaces exported from within the index.d.ts file? + +Usually when beginning this process, you may bump into a situation where you find that some of the interfaces you've used in your Stencil component +library aren't working in your Angular component library. You can resolve this issue by adding an `interfaces.d.ts` file located within the root +of your Stencil component library's project folder, then manually exporting types from that file e.g. `export * from './components';` + +When adding this file, it's also recommended to update your package.json's types property to be the distributed file, something like: +`"types": "dist/types/interfaces.d.ts"` diff --git a/versioned_docs/version-v4.22/framework-integration/ember.md b/versioned_docs/version-v4.22/framework-integration/ember.md new file mode 100644 index 000000000..a8cbdc304 --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/ember.md @@ -0,0 +1,69 @@ +--- +title: Ember Integration with Stencil +sidebar_label: Ember +description: Ember Integration with Stencil +slug: /ember +--- + +# Ember + +## For Monorepos (recommended) + +It's recommended to use the [getting started](https://stenciljs.com/docs/getting-started) docs for creating a Stencil project using the native Stencil tooling. +This way, in your Ember project, you don't need to configure anything extra, and you can use Stencil components natively. + +For example, if using the [Ionic Framework](https://ionicframework.com/) in your Ember project: + +1. Add the Ionic Framework to your app: +```bash npm2yarn +npm install @ionic/core +``` + +2. Install the components from the library: +```js title="app/app.js" +import '@ionic/core'; // installs all the components from Ionic Framework +``` + +3. Use the components anywhere: +```js title="app/components/example.gjs" +<template> + <ion-toggle></ion-toggle> +</template> +``` + +4. You can hook up events / state (controlled component pattern): +```js title="app/components/example-with-state.gjs" +import { on } from '@ember/modifier'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +export default class Demo extends Component { + <template> + <ion-toggle checked="{{this.isOn}}" {{on "ionChange" this.toggle}}></ion-toggle> + </template> + + @tracked isOn = true; + + toggle = () => this.isOn = !this.isOn; +} +``` + +[Live Demo](https://limber.glimdown.com/edit?c=MQAgqgzglgdg5iAygFwKYwMZQDYliAUQFsAjVAJwChKB9AQRhpAEMYIB3CkZAe24AsoEEAG0IyZhgDWPAG4UAZth7sQARwCuqcVB4wAuiLUBGfSAXkeREAAMAAgBkeABx42AdNSOmAXCH7IyM4QPgD0oeKSMvLkSiruGFahmtrIumyhAOwAbDkALAAMAMwArKGopBQAtCVVxlXsUMj8VRVk5FUY2FBV4uhY2FWsACZVzVDko87M5MgAnp1WrjDoyBBVMDzIDTzkUrBw1JQAPACEVVUglPjjwqjDTbsA5MIAREQzUsMqMCBEPMNUK8ADRXTggDCsEDdeStAAeqAwGjQEIBqBAJDmLGGD3gIFeMKB3D4zXRcAAVsJEoCFP1UJQLgA%2Bag2VkU4SE65EVyzEAAbxAehAAF9zJZrE87G0KKF-g8FFAKE8ANxcnnIEAAYSWelWYqsIElcG6RCIMsS3N1MGQKrVuw1AuQ5Ci9xF%2BoldmNUFNMqdUQOtsoXWYEGEABEKnxUHC0DBhsJtZaVtb%2BZQQCBjmhudhmGhmen00IAPIwPx8vm3dzFmDC4VpgvHEjkfMN9JjHhwY3ojD8RFSe4AXle5cr1drr35fKFr3Smv4rDgRMrvE72FQtcZx1CbZXXZb6cbzcP%2B4zJGRvF%2B5aFTy6UGkTwEQncu7XG%2BjaHIMGYuB7C9QW7PQI9BbLcs2cHM82odM7D9aRXWrEAB24cgtFVesX3RJCAAoAEpEMZR8ICrCAS0QkBTlHEiYFVdNKDrTMKnA3NUBAiAMHIKBnA1CByAwIcAiCEJwgwYYYHcSlARhch3BWZBQhgZwiFCOx0jvUJEnIVBQgecRtz0O9xIgV5NwidjOOQZl62OCN-hAUJmVAxiIJYyhWRsIA&format=glimdown) (using Ionic from a CDN) + +## Legacy + +Working with Stencil components in Ember is really easy thanks to the `ember-cli-stencil` addon. It handles: + +- Importing the required files into your `vendor.js` +- Copying the component definitions into your `assets` directory +- Optionally generating a wrapper component for improved compatibility with older Ember versions + +Start off by installing the Ember addon + +```bash +ember install ember-cli-stencil +``` + +Now, when you build your application, Stencil collections in your dependencies will automatically be discovered and pulled into your application. You can just start using the custom elements in your `hbs` files with no further work needed. For more information, check out the [`ember-cli-stencil` documentation](https://github.com/alexlafroscia/ember-cli-stencil). + +_NOTE_: `ember-cli-stencil` hasn't kept up with ember's evolution and will not work in newer ember apps. diff --git a/versioned_docs/version-v4.22/framework-integration/javascript.md b/versioned_docs/version-v4.22/framework-integration/javascript.md new file mode 100644 index 000000000..fc0945b04 --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/javascript.md @@ -0,0 +1,91 @@ +--- +title: Components without a Framework +sidebar_label: JavaScript +description: Components without a Framework +slug: /javascript +--- + +# Components without a Framework + +Integrating a component built with Stencil to a project without a JavaScript framework is straight forward. If you're using a simple HTML page, you can add your component via a script tag. For example, if we published a component to npm, we could load the component through a CDN like this: + +```markup +<html> + <head> + <script src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic.js"></script> + </head> + <body> + <ion-toggle></ion-toggle> + </body> +</html> +``` + +Alternatively, if you wanted to take advantage of ES Modules, you could include the components using an import statement. + +```markup +<html> + <head> + <script type="module"> + import { defineCustomElements } from 'https://cdn.jsdelivr.net/npm/@ionic/core/loader/index.es2017.mjs'; + defineCustomElements(); + </script> + </head> + <body> + <ion-toggle></ion-toggle> + </body> +</html> +``` + +## Passing object props from a non-JSX element + +### Setting the prop manually + +```tsx +import { Prop } from '@stencil/core'; + +export class TodoList { + @Prop() myObject: object; + @Prop() myArray: Array<string>; +} +``` + +```markup +<todo-list></todo-list> +<script> + const todoListElement = document.querySelector('todo-list'); + todoListElement.myObject = {}; + todoListElement.myArray = []; +</script> +``` + +### Watching props changes + +```tsx +import { Prop, State, Watch } from '@stencil/core'; + +export class TodoList { + @Prop() myObject: string; + @Prop() myArray: string; + @State() myInnerObject: object; + @State() myInnerArray: Array<string>; + + componentWillLoad() { + this.parseMyObjectProp(this.myObject); + this.parseMyArrayProp(this.myArray); + } + + @Watch('myObject') + parseMyObjectProp(newValue: string) { + if (newValue) this.myInnerObject = JSON.parse(newValue); + } + + @Watch('myArray') + parseMyArrayProp(newValue: string) { + if (newValue) this.myInnerArray = JSON.parse(newValue); + } +} +``` + +```tsx +<todo-list my-object="{}" my-array="[]"></todo-list> +``` diff --git a/versioned_docs/version-v4.22/framework-integration/react.md b/versioned_docs/version-v4.22/framework-integration/react.md new file mode 100644 index 000000000..c805c541c --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/react.md @@ -0,0 +1,453 @@ +--- +title: React Integration with Stencil +sidebar_label: React +description: Learn how to wrap your components so that people can use them natively in React +slug: /react +--- + +# React Integration + +**Supports: React v17+ • TypeScript v5+ • Stencil v4.2.0+** + +Automate the creation of React component wrappers for your Stencil web components. + +This package includes an output target for code generation that allows developers to generate a React component wrapper for each Stencil component and a minimal runtime package built around [@lit/react](https://www.npmjs.com/package/@lit/react) that is required to use the generated React components in your React library or application. + +- ♻️ Automate the generation of React component wrappers for Stencil components +- 🌐 Generate React functional component wrappers with JSX bindings for custom events and properties +- ⌨️ Typings and auto-completion for React components in your IDE + +To generate these framework wrappers, Stencil provides an Output Target library called [`@stencil/react-output-target`](https://www.npmjs.com/package/@stencil/react-output-target) that can be added to your `stencil.config.ts` file. This also enables Stencil components to be used within e.g. Next.js or other React based application frameworks. + +## Setup + +### Project Structure + +We recommend using a [monorepo](https://www.toptal.com/front-end/guide-to-monorepos) structure for your component library with component +wrappers. Your project workspace should contain your Stencil component library and the library for the generated React component wrappers. + +An example project set-up may look similar to: + +``` +top-most-directory/ +└── packages/ + ├── stencil-library/ + │ ├── stencil.config.js + │ └── src/components/ + └── react-library/ + └── src/ + ├── components/ + └── index.ts +``` + +This guide uses Lerna for the monorepo, but you can use other solutions such as Nx, Turborepo, etc. + +To use Lerna with this walk through, globally install Lerna: + +```bash npm2yarn +npm install --global lerna +``` + +#### Creating a Monorepo + +:::note +If you already have a monorepo, skip this section. +::: + +```bash npm2yarn +# From your top-most-directory/, initialize a workspace +lerna init + +# install dependencies +npm install + +# install typescript and node types +npm install typescript @types/node --save-dev +``` + +#### Creating a Stencil Component Library + +:::note +If you already have a Stencil component library, skip this section. +::: + +From the `packages/` directory, run the following commands to create a Stencil component library: + +```bash npm2yarn +npm init stencil components stencil-library +cd stencil-library +# Install dependencies +npm install +``` + +#### Creating a React Component Library + +:::note +If you already have a React component library, skip this section. +::: + +The first time you want to create the component wrappers, you will need to have a React library package to write to. + +Run the following commands from the root directory of your monorepo to create a React component library: + +```bash npm2yarn +# Create a project +lerna create react-library # fill out the prompts accordingly +cd packages/react-library + +# Install core dependencies +npm install react react-dom typescript @types/react --save-dev + +# Install output target runtime dependency +npm install @stencil/react-output-target --save +``` + +Lerna does not ship with a TypeScript configuration. At the root of the workspace, create a `tsconfig.json`: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": true, + "lib": ["es6"] + }, + "exclude": ["node_modules", "**/*.spec.ts", "**/__tests__/**"] +} +``` + +In your `react-library` project, create a project specific `tsconfig.json` that will extend the root config: + +```json title="packages/react-library/tsconfig.json" +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "lib": ["dom", "es2015"], + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2015", + "skipLibCheck": true, + "jsx": "react", + "allowSyntheticDefaultImports": true, + "declarationDir": "./dist/types" + }, + "include": ["lib"], + "exclude": ["node_modules"] +} +``` + +Update the generated `package.json` in your `react-library`, adding the following options to the existing config: + +```diff title="packages/react-library/package.json" +{ +- "main": "lib/react-library.js", ++ "main": "dist/index.js", ++ "module": "dist/index.js", ++ "types": "dist/types/index.d.ts", + "scripts": { +- "test": "node ./__tests__/react-library.test.js" ++ "test": "node ./__tests__/react-library.test.js", ++ "build": "npm run tsc", ++ "tsc": "tsc -p . --outDir ./dist" +- } ++ }, + "files": [ +- "lib" ++ "dist" + ], ++ "publishConfig": { ++ "access": "public" ++ }, ++ "dependencies": { ++ "stencil-library": "*" ++ } +} +``` + +:::note +The `stencil-library` dependency is how Lerna knows to resolve the internal Stencil library dependency. See Lerna's documentation on +[package dependency management](https://lerna.js.org/docs/getting-started#package-dependency-management) for more information. +::: + +### Adding the React Output Target + +#### Step 1 - Stencil Component Library + +Install the `@stencil/react-output-target` dependency to your Stencil component library package. + +```bash npm2yarn +# Install dependency +npm install @stencil/react-output-target --save-dev +``` + +In your project's `stencil.config.ts`, add the `reactOutputTarget` configuration to the `outputTargets` array: + +```ts title="stencil.config.ts" +import { reactOutputTarget } from '@stencil/react-output-target'; + +export const config: Config = { + namespace: 'stencil-library', + outputTargets: [ + reactOutputTarget({ + // Relative path to where the React components will be generated + outDir: '../react-library/lib/components/stencil-generated/', + }), + // dist-custom-elements output target is required for the React output target + { type: 'dist-custom-elements' }, + ], +}; +``` + +:::tip + +The `outDir` is the relative path to the file that will be generated with all of the React component wrappers. You will replace the +file path to match your project's structure and respective names. + +::: + +See the [API section below](#api) for details on each of the output target's options. + +:::note +In order to compile Stencil components optimized for server side rendering in e.g. Next.js applications that use [AppRouter](https://nextjs.org/docs/app), make sure to provide the [`hydrateModule`](#hydratemodule) property to the output target configuration. +::: + +You can now build your Stencil component library to generate the component wrappers. + +```bash npm2yarn +# Build the library and wrappers +npm run build +``` + +If the build is successful, you’ll see the new generated file in your React component library at the location specified by the `outDir` argument. + +#### Step 2 - React Component Library + +Install the `@stencil/react-output-target` dependency to your React component library package. This step is required to add the runtime dependencies required to use the generated React components. + +```bash npm2yarn +# Install dependency +npm install @stencil/react-output-target --save +``` + +Verify or update your `tsconfig.json` file to include the following settings: + +```json +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler" + } +} +``` + +:::info + +`moduleResolution": "bundler"` is required to resolve the secondary entry points in the `@stencil/react-output-target` runtime package. You can learn more about this setting in the [TypeScript documentation](https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution). + +::: + +Verify or install TypeScript v5.0 or later in your project: + +```bash npm2yarn +# Install dependency +npm install typescript@5 --save-dev +``` + +No additional configuration is needed in the React component library. The generated component wrappers will reference the runtime dependencies directly. + +### Add the Components to your React Component Library's Entry File + +In order to make the generated files available within your React component library and its consumers, you’ll need to export everything from within your entry file. First, rename `react-library.js` to `index.ts`. Then, modify the contents to match the following: + +```tsx title="packages/react-library/src/index.ts" +export * from './components/stencil-generated/components'; +``` + +### Link Your Packages (Optional) + +:::note +If you are using a monorepo tool (Lerna, Nx, etc.), skip this section. +::: + +Before you can successfully build a local version of your React component library, you will need to link the Stencil package to the React package. + +From your Stencil project's directory, run the following command: + +```bash npm2yarn +# Link the working directory +npm link +``` + +From your React component library's directory, run the following command: + +```bash npm2yarn +# Link the package name +npm link name-of-your-stencil-package +``` + +The name of your Stencil package should match the `name` property from the Stencil component library's `package.json`. + +Your component libraries are now linked together. You can make changes in the Stencil component library and run `npm run build` to propagate the +changes to the React component library. + +:::tip +As an alternative to `npm link` , you can also run `npm install` with a relative path to your Stencil component library. This strategy, however, will +modify your `package.json` so it is important to make sure you do not commit those changes. +::: + +## Consumer Usage + +### Creating a Consumer React App + +:::note +If you already have a React app, skip this section. +::: + +From the `packages/` directory, run the following commands to create a starter React app: + +<!-- TODO: see if we can convert this to use `npm2yarn` once related issues are resolved --> +<!-- See https://github.com/facebook/docusaurus/issues/5861 for more information --> + +```bash +# Create the React app +npm create vite@latest my-app -- --template react-ts +# of if using yarn +yarn create vite my-app --template react-ts + +cd ./my-app + +# install dependencies +npm install +# or if using yarn +yarn install +``` + +You'll also need to link your React component library as a dependency. This step makes it so your React app will be able to correctly resolve imports from your React library. This +is easily done by modifying your React app's `package.json` to include the following: + +```json +"dependencies": { + "react-library": "*" +} +``` + +### Consuming the React Wrapper Components + +This section covers how developers consuming your React component wrappers will use your package and component wrappers. + +Before you can consume your React component wrappers, you'll need to build your React component library. From `packages/react-library` run: + +```bash npm2yarn +npm run build +``` + +To make use of your React component library in your React application, import your components from your React component library in the file where you want to use them. + +```tsx title="App.tsx" +import './App.css'; +import { MyComponent } from 'react-library'; + +function App() { + return ( + <div className="App"> + <MyComponent first="Your" last="Name" /> + </div> + ); +} + +export default App; +``` + +## API + +### esModule + +**Optional** + +**Type: `boolean`** + +If `true`, the output target will generate a separate ES module for each React component wrapper. Defaults to `false`. + +### excludeComponents + +**Optional** + +**Type: `string[]`** + +An array of component tag names to exclude from the React output target. Useful if you want to prevent certain web components from being in the React library. + +### experimentalUseClient + +**Optional** + +**Type: `boolean`** + +If `true`, the generated output target will include the [use client;](https://react.dev/reference/react/use-client) directive. + +### outDir + +**Required** + +**Type: `string`** + +The directory where the React components will be generated. Accepts a relative path from the Stencil project's root directory. + +### stencilPackageName + +**Optional** + +**Type: `string`** + +The name of the package that exports the Stencil components. Defaults to the package.json detected by the Stencil compiler. + +### hydrateModule + +**Optional** + +**Type: `string`** + +Enable React server side rendering (short SSR) for e.g. [Next.js](https://nextjs.org/) applications by providing an import path to the [hydrate module](../guides/hydrate-app.md) of your Stencil project that is generated through the `dist-hydrate-script` output target, e.g.: + +```ts title="stencil.config.ts" +import type { Config } from '@stencil/core'; + +/** + * excerpt from the Stencil example project: + * https://github.com/ionic-team/stencil-ds-output-targets/tree/cb/nextjs/packages/example-project + */ +export const config: Config = { + namespace: 'component-library', + outputTargets: [ + reactOutputTarget({ + outDir: '../next-app/src/app', + hydrateModule: 'component-library/hydrate' + }), + { + type: 'dist-hydrate-script', + dir: './hydrate', + }, + // ... + ], +}; +``` + +:::note +Next.js support is only available for applications that use the [Next.js App Router](https://nextjs.org/docs/app). +::: + +## FAQ's + +### What is the best format to write event names? + +Event names shouldn’t include special characters when initially written in Stencil. Try to lean on using camelCased event names for interoperability between frameworks. + +### Can I use `dist` output target with the React output target? + +No, the React output target requires the `dist-custom-elements` output target to be present in the Stencil project's configuration. The `dist-custom-elements` output target generates a separate entry for each component which best aligns with the expectations of React developers. diff --git a/versioned_docs/version-v4.22/framework-integration/vue.md b/versioned_docs/version-v4.22/framework-integration/vue.md new file mode 100644 index 000000000..143a858a8 --- /dev/null +++ b/versioned_docs/version-v4.22/framework-integration/vue.md @@ -0,0 +1,673 @@ +--- +title: VueJS Integration with Stencil +sidebar_label: Vue +description: Learn how to wrap your components so that people can use them natively in Vue +slug: /vue +--- + +# Vue Integration + +**Supports: Vue 3 • TypeScript 4.0+ • Stencil v2.9.0+** + +Stencil can generate Vue component wrappers for your web components. This allows your Stencil components to be used within a Vue 3 application. The benefits of using Stencil's component wrappers over the standard web components include: + +- Type checking with your components. +- Integration with the router link and Vue router. +- Optionally, form control web components can be used with `v-model`. + +## Setup + +### Project Structure + +We recommend using a [monorepo](https://www.toptal.com/front-end/guide-to-monorepos) structure for your component library with component wrappers. Your project workspace should contain your Stencil component library and the library for the generate Vue component wrappers. + +An example project set-up may look similar to: + +``` +top-most-directory/ +└── packages/ + ├── vue-library/ + │ └── lib/ + │ ├── plugin.ts + │ └── index.ts + └── stencil-library/ + ├── stencil.config.js + └── src/components +``` + +This guide uses Lerna for the monorepo, but you can use other solutions such as Nx, Turborepo, etc. + +To use Lerna with this walk through, globally install Lerna: + +```bash npm2yarn +npm install --global lerna +``` + +#### Creating a Monorepo + +:::note +If you already have a monorepo, skip this section. +::: + +```bash npm2yarn +# From your top-most-directory/, initialize a workspace +lerna init + +# install dependencies +npm install + +# install typescript and node types +npm install typescript @types/node --save-dev +``` + +#### Creating a Stencil Component Library + +:::note +If you already have a Stencil component library, skip this section. +::: + +```bash npm2yarn +cd packages/ +npm init stencil components stencil-library +cd stencil-library +# Install dependencies +npm install +``` + +#### Creating a Vue Component Library + +:::note +If you already have a Vue component library, skip this section. +::: + +The first time you want to create the component wrappers, you will need to have a Vue library package to write to. + +Using Lerna and Vue's CLI, generate a workspace and a library for your Vue component wrappers: + +```bash npm2yarn +# From your top-most-directory/ +lerna create vue-library +# Follow the prompts and confirm +cd packages/vue-library +# Install Vue dependency +npm install vue@3 --save-dev +``` + +Lerna does not ship with a TypeScript configuration. At the root of the workspace, create a `tsconfig.json`: + +```json +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": true, + "lib": ["es6"] + }, + "exclude": ["node_modules", "**/*.spec.ts", "**/__tests__/**"] +} +``` + +In your `vue-library` project, create a project specific `tsconfig.json` that will extend the root config: + +```json +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "lib": ["dom", "es2020"], + "module": "es2015", + "moduleResolution": "node", + "target": "es2017", + "skipLibCheck": true + }, + "include": ["lib"], + "exclude": ["node_modules"] +} +``` + +Update the generated `package.json` in your `vue-library`, adding the following options to the existing config: + +```diff +{ +- "main": "lib/vue-library.js", ++ "main": "dist/index.js", ++ "types": "dist/index.d.ts", + "files": [ +- 'lib' ++ 'dist' + ], + "scripts": { +- "test": "echo \"Error: run tests from root\" && exit 1" ++ "test": "echo \"Error: run tests from root\" && exit 1", ++ "build": "npm run tsc", ++ "tsc": "tsc -p . --outDir ./dist" +- } ++ }, ++ "publishConfig": { ++ "access": "public" ++ }, ++ "dependencies": { ++ "stencil-library": "*" ++ } +} +``` + +:::note +The `stencil-library` dependency is how Lerna knows to resolve the internal Stencil library dependency. See Lerna's documentation on +[package dependency management](https://lerna.js.org/docs/getting-started#package-dependency-management) for more information. +::: + +### Adding the Vue Output Target + +Install the `@stencil/vue-output-target` dependency to your Stencil component library package. + +```bash npm2yarn +# Install dependency (from `packages/stencil-library`) +npm install @stencil/vue-output-target --save-dev +``` + +In your project's `stencil.config.ts`, add the `vueOutputTarget` configuration to the `outputTargets` array: + +```ts +import { vueOutputTarget } from '@stencil/vue-output-target'; + +export const config: Config = { + namespace: 'stencil-library', + outputTargets: [ + // By default, the generated proxy components will + // leverage the output from the `dist` target, so we + // need to explicitly define that output alongside the + // Vue target + { + type: 'dist', + esmLoaderPath: '../loader', + }, + vueOutputTarget({ + componentCorePackage: 'stencil-library', + proxiesFile: '../vue-library/lib/components.ts', + }), + ], +}; +``` + +:::tip +The `proxiesFile` is the relative path to the file that will be generated with all the Vue component wrappers. You will replace the file path to match +your project's structure and respective names. You can generate any file name instead of `components.ts`. + +The `componentCorePackage` should match the `name` field in your Stencil project's `package.json`. +::: + +You can now build your Stencil component library to generate the component wrappers. + +```bash npm2yarn +# Build the library and wrappers (from `packages/stencil-library`) +npm run build +``` + +If the build is successful, you will now have contents in the file specified in `proxiesFile`. + +### Registering Custom Elements + +To register your web components for the lazy-loaded (hydrated) bundle, you will need to create a new file for the Vue plugin: + +```ts +// packages/vue-library/lib/plugin.ts + +import { Plugin } from 'vue'; +import { applyPolyfills, defineCustomElements } from 'stencil-library/loader'; + +export const ComponentLibrary: Plugin = { + async install() { + applyPolyfills().then(() => { + defineCustomElements(); + }); + }, +}; +``` + +You can now finally export the generated component wrappers and the Vue plugin for your component library to make them available to implementers. Export +the `plugin.ts` file created in the previous step, as well as the file `proxiesFile` generated by the Vue Output Target: + +```ts +// packages/vue-library/lib/index.ts +export * from './components'; +export * from './plugin'; +``` + +### Link Your Packages (Optional) + +:::note +If you are using a monorepo tool (Lerna, Nx, etc.), skip this section. +::: + +Before you can successfully build a local version of your Vue component library, you will need to link the Stencil package to the Vue package. + +From your Stencil project's directory, run the following command: + +```bash npm2yarn +# Link the working directory +npm link +``` + +From your Vue component library's directory, run the following command: + +```bash npm2yarn +# Link the package name +npm link name-of-your-stencil-package +``` + +The name of your Stencil package should match the `name` property from the Stencil component library's `package.json`. + +Your component libraries are now linked together. You can make changes in the Stencil component library and run `npm run build` to propagate the +changes to the Vue component library. + +:::note +As an alternative to `npm link`, you can also run `npm install` with a relative path to your Stencil component library. This strategy, +however, will modify your `package.json` so it is important to make sure you do not commit those changes. +::: + +## Consumer Usage + +### Creating a Consumer Vue App + +From the `packages/` directory, run the following command to generate a Vue app: + +```bash npm2yarn +npm init vue@3 my-app +``` + +Follow the prompts and choose the options best for your project. + +You'll also need to link your Vue component library as a dependency. This step makes it so your Vue app will be able to correctly +resolve imports from your Vue library. This is easily done by modifying your Vue app's `package.json` to include the following: + +```json +"dependencies": { + "vue-library": "*" +} +``` + +For more information, see the Lerna documentation on [package dependency management](https://lerna.js.org/docs/getting-started#package-dependency-management). + +Lastly, you'll want to update the generated `vite.config.ts`: + +```diff +export default defineConfig({ +- plugins: [vue(), vueJsx()], ++ plugins: [ ++ vue({ ++ template: { ++ compilerOptions: { ++ // treat all tags with a dash as custom elements ++ isCustomElement: (tag) => tag.includes('-'), ++ }, ++ }, ++ }), ++ vueJsx(), ++ ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +}) +``` + +This will prevent Vue from logging a warning about failing to resolve components (e.g. "Failed to resolve component: my-component"). + +### Consuming the Vue Wrapper Components + +This section covers how developers consuming your Vue component wrappers will use your package and component wrappers. + +Before you can use your Vue proxy components, you'll need to build your Vue component library. From `packages/vue-library` simply run: + +```bash npm2yarn +npm run build +``` + +In your `main.js` file, import your component library plugin and use it: + +```js +// src/main.js +import { ComponentLibrary } from 'vue-library'; + +createApp(App).use(ComponentLibrary).mount('#app'); +``` + +In your page or component, you can now import and use your component wrappers: + +```html +<template> + <my-component first="Your" last="Name"></my-component> +</template> +``` + +## API + +### componentCorePackage + +**Optional** + +**Default: The `components.d.ts` file in the Stencil project's `package.json` types field** + +**Type: `string`** + +The name of the Stencil package where components are available for consumers (i.e. the value of the `name` property in your Stencil component library's `package.json`). +This is used during compilation to write the correct imports for components. + +For a starter Stencil project generated by running: + +```bash npm2yarn +npm init stencil component my-component-lib +``` + +The `componentCorePackage` would be set to: + +```ts +// stencil.config.ts + +export const config: Config = { + ..., + outputTargets: [ + vueOutputTarget({ + componentCorePackage: 'my-component-lib', + // ... additional config options + }) + ] +} +``` + +Which would result in an import path like: + +```js +import { defineCustomElement as defineMyComponent } from 'my-component-lib/components/my-component.js'; +``` + +:::note +Although this field is optional, it is _highly_ recommended that it always be defined to avoid potential issues with paths not being generated correctly +when combining other API arguments. +::: + +### componentModels + +**Optional** + +**Default: `[]`** + +**Type: `ComponentModelConfig[]`** + +This option is used to define which components should be integrated with `v-model`. It allows you to set what the target prop is (i.e. `value`), +which event will cause the target prop to change, and more. + +```tsx +const componentModels: ComponentModelConfig[] = [ + { + elements: ['my-input', 'my-textarea'], + event: 'v-on-change', + externalEvent: 'on-change', + targetAttr: 'value', + }, +]; + +export const config: Config = { + namespace: 'stencil-library', + outputTargets: [ + vueOutputTarget({ + componentCorePackage: 'component-library', + proxiesFile: '{path to your proxy file}', + componentModels: componentModels, + }), + ], +}; +``` + +### customElementsDir + +**Optional** + +**Default: 'dist/components'** + +**Type: `string`** + +If [includeImportCustomElements](#includeimportcustomelements) is `true`, this option can be used to specify the directory where the generated +custom elements live. This value only needs to be set if the `dir` field on the `dist-custom-elements` output target was set to something other than +the default directory. + +### excludeComponents + +**Optional** + +**Default: `[]`** + +**Type: `string[]`** + +This lets you specify component tag names for which you don't want to generate Vue wrapper components. This is useful if you need to write framework-specific versions of components. For instance, in Ionic Framework, this is used for routing components - like tabs - so that +Ionic Framework can integrate better with Vue's Router. + +### includeDefineCustomElements + +**Optional** + +**Default: `true`** + +**Type: `boolean`** + +If `true`, all Web Components will automatically be registered with the Custom Elements Registry. This can only be used when lazy loading Web Components and will not work when `includeImportCustomElements` is `true`. + +### includeImportCustomElements + +**Optional** + +**Default: `undefined`** + +**Type: `boolean`** + +If `true`, the output target will import the custom element instance and register it with the Custom Elements Registry when the component is imported inside of a user's app. This can only be used with the [Custom Elements Bundle](../output-targets/custom-elements.md) and will not work with lazy loaded components. + +:::note +The configuration for the [Custom Elements](../output-targets/custom-elements.md) output target must set the +[export behavior](../output-targets/custom-elements.md#customelementsexportbehavior) to `single-export-module` for the wrappers to generate correctly. +::: + +### includePolyfills + +**Optional** + +**Default: `true`** + +**Type: `boolean`** + +If `true`, polyfills will automatically be imported and the `applyPolyfills` function will be called in your proxies file. This can only be used when lazy loading Web Components and will not work when `includeImportCustomElements` is enabled. + +### loaderDir + +**Optional** + +**Default: `/dist/loader`** + +**Type: `string`** + +The path to where the `defineCustomElements` helper method exists within the built project. This option is only used when `includeDefineCustomElements` is enabled. + +### proxiesFile + +**Required** + +**Type: `string`** + +This parameter allows you to name the file that contains all the component wrapper definitions produced during the compilation process. This is the first file you should import in your Vue project. + +## FAQ + +### Do I have to use the `dist` output target? + +No! By default, this output target will look to use the `dist` output, but the output from `dist-custom-elements` can be used alternatively. + +To do so, simply set the `includeImportCustomElements` option in the output target's config and ensure the +[custom elements output target](../output-targets/custom-elements.md) is added to the Stencil config's output target array: + +```ts +// stencil.config.ts + +export const config: Config = { + ..., + outputTargets: [ + // Needs to be included + { + type: 'dist-custom-elements' + }, + vueOutputTarget({ + componentCorePackage: 'component-library', + proxiesFile: '{path to your proxy file}', + // This is what tells the target to use the custom elements output + includeImportCustomElements: true + }) + ] +} +``` + +Now, all generated imports will point to the default directory for the custom elements output. If you specified a different directory +using the `dir` property for `dist-custom-elements`, you need to also specify that directory for the Vue output target. See +[the API section](#customelementsdir) for more information. + +In addition, all the Web Components will be automatically defined as the generated component modules are bootstrapped. + +### TypeError: Cannot read properties of undefined (reading 'isProxied') + +If you encounter this error when running the Vue application consuming your proxy components, you can set the [`enableImportInjection`](../config/extras.md#enableimportinjection) +flag on the Stencil config's `extras` object. Once set, this will require you to rebuild the Stencil component library and the Vue component library. + +### Vue warns "Failed to resolve component: my-component" + +#### Lazy loaded bundle + +If you are using Vue CLI, update your `vue.config.js` to match your custom element selector as a custom element: + +```js +const { defineConfig } = require('@vue/cli-service'); +module.exports = defineConfig({ + transpileDependencies: true, + chainWebpack: (config) => { + config.module + .rule('vue') + .use('vue-loader') + .tap((options) => { + options.compilerOptions = { + ...options.compilerOptions, + // The stencil-library components start with "my-" + isCustomElement: (tag) => tag.startsWith('my-'), + }; + return options; + }); + }, +}); +``` + +#### Custom elements bundle + +If you see this warning, then it is likely you did not import your component from your Vue library: `vue-library`. By default, all Vue components are locally registered, meaning you need to import them each time you want to use them. + +Without importing the component, you will only get the underlying Web Component, and Vue-specific features such as `v-model` will not work. + +To resolve this issue, you need to import the component from `vue-library` (your package name) and provide it to your Vue component: + +```html +<template> + <my-component first="Your" last="Name"></my-component> +</template> + +<script lang="ts"> + import { MyComponent } from 'vue-library'; + import { defineComponent } from 'vue'; + + export default defineComponent({ + components: { MyComponent }, + }); +</script> +``` + +### Vue warns: "slot attributes are deprecated vue/no-deprecated-slot-attribute" + +The slots that are used in Stencil are [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots) slots, which are different than the slots used in Vue 2. Unfortunately, the APIs for both are very similar, and your linter is likely getting the two confused. + +You will need to update your lint rules in `.eslintrc.js` to ignore this warning: + +```js +module.exports = { + rules: { + 'vue/no-deprecated-slot-attribute': 'off', + }, +}; +``` + +If you are using VSCode and have the Vetur plugin installed, you are likely getting this warning because of Vetur, not ESLint. By default, Vetur loads the default Vue 3 linting rules and ignores any custom ESLint rules. + +To resolve this issue, you will need to turn off Vetur's template validation with `vetur.validation.template: false`. See the [Vetur Linting Guide](https://vuejs.github.io/vetur/guide/linting-error.html#linting) for more information. + +### Method on component is not a function + +In order to access a method on a Stencil component in Vue, you will need to access the underlying Web Component instance first: + +```ts +// ✅ This is correct +myComponentRef.value.$el.someMethod(); + +// ❌ This is incorrect and will result in an error. +myComponentRef.value.someMethod(); +``` + +### Output commonjs bundle for Node environments + +First, install `rollup` and `rimraf` as dev dependencies: + +```bash npm2yarn +npm i -D rollup rimraf +``` + +Next, create a `rollup.config.js` in `/packages/vue-library/`: + +```js +const external = ['vue', 'vue-router']; + +export default { + input: 'dist-transpiled/index.js', + output: [ + { + dir: 'dist/', + entryFileNames: '[name].esm.js', + chunkFileNames: '[name]-[hash].esm.js', + format: 'es', + sourcemap: true, + }, + { + dir: 'dist/', + format: 'commonjs', + preferConst: true, + sourcemap: true, + }, + ], + external: (id) => external.includes(id) || id.startsWith('stencil-library'), +}; +``` + +:::info +Update the `external` list for any external dependencies. Update the `stencil-library` to match your Stencil library's package name. +::: + +Next, update your `package.json` to include the scripts for rollup: + +```json +{ + "scripts": { + "build": "npm run clean && npm run tsc && npm run bundle", + "bundle": "rollup --config rollup.config.js", + "clean": "rimraf dist dist-transpiled" + } +} +``` diff --git a/versioned_docs/version-v4.22/guides/_category_.json b/versioned_docs/version-v4.22/guides/_category_.json new file mode 100644 index 000000000..afdb49ff4 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Guides", + "position": 8 +} diff --git a/versioned_docs/version-v4.22/guides/assets.md b/versioned_docs/version-v4.22/guides/assets.md new file mode 100644 index 000000000..f6dda9fce --- /dev/null +++ b/versioned_docs/version-v4.22/guides/assets.md @@ -0,0 +1,284 @@ +--- +title: Assets +sidebar_label: Assets +description: Learn how to reference assets in your components +slug: /assets +--- + +# Assets + +Stencil components may need one or more static files as a part of their design. +These types of files are referred to as 'assets', and include images, fonts, etc. + +In this guide, we describe different strategies for resolving assets on the filesystem. + +:::note +CSS files are handled differently than assets; for more on using CSS, please see the [styling documentation](../components/styling.md). +::: + +## Asset Base Path + +The **asset base path** is the directory that Stencil will use to resolve assets. +When a component uses an asset, the asset's location is resolved relative to the asset base path. + +The asset base path is automatically set for the following output targets: +- [dist](../output-targets/dist.md) +- [hydrate](./hydrate-app.md) +- [www](../output-targets/www.md) + +For all other output targets, assets must be [moved](#manually-moving-assets) and the asset base path must be [manually set](#setassetpath). + +For each instance of the Stencil runtime that is loaded, there is a single asset base path. +Oftentimes, this means there is only one asset base path per application using Stencil. + +## Resolution Overview + +The process of resolving an asset involves asking Stencil to build a path to the asset on the filesystem. + +When an asset's path is built, the resolution is always done in a project's compiled output, not the directory containing the original source code. + +The example below uses the output of the [`www` output target](../output-targets/www.md) to demonstrate how assets are resolved. +Although the example uses the output of `www` builds, the general principle of how an asset is found holds for all output targets. + +When using the `www` output target, a `build/` directory is automatically created and set as the asset base path. +An example `build/` directory and the assets it contains can be found below. + +``` +www/ +├── build/ +│ ├── assets/ +│ │ ├── logo.png +│ │ └── scenery/ +│ │ ├── beach.png +│ │ └── sunset.png +│ └── other-assets/ +│ └── font.tiff +└── ... +``` + +To resolve the path to an asset, Stencil's [`getAssetPath()` API](#getassetpath) may be used. +When using `getAssetPath`, the assets in the directory structure above are resolved relative to `build/`. + +The code sample below demonstrates the return value of `getAssetPath` for different `path` arguments. +The return value is a path that Stencil has built to retrieve the asset on the filesystem. +```ts +import { getAssetPath } from '@stencil/core'; + +// with an asset base path of "/build/": + +// '/build/assets/logo.png' +getAssetPath('assets/logo.png'); +// '/build/assets/scenery/beach.png' +getAssetPath('assets/scenery/beach.png'); +// '/build/other-assets/font.tiff' +getAssetPath('other-assets/font.tiff'); +``` + +## Making Assets Available + +In order to be able to find assets at runtime, they need to be found on the filesystem from the output of a Stencil build. +In other words, we need to ensure they exist in the distribution directory. +This section describes how to make assets available under the asset base path. + +### assetsDirs + +The `@Component` decorator can be [configured with the `assetsDirs` option](../components/component.md#component-options). +`assetsDirs` takes an array of strings, where each entry is a relative path from the component to a directory containing the assets the component requires. + +When using the `dist` or `www` output targets, setting `assetsDirs` instructs Stencil to copy that folder into the distribution folder. +When using other output targets, Stencil will not copy assets into the distribution folder. + +Below is an example project's directory structure containing an example component and an assets directory. + +``` +src/ +└── components/ + ├── assets/ + │ ├── beach.jpg + │ └── sunset.jpg + └── my-component.tsx +``` + +Below, the `my-component` component will correctly load the assets based on it's `image` prop. + +```tsx +// file: my-component.tsx +// 1. getAssetPath is imported from '@stencil/core' +import { Component, Prop, getAssetPath, h } from '@stencil/core'; + +@Component({ + tag: 'my-component', + // 2. assetsDirs lists the 'assets' directory as a relative + // (sibling) directory + assetsDirs: ['assets'] +}) +export class MyComponent { + + @Prop() image = "sunset.jpg"; + + render() { + // 3. the asset path is retrieved relative to the asset + // base path to use in the <img> tag + const imageSrc = getAssetPath(`./assets/${this.image}`); + return <img src={imageSrc} /> + } +} +``` + +In the example above, the following allows `my-component` to display the provided asset: +1. [`getAssetPath()`](#getassetpath) is imported from `@stencil/core` +2. The `my-component`'s component decorator has the `assetsDirs` property, and lists the sibling directory, `assets`. This will copy `assets` over to the distribution directory. +3. `getAssetPath` is used to retrieve the path to the image to be used in the `<img>` tag + +### Manually Moving Assets + +For the [dist-custom-elements](../output-targets/custom-elements.md) output target, options like `assetsDirs` do not copy assets to the distribution directory. + +It's recommended that a bundler (such as rollup) or a Stencil `copy` task is used to ensure the static assets are copied to the distribution directory. + +#### Stencil Copy Task + +[Stencil `copy` task](../output-targets/copy-tasks.md)s can be used to define files and folders to be copied over to the distribution directory. + +The example below shows how a copy task can be used to find all '.jpg' and '.png' files under a project's `src` directory and copy them to `dist/components/assets` at build time. + +```ts +import { Config } from '@stencil/core'; + +export const config: Config = { + namespace: 'your-component-library', + outputTargets: [ + { + type: 'dist-custom-elements', + copy: [ + { + src: '**/*.{jpg,png}', + dest: 'dist/components/assets', + warn: true, + } + ] + }, + ], + // ... +}; +``` +#### Rollup Configuration + +[Rollup Plugins](../config/plugins.md#rollup-plugins)'s can be used to define files and folders to be copied over to the distribution directory. + +The example below shows how a the `rollup-plugin-copy` NPM module can be used to find all '.jpg' and '.png' files under a project's `src` directory and copy them to `dist/components/assets` at build time. + +```javascript +import { Config } from '@stencil/core'; +import copy from 'rollup-plugin-copy'; + +export const config: Config = { + namespace: 'copy', + outputTargets: [ + { + type: 'dist-custom-elements', + }, + ], + rollupPlugins: { + after: [ + copy({ + targets: [ + { + src: 'src/**/*.{jpg,png}', + dest: 'dist/components/assets', + }, + ], + }), + ] + } +}; +``` + +## API Reference + +### getAssetPath + +`getAssetPath()` is an API provided by Stencil to build the path to an asset, relative to the asset base path. + +```ts +/** + * Builds a URL to an asset. This is achieved by combining the + * provided `path` argument with the base asset path. + * @param path the path of the asset to build a URL to + * @returns the built URL + */ +declare function getAssetPath(path: string): string; +``` + +The code sample below demonstrates the return value of `getAssetPath` for different `path` arguments, when an asset base path of `/build/` has been set. +```ts +import { getAssetPath } from '@stencil/core'; + +// with an asset base path of "/build/": +// "/build/" +getAssetPath(''); +// "/build/my-image.png" +getAssetPath('my-image.png'); +// "/build/assets/my-image.png" +getAssetPath('assets/my-image.png'); +// "/build/assets/my-image.png" +getAssetPath('./assets/my-image.png'); +// "/assets/my-image.png" +getAssetPath('../assets/my-image.png'); +// "/assets/my-image.png" +getAssetPath('/assets/my-image.png'); +``` + +### setAssetPath + +`setAssetPath` is an API provided by Stencil's runtime to manually set the asset base path where assets can be found. If you are using `getAssetPath` to compose the path for your component assets, `setAssetPath` allows you or the consumer of the component to change that path. + +```ts +/** + * Set the base asset path for resolving components + * @param path the base asset path + * @returns the new base asset path + */ +export declare function setAssetPath(path: string): string; +``` + +Calling this API will set the asset base path for all Stencil components attached to a Stencil runtime. As a result, calling `setAssetPath` should not be done from within a component in order to prevent unwanted side effects when using a component. + +Make sure as component author to export this function as part of your module in order to also make it accessible to the consumer of your component, e.g. in your package entry file export the function via: + +```ts title="/src/index.ts" +export { setAssetPath } from '@stencil/core'; +``` + +Now your users can import it directly from your component library, e.g.: + +```ts +import { setAssetPath } from 'my-component-library'; +setAssetPath(`${window.location.protocol}//assets.${window.location.host}/`); +``` + +Alternatively, one may use [`document.currentScript.src`](https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript) when working in the browser and not using modules or environment variables (e.g. `document.env.ASSET_PATH`) to set the +asset base path. This configuration depends on how your script is bundled, (or lack of bundling), and where your assets can be loaded from. + +:::note + +If your component library exports components compiled with [`dist-output-target`](/output-targets/custom-elements.md) and `externalRuntime` set to `true`, then `setAssetPath` has to be imported from `@stencil/core` directly. + +::: + +In case you import a component directly via script tag, this would look like: + +```html +<html> + <head> + <script src="https://cdn.jsdelivr.net/npm/my-component-library/dist/my-component-library.js"></script> + <script type="module"> + import { setAssetPath } from 'https://cdn.jsdelivr.net/npm/my-component-library/dist/my-component-library.js'; + setAssetPath(`${window.location.origin}/`); + </script> + </head> + <body> + <ion-toggle></ion-toggle> + </body> +</html> +``` diff --git a/versioned_docs/version-v4.22/guides/build-conditionals.md b/versioned_docs/version-v4.22/guides/build-conditionals.md new file mode 100644 index 000000000..0f540e8de --- /dev/null +++ b/versioned_docs/version-v4.22/guides/build-conditionals.md @@ -0,0 +1,41 @@ +--- +title: Build Conditionals +description: Build Conditionals +--- + +# Build Conditionals + +Build Conditionals in Stencil allow you to run specific code only when Stencil is running in development mode. This code is stripped from your bundles when doing a production build, therefore keeping your bundles as small as possible. + +### Using Build Conditionals + +Lets dive in and look at an example of how to use our build conditional: + +```tsx +import { Component, Build } from '@stencil/core'; + +@Component({ + tag: 'stencil-app', + styleUrl: 'stencil-app.css' +}) +export class StencilApp { + + componentDidLoad() { + if (Build.isDev) { + console.log('im in dev mode'); + } else { + console.log('im running in production'); + } + } +} +``` + +As you can see from this example, we just need to import `Build` from `@stencil/core` and then we can use the `isDev` constant to detect when we are running in dev mode or production mode. + +### Use Cases + +Some use cases we have come up with are: + +- Diagnostics code that runs in dev to make sure logic is working like you would expect +- `console.log()`'s that may be useful for debugging in dev mode but that you don't want to ship +- Disabling auth checks when in dev mode diff --git a/versioned_docs/version-v4.22/guides/csp-nonce.md b/versioned_docs/version-v4.22/guides/csp-nonce.md new file mode 100644 index 000000000..e18636cd8 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/csp-nonce.md @@ -0,0 +1,119 @@ +--- +title: Content Security Policy Nonces +description: How to leverage CSP nonces in Stencil projects. +slug: /csp-nonce +--- + +# Using Content Security Policy Nonces in Stencil + +[Content Security Policies (CSPs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) can help protect an application from Cross-Site Scripting (XSS) +attacks by adding a security layer to help prevent unauthorized code from running in the browser. + +An application that is served with a CSP other than 'unsafe-inline' and contains web components without a Shadow DOM will likely run into errors on load. +This is often first detected in the browser's console, which reports an error stating that certain styles or scripts violate the effective CSP. + +To resolve this issue, Stencil supports using [CSP nonces](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) in +many of the output targets. + +:::caution +NOTE: CSPs and some CSP strategies are not supported by certain browsers. For a more detailed list, please see the [CSP browser compatibility table](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#browser_compatibility). +::: + +## How to Use a Nonce + +The actual generation of the nonce value and enforcement of the correct CSP are not the responsibility of Stencil. Instead, the server of +the application will need to generate the nonce value for each page view, construct the CSP, and then correctly handle passing the generated nonce to +Stencil based on which output target is being consumed. + +There are many resources available that walk through setting up a CSP and using the nonce behavior. +[This](https://towardsdatascience.com/content-security-policy-how-to-create-an-iron-clad-nonce-based-csp3-policy-with-webpack-and-nginx-ce5a4605db90) +article walks through the process using Nginx and Webpack. Obviously, these resources don't account for the Stencil specifics, but any specifics will +be called out in this guide. + +Per the [MDN Guide on nonces](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce#generating_values), a nonce should be "a random base64-encoded string of at least 128 bits of data from a cryptographically secure random number generator". + +### Output Targets + +Using nonces may differ slightly between output targets, so please be sure to use the correct pattern based on the context in which your +Stencil components are consumed. + +#### Dist + +Consuming a `nonce` in the `dist` output target is easy using the provided `setNonce` helper function. This function is exported from the index +file of the output target's designated output directory. + +This function simply accepts the `nonce` string value that you want set for every `style` and `script` tag. + +This is an example of consuming the `dist` output in an Angular app's entrypoint: + +```ts +// main.ts + +import { defineCustomElements, setNonce } from 'my-lib/loader'; + +// Will set the `nonce` attribute for all scripts/style tags +// i.e. will run styleTag.setAttribute('nonce', 'r4nd0m') +// Obviously, you should use the nonce generated by your server +setNonce('r4nd0m'); + +// Generic Angular bootstrapping +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.log(err)); + +defineCustomElements(); +``` + +#### Custom Elements + +Consuming a `nonce` in the `dist-custom-elements` output target is easy using the provided `setNonce` helper function. This function is exported +from the index file of the output target's designated output directory. + +This function simply accepts the `nonce` string value that you want set for every `style` and `script` tag. + +This is an example of consuming the `dist-custom-elements` output in an Angular app's entrypoint: + +```ts +// main.ts + +import { defineCustomElements, setNonce } from 'my-lib/dist/components'; +// Assume `customElementsExportBehavior: 'auto-define-custom-elements'` is set +import 'my-lib/dist/components/my-component'; + +// Will set the `nonce` attribute for all scripts/style tags +// i.e. will run styleTag.setAttribute('nonce', 'r4nd0m') +// Obviously, you should use the nonce generated by your server +setNonce('r4nd0m'); + +// Generic Angular bootstrapping +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.log(err)); +``` + +#### WWW + +Unfortunately, setting `nonce` attributes gets a bit trickier when it comes to [SSR and SSG](../static-site-generation/01-overview.md). As a `nonce` needs +to be unique per page view, it cannot be defined/set at build time. So, this responsibility now falls on the +[hydrate app](../guides/hydrate-app.md)'s execution of runtime code. + +**SSR** + +Since there is not an easy way (or any way) of exposing and executing helper functions to manipulate the outcome of the runtime code, Stencil +has fallback behavior for pulling the `nonce` off of a `meta` tag in the DOM head. + +So, for SSR, your app can simply inject a `meta` element into the header _on each page request_. Yes, this does involve some manual configuration +for the code served by your server. To work correctly, the created tag must be generated as follows: + +```html +<meta name="csp-nonce" content="{ your nonce value here }" /> +``` + +This isn't a security risk because, for an attacker to execute a script to pull the nonce from the meta tag, they would have needed to know the +nonce _ahead_ of the script's execution. + +**SSG** + +Stencil cannot support CSP nonces with SSG. Because all of the code is generated during [pre-rendering](../static-site-generation/01-overview.md#how-static-site-generation-and-prerendering-works), Stencil doesn't generate the `style` or `script` tags at runtime. +If an application wants to leverage nonces in SSG, they can build a mechanism to scrape the pre-rendered code and apply the attribute server-side before +it is served to the client. diff --git a/versioned_docs/version-v4.22/guides/design-systems.md b/versioned_docs/version-v4.22/guides/design-systems.md new file mode 100644 index 000000000..356f9dcbc --- /dev/null +++ b/versioned_docs/version-v4.22/guides/design-systems.md @@ -0,0 +1,77 @@ +--- +title: Design Systems +sidebar_label: Design Systems +description: Design Systems in Stencil +slug: /design-systems +--- + +# Design Systems + +## What is a Design System? + +A Design System consists of UI components and a clearly defined visual style, released as both code implementations and design artifacts. +When adopted by all product teams, a more cohesive customer experience emerges. + +There are several aspects that Design Systems consist of: + +### Components +A component is a standalone UI element designed to be reusable across many projects. +Its goal is to do one thing well, while remaining abstract enough to allow for a variety of use cases. +Developers can use them as building blocks to build new user experiences. +One of the key benefits of reusable components is that developers don't have to worry about the core design and functionality of each component every time they use them. +Examples include buttons, links, forms, input fields, and modals. + +### Patterns +A pattern is an opinionated use of components. +Often, multiple components are combined in order to create a standardized user experience (UX). +As a result, they improve both the user and developer experience. +After implementing patterns, users will understand the application better and accomplish their tasks faster. +When the development team understands the proper way to use components together, software applications become easier to use. +Examples include saving data to the system, capturing data from forms, and filtering and analyzing data. + +### Visual Language +A cohesive company brand strengthens its value in the minds of the customer. +In the context of design systems, this means defining various aspects of the visual style, including colors, typography, and icons. +Defining primary, secondary, and tertiary colors helps an application stand out and is more user-friendly. +The right typography ensures users are not distracted while using an app. +Finally, icons increase engagement in a product and make it “pop” visually. + +### Design Artifacts and Code Implementations +By leveraging the components, patterns, and visual language of the design system, designers can create design artifacts representing UI workflows. +Developers refer to the artifacts as guidance for implementing the design with code. + +## The Value of Design Systems +With a design system in place, its true value is revealed. +The entire product development team is freed up to focus on what matters most: solving customer problems and delivering value. +Additionally, the avoidance of having teams working in parallel, recreating the same UI elements over and over, has a real-world project impact in terms of reduced time to market and increased cost savings. + +Design systems allow project teams to work better together. +Designers define a centralized “source of truth” for software application best practices which can be referenced by anyone in a product organization. +Developers no longer need to spend time rethinking how to build common app scenarios, such as application search or data table grids. +When the business inevitably makes changes to the design system, they can easily be applied to all projects. +The end result is a better product for your users. + +## Using Stencil to Build a Design System + +There’s a lot that goes into creating amazing UI components. +Performance, accessibility, cross-platform capabilities, and user experience (not only of the UI component itself but how it fits into the entire design system) all must be considered. + +These aspects take real effort to do well. + +Enter Stencil, a robust and highly extensible tool for building components and patterns, the building blocks of a design system. +With its intentionally minimalistic tooling and API footprint, it’s simple to incorporate into your existing development workflows. +It brings substantial performance out of the box by leveraging a tiny runtime. +Most importantly, all UI components built with Stencil are based 100% on open web standards. + +### The Importance of Open Web Standards +By using the web components standard, supported in all modern browsers, Stencil-built UI components offer many distinct advantages for use in a design system, namely: + +* They work on any platform or device +* They work with any front-end framework, so they can easily be used across teams and projects using different tech stacks +* They facilitate the creation of one company-wide code implementation instead of one per framework or platform + +Learn more about why web components are ideal for design systems in [this blog post](https://blog.ionicframework.com/5-reasons-web-components-are-perfect-for-design-systems/). + +### How to Get Started +Stencil’s out-the-box features will help you build your own library of universal UI components that will work across platforms, devices, and front-end frameworks. +Review the documentation on this site to get started. diff --git a/versioned_docs/version-v4.22/guides/forms.md b/versioned_docs/version-v4.22/guides/forms.md new file mode 100644 index 000000000..555162b98 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/forms.md @@ -0,0 +1,165 @@ +--- +title: Forms +sidebar_label: Forms +description: Forms +slug: /forms +--- + +# Forms + +## Basic forms + +Here is an example of a component with a basic form: + +```tsx +@Component({ + tag: 'my-name', + styleUrl: 'my-name.css' +}) +export class MyName { + + @State() value: string; + + handleSubmit(e) { + e.preventDefault() + console.log(this.value); + // send data to our backend + } + + handleChange(event) { + this.value = event.target.value; + } + + render() { + return ( + <form onSubmit={(e) => this.handleSubmit(e)}> + <label> + Name: + <input type="text" value={this.value} onInput={(event) => this.handleChange(event)} /> + </label> + <input type="submit" value="Submit" /> + </form> + ); + } +} +``` + +Let's go over what is happening here. First we bind the value of the input to a state variable, in this case `this.value`. We then set our state variable to the new value of the input with the `handleChange` method we have bound to `onInput`. `onInput` will fire every keystroke that the user types into the input. + +## Using form-associated custom elements + +In addition to using a `<form>` element inside of a Stencil component, as shown +in the above example, you can also use Stencil's support for building +form-associated custom elements to create a Stencil component that integrates +in a native-like way with a `<form>` tag _around_ it. This allows you to build +rich form experiences using Stencil components which leverage built-in form +features like validation and accessibility. + +As an example, translating the above example to be a form-associated component +(instead of one which includes a `<form>` element in its JSX) would look like +this: + +```tsx +@Component({ + tag: 'my-name', + styleUrl: 'my-name.css', + formAssociated: true +}) +export class MyName { + @State() value: string; + @AttachInternals() internals: ElementInternals; + + handleChange(event) { + this.internals.setFormValue(event.target.value); + } + + render() { + return ( + <label> + Name: + <input + type="text" + value={this.value} + onInput={(event) => this.handleChange(event)} + /> + </label> + ); + } +} +``` + +For more check out the docs for [form-association in Stencil](../components/form-associated.md). + +## Advanced forms + +Here is an example of a component with a more advanced form: + +```tsx +@Component({ + tag: 'my-name', + styleUrl: 'my-name.css' +}) +export class MyName { + selectedReceiverIds = [102, 103]; + @State() value: string; + @State() selectValue: string; + @State() secondSelectValue: string; + @State() avOptions: any[] = [ + { 'id': 101, 'name': 'Mark' }, + { 'id': 102, 'name': 'Smith' } + ]; + + handleSubmit(e) { + e.preventDefault(); + console.log(this.value); + } + + handleChange(event) { + this.value = event.target.value; + + if (event.target.validity.typeMismatch) { + console.log('this element is not valid') + } + } + + handleSelect(event) { + console.log(event.target.value); + this.selectValue = event.target.value; + } + + handleSecondSelect(event) { + console.log(event.target.value); + this.secondSelectValue = event.target.value; + } + + render() { + return ( + <form onSubmit={(e) => this.handleSubmit(e)}> + <label> + Email: + <input type="email" value={this.value} onInput={(e) => this.handleChange(e)} /> + </label> + + <select onInput={(event) => this.handleSelect(event)}> + <option value="volvo" selected={this.selectValue === 'volvo'}>Volvo</option> + <option value="saab" selected={this.selectValue === 'saab'}>Saab</option> + <option value="mercedes" selected={this.selectValue === 'mercedes'}>Mercedes</option> + <option value="audi" selected={this.selectValue === 'audi'}>Audi</option> + </select> + + <select onInput={(event) => this.handleSecondSelect(event)}> + {this.avOptions.map(recipient => ( + <option value={recipient.id} selected={this.selectedReceiverIds.indexOf(recipient.id) !== -1}>{recipient.name}</option> + ))} + </select> + + <input type="submit" value="Submit" /> + </form> + ); + } +} +``` + +This form is a little more advanced in that it has two select inputs along with an email input. We also do validity checking of our email input in the `handleChange` method. We handle the `select` element in a very similar manner to how we handle text inputs. + +For the validity checking, we are #usingtheplatform and are using the [constraint validation api](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) that is built right into the browser to check if the user is actually entering an email or not. diff --git a/versioned_docs/version-v4.22/guides/hydrate-app.md b/versioned_docs/version-v4.22/guides/hydrate-app.md new file mode 100644 index 000000000..c1d449864 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/hydrate-app.md @@ -0,0 +1,268 @@ +--- +title: Hydrate App +sidebar_label: Hydrate App +description: Hydrate App +slug: /hydrate-app +--- + +# Hydrate App + +The hydrate app is a Stencil output target which generates a module that can be +used on a NodeJS server to hydrate HTML and implement server side rendering (SSR). +This functionality is used internally by the Stencil compiler for +prerendering, as well as for the Angular Universal SSR for the Ionic +framework. However, like Stencil components, the hydrate app itself is not +restricted to one framework. + +_Note that Stencil does **NOT** use Puppeteer for SSR or prerendering._ + +## How to Use the Hydrate App + +Server side rendering (SSR) can be accomplished in a similar way to +prerendering. Instead of using the `--prerender` CLI flag, you can an output +target of type `'dist-hydrate-script'` to your `stencil.config.ts`, like so: + +```ts +outputTargets: [ + { + type: 'dist-hydrate-script', + }, +]; +``` + +This will generate a `hydrate` app in your root project directory that can be +imported and used by your Node server. + +After publishing your component library, you can import the hydrate app into +your server's code like this: + +```javascript +import { createWindowFromHtml, hydrateDocument, renderToString, streamToString } from 'yourpackage/hydrate'; +``` + +The hydrate app module exports 3 functions, `hydrateDocument`, `renderToString` and `streamToString`. `hydrateDocument` takes a [document](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDocument) as its input while `renderToString` as well as `streamToString` takes a raw HTML string. While `hydrateDocument` and `renderToString` return a Promise which wraps a result object, `streamToString` returns a [`Readable`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) stream that can be passed into a server response. + +### hydrateDocument + +You can use `hydrateDocument` as a part of your server's response logic before serving the web page. `hydrateDocument` takes two arguments, a [document](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDocument) and a config object. The function returns a promise with the hydrated results, with the hydrated HTML under the `html` property. + +*Example taken from Ionic Angular server* + + ```ts +import { hydrateDocument, createWindowFromHtml } from 'yourpackage/hydrate'; + +export function hydrateComponents(template: string) { + const win = createWindowFromHtml(template, Math.random().toString()) + + return hydrateDocument(win.document) + .then((hydrateResults) => { + // execute logic based on results + console.log(hydrateResults.html); + return hydrateResults; + }); +} +``` + +You can call the `hydrateComponents` function from your Node.js server, e.g.: + +```ts +import Koa from 'koa'; + +const app = new Koa(); +app.use(async (ctx) => { + const res = await hydrateComponents(`<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Document</title> + <link rel="preconnect" href="https://some-url.com" /> + <style> + // custom styles here + </style> + </head> + <body> + <app-root></app-root> + </body> +</html>`) + ctx.body = res +}) +``` + +Please note that Stencil injects scoped component styles immediately after `<link />` tags with a `rel="preconnect"` attribute, but before your custom styles. This setup allows you to define custom styles for your components effectively. + +#### hydrateDocument Options + + - `canonicalUrl` - string + - `constrainTimeouts` - boolean + - `clientHydrateAnnotations` - boolean + - `cookie` - string + - `direction` - string + - `language` - string + - `maxHydrateCount` - number + - `referrer` - string + - `removeScripts` - boolean + - `removeUnusedStyles` - boolean + - `resourcesUrl` - string + - `timeout` - number + - `title` - string + - `url` - string + - `userAgent` - string + +### renderToString + +The hydrate app also has a `renderToString` function that takes an HTML string +and returns a promise of `HydrateResults`. The optional second parameter is a +config object that can alter the output of the markup. Like `hydrateDocument`, +the hydrated HTML can be found under the `html` property. + +*Example taken from Ionic Core* + +```javascript +const results = await hydrate.renderToString( + `<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>`, + { + fullDocument: false, + serializeShadowRoot: true, + prettyHtml: true, + } +); + +console.log(results.html); +/** + * outputs: + * ```html + * <my-component class="hydrated sc-my-component-h" first="Stencil" last="'Don't call me a framework' JS" s-id="1"> + * <template shadowrootmode="open"> + * <style sty-id="sc-my-component"> + * .sc-my-component-h{display:block} + * </style> + * <div c-id="1.0.0.0" class="sc-my-component"> + * <!--t.1.1.1.0--> + * Hello, World! I'm Stencil 'Don't call me a framework' JS\n" + + * </div> + * </template> + * <!--r.1--> + * </my-component> + * ``` + */ +``` + +#### renderToString Options + +##### `approximateLineWidth` + +__Type:__ `number` + +Determines when line breaks are being set when serializing the component. + +##### `prettyHtml` + +__Default:__ `false` + +__Type:__ `boolean` + +If set to `true` it prettifies the serialized HTML code, intends elements and escapes text nodes. + +##### `removeAttributeQuotes` + +__Type:__ `boolean` + +__Default:__ `false` + +If set to `true` it removes attribute quotes when possible, e.g. replaces `someAttribute="foo"` to `someAttribute=foo`. + +##### `removeEmptyAttributes` + +__Type:__ `boolean` + +__Default:__ `true` + +If set to `true` it removes attribute that don't have values, e.g. remove `class=""`. + +##### `removeHtmlComments` + +__Type:__ `boolean` + +__Default:__ `false` + +If set to `true` it removes any abundant HTML comments. Stencil still requires to insert hydration comments to be able to reconcile the component. + +##### `beforeHydrate` + +__Type:__ `(document: Document, url: URL) => <void> | Promise<void>` + +Allows to modify the document and all its containing components to be modified before the hydration process starts. + +##### `afterHydrate` + +__Type:__ `(document: Document, url: URL, results: PrerenderUrlResults) => <void> | Promise<void>` + +Allows to modify the document and all its containing components after the component was rendered in the virtual DOM and before the serialization process starts. + +##### `serializeShadowRoot` + +__Default:__ `true` + +__Type:__ `boolean` + +If set to `true` Stencil will render a component defined with a `shadow: true` flag into a [Declarative Shadow DOM](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom), e.g.: + +```javascript +const results = await hydrate.renderToString( + `<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>`, + { + fullDocument: false, + serializeShadowRoot: true, + prettyHtml: true, + } +); + +console.log(results.html); +/** + * outputs: + * ```html + * <my-component class="hydrated sc-my-component-h" first="Stencil" last="'Don't call me a framework' JS" s-id="1"> + * <template shadowrootmode="open"> + * <style sty-id="sc-my-component"> + * .sc-my-component-h{display:block} + * </style> + * <div c-id="1.0.0.0" class="sc-my-component"> + * <!--t.1.1.1.0--> + * Hello, World! I'm Stencil 'Don't call me a framework' JS + * </div> + * </template> + * <!--r.1--> + * </my-component> + * ``` + */ +``` + +When set to `false`, the component renders with its light DOM but delays hydration until runtime. + +```javascript +const results = await hydrate.renderToString( + `<my-component first="Stencil" last="'Don't call me a framework' JS">👋</my-component>`, + { + fullDocument: false, + serializeShadowRoot: false, + prettyHtml: true, + } +); + +console.log(results.html); +/** + * outputs: + * ```html + * <my-component class="hydrated" first=Stencil last="'Don't call me a framework' JS" s-id=1>👋</my-component> + * ``` + */ +``` + +##### `fullDocument` + +__Type:__ `boolean` + +__Default:__ `true` + +If set to `true`, Stencil will serialize a complete HTML document for a server to respond. If set to `false` it will only render the components within the given template. diff --git a/versioned_docs/version-v4.22/guides/module-bundling.md b/versioned_docs/version-v4.22/guides/module-bundling.md new file mode 100644 index 000000000..769bea76b --- /dev/null +++ b/versioned_docs/version-v4.22/guides/module-bundling.md @@ -0,0 +1,150 @@ +--- +title: Module Bundling +sidebar_label: Bundling +description: Module Bundling +slug: /module-bundling +--- + +# Module Bundling + +Stencil uses [Rollup](https://rollupjs.org/guide/en/) under the hood to bundle your components. This guide will explain and recommend certain workarounds for some of the most common bundling issues you might encounter. + +## One Component Per Module + +For Stencil to bundle your components most efficiently, you must declare a single component (class decorated with `@Component`) per *TypeScript* file, and the component itself **must** have a unique `export`. By doing so, Stencil is able to easily analyze the entire component graph within the app, and best understand how components should be bundled together. Under-the-hood it uses the Rollup bundler to efficiently bundled shared code together. Additionally, lazy-loading is a default feature of Stencil, so code-splitting is already happening automatically, and only dynamically importing components which are being used on the page. + +Modules that contain a component are entry-points, which means that no other module should import anything from them. + +The following example is **NOT** valid: + +```tsx title="src/components/my-cmp.tsx" +// This module has a component, you cannot export anything else +export function someUtilFunction() { + console.log('do stuff'); +} + +@Component({ + tag: 'my-cmp' +}) +export class MyCmp {} +``` + +In this case, the compiler will emit an error that looks like this: + +```bash +[ ERROR ] src/components/my-cmp.tsx:4:1 + To allow efficient bundling, modules using @Component() can only have a single export which is the component + class itself. Any other exports should be moved to a separate file. For further information check out: + https://stenciljs.com/docs/module-bundling + + L4: export function someUtilFunction() { + L5: console.log('do stuff'); +``` + +The solution is to move any shared functions or classes to a different `.ts` file, like this: + +```tsx title="src/utils.ts" +export function someUtilFunction() { + console.log('do stuff'); +} +``` + +```tsx title="src/components/my-cmp.tsx" +import { someUtilFunction } from '../utils.ts'; + +@Component({ + tag: 'my-cmp' +}) +export class MyCmp {} +``` + +```tsx title="src/components/my-cmp-two.tsx" +import { someUtilFunction } from '../utils.ts'; + +@Component({ + tag: 'my-cmp-two' +}) +export class MyCmpTwo {} +``` + + +## CommonJS Dependencies + +Rollup depends on [ES modules (ESM)](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) to properly tree-shake the module graph, unfortunately, some third-party libraries ship their code using the [CommonJS](https://requirejs.org/docs/commonjs.html) module system, which is not ideal. + +Since CommonJS libraries are still common today, Stencil comes with [`rollup-plugin-commonjs`](https://github.com/rollup/rollup-plugin-commonjs) already installed and configured. + +At compiler-time, the `rollup-plugin-commonjs` plugin does a best-effort to **transform CommonJS into ESM**, but this is not always an easy task. CommonJS is dynamic by nature, while ESM is static by design. + +For further information, check out the [rollup-plugin-commonjs docs](https://github.com/rollup/plugins/tree/master/packages/commonjs). + + + +## Custom Rollup plugins + +Stencil provides an API to pass custom rollup plugins to the bundling process in `stencil.config.ts`. Under the hood, stencil ships with some built-in plugins including `node-resolve` and `commonjs`, since the execution order of some rollup plugins is important, stencil provides an API to inject custom plugin **before node-resolve** and after **commonjs transform**: + +```tsx +export const config = { + rollupPlugins: { + before: [ + // Plugins injected before rollupNodeResolve() + resolvePlugin() + ], + after: [ + // Plugins injected after commonjs() + nodePolyfills() + ] + } +} +``` + +As a rule of thumb, plugins that need to resolve modules, should be place in `before`, while code transform plugins like: `node-polyfills`, `replace`... should be placed in `after`. Follow the plugin's documentation to make sure it's executed in the right order. + + +## Node Polyfills + +Depending on which libraries a project is dependent on, the [rollup-plugin-node-polyfills](https://www.npmjs.com/package/rollup-plugin-node-polyfills) plugin may be required. In such cases, an error message similar to the following will be displayed at build time. + +```bash +[ ERROR ] Bundling Node Builtin + For the import "crypto" to be bundled from 'problematic-dep', + ensure the "rollup-plugin-node-polyfills" plugin is installed + and added to the stencil config plugins. +``` + +This is caused by some third-party dependencies that use [Node APIs](https://nodejs.org/dist/latest-v10.x/docs/api/) that are not available in the browser, the `rollup-plugin-node-polyfills` plugin works by transparently polyfilling this missing APIs in the browser. + +### 1. Install `rollup-plugin-node-polyfills`: + +```bash npm2yarn +npm install rollup-plugin-node-polyfills --save-dev +``` + +### 2. Update the `stencil.config.ts` file including the plugin: + +```tsx +import { Config } from '@stencil/core'; +import nodePolyfills from 'rollup-plugin-node-polyfills'; + +export const config: Config = { + namespace: 'mycomponents', + rollupPlugins: { + after: [ + nodePolyfills(), + ] + } +}; +``` + +:::note +`rollup-plugin-node-polyfills` is a code-transform plugin, so it needs to run AFTER the commonjs transform plugin, that's the reason it's placed in the "after" array of plugins. +::: + +## Strict Mode + +ES modules are always parsed in strict mode. That means that certain non-strict constructs (like octal literals) will be treated as syntax errors when Rollup parses modules that use them. Some older CommonJS modules depend on those constructs, and if you depend on them your bundle will blow up. There's nothing we can do about that. + +Luckily, there is absolutely no good reason not to use strict mode for everything — so the solution to this problem is to lobby the authors of those modules to update them. + +*Source: [https://github.com/rollup/rollup-plugin-commonjs#strict-mode](https://github.com/rollup/rollup-plugin-commonjs#strict-mode)* diff --git a/versioned_docs/version-v4.22/guides/publishing.md b/versioned_docs/version-v4.22/guides/publishing.md new file mode 100644 index 000000000..1c4acdf01 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/publishing.md @@ -0,0 +1,159 @@ +--- +title: Publishing A Component Library +sidebar_label: Publishing +description: Publishing A Component Library +slug: /publishing +--- + +There are numerous strategies to publish and distribute your component library to be consumed by external projects. One of the benefits of Stencil is that is makes it easy to generate the various [output targets](../output-targets/01-overview.md) that are right for your use-case. + +## Use Cases + +To use your Stencil components in other projects, there are two different output targets to consider: [`dist`](../output-targets/dist.md) and [`dist-custom-elements`](../output-targets/custom-elements.md). Both export your components for different use cases. Luckily, both can be generated at the same time, using the same source code, and shipped in the same distribution. It would be up to the consumer of your component library to decide which build to use. + +### Lazy Loading + +If you prefer to have your components automatically loaded when used in your application, we recommend enabling the [`dist`](../output-targets/dist.md) output target. The bundle gives you a small entry file that registers all your components and defers loading the full component logic until it is rendered in your application. It doesn't matter if the actual application is written in HTML or created with vanilla JavaScript, jQuery, React, etc. + +Your users can import your component library, e.g. called `my-design-system`, either via a `script` tag: + +```html +<script type="module" src="https://unpkg.com/my-design-system"></script> +``` + +or by importing it in the bootstrap script of your application: + +```ts +import 'my-design-system'; +``` + +To ensure that the right entry file is loaded when importing the project, define the following fields in your `package.json`: + +```json +{ + "exports": "./dist/esm/my-design-system.js", + "main": "./dist/cjs/my-design-system.js", + "unpkg": "dist/my-design-system/my-design-system.esm.js", +} +``` + +Read more about various options when it comes to configuring your project's components for lazy loading in the [`dist`](../output-targets/dist.md) output target section. + +#### Considerations + +To start, Stencil was designed to lazy-load itself only when the component was actually used on a page. There are many benefits to this approach, such as simply adding a script tag to any page and the entire library is available for use, yet only the components actually used are downloaded. For example, [`@ionic/core`](https://www.npmjs.com/package/@ionic/core) comes with over 100 components, but a webpage may only need `ion-toggle`. Instead of requesting the entire component library, or generating a custom bundle for just `ion-toggle`, the `dist` output target is able to generate a tiny entry build ready to load any of its components on-demand. + +However be aware that this approach is not ideal in all cases. It requires your application to ship the bundled components as static assets in order for them to load properly. Furthermore, having many nested component dependencies can have an impact on the performance of your application. For example, given you have a component `CmpA` which uses a Stencil component `CmpB` which itself uses another Stencil component `CmpC`. In order to fully render `CmpA` the browser has to load 3 scripts sequentially which can result in undesired rendering delays. + +### Standalone + +The [`dist-custom-elements`](../output-targets/custom-elements.md) output target builds each component as a stand-alone class that extends `HTMLElement`. The output is a standardized custom element with the styles already attached and without any of Stencil's lazy-loading. This may be preferred for projects that are already handling bundling, lazy-loading and defining the custom elements themselves. + +The generated files will each export a component class and will already have the styles bundled. However, this build does not define the custom elements or apply any polyfills. Static assets referenced within components will need to be set using `setAssetPath` (see [Making Assets Available](../output-targets/custom-elements.md#making-assets-available)). + +You can use these standalone components by importing them via: + +```ts +import { MyComponent, defineCustomElementMyComponent } from 'my-design-system' + +// register to CustomElementRegistry +defineCustomElementMyComponent() + +// or extend custom element via +class MyCustomComponent extends MyComponent { + // ... +} +define('my-custom-component', MyCustomComponent) +``` + +To ensure that the right entry file is loaded when importing the project, define different [exports fields](https://nodejs.org/api/packages.html#exports) in your `package.json`: + +```json +{ + "exports": { + ".": { + "import": "./dist/components/index.js", + "types": "./dist/components/index.d.ts" + }, + "./my-component": { + "import": "./dist/components/my-component.js", + "types": "./dist/components/my-component.d.ts" + } + }, + "types": "dist/components/index.d.ts", +} +``` + +This allows us to map certain import paths to specific components within our project and allows users to only import the component code they are interested in and reduce the amount of code that needs to downloaded by the browser, e.g.: + +```js +// this import loads all compiled components +import { MyComponent } from 'my-design-system' +// only import compiled code for MyComponent +import { MyComponent } from 'my-design-system/my-component' +``` + +If you define exports targets for all your components as shown above and by using [`customElementsExportBehavior: 'auto-define-custom-elements'`](../output-targets/custom-elements.md#customelementsexportbehavior) as output target option, you can skip the `defineCustomElement` call and directly import the component where you need it: + +```ts +import 'my-design-system/my-component' +``` + +:::note +If you are distributing both the `dist` and `dist-custom-elements`, then it's best to pick one of them as the main entry depending on which use case is more prominent. +::: + +Read more about various options when it comes to distributing your components as standalone components in the [`dist-custom-elements`](../output-targets/custom-elements.md) output target section. + +The output directory will also contain an `index.js` file which exports some helper methods by default. The contents of the file will look something like: + +```js +export { setAssetPath, setPlatformOptions } from '@stencil/core/internal/client'; +``` + +:::note +The contents may look different if [`customElementsExportBehavior`](../output-targets/custom-elements.md#customelementsexportbehavior) is specified! +::: + +#### Considerations + +The `dist-custom-elements` is a direct build of the custom element that extends `HTMLElement`, without any lazy-loading. This distribution strategy may be preferred for projects that use an external bundler such as [Vite](https://vitejs.dev/), [WebPack](https://webpack.js.org/) or [Rollup](https://rollupjs.org) to compile the application. They ensure that only the components used within your application are bundled into compilation. + +#### Usage in TypeScript + +If you plan to support consuming your component library in TypeScript you'll need to set `generateTypeDeclarations: true` on the output target in your `stencil.config.ts`, like so: + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'dist-custom-elements', + generateTypeDeclarations: true, + }, + // ... + ], + // ... +}; +``` + +Then you can set the `types` property in `package.json` so that consumers of your package can find the type definitions, like so: + +```json title="package.json" +{ + "types": "dist/components/index.d.ts", + "dependencies": { + "@stencil/core": "latest" + }, + ... +} +``` + +:::note +If you set the `dir` property on the output target config, replace `dist/components` in the above snippet with the path set in the config. +::: + +## Publishing to NPM + +[NPM](https://www.npmjs.com/) is an online software registry for sharing libraries, tools, utilities, packages, etc. To make your Stencil project widely available to be consumed, it's recommended to [publish the component library to NPM](https://docs.npmjs.com/getting-started/publishing-npm-packages). Once the library is published to NPM, other projects are able to add your component library as a dependency and use the components within their own projects. diff --git a/versioned_docs/version-v4.22/guides/service-workers.md b/versioned_docs/version-v4.22/guides/service-workers.md new file mode 100644 index 000000000..51f37237a --- /dev/null +++ b/versioned_docs/version-v4.22/guides/service-workers.md @@ -0,0 +1,293 @@ +--- +title: Service Workers +sidebar_label: Service Workers +description: Service Workers +slug: /service-workers +--- + +# Service Workers + +[Service workers](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers) are a very powerful api that is essential for [PWAs](https://blog.ionic.io/what-is-a-progressive-web-app/), but can be hard to use. To help with this, we decided to build support for Service Workers into Stencil itself using [Workbox](https://workboxjs.org/). + +## What is Workbox? + +Workbox is a library that greatly simplifies the Service Worker API. It allows you to quickly generate a service worker that can handle caching static assets, cache remote assets using routes (similar to Express) or even do offline Google Analytics. Because we are built on top of Workbox, you can easily use any of the functionality they offer. For more info on Workbox, [check out their docs](https://developers.google.com/web/tools/workbox/) + +## Usage + +When doing a production build of an app built using Stencil, the Stencil compiler will automatically generate a service worker for you and inject the necessary code to register the service worker in your index.html. Also, because the files Stencil generates are hashed, every time you do a production build and push an update to your app, the service worker will know to update, therefore ensuring your users are never stuck on a stale version of your site. + +Lets run through the steps needed to enable service workers for your project: + +- `cd` into your project +- Run `npm run build` + +And that's it! You should now have an `sw.js` file in your `www` folder and the code to register the service worker in your `www/index.html` file. + +:::note +The component starter by default does not have service workers enabled as a service worker is not needed for component collections +::: + +## Config + +Stencil uses Workbox underneath, and by default generates a service worker from a config object using the `generateSW` mode. Therefore it supports all of the [Workbox generateSW config options](https://developers.google.com/web/tools/workbox/modules/workbox-build#full_generatesw_config). Here is the default config Stencil uses: + +```tsx +{ + globPatterns: [ + '**/*.{js,css,json,html}' + ] +}; +``` + +This configuration does pre-caching of all of your app's assets. + +To modify this config you can use the `serviceWorker` param of your Stencil config. Here is an example: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'www', + serviceWorker: { + globPatterns: [ + '**/*.{js,css,json,html,ico,png}' + ] + } + } + ] +}; +``` + +### Disabling the service worker + +If you do not want a service worker to be generated during the build, this can be turned off. To disable this feature, set the `serviceWorker` property to `null` in the `www` output target. + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'www', + serviceWorker: null + } + ] +}; +``` + +## Using a custom service worker + +Already have a service worker or want to include some custom code? We support that, too. By specifying a source file for your service worker, Stencil switches to the `injectManifest` mode of Workbox. That gives you full control over your service worker, while still allowing you to automatically inject a precache manifest. + +Let's go through the steps needed for this functionality: + +- First we need to pass the path to our custom service worker to the `swSrc` command in the `serviceWorker` config. Here is an example: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'www', + serviceWorker: { + swSrc: 'src/sw.js' + } + } + ] +}; +``` + +- Now we need to include some boilerplate code in our custom service worker: + +```tsx +// change to the version you get from `npm ls workbox-build` +importScripts('workbox-v4.3.1/workbox-sw.js'); + +// your custom service worker code + +// the precache manifest will be injected into the following line +self.workbox.precaching.precacheAndRoute([]); +``` + +This code imports the Workbox library, creates a new instance of the service worker and tells Workbox where to insert the pre-cache array. + +### Showing a reload toast when an update is available + +When a new service worker is available, by default, it will be downloaded and then go into a state of waiting to be activated. The new service worker won't take over until all tabs of the site are closed and the site is visited again. This is to avoid unexpected behavior from conflicts with files being served from cache, and works well in many cases. + +If you want to give your users the option to immediately access the new update, a common way is to show them a toast that lets them know about the update and offers a "reload" button. The reload let's the new service worker take over, serving the fresh content, and triggers a page reload, to avoid cache issues. + +The following example showcases this in combination with the Ionic framework, but the toast-related code should be easily adaptable to any UI. Add the following to your root component (commonly `app-root.tsx`). + +```tsx +@Listen("swUpdate", { target: 'window' }) +async onServiceWorkerUpdate() { + const registration = await navigator.serviceWorker.getRegistration(); + + if (!registration?.waiting) { + // If there is no waiting registration, this is the first service + // worker being installed. + return; + } + + const toast = await toastController.create({ + message: "New version available.", + buttons: [{ text: 'Reload', role: 'reload' }], + duration: 0 + }); + + await toast.present(); + + const { role } = await toast.onWillDismiss(); + + if (role === 'reload') { + registration.waiting.postMessage("skipWaiting"); + } +} +``` + +The `swUpdate` event is emitted by Stencil every time a new service worker is installed. When a service worker is waiting for registration, the toast is shown. After clicking the reload button, a message is posted to the waiting service worker, letting it know to take over. This message needs to be handled by the service worker; therefore we need to create a custom one (e. g. `src/sw.js`) and add a listener to call `skipWaiting()`. + +```tsx +importScripts("workbox-v4.3.1/workbox-sw.js"); + +self.addEventListener("message", ({ data }) => { + if (data === "skipWaiting") { + self.skipWaiting(); + } +}); + +self.workbox.precaching.precacheAndRoute([]); +``` + +:::note +Don't forget to set `swSrc` in your Stencil config. +::: + +Finally, we want our app to reload when the new service worker has taken over, so that no outdated code is served from the cache anymore. We can use the service worker's `controllerchange` event for that, by attaching an event listener in our root component's `componentWillLoad` lifecycle hook. + +```tsx +componentWillLoad() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker + .getRegistration() + .then(registration => { + if (registration?.active) { + navigator.serviceWorker.addEventListener( + 'controllerchange', + () => window.location.reload() + ); + } + }) + } +} +``` + +### Handle push events + +A common use case for custom service workers is to handle browser push notifications. But before we will be able show push notifications, we first need to use the Notifications API to request permissions from the user to do so. + +```tsx +if ('Notification' in window && 'serviceWorker' in navigator) { + Notification.requestPermission(status => { + // status will either be 'default', 'granted' or 'denied' + console.log(`Notification permissions have been ${status}`); + }); +} +``` + +The current permission status can always be checked using `Notification.permission`. + +To show a notification to the user after being granted permission, we can use the `showNotification` method of our service worker's registration (within our custom service worker). + +```tsx +self.registration.showNotification('Hakuna matata.'); +``` + +Usually we will have a backend that will send out push notifications to clients, and we want our service worker to handle them. To do that, we can register an event listener in our worker for the `push` event. The event will be of type [`PushEvent`](https://developer.mozilla.org/en-US/docs/Web/API/PushEvent) and have a `data` field of type [`PushMessageData`](https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData). + +```tsx +self.addEventListener('push', event => { + console.log(`Push received with data "${event.data.text()}"`); + + const title = 'Push Notification'; + const options = { + body: `${event.data.text()}`, + data: { href: '/users/donald' }, + actions: [ + { action: 'details', title: 'Details' }, + { action: 'dismiss', title: 'Dismiss' }, + ], + }; + + event.waitUntil(self.registration.showNotification(title, options)); +}); +``` + +If the data is a JSON string, then `data.json()` can be used to immediately get the parsed data. The `event.waitUntil` method is used to ensure that the service worker doesn't terminate before the asynchronous `showNotification` operation has completed. + +Furthermore, we will likely want to handle notification clicks. The API provides the events `notificationclick` and `notificationclose` for that. + +```tsx +self.addEventListener('notificationclick', event => { + const notification = event.notification; + const action = event.action; + + if (action === 'dismiss') { + notification.close(); + } else { + // This handles both notification click and 'details' action, + // because some platforms might not support actions. + clients.openWindow(notification.data.href); + notification.close(); + } +}); +``` + +Now our service worker is able to receive and process push notifications, however we still need to register the client with our backend. Browsers provide a push service for that reason, which your app can subscribe to. The subscription object contains an endpoint URL with a unique identifier for each client. You can send your notifications to that URL, encrypted with a public key which is also provided by the subscription object. + +In order to implement this, we first need to get each client to subscribe to the browser's push service, and then send the subscription object to our backend. Then our backend can generate the push notifications, encrypt them with the public key, and send them to the subscription endpoint URL. + +First we will implement a function to subscribe the user to the push service, which as a best practice should be triggered from a user action signalling that they would like to receive push notifications. Assuming that notification permissions have already been granted, the following function can be used for that. + +```tsx +async function subscribeUser() { + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.ready; + + const subscription = await registration.pushManager + .subscribe({ userVisibleOnly: true }) + .catch(console.error); + + if (!subscription) { + return; + } + + // the subscription object is what we want to send to our backend + console.log(subscription.endpoint); + } +} +``` + +We should also check our subscription every time our app is accessed, because the subscription object can change. + +```tsx +self.registration.pushManager.getSubscription().then(subscription => { + if (!subscription) { + // ask the user to register for push + return; + } + + // update the database + console.log(subscription); +}); +``` + +### Further Reading + +* For more information on push notifications and the related APIs please refer to the [Web Fundamentals Introduction to Push Notifications](https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications) and the [MDN Push API docs](https://developer.mozilla.org/en-US/docs/Web/API/Push_API). +* [This Twitter thread by David Brunelle](https://twitter.com/davidbrunelle/status/1073394572980453376) explains how to implement versioning in your PWA in order to handle breaking API changes. The problem here is that your service worker enabled app will continue to serve an outdated (cached) app against your updated API. In order to solve this a version check can be implemented. diff --git a/versioned_docs/version-v4.22/guides/store.md b/versioned_docs/version-v4.22/guides/store.md new file mode 100644 index 000000000..b293435ab --- /dev/null +++ b/versioned_docs/version-v4.22/guides/store.md @@ -0,0 +1,131 @@ +--- +title: 'Store' +sidebar_label: Stencil Store +description: Store +slug: /stencil-store +--- + +# @stencil/store + +[Store](https://github.com/ionic-team/stencil-store) is a lightweight shared state library by the stencil core team. It implements a simple key/value map that efficiently re-renders components when necessary. + +- Lightweight +- Zero dependencies +- Simple API, like a reactive Map +- Best performance + +## Installation + +```bash npm2yarn +npm install @stencil/store --save-dev +``` + +## Example + +**store.ts:** + +```tsx +import { createStore } from "@stencil/store"; + +const { state, onChange } = createStore({ + clicks: 0, + seconds: 0, + squaredClicks: 0 +}); + +onChange('clicks', value => { + state.squaredClicks = value ** 2; +}); + +export default state; +``` + +**component.tsx:** + +```tsx +import { Component, h } from '@stencil/core'; +import state from '../store'; + +@Component({ + tag: 'app-profile', +}) +export class AppProfile { + + componentWillLoad() { + setInterval(() => state.seconds++, 1000); + } + + render() { + return ( + <div> + <p> + <MyGlobalCounter /> + <p> + Seconds: {state.seconds} + <br /> + Squared Clicks: {state.squaredClicks} + </p> + </p> + </div> + ); + } +} + +const MyGlobalCounter = () => { + return ( + <button onClick={() => state.clicks++}> + {state.clicks} + </button> + ); +}; +``` + +## API + +### `createStore<T>(initialState)` + +Create a new store with the given initial state. The type is inferred from `initialState`, or can be passed as the generic type `T`. + +Returns a `store` object with the following properties. + +### `store.state` + +The state object is proxied, I.E. you can directly get and set properties and Store will automatically take care of component re-rendering when the state object is changed. + +### `store.on(event, listener)` + +Add a listener to the store for a certain action. + +### `store.onChange(propName, listener)` + +Add a listener that is called when a specific property changes. + +### `store.get(propName)` + +Get a property's value from the store. + +### `store.set(propName, value)` + +Set a property's value in the store. + +### `store.reset()` + +Reset the store to its initial state. + +### `store.use(...subscriptions)` + +Use the given subscriptions in the store. A subscription is an object that defines one or more of the properties `get`, `set` or `reset`. + + +## Testing + +Like any global state library, state should be reset between each spec test. +Use the `dispose()` API in the `beforeEach` hook. + +```ts +import store from '../store'; + +beforeEach(() => { + store.dispose(); +}); +``` diff --git a/versioned_docs/version-v4.22/guides/style-guide.md b/versioned_docs/version-v4.22/guides/style-guide.md new file mode 100644 index 000000000..1f18b31d1 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/style-guide.md @@ -0,0 +1,269 @@ +--- +title: Stencil Style Guide +sidebar_label: Style Guide +description: Stencil Style Guide +slug: /style-guide +--- + +# Stencil Style Guide + +This is a component style guide created and enforced internally by the core team of Stencil, for the purpose of standardizing Stencil components. This should only be used as a reference for other teams in creating their own style guides. Feel free to modify to your team's own preference. + +:::note +In order to enforce this (or your team's) style guide, we recommend leveraging a static analysis tool like [ESLint](https://eslint.org/). [@stencil-community/eslint-plugin](https://www.npmjs.com/package/@stencil-community/eslint-plugin) provides rules specifically for writing Stencil components. +::: + +:::note +This guide once recommended TSLint as a static analysis tool. TSLint has been deprecated by its maintaining organization in favor of ESLint and is no longer recommended by the Stencil team. +::: + +## File structure + +- One component per file. +- One component per directory. Though it may make sense to group similar components into the same directory, we've found it's easier to document components when each one has its own directory. +- Implementation (.tsx) and styles of a component should live in the same directory. + +Example from ionic-core: + +```bash +├── my-card +│ ├── my-card.ios.css +│ ├── my-card.md.css +│ ├── my-card.css +│ ├── my-card.tsx +│ └── test +│ └── basic +│ ├── e2e.js +│ └── index.html +├── my-card-content +│ ├── my-card-content.ios.css +│ ├── my-card-content.md.css +│ ├── my-card-content.css +│ └── my-card-content.tsx +├── my-card-title +│ ├── my-card-title.ios.css +│ ├── my-card-title.md.css +│ ├── my-card-title.css +``` + + +## Naming +### HTML tag + +#### Prefix +The prefix has a major role when you are creating a collection of components intended to be used across different projects, like [@ionic/core](https://www.npmjs.com/package/@ionic/core). Web Components are not scoped because they are globally declared within the webpage, which means a "unique" prefix is needed to prevent collisions. The prefix also helps to quickly identify the collection a component is part of. Additionally, web components are required to contain a "-" dash within the tag name, so using the first section to namespace your components is a natural fit. + +We do not recommend using "stencil" as prefix, since Stencil DOES NOT emit stencil components, but rather the output is standards compliant web components. + +DO NOT do this: +```markup +<stencil-component> +<stnl-component> +``` + +Instead, use your own naming or brand. For example, [Ionic](https://ionicframework.com/) components are all prefixed with `ion-`. +```markup +<ion-button> +<ion-header> +``` + +#### Name + +Components are not actions, they are conceptually "things". It is better to use nouns instead of verbs, such as "animation" instead of "animating". "input", "tab", "nav", "menu" are some examples. + + +#### Modifiers + +When several components are related and/or coupled, it is a good idea to share the name, and then add different modifiers, for example: + +```markup +<ion-card> +<ion-card-header> +<ion-card-content> +``` + + +### Component (TS class) + +The name of the ES6 class of the component SHOULD NOT have a prefix since classes are scoped. There is no risk of collision. + +```tsx +@Component({ + tag: 'ion-button' +}) +export class Button { ... } + +@Component({ + tag: 'ion-menu' +}) +export class Menu { ... } +``` + + +## TypeScript + +1. **Use private variables and methods as much possible:** They are useful to detect dead code and enforce encapsulation. Note that this is a feature which TypeScript provides to help harden your code, but using `private`, `public` or `protected` does not make a difference in the actual JavaScript output. + +2. **Code with Method/Prop/Event/Component decorators should have JSDocs:** This allows for documentation generation and for better user experience in an editor that has TypeScript intellisense + +## Code organization + +**Newspaper Metaphor from The Robert C. Martin's _Clean Code_** + +:::note +The source file should be organized like a newspaper article, with the highest level summary at the top, and more and more details further down. Functions called from the top function come directly below it, and so on down to the lowest level and most detailed functions at the bottom. This is a good way to organize the source code, even though IDEs make the location of functions less important, since it is so easy to navigate in and out of them. +::: + +### High level example (commented) + +```tsx +@Component({ + tag: 'ion-something', + styleUrls: { + ios: 'something.ios.css', + md: 'something.md.css', + wp: 'something.wp.css' + } +}) +export class Something { + + /** + * 1. Own Properties + * Always set the type if a default value has not + * been set. If a default value is being set, then type + * is already inferred. List the own properties in + * alphabetical order. Note that because these properties + * do not have the @Prop() decorator, they will not be exposed + * publicly on the host element, but only used internally. + */ + num: number; + someText = 'default'; + + /** + * 2. Reference to host HTML element. + * Inlined decorator + */ + @Element() el: HTMLElement; + + /** + * 3. State() variables + * Inlined decorator, alphabetical order. + */ + @State() isValidated: boolean; + @State() status = 0; + + /** + * 4. Public Property API + * Inlined decorator, alphabetical order. These are + * different than "own properties" in that public props + * are exposed as properties and attributes on the host element. + * Requires JSDocs for public API documentation. + */ + @Prop() content: string; + @Prop() enabled: boolean; + @Prop() menuId: string; + @Prop() type = 'overlay'; + + /** + * Prop lifecycle events SHOULD go just behind the Prop they listen to. + * This makes sense since both statements are strongly connected. + * - If renaming the instance variable name you must also update the name in @Watch() + * - Code is easier to follow and maintain. + */ + @Prop() swipeEnabled = true; + + @Watch('swipeEnabled') + swipeEnabledChanged(newSwipeEnabled: boolean, oldSwipeEnabled: boolean) { + this.updateState(); + } + + /** + * 5. Events section + * Inlined decorator, alphabetical order. + * Requires JSDocs for public API documentation. + */ + @Event() ionClose: EventEmitter; + @Event() ionDrag: EventEmitter; + @Event() ionOpen: EventEmitter; + + /** + * 6. Component lifecycle events + * Ordered by their natural call order, for example + * WillLoad should go before DidLoad. + */ + connectedCallback() {} + disconnectedCallback() {} + componentWillLoad() {} + componentDidLoad() {} + componentShouldUpdate(newVal: any, oldVal: any, propName: string) {} + componentWillUpdate() {} + componentDidUpdate() {} + componentWillRender() {} + componentDidRender() {} + + /** + * 7. Listeners + * It is ok to place them in a different location + * if makes more sense in the context. Recommend + * starting a listener method with "on". + * Always use two lines. + */ + @Listen('click', { enabled: false }) + onClick(ev: UIEvent) { + console.log('hi!') + } + + /** + * 8. Public methods API + * These methods are exposed on the host element. + * Always use two lines. + * Public Methods must be async. + * Requires JSDocs for public API documentation. + */ + @Method() + async open(): Promise<boolean> { + // ... + return true; + } + + @Method() + async close(): Promise<void> { + // ... + } + + /** + * 9. Local methods + * Internal business logic. These methods cannot be + * called from the host element. + */ + prepareAnimation(): Promise<void> { + // ... + } + + updateState() { + // ... + } + + /** + * 10. render() function + * Always the last public method in the class. + * If private methods present, they are below public methods. + */ + render() { + return ( + <Host + attribute="navigation" + side={this.isRightSide ? 'right' : 'left'} + type={this.type} + class={{ + 'something-is-animating': this.isAnimating + }} + > + <div class='menu-inner page-inner'> + <slot></slot> + </div> + </Host> + ); + } +} +``` diff --git a/versioned_docs/version-v4.22/guides/typed-components.md b/versioned_docs/version-v4.22/guides/typed-components.md new file mode 100644 index 000000000..243efed31 --- /dev/null +++ b/versioned_docs/version-v4.22/guides/typed-components.md @@ -0,0 +1,46 @@ +--- +title: Typed Components +sidebar_label: Typed Components +description: Typed Components +slug: /typed-components +--- + +# Typed Components + +Web Components generated with Stencil come with type declaration files automatically generated by the Stencil compiler. + +In general, TypeScript declarations provide strong guarantees when consuming components: + +- Ensuring that proper values are passed down as properties +- Code autocompletion in modern IDEs such as VSCode +- Events' details +- Signature of components' methods + +These public types are automatically generated by Stencil in `src/component.d.ts`. +This file allows for strong typing in JSX (just like React) and `HTMLElement` interfaces for each component. + +:::tip +It is recommended that this file be checked in with the rest of your code in source control. +::: + +Because Web Components generated by Stencil are just vanilla Web Components, they extend the `HTMLElement` interface. +For each component a type named `HTML{CamelCaseTag}Element` is registered at the global scope. +This means developers DO NOT have to import them explicitly, just like `HTMLElement` or `HTMLScriptElement` are not imported. + +- `ion-button` => `HTMLIonButtonElement` +- `ion-menu-controller` => `HTMLIonMenuControllerElement` + +```tsx +const button: HTMLIonButtonElement = document.queryElement('ion-button'); +button.fill = 'outline'; +``` + +**IMPORTANT**: always use the `HTML{}Element` interfaces in order to hold references to components. + +## Properties + +This section has moved to [Property Types](../components/properties.md#types) + +### Required Properties + +This section has moved to [Required Properties](../components/properties.md#required-properties) diff --git a/versioned_docs/version-v4.22/guides/vs-code-debugging.md b/versioned_docs/version-v4.22/guides/vs-code-debugging.md new file mode 100644 index 000000000..835a3b66a --- /dev/null +++ b/versioned_docs/version-v4.22/guides/vs-code-debugging.md @@ -0,0 +1,126 @@ +--- +title: Debugging With VS Code +sidebar_label: VS Code Debugging +description: How to debug a Stencil component using VS Code +slug: /vs-code-debugging +--- + +# Debugging With VS Code + +VS Code offers a streamlined debugging experience that can be started with a single click when using [launch configurations](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations). + +If you're unfamiliar with using the VS Code debugger in general, please visit the [VS Code debugger documentation](https://code.visualstudio.com/docs/editor/debugging) for a primer. + +## Requirements for Debugging + +In order for a debugger to function, the Stencil project must be configured to generate source maps for the compiled web components back to the source code. As of Stencil v3, source maps are generated by default, but be sure to double-check the project's Stencil config does not disable this behavior. More information regarding source maps in Stencil can be found in the [project configuration documentation](../config/01-overview.md#sourcemap). + +## Debugging Stencil Components In a Web App + +It's a common use case to want to step through a web component's code as it executes in the browser and VS Code makes that process simple. Combining the debugger with Stencil's dev server in watch mode will allow you to debug changes as they're made. + +### Configuring the VS Code Debugger + +To debug Stencil components as they run in a browser (web app), create (or edit) the `.vscode/launch.json` file with the following configuration: + +```json title=".vscode/launch.json" +{ + ..., + "configurations": [ + ..., + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3333", + "sourceMaps": true, + "sourceMapPathOverrides": { + "*": "${webRoot}/*" + } + } + ] +} +``` + +:::note +If your Stencil project is within a monorepo structure, you may want (or need) to add the `webRoot` option to the above config to point to the Stencil project's directory. For instance, if you have your Stencil project at `/packages/stencil-library`, you would add the following to the config: + +```json +{ + ..., + "webRoot": "${workspaceFolder}/packages/stencil-library", +} +``` + +::: + +This will create a configuration to open a Chrome debugger instance on port 3333 (the default port used by the Stencil dev server). To use this configuration, start a Stencil dev server (running `npm start` in a [Stencil component starter project](https://stenciljs.com/docs/getting-started)) and then run the configuration from the VS Code debugger tab. At this point, any breakpoints set in the component source code will pause browser execution when hit. + +:::note +If your Stencil project is [configured to use a different port](https://stenciljs.com/docs/dev-server#dev-server-config) for the dev server, you will need to update the `url` property in the debugger configuration with the correct port. +::: + +## Debugging Static Site Generation (SSG) + +Static Site Generation, also known as prerendering, executes your components at build time to generate a snapshot of the rendered styles and markup to be efficiently served to search engines and users on first request. + +Since this step runs in a Node.js process instead of a browser, debugging can't be done directly in the browser. However, debugging is straightforward using existing Node.js debugging techniques. + +### Overview + +The `stencil build --prerender` command will first build the hydrate script for a NodeJS environment, then prerender the site using the build. For a production build this is probably ideal. + +However, while debugging you may not need to keep rebuilding the hydrate script, but you only need to debug through the prerendering process. Stencil creates a file in `dist/hydrate` that is used to actually execute your components. + +To only prerender (and avoid rebuilding), you can use the `stencil prerender dist/hydrate/index.js` command, with the path to the script as a flag. + +### Tips for Debugging Prerendering + +By default, prerendering will start by rendering the homepage, find links within the homepage, and continue to crawl the entire site as it finds more links. While debugging, it might be easier to _not_ crawl every URL in the site, but rather have it only prerender one page. To disable crawling, set the prerender config `crawlUrls: false`. + +Next, you can use the `entryUrls` config to provide an array of paths to prerender, rather than starting at the homepage. + +Additionally, console logs that are printed within the runtime are suppressed while prerendering (otherwise the terminal would be overloaded with logs). By setting `runtimeLogging: true`, the runtime console logs will be printed in the terminal. Below is an example setup for prerender debugging: + +```tsx title="prerender.config.ts" +import { PrerenderConfig } from '@stencil/core'; + +export const config: PrerenderConfig = { + crawlUrls: false, + entryUrls: ['/example'], + hydrateOptions: (_url) => { + return { + runtimeLogging: true, + }; + }, +}; +``` + +### Configuring the VS Code Debugger + +To debug the Stencil prerender process, create (or edit) the `launch.json` file with the following configuration: + +```json title="launch.json" +{ + ..., + "configurations": [ + ..., + { + "type": "node", + "request": "launch", + "name": "Prerender", + "args": [ + "${workspaceFolder}/node_modules/@stencil/core/bin/stencil", + "prerender", + "${workspaceFolder}/dist/hydrate/index.js", + "--max-workers=0", + "--config=${workspaceFolder}/stencil.config.ts" + ], + "protocol": "inspector" + } + ] +} +``` + +This creates a new debugging configuration using the script that hydrates the app. We're starting up the `stencil prerender` command, and providing it a path to where +the hydrate script can be found. Next we're using `--max-workers=0` so we do not fork numerous processes to each of your CPUs which will make it difficult to debug. diff --git a/versioned_docs/version-v4.22/guides/workers.md b/versioned_docs/version-v4.22/guides/workers.md new file mode 100644 index 000000000..f3e5385cf --- /dev/null +++ b/versioned_docs/version-v4.22/guides/workers.md @@ -0,0 +1,284 @@ +--- +title: Web Workers +sidebar_label: Web Workers +description: Web Workers +slug: /web-workers +--- + +# Web Workers + +[Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) are a widely supported technology (Chrome, Firefox, Safari and Edge) that allows JavaScript to execute in a different thread, maximizing the usage of multiple CPUs; but most importantly not blocking the **main thread**. + +The **main thread** is where JavaScript runs by default and has access to the `document`, `window` and other DOM APIs. The problem is that long-running JS prevents the browser from running smooth animations (CSS animations, transitions, canvas, svg...), making your site look frozen. That's why if your application needs to run CPU-intensive tasks, Web Workers are a great help. + + +## When to use Web Workers? + +The first thing to understand is when to use a Web Workers, and when *not* to use them since they come with a set of costs and limitations: + +- There is no access to the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction). This means you cannot interact with `document`, `window` or any elements in the page. +- There is no access to any of the `@stencil/core` APIs. For example, you cannot declare and use a component in a Web Worker, for the same reasons there is **no access to the DOM**. +- A Web Worker has its own **isolated state** since each worker has their own memory space. For example, a variable declared on the main thread cannot be directly referenced from a worker. +- There is an overhead when passing data between workers and the main thread. As a general rule, it's best to minimize the amount of data sent to and from the worker and be mindful if the work to send your data takes more time than doing it on the main thread. +- Communication is always **asynchronous**. Luckily Promises and async/await makes this relatively easy, but it's important to understand that communication between threads is always asynchronous. +- You can **only** pass [primitives](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Primitive_values) and objects that implement the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). Best way to think of it is any data that can be serialized to JSON is safe to use. + +In short, it's generally a good idea to use workers to move logic that is thread-blocking -- or UI-blocking, preventing users from interacting with the page -- into a Web Worker, such as real-time code syntax highlighting. + +## Best Practices when using Web Workers + +- Use pure and functional algorithms in workers. `(input1, input2) => output`. +- The worker logic itself can be as complex as it has to be, however, the input and output data should stay fairly simple. +- Look for ways to reduce passing data between the main thread and worker thread. +- Class instances cannot be passed as data. Instead, only work with data can be JSON serializable. +- Minimize state within the worker, or better yet, completely avoid maintaining any state (e.g., don't put redux inside a worker). +- The cost of a worker should be easily amortized because it would be doing some CPU-intensive jobs. + + +## How vanilla Web Workers "work"? + +The browser comes with a `Worker` API, that works the following way: + +```tsx +const worker = new Worker('/my-worker.js'); +worker.postMessage(['send message to worker']); +worker.onmessage = (ev) => { + console.log('data from worker', ev.data); +}; +``` + +This API, while powerful, is very low level and makes it tedious to write complex apps, since the event-driven paradigm leads easily to [spaghetti-code](https://en.wikipedia.org/wiki/Spaghetti_code), and quickly misses out on strongly-typed functions and data. + +For further information, check out [this fantastic tutorial](https://www.html5rocks.com/en/tutorials/workers/basics/) by our friends at HTML5Rocks. + +A Web Worker also requires the generation of a separate JavaScript bundle, such as the `my-worker.js` file in the example above. This means you usually need extra build scripts and tooling that transpiles and bundles the worker entry point into another `.js` file. Additionally, the main bundle must be able to reference the worker bundle's file location, which is oftentimes a challenge after transpiling, bundling, minifying, filename hashing and deploying to production servers. + +Fortunately, Stencil can help you solve these two problems: + +- Tooling: Transpiling, bundling, hashing, worker url path referencing +- Communication: Converting event-based communication to Promises, while still maintaining types. + +## Web Workers with Stencil + +As we already mention, Stencil's compiler can help you to use workers in production seamlessly. Any TypeScript file within the `src` directory that ends with `.worker.ts` will automatically use a worker. For example: + +**src/stuff.worker.ts:** + +```tsx + +export const sum = async (a: number, b: number) => { + return a + b; +} + +export const expensiveTask = async (buffer: ArrayBuffer) => { + for (let i = 0; i < buffer.length; i++) { + // do a lot of processing + } + return buffer; +}; +``` + +**src/my-app/my-app.tsx:** +```tsx +import { Component } from '@stencil/core'; + +// Import the worker directly. +// Stencil will automatically create +// a proxy and run the module in a worker. +// IDEs and TypeScript will treat this import +// no differently than any other ESM import. +import { sum, expensiveTask } from '../../stuff.worker'; + +@Component({ + tag: 'my-cmp' +} +export class MyApp { + + async componentWillLoad() { + // sum() will run inside a worker! and the result is a Promise<number> + const result = await sum(1, 2); + console.log(result); // 3 + + // expensiveTask() will not block the main thread, + // because it runs in parallel inside the worker. + // Note that the functions must be async. + const newBuffer = await expensiveTask(buffer); + console.log(newBuffer); + } +} +``` + + +Under the hood, Stencil compiles a worker file and uses the standard `new Worker()` API to instantiate the worker. Then it creates proxies for each of the exported functions, so developers can interact with it using [structured programming constructs](https://en.wikipedia.org/wiki/Structured_programming) instead of event-based ones. + +:::note +Workers are already placed in a different chunk, and dynamically loaded using `new Worker()`. You should avoid using a dynamic `import()` to load them, as this will cause two network requests. Instead, use ES module imports as it's only importing the proxies for communicating with the worker. +::: + +### Imports within a worker + +Normal `ESM` imports are possible when building workers in Stencil. Under the hood, the compiler bundles all the dependencies of a worker into a single file that becomes the worker's entry-point, a dependency-free file that can run without problems. + +**src/loader.worker.ts:** + +```tsx +import upngjs from 'upng-js'; +import { Images } from './materials'; + +export const loadTexture = async (imagesSrcs: Images) => { + const images = await Promise.all( + imagesSrcs.map(loadOriginalImage) + ); + return images; +} + +async function loadOriginalImage(src: string) { + const res = await fetch(src); + const png = upngjs.decode(await res.arrayBuffer()); + return png; +} +``` + +In this example, we are building a worker called `loader.worker.ts` that imports an NPM dependency (`upngjs`, used to parse png files), and a local module (`./materials`). Stencil will use [Rollup](https://rollupjs.org/guide/en/) to bundle all dependencies and remove all imports at runtime. Be aware that code will be duplicated if imported inside and outside a worker. + +#### Dynamic imports + +In order to load scripts dynamically inside of a worker, Web Workers come with a handy API, [`importScript()`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts). + +Here's an example of how to use `typescript` directly from a CDN with `importScript()`. + +```tsx +importScripts("https://cdn.jsdelivr.net/npm/typescript@latest/lib/typescript.js"); +``` + +:::note +Do not use `importScript()` to import NPM dependencies you have installed using `npm` or `yarn`. Use normal ES module imports as usual, so the bundler can understand it. +::: + +### Worker Callbacks + +In most cases, waiting for a Promise to resolve with the output data is all we'll need. However, a limitation with native Promises is that it provides only one returned value. Where a traditional callback still shines is that it can be called numerous times with different data. + +Let's say that we have a long running process that may take a few seconds to complete. With a Promise, we're unable to periodically receive the progress of the task, since all we can do is wait for Promise to resolve. + +A feature with Stencil's worker is the ability to pass a callback to the method, and within the worker, execute the callback as much as it's needed before the task resolves. + +In the example below, the task is given a number that it counts down from the number provided, and the task completes when it gets to `0`. During the count down, however, the main thread will still receive an update every second. This example will console log from `5` to `0` + + +**src/countdown.worker.ts:** + +```tsx +export const countDown = (num: number, progress: (p: number) => void) => { + return new Promise(resolve => { + const tmr = setInterval(() => { + num--; + if (num > 0) { + progress(num); + } else { + clearInterval(tmr); + resolve(num); + } + }, 1000); + }); +}; +``` + +**src/my-app/my-app.tsx:** +```tsx +import { Component } from '@stencil/core'; +import { countDown } from '../countdown.worker'; + +@Component({ + tag: 'my-cmp' +} +export class MyApp { + + componentWillLoad() { + const startNum = 5; + console.log('start', startNum); + + countDown(startNum, (p) => { + console.log('progress', p); + }).then(result => { + console.log('finish', result); + }); + } +} +``` + +When executed, the result would take 5 seconds and would log: + +``` +start 5 +progress 4 +progress 3 +progress 2 +progress 1 +finish 0 +``` + +## Advanced cases + +Sometimes it might be necessary to access the actual [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) instance, because manual usage of the [`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage) and [`onmessage`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/onmessage) is desired. However, there's still a tooling challenge in having to bundle the worker, and have the main bundle correctly reference the worker bundle url path. In that case, Stencil also has an API that exposes the worker directly so it can be used instead of the proxies mentioned early. + +For a direct worker reference, add `?worker` at the end of an ESM import. This virtual ES module will export: +- `worker`: The actual Worker instance. +- `workerPath`: The path to the worker's entry-point (usually a path to a `.js` file). +- `workerName`: The name of the worker, useful for debugging purposes. + + +**src/my-app/my-app.tsx:** + +```tsx +import { Component } from '@stencil/core'; +import { sum } from '../../stuff.worker'; + +// Using the ?worker query, allows to access the worker instance directly. +import { worker } from '../../stuff.worker.ts?worker'; + +@Component({ + tag: 'my-cmp' +} +export class MyApp { + + componentWillLoad() { + // Use worker api directly + worker.postMessage(['send data manually']); + + // Use the proxy + const result = await sum(1, 2); + console.log(result); // 3 + } +} +``` + +You can even use this feature you create multiple Worker manually: + +```tsx +import { workerPath } from '../../stuff.worker.ts?worker'; + +const workerPool = [ + new Worker(workerPath), + new Worker(workerPath), + new Worker(workerPath), + new Worker(workerPath), +]; +``` + +In this example, we exclusively take advantage of the bundling performed by the compiler to obtain the `workerPath` to the worker's entry point, then manually create a pool of workers. + +:::note +Stencil will not instantiate a worker if it's unused, it takes advantage of tree-shaking to do this. +::: + +#### Worker Termination + +Any Web Workers can be terminated using the [`Worker.terminate()`](https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate) API, but since Stencil creates one worker shared across all the proxied methods, it's not recommended to terminate it manually. If you have a use-case for using `terminate` and rebuilding workers, then we recommend using the `workerPath` and creating a new Worker directly: + +```tsx +import { workerPath } from '../../stuff.worker.ts?worker'; +const worker = new Worker(workerPath); +// ... +worker.terminate() +``` diff --git a/versioned_docs/version-v4.22/introduction/01-overview.md b/versioned_docs/version-v4.22/introduction/01-overview.md new file mode 100644 index 000000000..a05312401 --- /dev/null +++ b/versioned_docs/version-v4.22/introduction/01-overview.md @@ -0,0 +1,48 @@ +--- +title: Stencil - A Compiler for Web Components +sidebar_label: Overview +description: Stencil has a number of add-ons that you can use with the build process. +slug: /introduction +--- + +# Overview + +## Stencil: A Web Components Compiler + +Stencil is a compiler that generates Web Components (more specifically, Custom Elements). Stencil combines the best concepts of the most popular frameworks into a simple build-time tool. + +Stencil uses TypeScript, JSX, and CSS to create standards-compliant Web Components that can be used to craft high quality component libraries. + +Web Components generated with Stencil can be used with popular frameworks right +out of the box. In addition, Stencil can generate framework-specific wrappers that +allow Stencil components to be used with a framework-specific developer experience. + +Compared with using the [Custom Elements +APIs](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) +directly, Stencil provides [convenient APIs](../components/api.md) which make writing fast +components simpler. With a Virtual DOM, JSX, and async rendering, it is easy to +create fast and powerful components which are still 100% compatible with Web +Components standards. In addition to making it easier to author Custom +Elements, Stencil also adds a number of key capabilities on top of Web +Components, such as prerendering and objects-as-properties (instead of just +strings). + +The developer experience is also tuned, and comes with live reload and a small dev server baked in to the compiler. + +## How can I use Stencil? + +### Design Systems & Component Libraries + +Stencil's primary objective is providing amazing tools for design systems and component libraries. Components as a concept provide similar language for engineers and designers to have productive conversations about design implementation. [Visit the Stencil for Design Systems page to learn more.](../guides/design-systems.md) + +## The History of Stencil + +Stencil was originally created by the **[Ionic Framework](http://ionicframework.com/)** team in order to build faster, more capable components that worked across every major framework. + +The emergence of Progressive Web Apps as a rapidly growing target for web developers demanded a different approach to web app development performance. With Ionic's classic use of traditional frameworks and bundling techniques, the team was struggling to meet latency and code size demands for Progressive Web Apps that ran equally well on fast and slow networks, across a diversity of platforms and devices. + +Additionally, framework fragmentation had created a web development interoperability nightmare, where components built for one framework didn't work with another framework. + +Web Components offered a solution to both problems, pushing more work to the browser for better performance, and targeting a standards-based component model that all frameworks could use. + +Web Components by themselves, however, weren't enough. Building fast web apps required innovations that were previously locked up inside of traditional web frameworks. Stencil was built to pull these features out of traditional frameworks and bring them to the fast emerging Web Component standard. While Stencil is intended to be used primarily to build design systems and component libraries, these innovations allowed entire applications to be built using only Stencil. diff --git a/versioned_docs/version-v4.22/introduction/02-goals-and-objectives.md b/versioned_docs/version-v4.22/introduction/02-goals-and-objectives.md new file mode 100644 index 000000000..7538e2a8b --- /dev/null +++ b/versioned_docs/version-v4.22/introduction/02-goals-and-objectives.md @@ -0,0 +1,31 @@ +--- +title: Stencil Goals and Objectives +sidebar_label: Goals and Objectives +description: Stencil aims to combine the best concepts of the most popular frontend frameworks into a compile-time tool rather than run-time tool. +slug: /goals-and-objectives +--- + +# Stencil Goals And Objectives + +Stencil aims to combine the best concepts of the most popular frontend frameworks into a compile-time tool rather than run-time tool. It's important to stress that Stencil's goal is to *not* become or be seen as a "framework", but rather our goal is to provide a great developer experience and tooling expected from a framework, while using web-standards within the browser at run-time. In many cases, Stencil can be used as a drop in replacement for traditional frontend frameworks given the capabilities now available in the browser, though using it as such is certainly not required. + +## Web Standards +Components generated by Stencil in the end are built on top of web components, so they work in any major framework or with no framework at all. Additionally, other standards heavily relied on include ES Modules and dynamic imports which have proven to replace traditional bundlers which add unnecessary complexities and run-time JavaScript. By using web-standards, developers can learn and adopt a standard API documented across the world, rather than custom framework APIs that continue to change. + +## Automatic Optimizations +There are countless optimizations and tweaks developers must do to improve performance of components and websites. With a compiler, Stencil is able to analyze component code as an input, and generate optimized components as an output. + +## Future-Friendly +As the world of software development continues to evolve, so too can the compiler. Instead of requiring complete rewrites of components, the compiler can continue to make optimizations using the standard component model as the common input. The compiler allows developers to create future-friendly components, while still staying up-to-date on the latest optimizations without starting over again and again. Additionally, if something changes about any API, the compiler is able to make automatic adjustments and notify the developer exactly what needs to be updated. + +## Run-time Performance +Instead of writing custom client-side JavaScript which every user needs to download and parse for the app to work, Stencil instead prefers to use the already amazing APIs built directly within the browser. These APIs include Custom Elements. + +## Tiny API +Stencil purposely does not come with a large custom API which needs to be learned and re-learned, but rather heavily relies on, you guessed it, web-standards. Again, our goal is to not create yet-another-framework, but rather provide tooling for developers to generate future-friendly components using APIs already baked within the browser. The smaller the API, the easier to learn, and the less that can be broken. + +## Framework Features During Development +If you haven't noticed already we think web-standards are great and offer many benefits. While using web-standards without any structure is certainly possible, and there are actually many use-cases where this would be appropriate, we found that as apps and teams scale it quickly becomes difficult to manage. Developers often gravitate to frameworks because of their great tooling, defined structure, and ability to allow developers to build apps quickly. One of the largest goals of Stencil is to be that intersection of having great framework features and first-class tooling during development but generating future-proof web-standard code, rather than custom framework specific code. + +## Wide Browser Support +For the small minority of browsers that do not support modern browser features and APIs, Stencil will automatically polyfill them on-demand. What this means is that for browsers that already support the feature natively, they will not have to download and parse any unnecessary JavaScript. The great news is that in today's web landscape, most modern APIs are already shipping for what Stencil requires. diff --git a/versioned_docs/version-v4.22/introduction/03-getting-started.md b/versioned_docs/version-v4.22/introduction/03-getting-started.md new file mode 100644 index 000000000..c7a2bb064 --- /dev/null +++ b/versioned_docs/version-v4.22/introduction/03-getting-started.md @@ -0,0 +1,222 @@ +--- +title: Getting Started +sidebar_label: Getting Started +description: Getting Started +slug: /getting-started +--- + +# Getting Started + +## Starting a New Project + +### Prerequisites +Stencil requires a recent LTS version of [NodeJS](https://nodejs.org/) and npm/yarn. +Make sure you've installed and/or updated Node before continuing. + +### Running the `create-stencil` CLI +The `create-stencil` CLI can be used to scaffold a new Stencil project, and can be run using the following command: + +```bash npm2yarn + npm init stencil +``` + +Stencil can be used to create standalone components, or entire apps. +`create-stencil`, will provide a prompt so that you can choose the type of project to start: + +```text +? Select a starter project. + +Starters marked as [community] are developed by the Stencil +Community, rather than Ionic. For more information on the +Stencil Community, please see github.com/stencil-community + +❯ component Collection of web components that can be + used anywhere + app [community] Minimal starter for building a Stencil + app or website + ionic-pwa [community] Ionic PWA starter with tabs layout and routes +``` + +Selecting the 'component' option will prompt you for the name of your project. +Here, we'll name our project 'my-first-stencil-project': + +```bash +✔ Pick a starter › component +? Project name › my-first-stencil-project +``` + +After hitting `ENTER` to confirm your choices, the CLI will scaffold a Stencil project for us in a directory that matches the provided project name. +Upon successfully creating our project, the CLI will print something similar to the following to the console: + +```bash +✔ Project name › my-first-stencil-project +✔ A new git repo was initialized +✔ All setup in 26 ms + + We suggest that you begin by typing: + + $ cd my-first-stencil-project + $ npm install + $ npm start + + $ npm start + Starts the development server. + + $ npm run build + Builds your project in production mode. + + $ npm test + Starts the test runner. + + Further reading: + + - https://github.com/ionic-team/stencil-component-starter + + Happy coding! 🎈 +``` + +The first section describes a few commands required to finish getting your project bootstrapped. + +```bash npm2yarn +cd my-first-stencil-project +npm install +npm start +``` + +This will change your current directory to `my-first-stencil-project`, install your dependencies for you, and start the development server. + +### Useful Initial Commands + +The second section of the `create-stencil` output describes a few useful commands available during the development process: + +- `npm start` starts a local development server. The development server will open a new browser tab containing your +project's components. The dev-server uses hot-module reloading to update your components in the browser as you modify +them for a rapid feedback cycle. + +- `npm run build` creates a production-ready version of your components. The components generated in this step are not +meant to be used in the local development server, but rather within a project that consumes your components. + +- `npm test` runs your project's tests. The `create-stencil` CLI has created both end-to-end and unit tests when scaffolding your project. + +### Source Control + +As of create-stencil v3.3.0, a new git repository will be automatically created for you when you initialize a project if: +1. git is installed +2. Your project is not created under another git work tree (e.g. if you create a new project in a monorepo, a new git repo will not be created) + +Versions of create-stencil prior to v3.3.0 do not interact with any version control systems (VCS). +If you wish to place your project under version control, we recommend initializing your VCS now. +If you wish to use git, run the following after changing your current directory to the root of your Stencil project: + +```bash +$ git init +$ git add -A +$ git commit -m "initialize project using stencil cli" +``` + +## My First Component + +Stencil components are created by adding a new file with a `.tsx` extension, such as `my-component.tsx`. +The `.tsx` extension is required since Stencil components are built using [JSX](../components/templating-and-jsx.md) and TypeScript. + +When we ran `create-stencil` above, it generated a component, `my-component.tsx`, that can be found in the `src/components/my-component` directory: + +```tsx title="my-component.tsx" +import { Component, Prop, h } from '@stencil/core'; +import { format } from '../../utils/utils'; + +@Component({ + tag: 'my-component', + styleUrl: 'my-component.css', + shadow: true, +}) +export class MyComponent { + @Prop() first: string; + @Prop() middle: string; + @Prop() last: string; + + private getText(): string { + return format(this.first, this.middle, this.last); + } + + render() { + return <div>Hello, World! I'm {this.getText()}</div>; + } +} +``` + +Once compiled, this component can be used in HTML just like any other tag. + +```markup +<my-component first="Stencil" middle="'Don't call me a framework'" last="JS"></my-component> +``` + +When rendered, the browser will display `Hello World! I'm Stencil 'Don't call me a framework' JS`. + +### Anatomy of `my-component` + +Let's dive in and describe what's happening in `my-component`, line-by-line. + +After the import statements, the first piece we see is the [`@Component` decorator](../components/component.md): +```tsx +@Component({ + tag: 'my-component', + styleUrl: 'my-component.css', + shadow: true, +}) +``` +This decorator provides metadata about our component to the Stencil compiler. +Information, such as the custom element name (`tag`) to use, can be set here. +This decorator tells Stencil to: +- Set the [element's name](../components/component.md#tag) to 'my-component' +- [Apply the stylesheet](../components/component.md#styleurl) 'my-component.css' to the component +- Enable [native Shadow DOM functionality](../components/component.md#shadow) for this component + +Below the `@Component()` decorator, we have a standard JavaScript class declaration: + +```tsx +export class MyComponent { +``` + +Within this class is where you'll write the bulk of your code to bring your Stencil component to life. + +Next, the component contains three class members, `first`, `middle` and `last`. +Each of these class members have the [`@Prop()` decorator](../components/properties.md#the-prop-decorator-prop) applied to them: +```ts + @Prop() first: string; + @Prop() middle: string; + @Prop() last: string; +``` +`@Prop()` tells Stencil that the property is public to the component, and allows Stencil to rerender when any of these public properties change. +We'll see how this works after discussing the `render()` function. + +In order for the component to render something to the screen, we must declare a [`render()` function](../components/templating-and-jsx.md#basics) that returns JSX. +If you're not sure what JSX is, be sure to reference the [Using JSX](../components/templating-and-jsx.md) docs. + +The quick idea is that our render function needs to return a representation of the HTML we want to push to the DOM. + +```tsx + private getText(): string { + return format(this.first, this.middle, this.last); + } + + render() { + return <div>Hello, World! I'm {this.getText()}</div>; + } +``` + +This component's `render()` returns a `<div>` element, containing text to render to the screen. + +The `render()` function uses all three class members decorated with `@Prop()`, through the `getText` function. +Declaring private functions like `getText` helps pull logic out of the `render()` function's JSX. + +Any property decorated with `@Prop()` is also automatically watched for changes. +If a user of our component were to change the element's `first`, `middle`, or `last` properties, our component would fire its `render()` function again, updating the displayed content. + +## Updating Stencil + +To get the latest version of @stencil/core you can run: + +```bash npm2yarn +npm install @stencil/core@latest --save-exact +``` diff --git a/versioned_docs/version-v4.22/introduction/_category_.json b/versioned_docs/version-v4.22/introduction/_category_.json new file mode 100644 index 000000000..fa1c06ac8 --- /dev/null +++ b/versioned_docs/version-v4.22/introduction/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Introduction", + "position": 1 +} diff --git a/versioned_docs/version-v4.22/introduction/upgrading-to-stencil-four.md b/versioned_docs/version-v4.22/introduction/upgrading-to-stencil-four.md new file mode 100644 index 000000000..56b492853 --- /dev/null +++ b/versioned_docs/version-v4.22/introduction/upgrading-to-stencil-four.md @@ -0,0 +1,257 @@ +--- +title: Upgrading to Stencil v4.0.0 +description: Upgrading to Stencil v4.0.0 +url: /docs/upgrading-to-stencil-4 +--- + +# Upgrading to Stencil v4.0.0 + +## Getting Started + +We recommend that you only upgrade to Stencil v4 from Stencil v3. +If you're a few versions behind, we recommend upgrading one major version at a time (from v1 to v2, then v2 to v3, finally v3 to v4). +This will minimize the number of breaking changes you have to deal with at the same time. + +For breaking changes introduced in previous major versions of the library, see: +- [Stencil v3 Breaking Changes](https://github.com/ionic-team/stencil/blob/main/BREAKING_CHANGES.md#stencil-v300) +- [Stencil v2 Breaking Changes](https://github.com/ionic-team/stencil/blob/main/BREAKING_CHANGES.md#stencil-two) +- [Stencil v1 Breaking Changes](https://github.com/ionic-team/stencil/blob/main/BREAKING_CHANGES.md#stencil-one) + +For projects that are on Stencil v3, install the latest version of Stencil v4: `npm install @stencil/core@4` + + +## Updating Your Code + +### New Configuration Defaults +Starting with Stencil v4.0.0, the default configuration values have changed for a few configuration options. +The following sections lay out the configuration options that have changed, their new default values, and ways to opt-out of the new behavior (if applicable). + +#### `transformAliasedImportPaths` + +TypeScript projects have the ability to specify a path aliases via the [`paths` configuration in their `tsconfig.json`](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) like so: +```json title="tsconfig.json" +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@utils": ["src/utils/index.ts"] + } + } +} +``` +In the example above, `"@utils"` would be mapped to the string `"src/utils/index.ts"` when TypeScript performs type resolution. +The TypeScript compiler does not however, transform these paths from their keys to their values as a part of its output. +Instead, it relies on a bundler/loader to do the transformation. + +The ability to transform path aliases was introduced in [Stencil v3.1.0](https://github.com/ionic-team/stencil/releases/tag/v3.1.0) as an opt-in feature. +Previously, users had to explicitly enable this functionality in their `stencil.config.ts` file with `transformAliasedImportPaths`: +```ts title="stencil.config.ts - enabling 'transformAliasedImportPaths' in Stencil v3.1.0" +import { Config } from '@stencil/core'; + +export const config: Config = { + transformAliasedImportPaths: true, + // ... +}; +``` + +Starting with Stencil v4.0.0, this feature is enabled by default. +Projects that had previously enabled this functionality that are migrating from Stencil v3.1.0+ may safely remove the flag from their Stencil configuration file(s). + +For users that run into issues with this new default, we encourage you to file a [new issue on the Stencil GitHub repo](https://github.com/ionic-team/stencil/issues/new?assignees=&labels=&projects=&template=bug_report.yml&title=bug%3A+). +As a workaround, this flag can be set to `false` to disable the default functionality. +```ts title="stencil.config.ts - disabling 'transformAliasedImportPaths' in Stencil v4.0.0" +import { Config } from '@stencil/core'; + +export const config: Config = { + transformAliasedImportPaths: false, + // ... +}; +``` + +For more information on this flag, please see the [configuration documentation](../config/01-overview.md#transformaliasedimportpaths) + +#### `transformAliasedImportPathsInCollection` + +Introduced in [Stencil v2.18.0](https://github.com/ionic-team/stencil/releases/tag/v2.18.0), `transformAliasedImportPathsInCollection` is a configuration flag on the [`dist` output target](../output-targets/dist.md#transformaliasedimportpathsincollection). +`transformAliasedImportPathsInCollection` transforms import paths, similar to [`transformAliasedImportPaths`](#transformaliasedimportpaths). +This flag however, only enables the functionality of `transformAliasedImportPaths` for collection output targets. + +Starting with Stencil v4.0.0, this flag is enabled by default. +Projects that had previously enabled this functionality that are migrating from Stencil v2.18.0+ may safely remove the flag from their Stencil configuration file(s). + +For users that run into issues with this new default, we encourage you to file a [new issue on the Stencil GitHub repo](https://github.com/ionic-team/stencil/issues/new?assignees=&labels=&projects=&template=bug_report.yml&title=bug%3A+). +As a workaround, this flag can be set to `false` to disable the default functionality. +```ts title="stencil.config.ts - disabling 'transformAliasedImportPathsInCollection' in Stencil v4.0.0" +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'dist', + transformAliasedImportPathsInCollection: false, + }, + // ... + ] + // ... +}; +``` + +For more information on this flag, please see the [`dist` output target's documentation](../output-targets/dist.md#transformaliasedimportpathsincollection). + +### In Browser Compilation Support Removed + +Prior to Stencil v4.0.0, components could be compiled from TSX to JS in the browser. +This feature was seldom used, and has been removed from Stencil. +At this time, there is no replacement functionality. +For additional details, please see the [request-for-comment](https://github.com/ionic-team/stencil/discussions/4134) on the Stencil GitHub Discussions page. + +### Legacy Context and Connect APIs Removed + +Previously, Stencil supported `context` and `connect` as options within the `@Prop` decorator. +Both of these APIs were deprecated in Stencil v1 and are now removed. + +```ts +@Prop({ context: 'config' }) config: Config; +@Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl: Lazy<MenuController>; +``` +To migrate away from usages of `context`, please see [the original deprecation announcement](https://github.com/ionic-team/stencil/blob/main/BREAKING_CHANGES.md#propcontext). +To migrate away from usages of `connect`, please see [the original deprecation announcement](https://github.com/ionic-team/stencil/blob/main/BREAKING_CHANGES.md#propconnect). + +### Legacy Browser Support Removed + +In Stencil v3.0.0, we announced [the deprecation of IE 11, pre-Chromium Edge, and Safari 10 support](https://github.com/ionic-team/stencil/blob/1a8ff39073a88d1372beaa98434dbe2247f68a85/BREAKING_CHANGES.md?plain=1#L78). +In Stencil v4.0.0, support for these browsers has been dropped (for a full list of supported browsers, please see our [Browser Support policy](../reference/support-policy.md#browser-support)). + +By dropping these browsers, a few configuration options are no longer valid in a Stencil configuration file: + +#### `__deprecated__cssVarsShim` + +The `extras.__deprecated__cssVarsShim` option caused Stencil to include a polyfill for [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/--*). +This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). + +#### `__deprecated__dynamicImportShim` + +The `extras.__deprecated__dynamicImportShim` option caused Stencil to include a polyfill for +the [dynamic `import()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) +for use at runtime. +This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). + +#### `__deprecated__safari10` + +The `extras.__deprecated__safari10` option would patch ES module support for Safari 10. +This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). + +#### `__deprecated__shadowDomShim` + +The `extras.__deprecated__shadowDomShim` option would check whether a shim for [shadow +DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) +was needed in the current browser, and include one if so. +This field should be removed from a project's Stencil configuration file (`stencil.config.ts`). + +### Legacy Cache Stats Config Flag Removed + +The `enableCacheStats` flag was used in legacy behavior for caching, but has not been used for some time. This +flag has been removed from Stencil's API and should be removed from a project's Stencil configuration file (`stencil.config.ts`). + +### Drop Node 14 Support + +Stencil no longer supports Node 14. +Please upgrade local development machines, continuous integration pipelines, etc. to use Node v16 or higher. +For the full list of supported runtimes, please see [our Support Policy](../reference/support-policy.md#javascript-runtime). + +## Information Included in `docs-json` Expanded + +For Stencil v4 the information included in the output of the `docs-json` output +target was expanded to include more information about the types of properties +and methods on Stencil components. + +For more context on this change, see the [documentation for the new `supplementalPublicTypes`](../documentation-generation/docs-json.md#supplementalpublictypes) +option for the JSON documentation output target. + +### `JsonDocsEvent` + +The JSON-formatted documentation for an `@Event` now includes a field called +`complexType` which includes more information about the types referenced in the +type declarations for that property. + +Here's an example of what this looks like for the [ionBreakpointDidChange +event](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/modal/modal.tsx#L289-L292) +on the `Modal` component in Ionic Framework: + +```json +{ + "complexType": { + "original": "ModalBreakpointChangeEventDetail", + "resolved": "ModalBreakpointChangeEventDetail", + "references": { + "ModalBreakpointChangeEventDetail": { + "location": "import", + "path": "./modal-interface", + "id": "src/components/modal/modal.tsx::ModalBreakpointChangeEventDetail" + } + } + } +} +``` + +### `JsonDocsMethod` + +The JSON-formatted documentation for a `@Method` now includes a field called +`complexType` which includes more information about the types referenced in +the type declarations for that property. + +Here's an example of what this looks like for the [open +method](https://github.com/ionic-team/ionic-framework/blob/1f0c8049a339e3a77c468ddba243041d08ead0be/core/src/components/select/select.tsx#L261-L313) +on the `Select` component in Ionic Framework: + +```json +{ + "complexType": { + "signature": "(event?: UIEvent) => Promise<any>", + "parameters": [ + { + "tags": [ + { + "name": "param", + "text": "event The user interface event that called the open." + } + ], + "text": "The user interface event that called the open." + } + ], + "references": { + "Promise": { + "location": "global", + "id": "global::Promise" + }, + "UIEvent": { + "location": "global", + "id": "global::UIEvent" + }, + "HTMLElement": { + "location": "global", + "id": "global::HTMLElement" + } + }, + "return": "Promise<any>" + } +} +``` + +## Additional Packages + +To ensure the proper functioning of other `@stencil/` packages, it is advisable for projects utilizing any of the packages mentioned below to upgrade to the minimum package version specified. + +| Package | Minimum Package Version | GitHub | Documentation | +|----------------------------------|--------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|-------------------------------------------------------------| +| `@stencil/angular-output-target` | [0.7.1](https://github.com/ionic-team/stencil-ds-output-targets/releases/tag/%40stencil%2Fangular-output-target%400.7.1) | [GitHub](https://github.com/ionic-team/stencil-ds-output-targets) | [Stencil Doc Site](../framework-integration/angular.md) | +| `@stencil/sass` | [3.0.4](https://github.com/ionic-team/stencil-sass/releases/tag/v3.0.4) | [GitHub](https://github.com/ionic-team/stencil-sass) | [GitHub README](https://github.com/ionic-team/stencil-sass) | +| `@stencil/store` | [2.0.8](https://github.com/ionic-team/stencil-store/releases/tag/v2.0.8) | [GitHub](https://github.com/ionic-team/stencil-store) | [Stencil Doc Site](../guides/store.md) | +| `@stencil/react-output-target` | [0.5.1](https://github.com/ionic-team/stencil-ds-output-targets/releases/tag/%40stencil%2Freact-output-target%400.5.1) | [GitHub](https://github.com/ionic-team/stencil-ds-output-targets) | [Stencil Doc Site](../framework-integration/react.md) | +| `@stencil/vue-output-target` | [0.8.6](https://github.com/ionic-team/stencil-ds-output-targets/releases/tag/%40stencil%2Fvue-output-target%400.8.6) | [GitHub](https://github.com/ionic-team/stencil-ds-output-targets) | [Stencil Doc Site](../framework-integration/vue.md) | + +## Need Help Upgrading? + +Be sure to look at the Stencil [v4.0.0 Breaking Changes Guide](https://github.com/ionic-team/stencil/blob/main/BREAKING_CHANGES.md#stencil-v400). + +If you need help upgrading, please post a thread on the [Stencil Discord](https://chat.stenciljs.com). diff --git a/versioned_docs/version-v4.22/output-targets/01-overview.md b/versioned_docs/version-v4.22/output-targets/01-overview.md new file mode 100644 index 000000000..550880879 --- /dev/null +++ b/versioned_docs/version-v4.22/output-targets/01-overview.md @@ -0,0 +1,68 @@ +--- +title: Stencil Output Targets +sidebar_label: Overview +description: Stencil Output Targets +slug: /output-targets +--- + +# Output Targets + +One of the more powerful features of the compiler is its ability to generate various builds depending on _"how"_ the components are going to be used. Stencil is able to take an app's source and compile it to numerous targets, such as a webapp to be deployed on an http server, as a third-party component lazy-loaded library to be distributed on [npm](https://www.npmjs.com/), or a vanilla custom elements bundle. By default, Stencil apps have an output target type of `www`, which is best suited for a webapp. + + +## Output Target Types: + - [`dist`: Distribution](./dist.md) + - [`www`: Website](./www.md) + - [`dist-custom-elements`: Custom Elements](./custom-elements.md) + +## Example: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'dist' + }, + { + type: 'www' + } + ] +}; +``` + +## Primary Package Output Target Validation + +If `validatePrimaryPackageOutputTarget: true` is set in your project's [Stencil config](../config/01-overview.md#validateprimarypackageoutputtarget) Stencil will +attempt to validate certain fields in your `package.json` that correspond with the generated distribution code. Because Stencil can output many different formats +from a single project, it can only validate that the `package.json` has field values that align with one of the specified output targets in your project's config. +So, Stencil allows you to designate which output target should be used for this validation and thus which will be the default distribution when bundling your +project. + +This behavior only affects a small subset of output targets so a flag exists on the following targets that are eligible for this level of validation: `dist`, `dist-types`, +`dist-collection`, and `dist-custom-elements`. For any of these output targets, you can configure the target to be validated as follows: + +```ts title='stencil.config.ts' +import { Config } from '@stencil/core'; + +export const config: Config = { + ..., + outputTargets: [ + { + type: 'dist', + // This flag is what tells Stencil to use this target for validation + isPrimaryPackageOutputTarget: true, + ... + }, + ... + ], + // If this is not set, Stencil will not validate any targets + validatePrimaryPackageOutputTarget: true, +}; +``` + +:::note +Stencil can only validate one of these output targets for your build. If multiple output targets are marked for validation, Stencil will use +the first designated target in the array and ignore all others. +::: diff --git a/versioned_docs/version-v4.22/output-targets/_category_.json b/versioned_docs/version-v4.22/output-targets/_category_.json new file mode 100644 index 000000000..ae7d15137 --- /dev/null +++ b/versioned_docs/version-v4.22/output-targets/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Output Targets", + "position": 6 +} diff --git a/versioned_docs/version-v4.22/output-targets/copy-tasks.md b/versioned_docs/version-v4.22/output-targets/copy-tasks.md new file mode 100644 index 000000000..c946f92c3 --- /dev/null +++ b/versioned_docs/version-v4.22/output-targets/copy-tasks.md @@ -0,0 +1,160 @@ +--- +title: Stencil Copy Tasks +sidebar_label: Copy Tasks +description: Stencil Copy Tasks +slug: /copy-tasks +--- + + +# Copy Tasks for Output Targets + +All of Stencil's non-documentation output targets +([`dist-custom-elements`](./custom-elements.md), [`dist`](./dist.md), and +[`www`](./www.md)) support a `copy` config which allows you to define file copy +operations which Stencil will automatically perform as part of the build. This +could be useful if, for instance, you had some static assets like images which +should be distributed alongside your components. + +The `copy` attribute on these output targets expects an array of objects corresponding to the following `CopyTask` interface: + +```ts reference title="CopyTask" +https://github.com/ionic-team/stencil/blob/6ed2d4e285544945949ad8e4802fe7f70e392636/src/declarations/stencil-public-compiler.ts#L1594-L1665 +``` + +## Options + +A copy task can take the following options: + +#### `src` + +The source file path for a copy operation. This may be an absolute or relative path to a directory or a file, and may also include a glob pattern. + +If the path is a relative path it will be treated as relative to `Config.srcDir`. + +__Type:__ `string` + +#### `dest` + +An optional destination file path for a copy operation. This may be an absolute or relative path. + +If relative, this will be treated as relative to the output directory for the output target for which this copy operation is configured. + +__Type:__ `string` + +#### `ignore` + +An optional array of glob patterns to exclude from the copy operation. + +__Type:__ `string[]` + +__Default:__ `['**\/__mocks__/**', '**\/__fixtures__/**', '**\/dist/**', '**\/.{idea,git,cache,output,temp}/**', '.ds_store', '.gitignore', 'desktop.ini', 'thumbs.db']` + +#### `warn` + +Whether or not Stencil should issue warnings if it cannot find the specified source files or directories. Defaults to `false`. + +To receive warnings if a copy task source can't be found set this to `true`. + +__Type:__ `boolean` + +#### `keepDirStructure` + +Whether or not directory structure should be preserved when copying files from a source directory. Defaults to `true` if no `dest` path is supplied, else it defaults to `false`. + +If this is set to `false`, all the files from a source directory will be copied directly to the destination directory, but if it's set to `true` they will be copied to a new directory inside the destination directory with the same name as their original source directory. + +So if, for instance, `src` is set to `"images"` and `keepDirStructure` is set to `true` the copy task will then produce the following directory structure: + +``` +images +└── foo.png +dist +└── images + └── foo.png +``` + +Conversely if `keepDirStructure` is set to `false` then files in `images/` will be copied to `dist` without first creating a new subdirectory, resulting in the following directory structure: + +``` +images +└── foo.png +dist +└── foo.png +``` + +If a `dest` path is supplied then `keepDirStructure` will default to `false`, so that Stencil will write the copied files directly into the `dest` directory without creating a new subdirectory. This behavior can be overridden by setting `keepDirStructure` to `true`. + +## Examples + +### Images in the `www` Output Target + +The `copy` config within the following [`www` output target](./www.md) config +will cause Stencil to copy the entire directory from `src/images` to +`www/images`: + +```tsx + outputTargets: [ + { + type: 'www', + copy: [ + { src: 'images' } + ] + } + ] +``` + +In this example, since the `srcDir` property is not set, the default source +directory is `src/`, and since `dest` is not set the contents of `src/images` +will be copied to a new `images` directory in `www`, the default destination +directory for the `www` Output Target. + + +### Setting the `dest` option + +The `dest` property can also be optionally set on a `CopyTask` to either an +absolute path or a path relative to the build directory of the output target. + +In this example we've customized the build directory to be `public` instead of +the default (`'www'`), which, in combination with `dest: 'static/web-fonts'` +will copy the contents of `src/files/fonts` over to `public/static/web-fonts`: + +```tsx + outputTargets: [ + { + type: 'www', + dir: 'public', + copy: [ + { src: 'files/fonts', dest: 'static/web-fonts' } + ] + } + ] +``` + +:::note +When `dest` is set on a `CopyTask` Stencil will, by default, copy all the contents +of the `src` directory to the `dest` directory without creating a new +subdirectory for the contents of `src`. The `keepDirStructure` option can +control this behavior. If it's set to `true` Stencil will always create a +new subdirectory within `dest` with the same name as the `src` directory. In the +above example this would result in the contents of `src/files/fonts` being copied +to `public/static/web-fonts/fonts` instead of `public/static/web-fonts`. + +See the above documentation for the `keepDirStructure` option for more details. +::: + +### Opting-in to warnings + +By default, Stencil will not log a warning if a file or directory specified in +`src` cannot be located. To opt-in to warnings if a copy task source cannot be +found, set `warn: true` in the `CopyTask` object, like so: + +```tsx + outputTargets: [ + { + type: 'dist', + copy: [ + { src: 'fonts', warn: true } + ] + } + ] +``` diff --git a/versioned_docs/version-v4.22/output-targets/custom-elements.md b/versioned_docs/version-v4.22/output-targets/custom-elements.md new file mode 100644 index 000000000..b6fed7bbe --- /dev/null +++ b/versioned_docs/version-v4.22/output-targets/custom-elements.md @@ -0,0 +1,236 @@ +--- +title: Custom Elements with Stencil +sidebar_label: dist-custom-elements +description: Custom Elements with Stencil +slug: /custom-elements +--- + +# Custom Elements + +The `dist-custom-elements` output target creates custom elements that directly extend `HTMLElement` and provides simple utility functions for easily defining these elements on the [Custom Element Registry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry). This output target excels in use in frontend frameworks and projects that will handle bundling, lazy-loading, and custom element registration themselves. + +This target can be used outside of frameworks as well, if lazy-loading functionality is not required or desired. For using lazily loaded Stencil components, please refer to the [dist output target](./dist.md). + +To generate components using the `dist-custom-elements` output target, add it to a project's `stencil.config.ts` file like so: + +```tsx title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + // Other top-level config options here + outputTargets: [ + { + type: 'dist-custom-elements', + // Output target config options here + }, + // Other output targets here + ], +}; +``` + +## Config + +### copy + +_default: `undefined`_ + +An array of [copy tasks](./copy-tasks.md) to be executed during the build process. + +### customElementsExportBehavior + +_default: `'default'`_ + +By default, the `dist-custom-elements` output target generates a single file per component, and exports each of those files individually. + +In some cases, library authors may want to change this behavior, for instance to automatically define component children, provide a single file containing all component exports, etc. + +This config option provides additional behaviors that will alter the default component export _OR_ custom element definition behaviors +for this target. The desired behavior can be set via the following in a project's Stencil config: + +```ts +// stencil.config.ts +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'dist-custom-elements', + customElementsExportBehavior: 'default' | 'auto-define-custom-elements' | 'bundle' | 'single-export-module', + }, + // ... + ], + // ... +}; +``` + +| Option | Description | +| ----------------------------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `default` | No additional re-export or auto-definition behavior will be performed.<br/><br/>This value will be used if no explicit value is set in the config, or if a given value is not a valid option. | +| `auto-define-custom-elements` | A component and its children will be automatically defined with the `CustomElementRegistry` when the component's module is imported. | +| `bundle` | A utility `defineCustomElements()` function is exported from the `index.js` file of the output directory. This function can be used to quickly define all Stencil components in a project on the custom elements registry. | +| `single-export-module` | All component and custom element definition helper functions will be exported from the `index.js` file in the output directory. This file can be used as the root module when distributing your component library, see [Publishing](/publishing) for more details. | + +:::note +At this time, components that do not use JSX cannot be automatically +defined. This is a known limitation of Stencil that users should be aware of. +::: + +### dir + +_default: `'dist/components'`_ + +This config option allows you to change the output directory where the compiled output for this output target will be written. + +### empty + +_default: `true`_ + +Setting this flag to `true` will remove the contents of the [output directory](#dir) between builds. + +### externalRuntime + +_default: `true`_ + +Setting this flag to `true` results in the following behaviors: + +1. Minification will follow what is specified in the [Stencil config](../config/01-overview.md#minifyjs). +2. Filenames will not be hashed. +3. All imports from packages under `@stencil/core/*` will be marked as external and therefore not included in the generated Rollup bundle. + +Ensure that `@stencil/core` is included in your list of dependencies if you set this option to `true`. This is crucial to prevent any runtime errors. + +### generateTypeDeclarations + +_default: `true`_ + +By default, Stencil will generate type declaration files (`.d.ts` files) as a part of the `dist-custom-elements` output target through the `generateTypeDeclarations` field on the target options. Type declaration files will be placed in the `dist/types` directory in the root of a Stencil project. At this time, this output destination is not able to be configured. + +Setting this flag to `false` will not generate type declaration files for the `dist-custom-elements` output target. + +:::note +When set to generate type declarations, Stencil respects the export behavior selected via `customElementsExportBehavior` and generates type declarations specific to the content of the generated [output directory's](#dir) `index.js` file. +::: + +### includeGlobalScripts + +_default: `false`_ + +Setting this flag to `true` will include [global scripts](../config/01-overview.md#globalscript) in the bundle and execute them once the bundle entry point in loaded. + +### isPrimaryPackageOutputTarget + +_default: `false`_ + +If `true`, this output target will be used to validate `package.json` fields for your project's distribution. See the overview of [primary package output target validation](./01-overview.md#primary-package-output-target-validation) +for more information. + +### minify + +_default: `false`_ + +Setting this flag to `true` will cause file minification to follow what is specified in the [Stencil config](../config/01-overview.md#minifyjs). _However_, if [`externalRuntime`](#externalruntime) is enabled, it will override this option and always result in minification being disabled. + +## Making Assets Available + +For performance reasons, the generated bundle does not include [local assets](../guides/assets.md) built within the JavaScript output, +but instead it's recommended to keep static assets as external files. By keeping them external this ensures they can be requested on-demand, rather +than either welding their content into the JS file, or adding many URLs for the bundler to add to the output. +One method to ensure [assets](../guides/assets.md) are available to external builds and http servers is to set the asset path using `setAssetPath()`. + +The `setAssetPath()` function is used to manually set the base path where static assets can be found. +For the lazy-loaded output target the asset path is automatically set and assets copied to the correct +build directory. However, for custom elements builds, the `setAssetPath(path)` should be +used to customize the asset path depending on where they are found on the http server. + +If the component's script is a `type="module"`, it's recommended to use `import.meta.url`, such +as `setAssetPath(import.meta.url)`. Other options include `setAssetPath(document.currentScript.src)`, or using a bundler's replace plugin to +dynamically set the path at build time, such as `setAssetPath(process.env.ASSET_PATH)`. + +```tsx +import { setAssetPath } from 'my-library/dist/components'; + +setAssetPath(document.currentScript.src); +``` + +Make sure to copy the assets over to a public directory in your app. This configuration depends on how your script is bundled, or lack of +bundling, and where your assets can be loaded from. How the files are copied to the production build directory depends on the bundler or tooling. +The configs below provide examples of how to do this automatically with popular bundlers. + +## Example Bundler Configs + +Instructions for consuming the custom elements bundle vary depending on the bundler you're using. These examples will help your users consume your components with webpack and Rollup. + +The following examples assume your component library is published to NPM as `my-library`. You should change this to the name you actually publish your library with. + +Users will need to install your library before importing them. + +```bash npm2yarn +npm install my-library +``` + +### webpack.config.js + +A webpack config will look something like the one below. Note how assets are copied from the library's `node_module` folder to `dist/assets` via the `CopyPlugin` utility. This is important if your library includes [local assets](../guides/assets.md). + +```js +const path = require('path'); +const CopyPlugin = require('copy-webpack-plugin'); + +module.exports = { + entry: './src/index.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'], + }, + ], + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: path.resolve(__dirname, 'node_modules/my-library/dist/my-library/assets'), + to: path.resolve(__dirname, 'dist/assets'), + }, + ], + }), + ], +}; +``` + +### rollup.config.js + +A Rollup config will look something like the one below. Note how assets are copied from the library's `node_module` folder to `dist/assets` via the `rollup-copy-plugin` utility. This is important if your library includes [local assets](../guides/assets.md). + +```js +import path from 'path'; +import commonjs from '@rollup/plugin-commonjs'; +import copy from 'rollup-plugin-copy'; +import postcss from 'rollup-plugin-postcss'; +import resolve from '@rollup/plugin-node-resolve'; + +export default { + input: 'src/index.js', + output: [{ dir: path.resolve('dist/'), format: 'es' }], + plugins: [ + resolve(), + commonjs(), + postcss({ + extensions: ['.css'], + }), + copy({ + targets: [ + { + src: path.resolve(__dirname, 'node_modules/my-library/dist/my-library/assets'), + dest: path.resolve(__dirname, 'dist'), + }, + ], + }), + ], +}; +``` diff --git a/versioned_docs/version-v4.22/output-targets/dist.md b/versioned_docs/version-v4.22/output-targets/dist.md new file mode 100644 index 000000000..5c33af5d2 --- /dev/null +++ b/versioned_docs/version-v4.22/output-targets/dist.md @@ -0,0 +1,106 @@ +--- +title: Distributing Web Components Built with Stencil +sidebar_label: dist +description: Distributing Web Components Built with Stencil +slug: /distribution +--- + +# Distribution Output Target + +The `dist` type is to generate the component(s) as a reusable library that can be self-lazy loading, such as [Ionic](https://www.npmjs.com/package/@ionic/core). When creating a distribution, the project's `package.json` will also have to be updated. However, the generated bundle is tree-shakable, ensuring that only imported components will end up in the build. + +```tsx +outputTargets: [ + { + type: 'dist' + } +] +``` + +## Config + +### collectionDir + +The `collectionDir` config specifies the output directory within the [distribution directory](#dir) where the transpiled output of Stencil components will be written. + +This option defaults to `collection` when omitted from a Stencil configuration file. + +### dir + +The `dir` config specifies the public distribution directory. This directory is commonly the `dist` directory found within [npm packages](https://docs.npmjs.com/getting-started/packages). This directory is built and rebuilt directly from the source files. Additionally, since this is a build target, all files will be deleted and rebuilt after each build, so it's best to always copy source files into this directory. It's recommended that this directory not be committed to a repository. + +This option defaults to `dist` when omitted from a Stencil configuration file. + +### empty + +By default, before each build the `dir` directory will be emptied of all files. To prevent this directory from being emptied, change this value to `false`. + +This flag defaults to `true` when omitted from a Stencil configuration file. + +### isPrimaryPackageOutputTarget + +_default: `false`_ + +If `true`, this output target will be used to validate `package.json` fields for your project's distribution. See the overview of [primary package output target validation](./01-overview.md#primary-package-output-target-validation) +for more information. + +### transformAliasedImportPathsInCollection + +*default: `true`* + +This option will allow [path aliases](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) defined in a project's `tsconfig.json` to be transformed into relative paths in the code output under the [collectionDir](#collectiondir) subdirectory for this output target. This does not affect imports for external packages. + +An example of path transformation could look something like: + +```ts +// Source code +import * as utils from '@utils'; + +// Output code +import * as utils from '../path/to/utils'; +``` + +:::tip +If using the `dist-collection` output target directly, the same result can be achieved using the [`transformAliasedImportPaths`](../output-targets/dist.md#transformaliasedimportpathsincollection) flag on the target's config. +::: + +### esmLoaderPath + +*default: `/dist/loader`* + +Provide a custom path for the ESM loader directory, containing files you can import in an initiation script within your application to register all your components for lazy loading. Read more about the loader directory in the [section below](#loader). + +If you don't use a custom [exports](https://nodejs.org/api/packages.html#exports) map, users would have to import the loader script via: + +```js +import { defineCustomElements } from 'stencil-library/dist/loader' +``` + +By setting `esmLoaderPath` to e.g. `../loader` you can shorten or rename the import path to: + +```js +import { defineCustomElements } from 'stencil-library/loader' +``` + +## Publishing + +Next you can publish your library to [Node Package Manager (NPM)](https://www.npmjs.com/). For more information about setting up the `package.json` file, and publishing, see: [Publishing A Component Library](../guides/publishing.md). + +## Loader + +The `dist` output target generates a loader directory that exports `setNonce`, `applyPolyfills` and `defineCustomElements` helper functions when imported within an ESM context. This allows you to register all components of your library to be used in your project in an application setup script, e.g.: + +```ts +import { applyPolyfills, defineCustomElements, setNonce } from 'stencil-library/loader'; + +// Will set the `nonce` attribute for all scripts/style tags +// i.e. will run styleTag.setAttribute('nonce', 'r4nd0m') +// Obviously, you should use the nonce generated by your server +setNonce('r4nd0m'); + +applyPolyfills().then(() => { + defineCustomElements(); +}); +``` + +This is an alternative approach to e.g. loading the components directly through a script tag as mentioned below. Read more about `setNonce` and when to set it in our guide on [Content Security Policy Nonces](../guides/csp-nonce.md). diff --git a/versioned_docs/version-v4.22/output-targets/www.md b/versioned_docs/version-v4.22/output-targets/www.md new file mode 100644 index 000000000..6c3079364 --- /dev/null +++ b/versioned_docs/version-v4.22/output-targets/www.md @@ -0,0 +1,32 @@ +--- +title: Webapp Output Target +sidebar_label: www +description: Webapp Output Target +slug: /www +--- + +# Webapp Output Target: `www` + +The `www` output target type is oriented for webapps and websites, hosted from an http server, which can benefit from prerendering and service workers, such as this very site you're reading. If the `outputTarget` config is not provided it'll default to having just the `www` type. + +Even if a project is meant to only build a reusable component library, the `www` output target is useful to build out and test the components throughout development. + +```tsx +outputTargets: [ + { + type: 'www' + } +] +``` + +## Config + +| Property | Description | Default | +|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| `baseUrl` | The `baseUrl` represents the site's "base" url to be served from. For example, this site's base url is `https://stenciljs.com/`. However, if the entire site's output is to live within a sub directory, then this directory's path should be the `baseUrl`. For example, Ionic's documentation is a stand-alone Stencil site that lives in the `/docs` directory within `https://ionicframework.com/`. In this example, `https://ionicframework.com/docs/` would be the base url. | `/` | +| `buildDir` | The `buildDir` is the directory of Stencil's generated scripts, such as the component files. For production builds, this directory will contain both `es5` and `esm` builds for each component. (Don't worry, users only request the one their browser needs.) | `build` | +| `dir` | The `dir` config specifies the public web distribution directory. This directory is commonly the root directory of an app to be served, such as serving static files from. This directory is built and rebuilt directly from the source files. Additionally, since this is a build target, all files will be deleted and rebuilt after each build, so it's best to always copy source files into this directory. It's recommended this directory is not committed to a repository. | `www` | +| `empty` | By default, before each build the `dir` directory will be emptied of all files. However, to prevent this directory from being emptied change this value to `false`. | `true` | +| `indexHtml` | The `indexHtml` property represents the location of the root index html file. | `index.html` | +| `serviceWorker` | The `serviceWorker` config lets you customize the service worker that gets automatically generated by the Stencil compiler. To override Stencil's defaults, set any of the values listed in the [Workbox documentation](https://developers.google.com/web/tools/workbox/modules/workbox-build#full_generatesw_config). + diff --git a/versioned_docs/version-v4.22/reference/_category_.json b/versioned_docs/version-v4.22/reference/_category_.json new file mode 100644 index 000000000..75879876e --- /dev/null +++ b/versioned_docs/version-v4.22/reference/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Reference", + "position": 10 +} diff --git a/versioned_docs/version-v4.22/reference/faq.md b/versioned_docs/version-v4.22/reference/faq.md new file mode 100644 index 000000000..eab0321a9 --- /dev/null +++ b/versioned_docs/version-v4.22/reference/faq.md @@ -0,0 +1,192 @@ +--- +title: Stencil Frequently Asked Questions +sidebar_label: FAQ +description: Stencil is a developer-focused toolchain for building reusable, scalable component libraries. +slug: /faq +--- + +# FAQ + +## Introduction + +### What is Stencil? + +Stencil is a developer-focused toolchain for building reusable, scalable component libraries. It provides a compiler that generates highly optimized Web Components, and combines the best concepts of the most popular frameworks into a simple build-time tool. + +Stencil focuses on building components with web standards. It’s used by developers and organizations around the world, and is [100% free and MIT open source](https://github.com/ionic-team/stencil/blob/main/LICENSE.md). + + +### What does Stencil do? + +Stencil helps developers and teams build and share custom components. Since Stencil generates standards-compliant Web Components, the components you build with Stencil will work with many popular frameworks right out of the box, and can even be used without a framework because they are just Web Components. Stencil also enables a number of key capabilities on top of Web Components, in particular, prerendering, and objects-as-properties (instead of just strings). + + +### Who is Stencil for? + +Stencil is for developers and teams that want to build custom component libraries that can be shared across teams, frameworks and large organizations. + +Stencil can also be used by designers who want their original design visions delivered consistently, with high fidelity, to all users. + + +### Who makes Stencil? + +Stencil is an open source project started by the [Ionic core team](https://ionicframework.com/), with contributions also coming from the community. + + +### Why was Stencil created? + +Stencil was created by the Ionic Framework team to make our own component library faster, smaller, and compatible with all major frameworks. Web Components offered a solution by pushing more work to the browser for better performance, and targeting a standards-based component model that all frameworks could use. + + +### Who uses Stencil? + +Stencil was initially developed for Ionic Framework which is a very successful Web Component-based library & UI framework. Web Components are now in thousands of app store apps, and nearly 4 million new Ionic Framework projects are being created every year. + + +### How does Stencil compare to traditional frameworks? + +The Web Component ecosystem has a diverse set of players, each with a different long-term vision for what Web Components can and should do. + +Some think Web Components should replace third-party app frameworks, while others think that Web Components are more suited for leaf/style/design nodes and shouldn’t get in the business of your app’s component system. There are also many framework developers that don’t see the point of Web Components, or consider them to be an affront to front-end innovation. + +With Stencil, our vision is somewhere in the middle. In the long term, we see app development teams continuing to use their framework of choice. We envision these frameworks continuing to get better, smaller, and more efficient, with increasingly good support for targeting and consuming Web Components -- and big teams will be consuming an increasing amount of Web Components as companies continue to embrace them for shared component libraries. + +At the same time, we believe an indispensable feature for Web Components is solving those component distribution and design system problems. We also believe, however, that 90% of the market doesn’t have those problems to begin with, so the current debate about the merits of Web Components is somewhat unproductive. + + +### Why is Stencil considered framework-agnostic? + +Perhaps the most appealing benefit of Web Components is that they give your development teams the flexibility to choose the underlying tools and frameworks - and versions of those frameworks - and tools that they prefer. As pointed out earlier, one of the great challenges of implementing a universal set of components is getting all of your development teams to standardize on just one set of technologies. With Web Components, each team can use what works best for them, giving them complete freedom to use the tools they love—today and tomorrow. + + +## What does Stencil provide? + +### Does Stencil have a component library? + +The most widely used Stencil component library is the Ionic Framework, however, Stencil itself is only a toolchain and does not provide its own component library. We encourage you to first review Ionic components if you are building an application. + + +### Is Stencil a framework? + +Stencil purposely does not strive to act as a stand-alone framework, but rather a tool which allows developers to scale framework-agnostic components across many projects, teams and large organizations. One of Stencil’s superpowers is its flexibility: its components could be used stand-alone, or within traditional frameworks. + + +### Does Stencil come with a testing framework? + +Yes, Stencil provides a rich set of APIs for unit and End-to-end tests. [Learn more about testing with Stencil](../testing/01-overview.md). + + + +## Technology + + +### Why does Stencil use web components? + +By using a consistent set of web standards, Web Components do not depend on a specific framework runtime to execute. As frameworks change their APIs, Web Components do not, allowing for the original source to continue to work natively in a browser. + + +### How is Stencil able to optimize component file size and startup? + +Traditional frameworks provide a runtime API, and developers can pick and choose which APIs to use per component. However, this means every feature needs to be available to every component, just in case the component may or may not use the API. + +With Stencil, the compiler is able to perform static analysis on each component in order to understand which APIs are and are not being used. By doing so, Stencil is able to customize each build to use exactly what each component needs, making for a highly optimized runtime with minimal size. + +Since Stencil uses a compiler, it is able to adjust code as new improvements and features become available. Source code can continue to be written using the same public API and syntax, while the compiler can adjust the code to further take advantage of modern features, without requiring re-writes. + + +### What template syntax does Stencil use? + +Stencil uses [JSX](https://react.dev/learn/writing-markup-with-jsx), a markup language popularized by the React library. + + +### Can Stencil components be lazy loaded? + +Yes! Lazy loading components helps to reduce application startup times, decrease bundle sizes, and improve distribution. + +Because users are able to dynamically load only what is used, startup times are drastically reduced and users only load exactly what their application’s first paint requires. + +At the same time, components built with Stencil can still be imported and consumed by traditional bundlers. + + +### Why are Stencil components written with TypeScript? + +Stencil was originally built for Ionic, and in our experience we’ve found TypeScript to be a valuable tool for maintaining a large codebase across multiple teams. + + +### What dependencies does the Stencil runtime have? + +None. The code generated by Stencil does not rely on Stencil, but rather it generates highly-optimized, framework-free, stand-alone code which runs natively in the browser. + + +### Can data be passed to Web Components? + +Just like any other DOM element in a webpage, any data in the form of arrays, objects, strings and numbers can be passed to element properties. Stencil is designed from the ground up to ensure this capability stays unlocked for application developers. + + +### What technology is Stencil built with? + +The Stencil compiler is built with TypeScript and is [distributed on npm](https://www.npmjs.com/package/@stencil/core). Its distribution includes types, making it easier for developers to use Stencil APIs. + + +## Capabilities + +### Where can Stencil components be used? + +One great advantage of using Web Components is that your component library will work across all projects, not just desktop web apps. + +For example, using a hybrid mobile framework like Ionic, you can deploy Web Components across just about any platform or device, from native iOS and Android apps, to Electron and desktop web apps, and even Progressive Web Apps. + + +### What are the limitations of Web Components? + +The [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components) specs are purposely low-level, and on their own, they do not provide a framework quality developer experience. Web Components run on a fairly primitive set of standards, so you will need a little help to get them to meet your objectives. + +Some limitations include: + +When you try to use pure vanilla Web Components in an application, functionality like server-side rendering and progressive enhancement is not supported by default, and +some out-of-date clients don’t support the Web Components standard. + +In addition, while Web Components technically work with any framework, there are some limitations like lack of type support and input bindings, and challenges passing properties to components, as noted above. + +The good news is that, with help from open source tools like Stencil, you can overcome all of these challenges. Stencil includes framework bindings for Angular, React, and Vue, so you can easily import Web Component libraries into any framework, and interact with them just like they were native to that framework, with all the functionality you’re used to. + + +### What are framework bindings? + +While Web Components can be paired with any JavaScript framework, Stencil has built-in special-purpose bindings to deliver the more advanced features enterprise teams expect when building applications in Angular, React, and Vue. + + +### What features does Stencil add to Web Components? + +Web Components by themselves weren't enough to provide a quality development experience. Building fast web apps required innovations that were previously locked up inside traditional web frameworks. Stencil was built to pull these features out of traditional frameworks and bring them to the fast emerging Web Component standard. + +Compared to using Web Components directly, Stencil provides extra APIs that make writing fast components simpler. APIs like Virtual DOM, JSX, and async rendering make fast, powerful components easy to create, while still maintaining 100% compatibility with Web Components. + + +### What browsers can support Stencil components? + +Stencil works on modern browsers. + +[Learn more about browser support](./support-policy.md#browser-support). + + +## Stencil Project + +### How do I get involved? + +Stencil is an open source project, and we encourage you to contribute. You can start by creating issues on GitHub, submitting feature requests, and helping to replicate bugs. If you’re interested in contributing, please see our [Contributor Guide](https://github.com/ionic-team/stencil/blob/main/.github/CONTRIBUTING.md) and check out our [issue tracker](https://github.com/ionic-team/stencil/issues). + + +### Is Stencil open source? + +Yes, Stencil is open source and its source code can be [found on GitHub](https://github.com/ionic-team/stencil). Contributions are welcomed from the community. + +### Which software license does Stencil use? + +Stencil’s software [license is MIT](https://github.com/ionic-team/stencil/blob/main/LICENSE.md). + + +### Who works on Stencil? + +The majority of the development is done by engineers at [Ionic](https://github.com/ionic-team/ionic). If you’re excited about Stencil, we encourage you to join the community and contribute! Best place to start is on the [discord channel](https://chat.stenciljs.com/). + diff --git a/versioned_docs/version-v4.22/reference/support-policy.md b/versioned_docs/version-v4.22/reference/support-policy.md new file mode 100644 index 000000000..9cbc6428a --- /dev/null +++ b/versioned_docs/version-v4.22/reference/support-policy.md @@ -0,0 +1,155 @@ +--- +title: Support Policy +sidebar_label: Support Policy +description: Support Policy +slug: /support-policy +--- + +# Support Policy + +## Community Maintenance + +Stencil is a 100% open source (MIT) project. Developers can ensure Stencil is the right choice for building web +components through Ionic’s community maintenance strategy. The Stencil team regularly ships new releases, bug fixes, and +is welcoming to community pull requests. + +## Stencil Maintenance and Support Status + +Given the reality of time and resource constraints as well as the desire to keep innovating in the frontend development +space, over time it becomes necessary for the Stencil team to shift focus to newer versions of the library. However, the +Stencil team will do everything it can to make the transition to newer versions as smooth as possible. The Stencil team +recommends updating to the newest version of the Stencil for the latest features, improvements and stability updates. + +The current status of each Stencil version is: + +| Version | Status | Released | Maintenance Ends | Ext. Support Ends | +|:-------:|:----------------:|:------------:|:----------------:|:-----------------:| +| V4 | **Active** | Jun 26, 2023 | TBD | TBD | +| V3 | Extended Support | Jan 25, 2023 | Dec 26, 2023 | Jun 26, 2024 | +| V2 | End of Support | Aug 08, 2020 | Jul 25, 2023 | Jan 25, 2024 | +| V1 | End of Support | Jun 03, 2019 | Aug 08, 2020 | Aug 08, 2020 | + +**Maintenance Period**: Only critical bug and security fixes. No major feature improvements. + +**Extended Support Period**: Available to Stencil Enterprise customers. + +### Stencil Support Details + +Starting with Stencil v2, the Stencil team is adopting a newly revised maintenance policy. When a new major version of +Stencil is released, the previous major version release will enter maintenance mode. While a version of Stencil is in +maintenance mode, only critical bug & security fixes will be applied to that version, and no major feature improvements +will be added. The maintenance period shall last six months from the release of the new major version. + +Once the maintenance period has ended for a version of Stencil, that version enters the extended support period. During +the extended support period, only critical bug and security fixes will be applied for teams and organizations using +Stencil's Enterprise offerings. The extended support period lasts for twelve months from the release of the new major +version. + +The table below describes a theoretical timeline of releases: + +| Version | Status | Released | Maintenance Ends | Ext. Support Ends | +|:-------:|:---------------------:|:------------:|:----------------:|:-----------------:| +| D | Active | Jan 01, 2022 | TBD | TBD | +| C | Maintenance | Jul 07, 2021 | Jul 01, 2022 | Jan 01, 2023 | +| B | Extended Support Only | Jan 01, 2021 | Jan 07, 2022 | Jul 07, 2022 | +| A | End of Support | Jul 07, 2020 | Jul 01, 2021 | Jan 01, 2021 | + +In the example above, when Version D is released, Version C enters maintenance mode. Version D was released on January +1st, 2022. Version C shall be in maintenance mode until July 1st, 2022, three months after the release of Version D. +After July 1st 2022, Version C will be in extended support until Jun 1st, 2023, twelve months after the release of +Version D. + +## Browser Support + +Stencil builds Web Components that run natively in all widely used desktop and mobile browsers. +Custom Elements are natively supported in Chrome, Edge, Firefox, and Safari (including iOS)! + +Stencil supports the following browsers: + +| Stencil Version | Chrome | Edge | Firefox | Safari | Internet Explorer | Pre-Chromium Edge | +|:---------------:|:------:|:----:|:-------:|:------:|:-----------------:|:-----------------:| +| V4 | v79+ | v79+ | v70+ | v14+ | ❌ | ❌ | +| V3 | v79+ | v79+ | v70+ | v14+ | ✅ | ✅ | +| V2 | v60+ | v79+ | v63+ | v10.1+ | ✅ | ✅ | + +## TypeScript Support + +Stencil acts as a compiler for a project's web components, and works closely with the TypeScript compiler to transform +TSX to vanilla JavaScript. To ensure compatibility between the two, Stencil takes an opinionated stance on which version +of the TypeScript compiler must be used. + +Stencil includes a recent copy of the TypeScript compiler in its distributable* to guarantee this compatibility. +The Stencil team is committed to keeping its version of TypeScript up to date and, as of Stencil v2.10.0, attempts to be +within one minor version of the latest TypeScript release. + +The table below describes recent versions of Stencil and the version of TypeScript each version shipped with. + +| Stencil Version | TypeScript Version | +|:---------------:|:------------------:| +| v4.20.0 | v5.5.3 | +| v4.16.0 | v5.4.5 | +| v4.15.0 | v5.4.4 | +| v4.14.0 | v5.4.3 | +| v4.13.0 | v5.4.2 | +| v4.10.0 | v5.3.0 | +| v4.4.0 | v5.2.2 | +| v4.2.0 | v5.1.6 | +| v3.3.0 | v5.0.4 | +| v3.0.0 | v4.9.4 | +| v2.21.0 | v4.9.3 | +| v2.20.0 | v4.8.4 | +| v2.18.0 | v4.7.4 | +| v2.14.0 | v4.5.4 | +| v2.10.0 | v4.3.5 | +| v2.5.0 | v4.2.3 | +| v2.4.0 | v4.1.3 | +| v2.2.0 | v4.0.5 | + +The TypeScript team releases a new minor version of the TypeScript compiler approximately once every three months. To +accomplish its goal of staying within one minor version of the latest release, Stencil will update its version of +TypeScript once every three months as well. Updates to the version of TypeScript will often, but not always, occur in a +[minor version release](./versioning.md#minor-release) of Stencil. + +The Stencil team acknowledges that TypeScript minor version releases may contain breaking changes. The Stencil team will +do everything in its power to avoid propagating breaking changes to its user base. + +\* The TypeScript compiler is never included in the output of your Stencil project, and is only used for compilation +and type checking purposes. + +## Compatibility Recommendations + +Stencil is in many regards an opinionated library, and includes much of the software necessary to get users building web +components as quickly as possible. There are a few pieces of software that Stencil allows users to choose to best fit +their team, organizational structure, and existing technical stack. The Stencil team has compiled a series of +compatibility tables to describe the interoperability requirements of these pieces of software and Stencil. + +### JavaScript Runtime + +| Stencil Version | Node v10 | Node v12 | Node v14 | Node v16 | Node v18 | Node v20 | Node v22 | +|:---------------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| +| V4 | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | +| V3 | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | +| V2 | ❌ | ✅ | ✅ | ✅ | ⚠ | ❌ | ❌ | +| V1 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | + +### Testing Libraries + +#### Jest + +| Stencil Version | Jest v24-26 | Jest v27 | Jest v28 * | Jest v29 * | +|:---------------:|:-----------:|:--------:|:----------:|:-----------:| +| V4 | ✅ | ✅ | ✅ | ✅ | +| V3 | ✅ | ✅ | ❌ | ❌ | +| V2 | ✅ | ✅ | ❌ | ❌ | +| V1 | ✅ | ❌ | ❌ | ❌ | + +\* Support for Jest 28 & 29 has been included since Stencil v4.7.0. + +#### Puppeteer + +| Stencil Version | Puppeteer v5-9 | Puppeteer v10 | Puppeteer v11-21 | +|:---------------:|:--------------:|:-------------:|:----------------:| +| V4 | ❌ | ✅ | ✅ | +| V3 | ❌ | ✅ | ✅ | +| V2 | ✅ | ✅ | ❌ | +| V1 | ✅ | ❌ | ❌ | diff --git a/versioned_docs/version-v4.22/reference/versioning.md b/versioned_docs/version-v4.22/reference/versioning.md new file mode 100644 index 000000000..09d81c7dd --- /dev/null +++ b/versioned_docs/version-v4.22/reference/versioning.md @@ -0,0 +1,44 @@ +--- +title: Versioning +sidebar_label: Versioning +description: Versioning +slug: /versioning +--- + +# Versioning + +Stencil follows the <a href="https://semver.org/" target="_blank">Semantic Versioning (SemVer)</a> convention: +<code>major.minor.patch.</code> Incompatible API changes increment the <code>major</code> version, adding +backwards-compatible functionality increments the <code>minor</code> version, and backwards-compatible bug fixes +increment the <code>patch</code> version. + +## Release Schedule + +### Major Release + +A major release will be published when there is a breaking change introduced in the API. Major releases will occur +roughly every **6 months** and may contain breaking changes. Release candidates will be published prior to a major +release in order to get feedback before the final release. An outline of what is changing and why will be included with +the release candidates. + +### Minor Release + +A minor release will be published when a new feature is added or API changes that are non-breaking are introduced. +We will heavily test any changes so that we are confident with the release, but with new code comes the potential for +new issues*. Minor releases are scheduled to occur at least **once a month**, although this cadence may vary according +to team priorities. + +\* This statement applies to the Stencil team upgrading its version of TypeScript as well. For more information, please +see the team's [support policy regarding TypeScript](./support-policy.md#typescript-support) + +### Patch Release + +A patch release will be published when bug fixes were included, but the API has not changed and no breaking changes were +introduced. Patch releases are scheduled to occur at least **once a month**, although this cadence may vary according +to team priorities. There may be times where patch releases need to released more often than scheduled. + +## Changelog + +To see a list of all notable changes to Stencil, please refer to the [releases +page](https://github.com/ionic-team/stencil/releases). This contains an ordered +list of all bug fixes and new features under each release. diff --git a/versioned_docs/version-v4.22/static-site-generation/01-overview.md b/versioned_docs/version-v4.22/static-site-generation/01-overview.md new file mode 100644 index 000000000..3509f33e2 --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/01-overview.md @@ -0,0 +1,49 @@ +--- +title: Static Site Generation +sidebar_label: Overview +slug: /static-site-generation +--- + +# Static Site Generation with Stencil + +One of the best ways to build fast, interactive web sites and web apps is to utilize Static Site Generation instead of Server Side Rendering (known as SSR) or Client Side Rendering (known as Single Page Apps, or SPAs). + +Static Site Generation (SSG) means building and rendering components and routes at build time (aka prerendering) rather than server request time (SSR) or at client run-time (SPA). Because a route is already prerendered, all of the content for the route is available to search engines and clients _immediately_, so SEO and performance are maximized. + +Static Site Generation doesn't mean your pages have to be and/or _stay_ static! Stencil utilizes hydration to efficiently load client-side components at runtime to get the best of both worlds. + +Since Static Site Generation prerenders components, there are some tradeoffs and things to keep in mind, but most components can be easily prerendered without much modification. + +Stencil makes SSG easy, so read on to see how to incorporate it into your apps. + +## Benefits of Static Site Generation + +- Great [Lighthouse](https://developers.google.com/web/tools/lighthouse/) scores +- Faster time to [Largest Contentful Paint (LCP)](https://web.dev/lcp/) +- Better [Search Engine Optimization (SEO)](https://support.google.com/webmasters/answer/7451184) +- Provides functionality for users with JavaScript disabled + +## How Static Site Generation and Prerendering Works + +**Build Hydrate App**: The first step in prerendering is for the compiler to generate a "hydrate" app, which is a single directory to be used by Node.js. The "hydrate" app is automatically generated when the `--prerender` CLI flag is provided and by default the app is saved to `dist/hydrate`. Prerendering uses the hydrate app internally, however it can be used directly at a lower-level. [Learn more about the Hydrate App](../guides/hydrate-app.md) + +**Fork Prerender Tasks to Available CPUs**: Stencil can efficiently divide out the prerendering to each of the current machine's CPUs using [Node.js' Child Process API](https://nodejs.org/api/child_process.html). By tasking each CPU on the machine, the compiler can drastically speed up prerendering times. + +**Prerender Index**: After the compiler has completed the build and created child processes on each available CPU, it will then kick off the prerendering by starting at the single base URL, or the configured entry URLs. Once the page has finished prerendering it'll be written to the configured `www` directory as an `index.html` file. + +**Crawl App**: During each page prerender, Stencil also collects the anchor elements and URLs used within the page. With this information, it's able to inform the main thread of which pages should be prerendered next. The main thread is in charge of orchestrating all of the URLs, and the job is finished once all of the pages have been crawled and prerendered. + +**Deploy Static Files to Production**: Now that all of the pages have been prerendered and written as static HTML files, the `www` directory can now be deployed to a server. A significant difference from prerendering and Server-side Rendering (SSR), is that the HTTP server is just serving up static HTML files rather than dynamically generating the HTML on the server. + +**Static HTML Response**: With the static HTML files deploy to a server, visitors of each prerendered page first receive the HTML with inline styles, and no blocking JS or CSS. Additionally, the compiler is already aware of the exact modules the visitor will need for this page, and will asynchronously preload the modules using [link `modulepreload`](https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload). + +**Client-side Hydration**: After the HTML and inlined styles have rendered the first paint, the next step is for the same nodes within the DOM to be hydrated by the client-side JavaScript. Each component within the page will asynchronously hydrate using the initial order they were found in the DOM structure. Next, as each component lazily hydrates they're able to reuse the existing nodes found in the DOM. + +## Tooling + +To be clear, Stencil does _not_ use `Puppeteer` or `jsdom` for prerendering. Puppeteer is great for End-to-End +testing, but for performance reasons it's not ideal to quickly generate a large website with hundreds or thousands of pages. Additionally, `jsdom` is often used for unit testing, but in our experience it's difficult to use with async components and its global environment nature. + +Instead, Stencil uses its own internal DOM APIs which strictly follow the web standards, but optimized for prerendering, Static Site Generation and Server-side Rendering. By doing so, developers can still use all the same APIs they're already familiar with, but they'll seamlessly work within a NodeJS environment too. This means developers often do not have to write code differently in how they're building components, but rather they focus only on writing one type of component, and coding it using the standards they already know. To reiterate, developers do not have to learn a new API for prerendering. It's just the same web APIs your components are already using. + +Every component, machine and environment will perform differently, so it's difficult to provide a consistent benchmark. However, what we do know is that [Ionic's Documentation site](https://ionicframework.com/docs) has hundreds of pages and Stencil is able to prerender the entire site in a few seconds. diff --git a/versioned_docs/version-v4.22/static-site-generation/_category_.json b/versioned_docs/version-v4.22/static-site-generation/_category_.json new file mode 100644 index 000000000..000fe5643 --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Static Site Generation", + "position": 4 +} diff --git a/versioned_docs/version-v4.22/static-site-generation/basics.md b/versioned_docs/version-v4.22/static-site-generation/basics.md new file mode 100644 index 000000000..573c5aa52 --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/basics.md @@ -0,0 +1,72 @@ +--- +title: Static Site Generation Basics in Stencil +sidebar_label: Basics +description: Quick introduction to configuring and using Static Site Generation in Stencil +slug: /static-site-generation-basics +--- + +# Static Site Generation Basics + +Rendering components at build time (rather than purely server or client-time), can add significant performance improvements to your app, and maximize SEO impact. + +Using Static Site Generation in Stencil requires running a build command, returning promises from component lifecycle methods that fetch dynamic data, and ensuring all known URLs are properly discovered and built. + +## Static Build + +Stencil doesn't prerender components by default. However, the build can be made to prerender using the `--prerender` flag: + +```bash +stencil build --prerender +``` + +## Rendering Dynamic Data + +Many components need to render based on data fetched from a server. Stencil handles this by allowing components to return `Promise`'s from lifecycle methods like `componentWillLoad` (this can be achieved by using `async/await` as well). + +For example, this is how to have Stencil wait to render a component until it fetches data from the server: + +```typescript +async componentWillLoad() { + const ret = await fetch('https://.../api'); + + this.thing = await ret.json(); +} +``` + +## Integration with a Router + +Since Stencil will actually navigate to and execute components, it has full support for a router, including Stencil Router. + +There are no changes necessary to access route params and matches. However, make sure your routes can accept a trailing slash as prerendered static content will be treated as loading an `index.html` file at that path, and so the browser may append a trailing slash. + +In particular, if using Stencil Router, double check usage of `exact={true}` which could cause your routes to not match when loaded with a trailing slash. + +## Page and URL Discovery + +By default, Stencil crawls your app starting at base URL of `/` and discovers all paths that need to be indexed. By default this will only discover pages that are linked at build time, but can be easily configured to build any possible URL for the app. + +As each page is generated and new links are found, Stencil will continue to crawl and prerender pages. + +See the [prerender config](./prerender-config.md) docs to see how this can be customized further. + + +## Things to Watch For + +There may be some areas of your code that should absolutely not run while prerendering. To help avoid certain code Stencil provides a `Build.isBrowser` build conditional to tell prerendering to skip over. Here is an example of how to use this utility: + +```tsx +import { Build } from '@stencil/core'; + +connectedCallback() { + // Build.isBrowser is true when running in the + // browser and false when being prerendered + + if (Build.isBrowser) { + console.log('running in browser'); + } else { + console.log('running in node while prerendering'); + } +} +``` + +Also note that the actual runtime generated for the browser builds will not include code that has been excluded because of the `if (Build.isBrowser)` statement. In the above example, only `console.log('running in browser')` would be included within the component's runtime. diff --git a/versioned_docs/version-v4.22/static-site-generation/deployment.md b/versioned_docs/version-v4.22/static-site-generation/deployment.md new file mode 100644 index 000000000..b76972bcd --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/deployment.md @@ -0,0 +1,16 @@ +--- +title: Deploying a Static Site +sidebar_label: Deployment +description: Deploying a Static Site +slug: /static-site-generation-deployment +--- + +# Deploying a Stencil Static Site + +Deploying a prerendered static site built with Stencil is exactly like deploying any static site, because the output is just a set of HTML files. + +Every path that Stencil detects (or is provided using `entryUrls` in the prerender config) is generated in the `www` output target's directory, with each url given an `index.html` that becomes the root for the app. + +Think of it as turning every URL in your app into a standalone web page that bootstraps the entire app. No matter what URL a visitor comes to, they will be served an `index.html` file with that page's specific content already rendered, but with the entire app then hydrating and loading. + +This means you can simply deploy the `www` output target's directory to any static host! \ No newline at end of file diff --git a/versioned_docs/version-v4.22/static-site-generation/meta.md b/versioned_docs/version-v4.22/static-site-generation/meta.md new file mode 100644 index 000000000..606e49072 --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/meta.md @@ -0,0 +1,37 @@ +--- +title: SEO Meta Tags in SSG +sidebar_label: Meta tags +description: Managing meta tags for SEO and social media embedding in Stencil Static Sites +slug: /static-site-generation-meta-tags +--- + +# SEO Meta Tags and Static Site Generation + +Web Apps need to list detailed meta information about content in order to maximize SEO and provide good social media embed experiences. + +One of the benefits to Stencil's prerendering is that most DOM apis are available in the NodeJS environment too. +For example, to set the document title, simply run `document.title = "Page Title"`. +Similarly, meta tags can be set using standard DOM APIs as found in the browser, such as `document.head` and `document.createElement('meta')`. +For this reason, your component's runtime can take care of much of this custom work during prerendering. + +That said, the Prerender Config also includes options that allow individual pages to be modified arbitrarily during prerendering. +For example, the `afterHydrate(document, url)` hook can be used to update the parsed `document` before it is serialized into an HTML string. +The `document` argument is a [standard `Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document), while the `url` argument is a [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) for the location of the page being rendered. + +In the example below, the `afterHydrate(document, url)` hook is setting the document title from url's pathname. + +```tsx +import { PrerenderConfig } from '@stencil/core'; + +export const config: PrerenderConfig = { + afterHydrate(document, url) { + document.title = url.pathname; + } +}; +``` + +## @stencil/helmet + +The `@stencil/helmet` package was a library for managing meta tags dynamically. +It has since been deprecated. +For additional information regarding this package, please see its [GitHub page](https://github.com/ionic-team/stencil-helmet) diff --git a/versioned_docs/version-v4.22/static-site-generation/prerender-config.md b/versioned_docs/version-v4.22/static-site-generation/prerender-config.md new file mode 100644 index 000000000..c94144943 --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/prerender-config.md @@ -0,0 +1,106 @@ +--- +title: Prerender Config +sidebar_label: Prerender Config +description: Prerender Config +slug: /prerender-config +--- + + +# Prerender Config for Static Site Generation (SSG) + +As of `1.13.0`, the optional prerender config can be used while prerendering a `www` output target. The `prerender.config.ts` file can further update the parsed document of each page, before it is serialized to HTML. + +Within `stencil.config.ts`, set the path to the prerendering config file path using the `prerenderConfig` +property, such as: + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + outputTargets: [ + { + type: 'www', + baseUrl: 'https://stenciljs.com/', + prerenderConfig: './prerender.config.ts', + } + ] +}; +``` + +Next, inside of the `prerender.config.ts` file, it should export a `config` object using the `PrerenderConfig` interface. + +```tsx +import { PrerenderConfig } from '@stencil/core'; +export const config: PrerenderConfig = { + ... +}; +``` + +| Config | Description | Default | +|--------|-------------|---------| +| `afterHydrate(document, url)` | Run after each `document` is hydrated, but before it is serialized into an HTML string. Hook is passed the `document` and its `URL`. | | +| `beforeHydrate(document, url)` | Run before each `document` is hydrated. Hook is passed the `document` it's `URL`. | | +| `afterSerializeTemplate(html)` | Runs after the template Document object has serialize into an HTML formatted string. Returns an HTML string to be used as the base template for all prerendered pages. | | +| `beforeSerializeTemplate(document)` | Runs before the template Document object is serialize into an HTML formatted string. Returns the Document to be serialized which will become the base template html for all prerendered pages. | | +| `canonicalUrl(url)` | A hook to be used to generate the canonical `<link>` tag which goes in the `<head>` of every prerendered page. Returning `null` will not add a canonical url tag to the page. | | +| `crawlUrls` | While prerendering, crawl same-origin URLs found within `<a href>` elements. | `true` | +| `entryUrls` | URLs to start the prerendering from. By default the root URL of `/` is used. | `['/']` | +| `filterAnchor(attrs, base)` | Return `true` the given `<a>` element should be crawled or not. | | +| `filterUrl(url, base)` | Return `true` if the given URL should be prerendered or not. | | +| `filePath(url, filePath)` | Returns the file path which the prerendered HTML content should be written to. | | +| `hydrateOptions(url)` | Returns the hydrate options to use for each individual prerendered page. | | +| `loadTemplate(filePath)` | Returns the template file's content. The template is the base HTML used for all prerendered pages. | | +| `normalizeUrl(href, base)` | Used to normalize the page's URL from a given a string and the current page's base URL. Largely used when reading an anchor's `href` attribute value and normalizing it into a `URL`. | | +| `staticSite` | Static Site Generated (SSG). Does not include Stencil's client-side JavaScript, custom elements or preload modules. | `false` | +| `trailingSlash` | If the prerendered URLs should have a trailing "/"" or not | `false` | + + +## Individual Page Hydrate Options + +Beyond settings for the entire prerendering process with `prerender.config.ts`, you can also +set individual hydrate options per each page. The `hydrateOptions(url)` hook can be used to further configure each page. Below is an example of the prerender config with the `hydrateOptions()` hook, which returns options for each page. + +```tsx +import { PrerenderConfig } from '@stencil/core'; + +export const config: PrerenderConfig = { + hydrateOptions(url) { + return { + prettyHtml: true + }; + } +}; + + +``` +| Option | Description | Default | +|--------|-------------|---------| +| `addModulePreloads` | Adds `<link rel="modulepreload">` for modules that will eventually be requested. | `true` | +| `approximateLineWidth` | Sets an approximate line width the HTML should attempt to stay within. Note that this is "approximate", in that HTML may often not be able to be split at an exact line width. Additionally, new lines created is where HTML naturally already has whitespace, such as before an attribute or spaces between words. | `100` | +| `canonicalUrl` | Sets the `href` attribute on the `<link rel="canonical">` tag within the `<head>`. If the value is not defined it will ensure a canonical link tag is no included in the `<head>`. | | +| `clientHydrateAnnotations` | Include the HTML comments and attributes used by the client-side JavaScript to read the structure of the HTML and rebuild each component. | `true` | +| `constrainTimeouts` | Constrain `setTimeout()` to 1ms, but still async. Also only allows `setInterval()` to fire once, also constrained to 1ms. | `true` | +| `cookie` | Sets `document.cookie`. | | +| `direction` | Sets the `dir` attribute on the top level `<html>`. | | +| `excludeComponents` | Component tag names listed here will not be prerendered, nor will hydrated on the client-side. Components listed here will be ignored as custom elements and treated no differently than a `<div>`. | | +| `inlineExternalStyleSheets` | External stylesheets from `<link rel="stylesheet">` are instead inlined into `<style>` elements. | `true` | +| `language` | Sets the `lang` attribute on the top level `<html>`. | | +| `maxHydrateCount` | Maximum number of components to hydrate on one page. | `300` | +| `minifyScriptElements` | Minify JavaScript content within `<script>` elements. | `true` | +| `minifyStyleElements` | Minify CSS content within `<style>` elements. | `true` | +| `prettyHtml` | Format the HTML in a nicely indented format. | `false` | +| `referrer` | Sets `document.referrer`. | | +| `removeAttributeQuotes` | Remove quotes from attribute values when possible. | `true` | +| `removeBooleanAttributeQuotes` | Remove the `=""` from standardized `boolean` attributes, such as `hidden` or `checked`. | `true` | +| `removeEmptyAttributes` | Remove these standardized attributes when their value is empty: `class`, `dir`, `id`, `lang`, and `name`, `title`. | `true` | +| `removeHtmlComments` | Remove HTML comments. | `true` | +| `removeScripts` | Removes every `<script>` element found in the `document`. | `false` | +| `removeUnusedStyles` | Removes CSS not used by elements within the `document`. | `true` | +| `resourcesUrl` | The url the runtime uses for the resources, such as the assets directory. | | +| `runtimeLogging` | Prints out runtime console logs to the NodeJS process. | `false` | +| `staticComponents` | Component tags listed here will only be prerendered or server-side rendered and will not be client-side hydrated. This is useful for components that are not dynamic and do not need to be defined as a custom element within the browser. For example, a header or footer component would be a good example that may not require any client-side JavaScript. | | +| `staticDocument` | Entire `document` should be static. This is useful for specific pages that should be static, rather than the entire site. If the whole site should be static, use the `staticSite` property on the prerender config instead. If only certain components should be static then use `staticComponents` instead. | `false` | +| `timeout` | The amount of milliseconds to wait for a page to finish rendering until a timeout error is thrown. | `15000` | +| `title` | Sets `document.title`. | | +| `url` | Sets `location.href`. | | +| `userAgent` | Sets `navigator.userAgent`. | | diff --git a/versioned_docs/version-v4.22/static-site-generation/server-side-rendering-ssr.md b/versioned_docs/version-v4.22/static-site-generation/server-side-rendering-ssr.md new file mode 100644 index 000000000..8f0353c43 --- /dev/null +++ b/versioned_docs/version-v4.22/static-site-generation/server-side-rendering-ssr.md @@ -0,0 +1,19 @@ +--- +title: Combining Server Side Rendering and Static Site Generation +sidebar_label: Server Side Rendering +description: How to combine both Server Side Rendering and Static Site Generation approaches +slug: /static-site-generation-server-side-rendering-ssr +--- + +# Combining Server Side Rendering and Static Site Generation + +Static Site Generation and Server Side Rendering are often confused but are very different approaches to solve the same problem: providing already rendered content to the client before the client has loaded and rendered itself. + +Server Side Rendering (SSR) is the process of rendering content to a client based on an HTTP request. A client makes a request and the server processes it, returning rendered HTML back to the client. The Client then hydrates that HTML and bootstraps the client-side JS app. + +Static Site Generation (SSG) does the rendering at build time instead of request time, so the server does not need to do any additional rendering and requests can be processed very quickly. The process for the client hydrating and bootstrapping is the same, however. + +SSG has limits, and pages that require some server-side processing before rendering won't benefit from using it. However, that set of pages that _truly_ need to be rendered at request time on the server is lower than most would think. For example, instead of using SSR, why not prerender and simply make an API request from the Client? Or configure the server to modify headers without having to run a classic expressjs/etc. server? + +If a page simply _must_ be Server Side Rendered, then that can be done using Stencil's [hydration functionality](../guides/hydrate-app.md) in any Node.js based server. + diff --git a/versioned_docs/version-v4.22/telemetry.md b/versioned_docs/version-v4.22/telemetry.md new file mode 100644 index 000000000..ea1fd18cb --- /dev/null +++ b/versioned_docs/version-v4.22/telemetry.md @@ -0,0 +1,68 @@ +--- +title: Telemetry +description: Stencil Telemetry usage information +--- + +# Stencil CLI telemetry + +As of version 2.7.0, Stencil collects anonymous telemetry data about usage of our command line interface. Participation in this program is optional, and [you may opt-out](#how-do-i-opt-out) if you'd prefer not to share any information. + +### Why Is Telemetry Collected? + +Ionic continues to invest in Stencil to make it the best web component compiler in the market. We spend hundreds of hours every year engaging with our community to learn how we can improve the product and create a more streamlined developer experience. + +However, qualitative conversations are only half the picture. To fully understand the behavior of our users, we also need to look at quantitative data. Telemetry allows us to accurately gauge Stencil feature usage. The combination of qualitative and quantitative data helps us generate the most informed roadmap possible, ensuring Stencil’s growth for years to come. + +### What Is Being Collected? + +We track command line usage including commands and options. Specifically, we track the following anonymously: + +- Command invoked (eg: stencil generate, stencil build, etc) with arguments +- Versions of Stencil and other dependencies like TypeScript +- Output targets +- Stencil packages +- General machine information like OS + +An example telemetry event looks like this: + +```javascript +{ + "yarn": false, + "duration": 2762, + "componentCount": 13, + "targets": [ "www", "dist-lazy", "docs-readme", "docs-vscode" ], + "packages": [ "@capacitor/cli@^3.1.1", "@capacitor/core@^3.1.1", "@stencil/core@latest", "@stencil/store@latest" ], + "arguments": [ "--debug" ], + "task": "build", + "stencil": "2.6.0", + "system": "node 16.4.2", + "os_name": "darwin", + "os_version": "20.5.0", + "cpu_model": "Apple M1", + "build": "20210714145743", + "typescript": "4.2.3", + "rollup": "2.42.3" +} +``` + +### What about Sensitive Data (e.g. Secrets)? + +We do not collect any metrics which may contain sensitive data. This includes, but is not limited to: environment variables, file paths, contents of files, logs, or serialized JavaScript errors. + +### Will This Data Be Shared? + +No. Data collected will not be shared outside of Ionic. The data collected is anonymous, not traceable to the source, and only meaningful in aggregate form. We take your privacy and our security very seriously. Stencil telemetry falls under the [Ionic Privacy Policy](https://ionicframework.com/privacy). + +### How Do I Opt-Out? + +You may opt-out by running the following command in the root of your project directory: + +`npx stencil telemetry off` + +You may check the status of telemetry collection at any time by running the command with no arguments in the root of your project directory: + +`npx stencil telemetry` + +You may re-enable telemetry if you'd like to re-join the program by running the following in the root of your project directory: + +`npx stencil telemetry on` diff --git a/versioned_docs/version-v4.22/testing/01-overview.md b/versioned_docs/version-v4.22/testing/01-overview.md new file mode 100644 index 000000000..2b54c069e --- /dev/null +++ b/versioned_docs/version-v4.22/testing/01-overview.md @@ -0,0 +1,40 @@ +--- +title: Testing +sidebar_label: Overview +description: Testing overview. +slug: /testing-overview +--- + +# Testing + +In order to ensure that your Stencil components work the way you expect, Stencil provides testing support out of the +box. Stencil offers both unit testing and end-to-end testing capabilities. + +## Unit Testing vs. End-to-end Testing + +Testing within Stencil is broken up into two distinct types: Unit tests and End-to-end (e2e) tests. + +There are several philosophies on how testing should be done, and how to differentiate what should be considered a unit +test versus an end-to-end test. Stencil takes an opinionated stance so developers have a description of each to better +choose when to use each type of testing: + +**Unit tests** focus on testing a component's methods in isolation. For example, when a method is given the argument +`X`, it should return `Y`. + +**End-to-end tests** focus on how the components are rendered in the DOM and how the individual components work +together. For example, when `my-component` has the `X` attribute, the child component then renders the text `Y`, and +expects to receive the event `Z`. + +## Library Support + +Stencil currently supports the following tools for testing components: + +- [Stencil Test Runner](./stencil-testrunner/01-overview.md): a built-in test runner based on Jest for unit and end-to-end testing with Puppeteer to run within an actual browser in order to provide more realistic results. +- [WebdriverIO](./webdriverio/01-overview.md): a browser and mobile automation test framework for Node.js that allows you to run component and end-to-end tests across all browsers. +- [Playwright](./playwright/01-overview.md): an automated end-to-end testing framework that can run across all major browsers + +:::info + +We are actively working to support a wider range of testing tools, including Playwright. Stay tuned for updates! + +::: diff --git a/versioned_docs/version-v4.22/testing/03-vitest.md b/versioned_docs/version-v4.22/testing/03-vitest.md new file mode 100644 index 000000000..a1395f24c --- /dev/null +++ b/versioned_docs/version-v4.22/testing/03-vitest.md @@ -0,0 +1,180 @@ +--- +title: Vitest +position: 5 +--- + +# Overview + +[Vitest](https://vitest.dev/) is a popular and modern test framework for unit testing. You can use Vitest to test Stencil components in the browser using its [browser mode feature](https://vitest.dev/guide/browser.html). + +:::caution +Vitest browser mode is an experimental feature and in early development. As such, it may not yet be fully optimized, and there may be some bugs or issues that have not yet been ironed out. +::: + +## Set Up + +To get started with Vitest, all you need to install it via: + +```bash npm2yarn +npm install vitest @vitest/browser unplugin-stencil webdriverio +``` + +This command installs: + +- `vitest`: the core test framework +- `@vitest/browser`: enables testing in browser environments +- `unplugin-stencil`: integrates Stencil's compiler with Vitest for seamless testing +- `webdriverio`: facilitates browser management for tests + +Next, we create a Vitest configuration as following: + +```ts title="vitest.config.ts" +import stencil from 'unplugin-stencil/vite' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + enabled: true, + headless: true, + name: 'chrome' + }, + }, + plugins: [stencil()] +}) +``` + +This configuration enables tests to run in a headless Chrome browser. + +## Writing Tests + +Once you've setup Vitest you can start write your first test. In order to render a Stencil component into the browser, all you need to do is import the component and initiate an instance of the component on the page: + +```ts title="src/components/my-component/my-component.test.ts" +import { expect, test } from 'vitest' + +import '../src/components/my-component/my-component.js' + +test('should render component correctly', async () => { + const cmp = document.createElement('my-component') + cmp.setAttribute('first', 'Stencil') + cmp.setAttribute('last', `'Don't call me a framework' JS`) + document.body.appendChild(cmp) + + await new Promise((resolve) => requestIdleCallback(resolve)) + expect(cmp.shadowRoot?.querySelector('div')?.innerText) + .toBe(`Hello, World! I'm Stencil 'Don't call me a framework' JS`) +}) +``` + +Lastly, let's add a Vitest script to our `package.json`: + +```json +{ + "scripts": { + "test": "vitest --run" + }, +} +``` + +Execute the tests using: + +```bash npm2yarn +npm test +``` + +Expected output: + +``` +❯ npm test + +> vitest@1.0.0 test +> vitest --run + +The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details. + + RUN v1.5.0 /private/tmp/vitest + Browser runner started at http://localhost:5173/ + +[19:39.9] build, vitest, prod mode, started ... +[19:39.9] transpile started ... +[19:40.0] transpile finished in 72 ms +[19:40.0] generate custom elements + source maps started ... +[19:40.1] generate custom elements + source maps finished in 137 ms +[19:40.1] build finished in 227 ms + + ✓ src/components/my-component/my-component.test.ts (1) + ✓ should render component correctly + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at 14:19:36 + Duration 3.19s (transform 0ms, setup 0ms, collect 721ms, tests 22ms, environment 0ms, prepare 0ms) + +``` + +### Use JSX + +The example above creates the Stencil instance using basic DOM primitives. If you prefer to use JSX also for rendering Stencil components in your test, just create a `jsx.ts` utility file with the following content: + +```ts title="src/utils/jsx.ts" +export const createElement = (tag, props, ...children) => { + if (typeof tag === 'function') { + return tag(props, ...children) + } + const element = document.createElement(tag) + + Object.entries(props || {}).forEach(([name, value]) => { + if (name.startsWith('on') && name.toLowerCase() in window) { + element.addEventListener(name.toLowerCase().substr(2), value) + } else { + element.setAttribute(name, value.toString()) + } + }) + + children.forEach((child) => { + appendChild(element, child) + }) + + return element +} + +export const appendChild = (parent, child) => { + if (Array.isArray(child)) { + child.forEach((nestedChild) => appendChild(parent, nestedChild)) + } else { + parent.appendChild(child.nodeType ? child : document.createTextNode(child)) + } +} + +export const createFragment = (_, ...children) => { + return children +} +``` + +Now update your test, import the `createElement` method and tell the JSX engine to use that method for rendering the JSX snippet. Our test should look as follows: + +```tsx title="src/components/my-component/my-component.test.tsx" +/** @jsx createElement */ +import { expect, test } from 'vitest' + +import { createElement } from '../../utils/jsx' +import './my-component.js' + +test('should render the component with jsx', async () => { + const cmp = <my-component first="Stencil" last="'Don't call me a framework' JS"></my-component> + document.body.appendChild(cmp) + await new Promise((resolve) => requestIdleCallback(resolve)) + expect(cmp.shadowRoot?.querySelector('div')?.innerText) + .toBe(`Hello, World! I'm Stencil 'Don't call me a framework' JS`) +}) +``` + +__Note:__ the `/** @jsx createElement */` at the top of the file tells JSX which rendering function it should use to parse the JSX snippet. + +## Limitations + +Be aware of the following limitations, when using Vitest as test framework for testing Stencil components: + +- __Mocking not yet supported__: you can't mock any files or dependencies when running with the Stencil browser feature +- __No auto-waits__: in order to ensure that the component is rendered, you have to manually wait via, e.g. calling `await new Promise((resolve) => requestIdleCallback(resolve))` \ No newline at end of file diff --git a/versioned_docs/version-v4.22/testing/_category_.json b/versioned_docs/version-v4.22/testing/_category_.json new file mode 100644 index 000000000..f98de9498 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Testing", + "position": 9 +} diff --git a/versioned_docs/version-v4.22/testing/playwright/01-overview.md b/versioned_docs/version-v4.22/testing/playwright/01-overview.md new file mode 100644 index 000000000..2fb178c32 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/playwright/01-overview.md @@ -0,0 +1,155 @@ +--- +title: Playwright Overview +sidebar_label: Overview +--- + +:::note +The Stencil Playwright adapter is currently an experimental package. Breaking changes may be introduced at any time. + +The Stencil Playwright adapter is designed to only work with **version 4.13.0 and higher** of Stencil! +::: + +[Playwright](https://playwright.dev/) is an automated end-to-end testing framework built to run on all modern browser engines and operating systems. +Playwright leverages the DevTools protocol to provide reliable tests that run in actual browsers. + +## Set Up + +To add Playwright to an existing Stencil project, leverage the [Stencil Playwright testing adapter](https://www.npmjs.com/package/@stencil/playwright). This +is a tool built by the Stencil team to help Stencil and Playwright work better together. The best part is you'll write your tests using the same APIs +as defined and documented by Playwright. So, be sure to [check out their documentation](https://playwright.dev/docs/writing-tests) for help writing your first tests! + +To install the Stencil Playwright adapter in an existing Stencil project, follow these steps: + +1. Install the necessary dependencies: + + ```bash npm2yarn + npm i @stencil/playwright @playwright/test --save-dev + ``` + +1. Install the Playwright browser binaries: + + ```bash + npx playwright install + ``` + +1. Create a Playwright config at the root of your Stencil project: + + ```ts title="playwright.config.ts" + import { expect } from '@playwright/test'; + import { matchers, createConfig } from '@stencil/playwright'; + + // Add custom Stencil matchers to Playwright assertions + expect.extend(matchers); + + export default createConfig({ + // Overwrite Playwright config options here + }); + ``` + + The `createConfig()` function is a utility that will create a default Playwright configuration based on your project's Stencil config. Read + more about how to use this utility in the [API docs](./03-api.md#createconfig-function). + + :::note + For `createConfig()` to work correctly, your Stencil config must be named either `stencil.config.ts` or `stencil.config.js`. + ::: + +1. update your project's `tsconfig.json` to add the `ESNext.Disposable` option to the `lib` array: + + ```ts title="tsconfig.json" + { + lib: [ + ..., + "ESNext.Disposable" + ], + ... + } + ``` + + :::note + This will resolve a build error related to `Symbol.asyncDispose`. If this is not added, tests may fail to run since the Stencil dev server will be unable + to start due to the build error. + ::: + +1. Ensure the Stencil project has a [`www` output target](../../output-targets/www.md). Playwright relies on pre-compiled output running in a dev server + to run tests against. When using the `createConfig()` helper, a configuration for the dev server will be automatically created based on + the Stencil project's `www` output target config and [dev server config](../../config/dev-server.md). If no `www` output target is specified, + tests will not be able to run. + +1. Add the `copy` option to the `www` output target config: + + ```ts title="stencil.config.ts" + { + type: 'www', + serviceWorker: null, + copy: [{ src: '**/*.html' }, { src: '**/*.css' }] + } + ``` + + This will clone all HTML and CSS files to the `www` output directory so they can be served by the dev server. If you put all testing related + files in specific directory(s), you can update the `copy` task glob patterns to only copy those files: + + ```ts title="stencil.config.ts" + { + type: 'www', + serviceWorker: null, + copy: [{ src: '**/test/*.html' }, { src: '**/test/*.css' }] + } + ``` + + :::note + If the `copy` property is not set, you will not be able to use the `page.goto` testing pattern! + ::: + +1. Test away! Check out the [e2e testing page](./02-e2e-testing.md) for more help getting started writing tests. + +## Migrating to Playwright + +If you are working in a Stencil project with an existing end-to-end testing setup via the [Stencil Test Runner](../stencil-testrunner/05-e2e-testing.md), +you can continue using your existing e2e testing setup while sampling the Playwright adapter or migrating tests over time. To do so, there are two +options: + +- **Option 1:** Update the Playwright config to match a different test file pattern: + + ```ts title="playwright.config.ts" + export default createConfig({ + // Example: match all test files with the 'e2e.playwright.ts' naming convention + testMatch: '*.e2e.playwright.ts', + }); + ``` + + By default, the Playwright adapter will match all files with the '.e2e.ts' extension, which is the same pattern used by the Stencil test + runner for e2e tests. See the [Playwright`testMatch` documentation](https://playwright.dev/docs/api/class-testconfig#test-config-test-match) + for more information on acceptable values. + + Changing this value is useful if you are just testing out Playwright, but haven't committed to migrating all test files. + +- **Option 2:** Update the Stencil Test Runner to match a different test file pattern: + + ```ts title="stencil.config.ts" + export config: Config = { + ..., + test: { + // Stencil Test Runner will no longer execute any 'e2e.ts` files + testRegex: '(/__tests__/.*|(\\.|/)(test|spec)|[//](e2e))\\.[jt]sx?$' + } + } + ``` + + Changing this value is useful if you intend on using Playwright as your main e2e testing solution and want to keep the 'e2e.ts` extension + for test files. + +:::tip +You could also separate tests into specific directories for each test runner and have the test patterns match only their respective directories. +::: + +In addition, Playwright will not execute as a part of the Stencil CLI's `test` command (i.e. `stencil test --e2e`). To have Playwright execute +alongside the Stencil Test Runner, you'll need to include an explicit call to the Playwright CLI as a part of your project's `package.json` test +script: + +```json title="package.json" +{ + "scripts": { + "test.e2e": "stencil test --e2e && playwright test" + } +} +``` diff --git a/versioned_docs/version-v4.22/testing/playwright/02-e2e-testing.md b/versioned_docs/version-v4.22/testing/playwright/02-e2e-testing.md new file mode 100644 index 000000000..c8019360d --- /dev/null +++ b/versioned_docs/version-v4.22/testing/playwright/02-e2e-testing.md @@ -0,0 +1,160 @@ +--- +title: End-to-End Testing +sidebar_label: E2E Testing +--- + +When it comes to writing end-to-end tests using the Stencil Playwright adapter, the best advice we can give is to leverage +[Playwright documentation](https://playwright.dev/docs/writing-tests). The adapter is set up in a way so that developers will use the +same public APIs with as little Stencil nuances as possible. + +## Writing Tests + +As far as writing the actual tests goes, the most important thing to be aware of for tests to run correctly is to import the `test` function from +`@stencil/playwright`, **not** directly from `@playwright/test`: + +```ts +// THIS IS CORRECT +import { test } from '@stencil/playwright'; + +// THIS IS NOT!! +// import { test } from '@playwright/test'; +``` + +The adapter package extends Playwright's stock `test` function to provide additional fixtures and handle nuances related to web component hydration. More +information is available in the [API documentation](./03-api.md#test-function). + +### Testing Patterns + +#### `page.goto()` + +The `goto()` method allows tests to load a pre-defined HTML template. This pattern is great if a test file has many tests to execute that all use the same HTML code +or if additional `script` or `style` tags need to be included in the HTML. However, with this pattern, developers are responsible for defining the necessary `script` +tags pointing to the Stencil entry code (so all web components are correctly loaded and registered). + +For the following snippets, we'll imagine this is the simplified file structure for a Stencil project: + +```tree +stencil-project-root +└── src + └── components + ├── my.component.tsx + └── test <-- directory containing test files + ├── my-component.e2e.ts + └── my-component.e2e.html +``` + +```ts title="stencil.config.ts" +export const config: Config = { + namespace: 'test-app', + outputTargets: [ + { + type: 'www', + serviceWorker: null, + copy: [{ src: '**/test/*.html' }, { src: '**/test/*.css' }], + }, + ], +}; +``` + +```html title="my-component.e2e.html" +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf8" /> + + <!-- Replace with the path to your entrypoint --> + <script src="../../../build/test-app.esm.js" type="module"></script> + <script src="../../../build/test-app.js" nomodule></script> + </head> + <body> + <my-component first="Stencil"></my-component> + </body> +</html> +``` + +In the above snippet, where it says "replace with the path to your entrypoint", the `src` attributes for the `script` tags should be the _relative_ path from the `www` output +target's [output directory (`dir` option)](../../output-targets/www.md#config) to the namespaced entry file. The entry file will have the name +`<namespace>.esm.js` for ESM output and `<namespace>.js` for CJS output. The "namespace" value is the kebab-case value of the `namespace` from your +[Stencil config](../../config/01-overview.md#namespace). + +```ts title="my-component.e2e.ts" +import { expect } from '@playwright/test'; +import { test } from '@stencil/playwright'; + +test.describe('my-component', () => { + test('should render the correct name', async ({ page }) => { + // The path here is the path to the www output relative to the dev server root directory + await page.goto('/components/my-component/test/my-component.e2e.html'); + + // Rest of test + const component = await page.locator('my-component'); + await expect(component).toHaveText(`Hello World! I’m Stencil`); + }); +}); +``` + +In the above snippet, the path string passed to `page.goto()` should be the _absolute_ path to the HTML file from the +[dev server's served directory](../../config/dev-server.md#root). + +#### `page.setContent()` + +The `setContent()` method allows tests to define their own HTML code on a test-by-test basis. This pattern is helpful if the HTML for a test is small or to +avoid affecting other tests using the `page.goto()` pattern and modifying a shared HTML template file. With this pattern, the `script` tags pointing to Stencil +entry code will be automatically injected into the generated HTML. + +```ts title="my-component.e2e.ts" +import { expect } from '@playwright/test'; +import { test } from '@stencil/playwright'; + +test.describe('my-component', () => { + test('should render the correct name', async ({ page }) => { + await page.setContent('<my-component first="Stencil"></my-component>'); + + // Rest of test + }); +}); +``` + +#### `page.waitForChanges()` + +The `waitForChanges()` method is a utility for waiting for Stencil components to rehydrate after an operation that results in a re-render: + +```ts title="my-component.e2e.ts" +import { expect } from '@playwright/test'; +import { test } from '@stencil/playwright'; + +test.describe('my-component', () => { + test('should render the correct name', async ({ page }) => { + // Assume we have a template setup with the `my-component` component and a `button` + await page.goto('/my-component/my-component.e2e.html'); + + const button = page.locator('button'); + // Assume clicking the button changes the `@Prop()` values on `my-component` + button.click(); + await page.waitForChanges(); + }); +}); +``` + +## Running Tests + +To run tests, either run `npx playwright test` from the **root** of the Stencil project, or update the project's `test` script in the `package.json` file to run the +Playwright command. + +By default, the adapter will execute all tests in a project with a `.e2e.ts` file suffix. This can be modified by passing the +[`testDir`](https://playwright.dev/docs/api/class-testproject#test-project-test-dir) and/or [`testMatch`](https://playwright.dev/docs/api/class-testproject#test-project-test-match) +configuration options as overrides to `createConfig()`. + +## Debugging + +Playwright offers several strategies for debugging e2e tests. See their [debugging documentation](https://playwright.dev/docs/running-tests#debugging-tests) +for more information of the tools available. + +Tests can also be launched in a headed mode. This is disabled by default, but can be enabled [in the Playwright config](https://playwright.dev/docs/api/class-testoptions#test-options-headless) +or with the `--headed` CLI flag. Running tests in a headed mode will open a browser tab for each test. + +## Screenshot Testing + +Playwright also has support for screenshot (or visual) testing. This functionality is available out of the box. Read the Playwright docs on +[screenshots](https://playwright.dev/docs/screenshots) and [performing visual comparisons](https://playwright.dev/docs/test-snapshots#generating-screenshots) +for more information on using these APIs. diff --git a/versioned_docs/version-v4.22/testing/playwright/03-api.md b/versioned_docs/version-v4.22/testing/playwright/03-api.md new file mode 100644 index 000000000..6a7ac3439 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/playwright/03-api.md @@ -0,0 +1,98 @@ +--- +title: Playwright Adapter API +sidebar_label: API +--- + +## `createConfig` Function + +**Signature:** `createConfig(overrides?: Partial<PlaywrightTestConfig>): Promise<PlaywrightTestConfig>` + +Returns a [Playwright test configuration](https://playwright.dev/docs/test-configuration#introduction). + +`overrides`, as the name implies, will overwrite the default configuration value(s) if supplied. These values can include any valid Playwright config option. Changing +option values in a nested object will use a "deep merge" to combine the default and overridden values. So, creating a config like the following: + +```ts title="playwright.config.ts" +import { expect } from '@playwright/test'; +import { matchers, createConfig } from '@stencil/playwright'; + +expect.extend(matchers); + +export default createConfig({ + // Change which test files Playwright will execute + testMatch: '*.spec.ts', + + webServer: { + // Only wait max 30 seconds for server to start + timeout: 30000, + }, +}); +``` + +Will result in: + +```ts +{ + testMatch: '*.spec.ts', + use: { + baseURL: 'http://localhost:3333', + }, + webServer: { + command: 'stencil build --dev --watch --serve --no-open', + url: 'http://localhost:3333/ping', + reuseExistingServer: !process.env.CI, + // Only timeout gets overridden, not the entire object + timeout: 30000, + }, +} +``` + +:::caution +Although any valid Playwright config option can be overridden, there are a few properties that may cause issues with tests if changed. In particular, +these are the [`webServer`](https://playwright.dev/docs/api/class-testconfig#test-config-web-server) and [`baseUrl`](https://playwright.dev/docs/test-webserver#adding-a-baseurl) +options. These properties are automatically generated based on the Stencil project's config (analyzing the output targets and dev server configuration), +so be cautious when overriding these values as it can result in issues with Playwright connecting to the Stencil dev server. +::: + +## `test` Function + +`test` designates which tests will be executed by Playwright. See the [Playwright API documentation](https://playwright.dev/docs/api/class-test#test-call) regarding usage. + +This package modifies the [`page` fixture](#page-fixture) and offers a [`skip` utility](#skip-function) as discussed below. + +## `page` Fixture + +The page fixture is a class that allows interacting with the current test's browser tab. In addition to the [default Playwright Page API](https://playwright.dev/docs/api/class-page), +Stencil extends the class with a few additional methods: + +- `waitForChanges()`: Waits for Stencil components to re-hydrate before continuing. +- `spyOnEvent()`: Creates a new EventSpy and listens on the window for an event to emit. + +## `skip` Function + +The `skip()` function is used to skip test execution for certain browsers or [component modes](https://stenciljs.com/docs/styling#style-modes): + +```ts +test('my-test', ({ page, skip }) => { + // Skip tests for certain browsers + skip.browser('firefox', 'This behavior is not available on Firefox'); + + // Skip tests for certain modes + skip.mode('md', 'This behavior is not available in Material Design'); + + ... +}) +``` + +## Matchers + +Playwright comes with [a set of matchers to do test assertions](https://playwright.dev/docs/test-assertions). However, the Stencil Playwright adapter +has additional custom matchers. + +| Assertion | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `toHaveFirstReceivedEvent` | Ensures that the first event received matches a specified [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) payload. | +| `toHaveNthReceivedEventDetail` | Ensures that the nth event received matches a specified [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) payload. | +| `toHaveReceivedEvent` | Ensures an event has been received at least once. | +| `toHaveReceviedEventDetail` | Ensures an event has been received with a specified [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) payload. | +| `toHaveReceivedEventTimes` | Ensures an event has been received a certain number of times. | diff --git a/versioned_docs/version-v4.22/testing/playwright/_category_.json b/versioned_docs/version-v4.22/testing/playwright/_category_.json new file mode 100644 index 000000000..ed1efac38 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/playwright/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Playwright", + "position": 2 +} diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/01-overview.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/01-overview.md new file mode 100644 index 000000000..bb1fa0a90 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/01-overview.md @@ -0,0 +1,127 @@ +--- +title: Stencil Test Runner Overview +sidebar_label: Overview +--- + +# Overview + +Stencil has a built-in test runner that uses [Jest](https://jestjs.io/) and [Puppeteer](https://pptr.dev/) as its testing libraries, and allows developers to install both libraries using their preferred package manager. + +If you created a project using `npm init stencil`, these libraries were installed for you. Depending on when your project was created, you may or may not have the latest supported version installed. + +To view current version support for both Jest and Puppeteer, please see the [Stencil support policy for testing libraries](../../reference/support-policy.md#testing-libraries). + +## Testing Commands + +Stencil tests are run using the command `stencil test`, followed by one or more optional flags: +- `--spec` to run unit tests +- `--e2e` to run end-to-end tests +- `--watchAll` to watch the file system for changes, and rerun tests when changes are detected + +When the `--spec` and/or `--e2e` flags are provided, Stencil will automatically run the tests associated with each flag. + +Below a series of example `npm` scripts which can be added to the project's `package.json` file to run Stencil tests: + +```json +{ + "scripts": { + "test": "stencil test --spec", + "test.watch": "stencil test --spec --watchAll", + "test.end-to-end": "stencil test --e2e" + } +} +``` + +Each command above begins with `stencil test`, which tells Stencil to run tests. Note that each `stencil test` command +in the example above is followed by one or more of the optional flags. Looking at each script, one at a time: +- the `test` script runs unit tests for our Stencil project. +- the `test.watch` script runs unit tests for our Stencil project. It watches the filesystem for changes, and reruns +tests when changes are detected. +- the `test.end-to-end` script runs the end-to-end tests for our Stencil project. + +If you created a project using `npm init stencil`, these scripts are provided to you automatically. + +Stencil does not prescribe any specific naming convention for the names of your scripts. The `test.watch` script could as easily be named `test-watch`, `test.incremental`, etc. As long as the script itself uses the `stencil test` command, your tests should be run. + +### Testing Configuration + +Stencil will apply defaults from data it has already gathered. For example, Stencil already knows what directories to look through, and what files are spec and e2e files. Jest can still be configured using the same config names, but now using the stencil config `testing` property. Please see the [Testing Config docs](./02-config.md) for more info. + +```tsx +import { Config } from '@stencil/core'; + +export const config: Config = { + testing: { + testPathIgnorePatterns: [...] + } +}; +``` + +### Command Line Arguments + +While the Stencil CLI offers a certain set of command line flags to specify e.g. which types of tests to run, you also have access to all Jest options through the CLI. For example to specify a single test, you can pass in a positional argument to Jest by adding a `--`, e.g.: + +```sh +# run a single unit test +npx stencil test --spec -- src/components/my-component/my-component.spec.ts +# run a single e2e test +npx stencil test --e2e -- src/components/my-component/my-component.e2e.ts +``` + +Next to positional arguments, Stencil also passes along [certain](https://github.com/ionic-team/stencil/blob/54d4ee252768e1d225baababee0093fdb0562b83/src/cli/config-flags.ts#L38-L85) Jest specific flags, e.g.: + +```sh +# enable code coverage +npx stencil test --spec --coverage +``` + +You can find more information about [Jest CLI options](https://jestjs.io/docs/cli) in the project documentation. + +## Running and Debugging Tests in VS Code + +Adding the following configurations to `.vscode/launch.json` will allow you to use the VS Code Debugger to run the Stencil test runner for the currently active file in your editor. + +To use the below configuration: +1. Ensure the test file you want to run is open and in the current active window in VS Code. +2. Select the debug configuration to run: + 1. 'E2E Test Current File' will run the end-to-end tests in the active test file + 2. 'Spec Test Current File' will run the spec tests in the active test file +3. Hit the play button to start the test. + +```json title=".vscode/launch.json" +{ + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "E2E Test Current File", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/node_modules/.bin/stencil", + "args": ["test", "--e2e", "--", "--maxWorkers=0", "${fileBasename}"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "launch", + "name": "Spec Test Current File", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/node_modules/.bin/stencil", + "args": ["test", "--spec", "--", "${fileBasename}"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} +``` + +:::tip +Windows users: The `program` value should be set to `"${workspaceFolder}/node_modules/bin/stencil"`. +If that value does not work, please try`"${workspaceFolder}/node_modules/@stencil/core/bin/stencil"`. +::: + +The configuration above makes use of special VS Code variables, such as `${workspaceFolder}`. These variables are substituted with actual values upon starting the tests. For more information regarding the values these variables resolve to, please see VS Code's [Variables Reference documentation](https://code.visualstudio.com/docs/editor/variables-reference). + +## Other Resources + +- [The Basics of Unit Testing in StencilJS](https://eliteionic.com/tutorials/the-basics-of-unit-testing-in-stencil-js/) diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/02-config.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/02-config.md new file mode 100644 index 000000000..53185733f --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/02-config.md @@ -0,0 +1,20 @@ +--- +title: Testing Config +sidebar_label: Config +description: Testing Config +slug: /testing-config +--- + +# Testing Config + +The `testing` config setting in `stencil.config.ts` specifies an object that corresponds to the jest configuration that should be used in your tests. Stencil provides a default configuration, which you likely won't need to edit, however it can be extended with the same configuration options as Jest. See the [Configuring Jest Guide](https://jestjs.io/docs/en/configuration.html) for configuration details. + +:::note +Keep in mind that the usual way of configuring Jest (`package.json` and `jest.config.js`) is not used with the `stencil testing` command. Jest can still be used, but configuring the presets, transpilation and setting up the correct commands must be done by the project. +::: + +Some additional Stencil specific options may be set here as well for configuring the e2e tests. + +```tsx reference title="" +https://github.com/ionic-team/stencil/blob/d847e92fbc297754cb8dbb7f633de9ce906f54ac/src/declarations/stencil-public-compiler.ts#L1892-L2044 +``` diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/03-unit-testing.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/03-unit-testing.md new file mode 100644 index 000000000..94af7305d --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/03-unit-testing.md @@ -0,0 +1,147 @@ +--- +title: Unit Testing +sidebar_label: Unit Testing +description: Unit Testing +slug: /unit-testing +--- + +# Unit Testing + +Stencil makes it easy to unit test components and app utility functions using [Jest](https://jestjs.io/). Unit tests validate the code in isolation. Well written tests are fast, repeatable, and easy to reason about. + +To run unit tests, run `stencil test --spec`. Files ending in `.spec.ts` will be executed. + + +## newSpecPage() + +In order to unit test a component as rendered HTML, tests can use `newSpecPage()` imported from `@stencil/core/testing`. This testing utility method is similar to `newE2EPage()`, however, `newSpecPage()` is much faster since it does not require a full [Puppeteer](https://pptr.dev/) instance to be running. Please see the [newE2EPage()](./05-e2e-testing.md) docs for more information about complete End-to-end testing with Puppeteer. + +Below is a simple example where `newSpecPage()` is given one component class which was imported, and the initial HTML to use for the test. In this example, when the component `MyCmp` renders it sets its text content as "Success!". The matcher `toEqualHtml()` is then used to ensure the component renders as expected. + +```typescript +import { newSpecPage } from '@stencil/core/testing'; +import { MyCmp } from '../my-cmp'; + +it('should render my component', async () => { + const page = await newSpecPage({ + components: [MyCmp], + html: `<my-cmp></my-cmp>`, + }); + expect(page.root).toEqualHtml(` + <my-cmp>Success!</my-cmp> + `); +}); +``` + +The example below uses the template option to test the component + +```tsx title="mycmp.spec.tsx" +// Since the 'template' argument to `newSpecPage` is using jsx syntax, this should be in a .tsx file. +import { h } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { MyCmp } from '../my-cmp'; + +it('should render my component', async () => { + const greeting = 'Hello World'; + const page = await newSpecPage({ + components: [MyCmp], + template: () => (<my-cmp greeting={greeting}></my-cmp>), + }); + expect(page.root).toEqualHtml(` + <my-cmp>Hello World</my-cmp> + `); +}); +``` + +You render functional components without defining them in the `components` list, e.g.: + +```tsx title="mycmp.spec.tsx" +import { Fragment, h } from '@stencil/core'; +import { newSpecPage } from '@stencil/core/testing'; +import { MyFunctionalComponent } from '../my-functional-cmp'; + +it('should render my component', async () => { + /** + * or define functional components directly in your test + */ + const AnotherFunctionalComponent = (props: any, children: typeof Fragment) => ( + <section>{children}</section> + ); + + const page = await newSpecPage({ + components: [], + template: () => ( + <AnotherFunctionalComponent> + <MyFunctionalComponent>Hello World!</MyFunctionalComponent> + </AnotherFunctionalComponent> + ), + }); + expect(page.root).toEqualHtml(` + <section> + <div> + Hello World! + </div> + </section> + `); +}); +``` + +:::note + +Declaring Stencil class components directly within class files is not supported. If you need such a component for your tests, we recommend to put this into an extra fixture file. + +::: + +### Spec Page Options + +The `newSpecPage(options)` method takes an options argument to help write tests: + +| Option | Description | +|--------|-------------| +| `components` | An array of components to test. Component classes can be imported into the spec file, then their reference should be added to the `component` array in order to be used throughout the test. Child components should also be imported. For example, if component Foo uses component Bar then both Foo and Bar should be imported *Required* | +| `html` | The initial HTML used to generate the test. This can be useful to construct a collection of components working together, and assign HTML attributes. This value sets the mocked `document.body.innerHTML`. | +| `template` | The initial JSX used to generate the test. Use `template` when you want to initialize a component using their properties, instead of their HTML attributes. It will render the specified template (JSX) into `document.body`. | +| `autoApplyChanges` | By default, any changes to component properties and attributes must call `page.waitForChanges()` in order to test the updates. As an option, `autoApplyChanges` continuously flushes the queue in the background. Defaults to `false` | +| `cookie` | Sets the mocked `document.cookie`. | +| `direction` | Sets the mocked `dir` attribute on `<html>`. | +| `language` | Sets the mocked `lang` attribute on `<html>`. | +| `referrer` | Sets the mocked `document.referrer`. | +| `supportsShadowDom` | Manually set if the mocked document supports Shadow DOM or not. Defaults to `true` | +| `userAgent` | Sets the mocked `navigator.userAgent`. | +| `url` | Sets the mocked browser's `location.href`. | + + +### Spec Page Results + +The returned "page" object from `newSpecPage()` contains the initial results from the first render. It's also important to note that the returned page result is a `Promise`, so for convenience it's recommended to use async/await. + +The most useful property on the page results would be `root`, which is for convenience to find the first root component in the document. For example, if a component is nested in many `<div>` elements, the `root` property goes directly to the component being tested in order to skip the query selector boilerplate code. + +| Result | Description | +|--------|-------------| +| `body` | Mocked testing `document.body`. | +| `doc` | Mocked testing `document`. | +| `root` | The first component found within the mocked `document.body`. If a component isn't found, then it'll return `document.body.firstElementChild`. | +| `rootInstance` | Similar to `root`, except returns the component instance. If a root component was not found it'll return `null`. | +| `setContent(html)` | Convenience function to set `document.body.innerHTML` and `waitForChanges()`. Function argument should be an html string. | +| `waitForChanges()` | After changes have been made to a component, such as a update to a property or attribute, the test page does not automatically apply the changes. In order to wait for, and apply the update, call `await page.waitForChanges()`. | +| `win` | Mocked testing `window`. | + + +## Testing Component Class Logic + +For simple logic only testing, unit tests can instantiate a component by importing the class and constructing it manually. Since Stencil components are plain JavaScript objects, you can create a new component and execute its methods directly. + +```typescript +import { MyToggle } from '../my-toggle.tsx'; + +it('should toggle the checked property', () => { + const toggle = new MyToggle(); + + expect(toggle.checked).toBe(false); + + toggle.someMethod(); + + expect(toggle.checked).toBe(true); +}); +``` diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/04-mocking.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/04-mocking.md new file mode 100644 index 000000000..f38fb047d --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/04-mocking.md @@ -0,0 +1,141 @@ +--- +title: Mocking +sidebar_label: Mocking +description: Mocking +slug: /mocking +--- + +# Mocking + +Since Stencil's testing capabilities are built on top of Jest, the mocking features of Jest can be utilized to mock out libraries or certain parts of your code. For further information have a look at the [Jest docs](https://jestjs.io/docs/en/manual-mocks). + +## Mocking a Library + +To create a mock for a library that is imported from `node_modules`, you can simply create a folder `__mocks__` on the same directory level as `node_modules` (usually in your project's root folder), then create a file in there with the same name as the package you want to mock, and that mock will automatically be applied. + +For example, if you want to mock `md5`, you'd create a file `__mocks__/md5.ts` with the following content: + +```ts +export default () => 'fakehash'; +``` + +:::note +If you want to mock a scoped package like `@capacitor/core`, you'll have to create the file as `__mocks__/@capacitor/core.ts`. +::: + +## Mocking Your Own Code + +To create a mock for some of your own code, you'll have to create the mocks folder on a different layer. + +Let's say you have a file `src/helpers/utils.ts` that exposes a `getRandomInt` helper, and a service that provides a function which uses this helper. + +```tsx +// src/helpers/utils.ts + +export const getRandomInt = (min: number, max: number) => + Math.round(Math.random() * (max - min)) + min; +``` + +```tsx +// src/services/foo.ts + +import { getRandomInt } from '../helpers/utils'; + +export const bar = () => getRandomInt(0, 10); +``` + +To mock this function, you create a file `src/helpers/__mocks__/utils.ts` and write your mock in that file. + +```tsx +// src/helpers/__mocks__/utils.ts + +export const getRandomInt = () => 42; +``` + +Because Jest only auto-mocks node modules, you'll also have to let your test know that you want it to apply that mock, by calling `jest.mock()`. + +```tsx +// src/foo.spec.ts + +jest.mock('./helpers/utils'); + +import { bar } from './services/foo'; + +describe('Foo', () => { + it('bar()', () => { + expect(bar()).toBe(42); + }); +}); +``` + +:::note +It's important that you call `jest.mock('...')` before your import. +::: + +Instead of creating a file in a `__mocks__` folder, there is an alternative approach of providing a mock: the `jest.mock()` function takes a module factory function as an optional second argument. The following test will work the same as the one before, without having to create a `src/helpers/__mocks__/utils.ts` file. + +```tsx +// src/foo.spec.ts + +jest.mock('./helpers/utils', () => ({ + getRandomInt: () => 42, +})); + +import { foo } from './services/foo'; + +describe('Foo', () => { + it('bar()', () => { + expect(bar()).toBe(42); + }); +}); +``` + +## Mocking in E2E Tests + +If you use `newE2EPage` in an end-to-end test, your component's code will be executed in a browser context (Stencil will launch a headless Chromium instance using Puppeteer). However your mocks will only be registered in the Node.js context, which means that your component will still call the original implementation. If you need to mock something in the browser context, you can either have a look at [using Jest with Puppeteer](https://jestjs.io/docs/en/puppeteer), or possibly switch to using `newSpecPage`, which creates a virtual (mocked) DOM in the node context. + +```tsx +// src/components/foo/foo.tsx + +import { h, Component, Method } from '@stencil/core'; +import { getRandomInt } from '../../helpers/utils'; + +@Component({ tag: 'foo-component' }) +export class Foo { + @Method() + async bar() { + return getRandomInt(0, 10); + } + + render() { + return <div />; + } +} +``` + +```tsx +// src/foo.e2e.ts + +jest.mock('./helpers/utils', () => ({ + getRandomInt: () => 42, +})); + +import { newSpecPage } from '@stencil/core/testing'; +import { Foo } from './components/foo/foo'; + +describe('Foo', () => { + it('bar()', async () => { + const page = await newSpecPage({ + components: [Foo], + html: '<foo-component></foo-component>', + }); + const foo = page.body.querySelector('foo-component'); + + if (!foo) { + throw new Error('Could not find Foo component'); + } + + expect(await foo.bar()).toBe(42); + }); +}); +``` diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/05-e2e-testing.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/05-e2e-testing.md new file mode 100644 index 000000000..c19a8aa20 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/05-e2e-testing.md @@ -0,0 +1,276 @@ +--- +title: End-to-end Testing +sidebar_label: End-to-end Testing +description: End-to-end Testing +slug: /end-to-end-testing +--- + +# End-to-end Testing + +E2E tests verify your components in a real browser. For example, when `my-component` has the X attribute, the child component then renders the text Y, and expects to receive the event Z. By using Puppeteer for rendering tests (rather than a Node environment simulating how a browser works), your end-to-end tests are able to run within an actual browser in order to give better results. + +Stencil provides many utility functions to help test [Jest](https://jestjs.io/) and [Puppeteer](https://pptr.dev/). For example, a component's shadow dom can be queried and tested with the Stencil utility functions built on top of Puppeteer. Tests can not only be provided mock HTML content, but they can also go to URLs of your app which Puppeteer is able to open up and test on Stencil's dev server. + +End-to-end tests require a fresh build, dev-server, and puppeteer browser instance created before the tests can actually run. With the added build complexities, the `stencil test` command is able to organize the build requirements beforehand. + +To run E2E tests, run `stencil test --e2e`. By default, files ending in `.e2e.ts` will be executed. + +Stencil's E2E test are provided with the following API, available via `@stencil/core/testing`. +Most methods are async and return Promises. Use `async` and `await` to declutter your tests. + +- `newE2EPage`: Should be invoked at the start of each test to instantiate a new `E2EPage` object + +`E2EPage` is a wrapper utility to Puppeteer to simplify writing tests. Some helpful methods on `E2EPage` include: + +- `find(selector: string)`: Find an element that matches the selector. Similar to `document.querySelector`. +- `setContent(html: string)`: Sets the content of a page. This is where you would include the markup of the component under test. +- `goto(url: string)`: open a page served by the dev server. URL has to start with `/`. +- `setViewport(viewport: Viewport)`: Updates the page to emulate a device display. This is helpful for testing a component's behavior in different orientations and viewport sizes. +- `waitForChanges()`: Both Stencil and Puppeteer have an asynchronous architecture, which is a good thing for performance. Since all calls are async, it's required that `await page.waitForChanges()` is called when changes are made to components. + +An example E2E test might have the following boilerplate: + +```typescript +import { newE2EPage } from '@stencil/core/testing'; + +describe('example', () => { + it('should render a foo-component', async () => { + const page = await newE2EPage(); + await page.setContent(`<foo-component></foo-component>`); + const el = await page.find('foo-component'); + expect(el).not.toBeNull(); + }); +}); +``` + +## Options + +The `newE2EPage` accepts the following options: + +- `html`: a HTML template, e.g. containing your Stencil component to be rendered in the browser (same as calling `page.setContent('...')`) +- `url`: opens a url to open a page served by the dev server (option will be ignored if `html` is set, same as calling `page.goto('...')`) +- `failOnConsoleError`: If set to `true`, Stencil will throw an error if a console error occurs (default: `false`) +- `failOnNetworkError`: If set to `true`, Stencil will throw an error if a network request fails (default: `false`) +- `logFailingNetworkRequests`: If set to `true`, Stencil will log failing network requests (default: `false`) + +## Example End-to-end Test + +```typescript +import { newE2EPage } from '@stencil/core/testing'; + +it('should create toggle, unchecked by default', async () => { + const page = await newE2EPage(); + + await page.setContent(` + <ion-toggle class="pretty-toggle"></ion-toggle> + `); + + const ionChange = await page.spyOnEvent('ionChange'); + + const toggle = await page.find('ion-toggle'); + + expect(toggle).toHaveClasses(['pretty-toggle', 'hydrated']); + + expect(toggle).not.toHaveClass('toggle-checked'); + + toggle.setProperty('checked', true); + + await page.waitForChanges(); + + expect(toggle).toHaveClass('toggle-checked'); + + expect(ionChange).toHaveReceivedEventDetail({ + checked: true, + value: 'on' + }); +}); +``` + +## E2E Testing Recipes + +### Find an element in the Shadow DOM + +Use the "piercing" selector `>>>` to query for an object inside a component's shadow root: + +```typescript +const el = await page.find('foo-component >>> .close-button'); +``` + +By default Stencil will look into all shadow roots to find the element. However if you want to restrict your query by specifying the shadow root of a particular component, you can chain multiple deep selectors within the same query, e.g.: + +```typescript +const el = await page.find('foo-component >>> div bar-component >>> div'); +``` + +### Set a @Prop() on a component + +Use `page.$eval` (part of the Puppeteer API) to set props or otherwise manipulate a component: + +```typescript +// create a new puppeteer page +// load the page with html content +await page.setContent(` + <prop-cmp></prop-cmp> + `); + +// select the "prop-cmp" element +// and run the callback in the browser's context +await page.$eval('prop-cmp', (elm: any) => { + // within the browser's context + // let's set new property values on the component + elm.first = 'Marty'; + elm.lastName = 'McFly'; +}); + +// we just made a change and now the async queue need to process it +// make sure the queue does its work before we continue +await page.waitForChanges(); +``` + +### Set a @Prop() on a component using an external reference + +Because `page.$eval` has an isolated scope, you’ll have to explicitly pass in outside references otherwise you’ll an encounter an `undefined` error. This is useful in case you’d like to import data from another file, or re-use mock data across multiple tests in the same file. + +```typescript +const props = { + first: 'Marty', + lastName: 'McFly', +}; + +await page.setContent(`<prop-cmp></prop-cmp>`); + +await page.$eval('prop-cmp', + (elm: any, { first, lastName }) => { + elm.first = first; + elm.lastName = lastName; + }, + props +); + +await page.waitForChanges(); +``` + + +### Call a @Method() on a component + +```typescript +const elm = await page.find('method-cmp'); +elm.setProperty('someProp', 88); +const methodRtnValue = await elm.callMethod('someMethod'); +``` + +### Type inside an input field + +```typescript +const page = await newE2EPage({ + html: ` + <dom-interaction></dom-interaction> + ` +}); + +const input = await page.find('dom-interaction >>> .input'); + +let value = await input.getProperty('value'); +expect(value).toBe(''); + +await input.press('8'); +await input.press('8'); +await input.press(' '); + +await page.keyboard.down('Shift'); +await input.press('KeyM'); +await input.press('KeyP'); +await input.press('KeyH'); +await page.keyboard.up('Shift'); +``` + +### Checking the text of a rendered component + +```typescript +await page.setContent(` + <prop-cmp first="Marty" last-name="McFly"></prop-cmp> + `); + +const elm = await page.find('prop-cmp >>> div'); +expect(elm).toEqualText('Hello, my name is Marty McFly'); +``` + +### Checking a component's HTML + +For shadowRoot content: + +```typescript + expect(el.shadowRoot).toEqualHtml(`<div> + <div class=\"nav-desktop\"> + <slot></slot> + </div> + </div>`); + }); +``` + +For non-shadow content: + +```typescript + expect(el).toEqualHtml(`<div> + <div class=\"nav-desktop\"> + <slot></slot> + </div> + </div>`); + }); +``` + +### Emulate a display + +If you need to test how a component behaves with a particular viewport you can set the viewport width and height like so: + +```ts +const page = await new E2EPage(); + +await page.setViewport({ + width: 900, + height: 600 +}); + +// Query an element that is hidden by a media query when width < 901px +const el = await page.find('.desktop'); +expect(el).not.toBeDefined(); +``` + +### Asynchronous Global Script + +If you are using an asynchronous [global script](/config/01-overview.md#globalscript), it may happen that your Stencil components haven't been hydrated at the time you are trying to interact with them. In this case it is recommend to wait for the hydration flag to be set on the component, e.g.: + +```ts +const page = await newE2EPage(); +await page.setContent(`<my-cmp></my-cmp>`); +await page.waitForSelector('.hydrated') +``` + +__Note:__ the hydrate selector may be different depending on whether you have a custom [`hydratedFlag`](/config/01-overview.md#hydratedflag) options set. + +## Caveat about e2e tests automation on CD/CI + +As it is a fairly common practice, you might want to automatically run your end-to-end tests on your Continuous Deployment/Integration (CD/CI) system. However, some environments might need you to tweak your configuration at times. If so, the `config` object in your `stencil.config.ts` file has a `testing` attribute that accepts parameters to modify how Headless Chrome is actually used in your pipeline. + +Example of a config you might need in a Gitlab CI environment : + +```typescript +export const config: Config = { + namespace: 'Foo', + testing: { + /** + * Gitlab CI doesn't allow sandbox, therefor this parameters must be passed to your Headless Chrome + * before it can run your tests + */ + browserArgs: ['--no-sandbox', '--disable-setuid-sandbox'], + }, + outputTargets: [ + { type: 'dist' }, + { + type: 'www', + }, + ], +}; +``` + +Check [the testing config docs](./02-config.md) to learn more about the possibilities on this matter. diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/06-screenshot-visual-diff.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/06-screenshot-visual-diff.md new file mode 100644 index 000000000..076c66de7 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/06-screenshot-visual-diff.md @@ -0,0 +1,62 @@ +--- +title: Screenshot Visual Diff +sidebar_label: Visual Screenshot Diff +description: Screenshot Visual Diff +slug: /screenshot-visual-diff +--- + +# Screenshot Visual Diff + +`EXPERIMENTAL`: screenshot visual diff testing is currently under heavy development and has not reached a stable status. However, any assistance testing would be appreciated. + +## Visual Regression Testing Commands + +```bash +stencil test --e2e --screenshot +``` + +## Quick Example + +[Puppeteer](https://github.com/GoogleChrome/puppeteer) is used to compare screenshots. In order to make one, you have to set up an e2e test, e.g.: +```javascript + it('render something', async () => { + const page: E2EPage = await newE2EPage(); + await page.setContent('<my-cmp></my-cmp>'); + await page.compareScreenshot('My Component (...is beautiful. Look at it!)', {fullPage: false}); + }); + +``` + +## Advanced Example + +```javascript +describe('stencil-avatar', () => { + it('renders and responds to the size property', async () => { + const page = await newE2EPage(); + + // In order to test against any global styles you may have, don't forget to set the link to the global css. You don't have to do this if your stencil.config.ts file doesn't build a global css file with globalStyle. + await page.setContent('<link href="http://localhost:3333/build/stellar-core.css" rel="stylesheet" /><stencil-avatar size="small"></stencil-avatar>'); + + const element = await page.find('stencil-avatar'); + expect(element).toHaveClass('hydrated'); + + // To start comparing the visual result, you first must run page.compareScreenshot; This will capture a screenshot, and save the file to "/screenshot/images". You'll be able to check that into your repo to provide those results to your team. You can only have one of these commands per test. + const results = await page.compareScreenshot(); + + // Finally, we can test against the previous screenshots. + // Test against hard pixels + expect(results).toMatchScreenshot({ allowableMismatchedPixels: 100 }) + + // Test against the percentage of changes. if 'allowableMismatchedRatio' is above 20% changed, + expect(results).toMatchScreenshot({ allowableMismatchedRatio: 0.2 }) + + }); +}); +``` + +After you've run your tests, you can open the `/screenshot/compare.html` page in your project to see the changes and their differences. + +## Current issues: +- Only screenshot the inner width of the items in the body itself. Currently the screenshot is taken at 600x600 pixels, so it makes "allowableMismatchedRatio" not a very valuable option. Something like a `await page.readjustSize()` that would clip the puppeteer page to the width of the rendered content would help make allowableMismatchedRatio more usable. +- Needs more testing! + diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/07-screenshot-connector.md b/versioned_docs/version-v4.22/testing/stencil-testrunner/07-screenshot-connector.md new file mode 100644 index 000000000..e62d24527 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/07-screenshot-connector.md @@ -0,0 +1,101 @@ +--- +title: Screenshot Connector +sidebar_label: Screenshot Connector +description: Screenshot Connector +slug: /screenshot-connector +--- + +# Screenshot connector +You can configure a screenshot connector module to be used by the screenshot testing process, to modify the default behavior of the caching, comparing and publishing of your tests. +Just create a file which defines a connector class and point to it in your stencil testing config: + +```tsx +export const config: Config = { + ... + testing: { + screenshotConnector: './connector.js' + } +}; +``` + +## Writing a connector +To write a connector, import the base `ScreenshotConnector` class from stencil and extend it: +```javascript +const { ScreenshotConnector } = require('@stencil/core/screenshot'); + +module.exports = class ScreenshotCustomConnector extends ScreenshotConnector { + ... +}; +``` + +:::note +For a good reference on how this can be done, have a look at the default `StencilLocalConnector` [here](https://github.com/ionic-team/stencil/blob/main/src/screenshot/connector-local.ts) +::: + +## Methods +The base connector which can be imported and extended from stencil has the following methods which can be overwritten: + +```tsx reference title="ScreenshotConnector" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1631-L1645 +``` +For references to the interfaces, [see here](#interfaces) + +### initBuild(options) +This method is being called to setup the connector and ready everything for running the tests. It is responsible for setting up the variables, filepaths and folder structures needed for running the screenshot tests. + +:::note +Only overwrite this method if you know what you do! For easy extension, make sure to call `super.initBuild` +::: + +### pullMasterBuild() +After initializing the connector, and setting up the build, this method is being run to give the possibility to pull the master build. This can be very useful in case the screenshots are stored somewhere else then on the machine on which the tests are running. + +### getMasterBuild() +Now that the tests are setup an ready to run this method is being called to return the master build. So instead of loading the master build from a file it could be fetched from an api and returned in this method. + +### getScreenshotCache() +This method is being called to return the screenshot cache which will then be extended with the current build results. + +### completeBuild(masterBuild) +After running the tests and generating the screenshots into the configured folder, this method is being called to create the result json data. At this time the images are there and the master build is being passed in as an option. + +:::note +Only overwrite this method if you know what you do! For easy extension, make sure to call `super.completeBuild` +::: + +### publishBuild(buildResults) +Now that the build has been completed and the results were generated, this method will be called with the result data. In here the results can be written to a json file, or sent to a remote location. In the default `StencilLocalConnector` this method will create the compare app html. + +### updateScreenshotCache(screenshotCache, buildResults) +At the end of the whole run, the screenshot cache should be updated with this method. So it can be written to a file or be sent to an api from here. + + +## Interfaces + +```tsx reference title="ScreenshotConnectorOptions" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1676-L1698 +``` + +```tsx reference title="ScreenshotBuild" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1725-L1734 +``` + +```tsx reference title="ScreenshotBuildResults" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1647-L1652 +``` + +```tsx reference title="ScreenshotCompareResults" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1654-L1674 +``` + +```ts reference title="ScreenshotCache" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1736-L1756 +``` + +```ts reference title="Screenshot" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1758-L1772 +``` + +```ts reference title="ScreenshotDiff" +https://github.com/ionic-team/stencil/blob/a2e119d059ba0d0fa6155dbd3d82c17612630828/src/declarations/stencil-private.ts#L1774-L1792 +``` diff --git a/versioned_docs/version-v4.22/testing/stencil-testrunner/_category_.json b/versioned_docs/version-v4.22/testing/stencil-testrunner/_category_.json new file mode 100644 index 000000000..30ee95d5f --- /dev/null +++ b/versioned_docs/version-v4.22/testing/stencil-testrunner/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Stencil Test Runner", + "position": 1 +} diff --git a/versioned_docs/version-v4.22/testing/webdriverio/01-overview.md b/versioned_docs/version-v4.22/testing/webdriverio/01-overview.md new file mode 100644 index 000000000..cd10595dc --- /dev/null +++ b/versioned_docs/version-v4.22/testing/webdriverio/01-overview.md @@ -0,0 +1,89 @@ +--- +title: WebdriverIO Overview +sidebar_label: Overview +--- + +# Overview + +WebdriverIO is a progressive automation framework built to automate modern web and mobile applications. It simplifies the interaction with your project and provides a set of plugins that help you create a scalable, robust and stable test suite. + +Testing with WebdriverIO has the following advantages: + +- __Cross Browser Support__: WebdriverIO is designed to support all platforms, either on desktop or mobile. You can run tests on actual browser your users are using, including covering different versions of them. +- __Real User Interaction__: Interaction with elements in WebdriverIO through the WebDriver protocol is much closer to native user-triggered interactions than what can be achieved with emulated DOM environments (such as JSDom or Stencil's own Mock-Doc). +- __Web Platform Support__: Running tests in actual browsers allows you to tap into the latest Web Platform features for testing your components, often not available when using virtual DOM environments. +- __Real Environments__: Run your tests in an environment that your users are using and not somewhere that re-implements web standards with polyfills like JSDOM. + +## Set Up + +To get started with WebdriverIO, all you need to do is to run their project starter: + +```bash npm2yarn +npm init wdio@latest . +``` + +This will initiate WebdriverIO's configuration wizard that walks you through the setup. Make sure you select the following options when walking through it: + +- __What type of testing would you like to do?__ Select either: + - `Component or Unit Testing - in the browser` if you are interested adding unit tests for your components + - `E2E Testing - of Web or Mobile Applications` if you like to test your whole application + + You can always add either of them later on +- __Which framework do you use for building components?__: if you select _Component or Unit Testing_ make sure to select `StencilJS` as preset so WebdriverIO knows how to compile your components properly + +The following questions can be answered as desired. Once setup the wizard has created a `wdio.conf.ts` file and a `wdio` script to run your tests. + +:::info CJS vs. ESM + +WebdriverIO's generated config and test files use ESM syntax for imports. If you generated a project via the [`create-stencil`](https://www.npmjs.com/package/create-stencil) starter package your project is likely setup for CommonJS. To avoid any incompatibility issues, we recommend to rename your `wdio.conf.ts` to `wdio.conf.mts` and update the `wdio` script in your `package.json`. + +::: + +:::info Type Clashes + +It's possible that you run into TypeScript issues as WebdriverIO uses Mocha for component testing and Stencil Jest. Both register the same globals, e.g. `it` which causes type clashes. To fix these we recommend to add the following to your `tsconfig.json`: + +```json + "types": ["jest"] +``` + +This will ensure that Jest types will be preferred. + +::: + +You should be able to run your first test on the auto-generated test file via: + +```bash npm2yarn +npm run wdio +``` + +More information on setting up WebdriverIO can be found in their [documentation](https://webdriver.io/docs/component-testing/stencil). + +## Integration with Stencil + +If you have been using Stencil's test runner for unit or end-to-end tests to can continue to do so. For basic implementation details that don't require any web platform features, running tests through the Stencil test runner might still be the faster choice, since no browser needs to be spawned. However you can also migrate over to only one single framework one test at a time. + +We recommend to create a new NPM script for running both, Stencil and WebdriverIO tests, starting with Stencil tests first as they are likely to run faster. In your `package.json` this can be structured like so: + +```json title="package.json" +{ + "scripts:": { + "test.e2e": "stencil test && wdio run wdio.conf.ts" + } +} +``` + +Make sure that each test runner picks up their respective tests by defining the `testRegex` property in your Stencil config, e.g.: + +```ts title="stencil.config.ts" +import { Config } from '@stencil/core'; + +export const config: Config = { + // ... + testing: { + testRegex: '(/__tests__/.*|\\.?(spec))\\.(ts|js)$', + }, +}; +``` + +This will make Stencil pick up all files ending with `.spec.ts` or `.spec.js` while WebdriverIO picks up tests ending with `.test.ts`. \ No newline at end of file diff --git a/versioned_docs/version-v4.22/testing/webdriverio/02-unit-testing.md b/versioned_docs/version-v4.22/testing/webdriverio/02-unit-testing.md new file mode 100644 index 000000000..e86faf3f0 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/webdriverio/02-unit-testing.md @@ -0,0 +1,69 @@ +--- +title: Unit Testing +sidebar_label: Unit Testing +description: Unit Testing +slug: /testing/webdriverio/unit-testing +--- + +# Unit Testing + +WebdriverIO makes it easy to unit test components and app utility functions in the browser. Unit tests validate the code in isolation. Well written tests are fast, repeatable, and easy to reason about. It tries to follow a simple guiding principle: the more your tests resemble the way your software is used, the more confidence they can give you. + +### Test Setup + +For a test to resemble as much as possible how your component is used in an actual application we need to render it into an actual DOM tree. WebdriverIO provides a helper package for this that you can use called `@wdio/browser-runner/stencil`. It exports a `render` method that allows us to mount our component to the DOM. + +For example, given the following component: + +```ts reference title="/src/components/my-component/my-component.tsx" +https://github.com/webdriverio/component-testing-examples/blob/main/stencil-component-starter/src/components/my-component/my-component.tsx +``` + +We import the component into our test to render it in the browser: + +```ts reference title="/src/components/my-component/my-component.test.tsx" +https://github.com/webdriverio/component-testing-examples/blob/main/stencil-component-starter/src/components/my-component/my-component.test.tsx#L2-L18 +``` + +If your component under test uses other Stencil components make sure you add these to the `components` list as well. For example, let's say `ComponentA` uses `ComponentB` which also imports `ComponentC` and `ComponentD`. In this case you will have to import and pass in all components you'd like to have rendered, e.g.: + +```ts +render({ + components: [ComponentA, ComponentB, ComponentC, ComponentD], + template: () => <component-a first="Stencil" last="'Don't call me a framework' JS" /> +}); +``` + +While this seems tedious at first, it gives you the flexibility to leave out components that are not relevant for your test, or have side effects that can cause flakiness. + +Find more information about the `render` method option and its return object in the WebdriverIO [documentation](https://webdriver.io/docs/component-testing/stencil#render-options). + +### Handling Asynchronicity + +Instead of directly working on DOM objects, with WebdriverIO you interact with references to DOM nodes through asynchronous WebDriver commands. Make sure you always use an `await` to ensure that all commands and assertion are executed sequentially. + +:::info + +Missing an `await` can be a simple oversight and can cause us long hours of debugging. To avoid this and ensure promises are handled properly, it is recommended to use an ESLint rule called [`require-await`](https://eslint.org/docs/latest/rules/require-await). + +::: + +### Matchers + +WebdriverIO provides their own matchers to make various assertions about an element. We recommend these over synchronous matchers like `toBe` or `toEqual` as they allow for retries and make your tests more resilient against flakiness. + +For example, instead of asserting the content of a component like this: + +```ts +expect(await $('my-component').getText()) + .toBe(`Hello, World! I'm Stencil 'Don't call me a framework' JS`) +``` + +It is better to use WebdriverIOs matchers for asserting text: + +```ts +await expect($('my-component')) + .toHaveText(`Hello, World! I'm Stencil 'Don't call me a framework' JS`) +``` + +You can read more about WebdriverIOs specific matchers, in the project [documentation](https://webdriver.io/docs/api/expect-webdriverio). \ No newline at end of file diff --git a/versioned_docs/version-v4.22/testing/webdriverio/03-mocking.md b/versioned_docs/version-v4.22/testing/webdriverio/03-mocking.md new file mode 100644 index 000000000..057da10f8 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/webdriverio/03-mocking.md @@ -0,0 +1,34 @@ +--- +title: Mocking +sidebar_label: Mocking +description: Mocking +label: Mocking +--- + +# Mocking + +WebdriverIO has support for file based module mocking as well as mocking of entire dependencies of your project. The framework provides a set of primitives for mocking as documented in the project [documentation](https://webdriver.io/docs/component-testing/mocking): + +```ts +import { mock, fn, unmock } from '@wdio/browser-runner' +``` + +To create a mock you can either create a file with the name of the module you would like to mock in the `__mocks__` directory, as described in [Manual Mocks](https://webdriver.io/docs/component-testing/mocking#manual-mocks), or mock the file directly as part of your test: + +```ts +import { mock, fn } from '@wdio/browser-runner' +import { format } from './utils/utils.ts' + +// mock files within the project +mock('./utils/utils.ts', () => ({ + format: fn().mockReturnValue(42) +})) +// mock whole modules and replace functionality with what is defined in `./__mocks__/leftpad.ts` +mock('leftpad') + +console.log(format()) // returns `42` +``` + +Once a module is mocked, importing it either from your test or your component will give you the mocked version of the module and not the actual one. + +Find more examples and documentation on mocking in the project [documentation](https://webdriver.io/docs/component-testing/mocking). \ No newline at end of file diff --git a/versioned_docs/version-v4.22/testing/webdriverio/04-visual-testing.md b/versioned_docs/version-v4.22/testing/webdriverio/04-visual-testing.md new file mode 100644 index 000000000..41c45acb7 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/webdriverio/04-visual-testing.md @@ -0,0 +1,60 @@ +--- +title: Visual Testing +sidebar_label: Visual Testing +description: Visual Testing +slug: /testing/webdriverio/visual-testing +--- + +# Visual Testing + +WebdriverIO supports [visual testing capabilities](https://webdriver.io/docs/visual-testing) out of the box through a plugin called [`@wdio/visual-service`](https://www.npmjs.com/package/@wdio/visual-service). It uses [ResembleJS](https://github.com/Huddle/Resemble.js) under the hood to do pixel perfect comparisons. + +## Adding Visual Testing to your Setup + +If you don't have a WebdriverIO project set up yet, please take a look at the set up instructions we provide on the [WebdriverIO Overview](./01-overview.md) page. + +Once you are set up, add the visual plugin to your project via: + +```bash npm2yarn +npm install --save-dev @wdio/visual-service +``` + +A plugin, also called [service](https://webdriver.io/docs/customservices) in WebdriverIO, has access to a variety of test lifecycle hooks to enable new functionality or integrate with another platform. To use a service, add it to your services list in your `wdio.conf.ts`: + +```ts reference title="wdio.conf.ts" +https://github.com/webdriverio/component-testing-examples/blob/2de295ab568b5163e67d716156221578b6536d9d/stencil-component-starter/wdio.conf.ts#L119-L126) +``` + +As shown in the [Visual Testing](https://webdriver.io/docs/visual-testing/writing-tests/) WebdriverIO documentation, the service adds 4 new matchers to visually assert your application: + +- `toMatchScreenSnapshot`: captures and compares the whole browser screen +- `toMatchElementSnapshot`: captures and compares the visual difference within the element boundaries +- `toMatchFullPageSnapshot`: captures and compares the whole document +- `toMatchTabbablePageSnapshot`: same as `toMatchFullPageSnapshot` with tab marks for accessibility testing + +In the context of testing StencilJS components the best choice is to use `toMatchElementSnapshot` to verify a single component visually. Such a test may appear as follows: + +```ts reference title="src/components/my-component/my-component.test.tsx" +https://github.com/webdriverio/component-testing-examples/blob/2de295ab568b5163e67d716156221578b6536d9d/stencil-component-starter/src/components/my-component/my-component.test.tsx#L20-L28 +``` + +The screenshots will be generated locally and the baseline should be checked into your project, so that everyone running the tests visually, compare against the same assumptions. If a test is failing, e.g. we set the color of the text to a different color, WebdriverIO will let the test fail with the following message: + +``` +Expected image to have a mismatch percentage of 0%, but was 6.488% +Please compare the images manually and update the baseline if the new screenshot is correct. + +Baseline: /stencil-project/__snapshots__/MyComponent-chrome-1200x1551-dpr-2.png +Actual Screenshot: /stencil-project/__snapshots__/.tmp/actual/MyComponent-chrome-1200x1551-dpr-2.png +Difference: /stencil-project/__snapshots__/.tmp/diff/MyComponent-chrome-1200x1551-dpr-2.png + +See https://webdriver.io/docs/api/visual-regression.html for more information. +``` + +You can see the visual differences highlighted in `/stencil-project/__snapshots__/.tmp/diff` which can look as following: + +![Example of visual difference](/img/testing/diff-example.png) + +If you believe the visual changes are correct, update the baseline by moving the image from `stencil-project/__snapshots__/.tmp/actual` into the baseline directory. + +For further information on Visual Testing in WebdriverIO visit their [documentation page](https://webdriver.io/docs/visual-testing). \ No newline at end of file diff --git a/versioned_docs/version-v4.22/testing/webdriverio/_category_.json b/versioned_docs/version-v4.22/testing/webdriverio/_category_.json new file mode 100644 index 000000000..dfa20f841 --- /dev/null +++ b/versioned_docs/version-v4.22/testing/webdriverio/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "WebdriverIO", + "position": 2 +} diff --git a/versioned_sidebars/version-v4.22-sidebars.json b/versioned_sidebars/version-v4.22-sidebars.json new file mode 100644 index 000000000..640a77ee8 --- /dev/null +++ b/versioned_sidebars/version-v4.22-sidebars.json @@ -0,0 +1,44 @@ +{ + "docs": [ + { + "type": "autogenerated", + "dirName": "." + }, + { + "type": "category", + "label": "Community", + "items": [ + { + "type": "link", + "label": "Stencil on Twitter", + "href": "https://twitter.com/stenciljs" + }, + { + "type": "link", + "label": "Stencil on Discord", + "href": "https://chat.stenciljs.com/" + }, + { + "type": "link", + "label": "Stencil on GitHub", + "href": "https://github.com/ionic-team/stencil" + } + ] + }, + { + "type": "category", + "label": "Legal", + "items": [ + { + "type": "doc", + "id": "telemetry" + }, + { + "type": "link", + "label": "Privacy Policy", + "href": "https://ionic.io/privacy" + } + ] + } + ] +} diff --git a/versions.json b/versions.json index 792fddffc..d9fabd3f1 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ [ + "v4.22", "v4.21", "v4.20", "v4.19",