diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/index.html b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/index.html new file mode 100644 index 000000000..d0fdb30d1 --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/my-element.ts b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/my-element.ts new file mode 100644 index 000000000..40810f9c3 --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/my-element.ts @@ -0,0 +1,33 @@ +import { html, LitElement } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js';import {ComplexAttributeConverter} from 'lit'; + +/** + * Bidirectionally converts an array from an attribute to a property of the + * following format: + * + * array-attribute='1, "2", 3' to [1, '2', 3] + */ +export const arrayConverter: ComplexAttributeConverter> = { + toAttribute: (array: Array) => { + return JSON.stringify(array).substring(1, JSON.stringify(array).length - 1); + }, + fromAttribute: (value: string) => { + try { + return JSON.parse(`[${value}]`); + } catch { + return []; + } + } +}; + +@customElement('my-element') +export class MyElement extends LitElement { + @property({ converter: arrayConverter, reflect: true }) + array: Array = []; + + render() { + return this.array.map((item) => + html`
${typeof item}: ${item}
` + ); + } +} \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/project.json b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/project.json new file mode 100644 index 000000000..9c791c2a3 --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/custom-attribute-converter/project.json @@ -0,0 +1,8 @@ +{ + "extends": "/samples/v3-base.json", + "files": { + "my-element.ts": {}, + "index.html": {} + }, + "previewHeight": "200px" +} diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/id-card.ts b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/id-card.ts new file mode 100644 index 000000000..0feccc9d3 --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/id-card.ts @@ -0,0 +1,30 @@ +import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +@customElement('id-card') +export class IdCard extends LitElement { + // Default attribute converter is string + @property() name = ''; + // Number attribute converter converts attribtues to numbers + @property({ type: Number }) age = 0; + // Boolean attribute converter converts attribtues to boolean using + // .hasAttribute(). NOTE: boolean-attribute="false" will result in `true` + @property({ type: Boolean }) programmer = false; + // You can also specify the attribute name + @property({ type: Boolean, attribute: 'is-cool' }) isCool = false; + + render() { + return html` +

${this.name}

+

Age: ${this.age}

