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`
+
+ `;
+ }
+
+ 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)
+