+ + + `; + } +} \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/index.html b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/index.html new file mode 100644 index 000000000..d5f0f24d5 --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/my-wallet.ts b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/my-wallet.ts new file mode 100644 index 000000000..b88d3de0e --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/my-wallet.ts @@ -0,0 +1,19 @@ +import { html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import './id-card.js'; + +@customElement('my-wallet') +export class MyWallet extends LitElement { + render() { + return html` + + + + + + `; + } +} \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/project.json b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/project.json new file mode 100644 index 000000000..b768f2dcd --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-properties/project.json @@ -0,0 +1,9 @@ +{ + "extends": "/samples/v3-base.json", + "files": { + "id-card.ts": {}, + "my-wallet.ts": {}, + "index.html": {} + }, + "previewHeight": "400px" +} diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/index.html b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/index.html new file mode 100644 index 000000000..57a0c9aca --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/my-element.ts b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/my-element.ts new file mode 100644 index 000000000..d73ac0ef2 --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/my-element.ts @@ -0,0 +1,66 @@ +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +@customElement('my-element') +export class MyElement extends LitElement { + // Duration affects render, so it should be reactive. Though we don't want it + // to be exposed to consumers of my-element because we only want to expose + // `start()`, `pause()`, `reset()`, so we use a private state. + @state() private _duration = 0; + // isPlaying affects render, so it should be reactive. Though we don't want it + // to be exposed to consumers of my-element, so we use a private state. + @state() private _isPlaying = false; + private lastTick = 0; + + render() { + const min = Math.floor(this._duration / 60000); + const sec = pad(min, Math.floor(this._duration / 1000 % 60)); + const hun = pad(true, Math.floor(this._duration % 1000 / 10)); + + return html` +
+ ${min ? `${min}:${sec}.${hun}` : `${sec}.${hun}`} +
+
+ ${this._isPlaying ? + html`` : + html`` + } + +
+ `; + } + + start() { + this._isPlaying = true; + this.lastTick = Date.now(); + this._tick(); + } + + pause() { + this._isPlaying = false; + } + + reset() { + this._duration = 0; + } + + private _tick() { + if (this._isPlaying) { + const now = Date.now(); + this._duration += Math.max(0, now - this.lastTick); + this.lastTick = now; + requestAnimationFrame(() => this._tick()); + } + } + + connectedCallback() { + super.connectedCallback(); + this.reset(); + } +} +/* playground-fold */ +function pad(pad: unknown, val: number) { + return pad ? String(val).padStart(2, '0') : val; +} +/* playground-fold-end */ diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/project.json b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/project.json new file mode 100644 index 000000000..816404c3d --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/reactive-state/project.json @@ -0,0 +1,8 @@ +{ + "extends": "/samples/v3-base.json", + "files": { + "my-element.ts": {}, + "index.html": {} + }, + "previewHeight": "100px" +} diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/index.html b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/index.html new file mode 100644 index 000000000..57a0c9aca --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/my-element.ts b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/my-element.ts new file mode 100644 index 000000000..dfb68d94d --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/my-element.ts @@ -0,0 +1,43 @@ +import { html, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +@customElement('my-element') +export class MyElement extends LitElement { + // Technically `@state` is not necessary for the way we modify + // `_requestUpdateArray`, but it's generally good practice to use it. + @state() private _requestUpdateArray: number[] = []; + @state() private _newReferenceArray: number[] = []; + + render() { + return html` +
+ Request Update Array: [${this._requestUpdateArray.join(', ')}] +
+ +
+
+ +
+ New Reference Array: [${this._newReferenceArray.join(', ')}] +
+ +
+
+ `; + } + + private _addToRequestUpdateArray() { + this._requestUpdateArray.push(this._requestUpdateArray.length); + // Call request update to tell Lit that something has changed. + this.requestUpdate(); + } + + private _addToNewReferenceArray() { + // This creates a new array / object reference, so it will trigger an update + // with the default change detection. Could be expensive for large arrays. + this._newReferenceArray = [ + ...this._newReferenceArray, + this._newReferenceArray.length, + ]; + } +} \ No newline at end of file diff --git a/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/project.json b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/project.json new file mode 100644 index 000000000..875a71aaf --- /dev/null +++ b/packages/lit-dev-content/samples/articles/lit-cheat-sheet/rerender-array-change/project.json @@ -0,0 +1,8 @@ +{ + "extends": "/samples/v3-base.json", + "files": { + "my-element.ts": {}, + "index.html": {} + }, + "previewHeight": "120px" +} diff --git a/packages/lit-dev-content/samples/examples/context-consume-provide/project.json b/packages/lit-dev-content/samples/examples/context-consume-provide/project.json index 2f44284be..600ac88dd 100644 --- a/packages/lit-dev-content/samples/examples/context-consume-provide/project.json +++ b/packages/lit-dev-content/samples/examples/context-consume-provide/project.json @@ -9,5 +9,6 @@ "my-heading.ts": {}, "level-context.ts": {}, "index.html": {} - } + }, + "previewHeight": "120px" } diff --git a/packages/lit-dev-content/site/articles/article/lit-cheat-sheet.md b/packages/lit-dev-content/site/articles/article/lit-cheat-sheet.md index 750ee3090..a4554628f 100644 --- a/packages/lit-dev-content/site/articles/article/lit-cheat-sheet.md +++ b/packages/lit-dev-content/site/articles/article/lit-cheat-sheet.md @@ -320,6 +320,116 @@ You can set shadow root options passed to `Element.attachShadow()` by overriding ## Properties and State +### Reactive Properties + +Reactive properties are properties within a component that automatically trigger +a re-render when they change. These properties can be set externally, from +outside the component's boundary. + +They also handle attributes by accepting them and converting them into +corresponding properties. + +You can define a reactive property with the @property decoratorstatic properties = { propertyName: {...}} code block and initializing them in the constructor(). + + +{% playground-ide "articles/lit-cheat-sheet/reactive-properties", true %} + +**Related Documentation & Topics:** + +- [Reactive Properties](/docs/components/properties/) +- [Public reactive properties](/docs/components/properties/#declare) +- [Attributes](/docs/components/properties/#attributes) +- [Custom property accessors](/docs/components/properties/#attributes) +- [Customizing change detection](/docs/components/properties/#haschanged) +- [Reactivity Tutorial](/tutorials/reactivity/) +- [Custom Attribute Converters Tutorial](/tutorials/custom-attribute-converter/) +- [What Are Elements Video](https://www.youtube.com/watch?v=x_mixcGEia4) +- [Introduction to Lit - Reactive Properties Video](https://www.youtube.com/watch?v=uzFakwHaSmw&t=576s) + +### Reactive State + +Reactive state is a property that is private to the component and is not exposed +to the outside world. These properties are used to store internal state of a +component that should trigger a re-render of the Lit lifecycle when they change. + +You can define a reactive property with the @state decoratorstatic properties = { propertyName: {state: true, ...}} code block and setting the state: true flag in the property's info. You can initialize them in the constructor(). + +{% playground-ide "articles/lit-cheat-sheet/reactive-state", true %} + +**Related Documentation & Topics:** + +- [Reactive Properties](/docs/components/properties/) +- [Internal Reactive State](/docs/components/properties/#internal-reactive-state) +- [Customizing change detection](/docs/components/properties/#haschanged) +- [Reactivity Tutorial](/tutorials/reactivity/) +- [What Are Elements Video](https://www.youtube.com/watch?v=x_mixcGEia4) +- [Introduction to Lit - Reactive Properties Video](https://www.youtube.com/watch?v=uzFakwHaSmw&t=576s) + +### Re-render an Array or Object Change + +Arrays are objects in JavaScript, and Lit's default change detection uses strict +equality to determine if an array has changed. If you need to re-render a +component when an array is mutated with something like `.push()` or `.pop()`, +you will need to let Lit know that the array has changed. + +The most common ways to do this are: + +- Use the `requestUpdate()` method to manually trigger a re-render +- Create a new array / object reference + +{% playground-ide "articles/lit-cheat-sheet/rerender-array-change", true %} + +{% aside "warn" %} + +Custom `hasChanged()` methods in the reactive property definition won't help +much here. + +The `hasChanged()` function is only called when the property is set, not when +the property is mutated. This would only be helpful when an array or object has +a new reference assigned to it and you _don't_ want to trigger a re-render. + +If this is your use case, you might generally be better off using a [`repeat()` +directive](#re-rendering-lists-efficiently). + +{% endaside %} + +**Related Documentation & Topics:** + +- [Lifecycle - Triggering an update](/docs/components/lifecycle/#reactive-update-cycle-triggering) +- [Rendering Arrays](/docs/templates/lists/#rendering-arrays) +- [Reactivity Tutorial - Triggerin an update](/tutorials/reactivity/#3) +- [Working With Lists Tutorial](/tutorials/working-with-lists/) + +### Custom Attribute Converters + +In advanced cases, you may need to convert an attribute value to a property in +a special way and vice versa. You can do this with a custom attribute converter. + +{% playground-ide "articles/lit-cheat-sheet/custom-attribute-converter", true %} + +**Related Documentation & Topics:** + +- [Reactive properties - Providing a custom converter](/docs/components/properties/#conversion-converter) +- [Reactive properties - Using the default converter](/docs/components/properties/#conversion-type) +- [Attributes](/docs/components/properties/#attributes) +- [Custom Attribute Converters Tutorial](/tutorials/custom-attribute-converter/) +- [Reactive Properties](/docs/components/properties/) +- [Public reactive properties](/docs/components/properties/#declare) +- [Custom attribute converter snippet](/playground/#sample=examples/properties-custom-converter) + +### Context + +If you need to pass data down to a subtree without using properties or "prop +drilling", you might want to use +[`@lit/context`](https://www.npmjs.com/package/@lit/context). + +{% playground-ide "examples/context-consume-provide", true %} + +**Related Documentation & Topics:** + +- [Context](/docs/data/context/) +- [WCCG Context Community Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md) (external) + ## Lifecycle ### Lifecycle order @@ -424,6 +534,8 @@ All Lit elements have asynchronous lifecycles. You need to wait for the `updateC - [updateComplete()](/docs/components/lifecycle/#updatecomplete) - [requestUpdate()](/docs/components/lifecycle/#requestUpdate) +## Events + ### Adding listeners to host element or globally A common pattern is to add event listeners to the host element or globally in the `connectedCallback` and remove them in the `disconnectedCallback`. @@ -486,3 +598,4 @@ If you want an event to bubble through shadow Roots, set `composed: true`. - [Expressions – Event listener expressions](/docs/templates/expressions/#event-listener-expressions) - [Events](/docs/components/events/) - [Customizing event listener options](/docs/components/events/#event-options-decorator) +