Skip to content

Commit

Permalink
add properties and state examples
Browse files Browse the repository at this point in the history
  • Loading branch information
e111077 committed Aug 28, 2024
1 parent 5cc518a commit cf5f9c1
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script type="module" src="./my-element.js"></script>

<my-element array='1,"2",3,4,"5"'></my-element>
Original file line number Diff line number Diff line change
@@ -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<Array<unknown>> = {
toAttribute: (array: Array<unknown>) => {
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<number|string> = [];

render() {
return this.array.map((item) =>
html`<div>${typeof item}: ${item}</div>`
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "/samples/v3-base.json",
"files": {
"my-element.ts": {},
"index.html": {}
},
"previewHeight": "200px"
}
Original file line number Diff line number Diff line change
@@ -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`
<h3>${this.name}</h3>
<p>Age: ${this.age}</p>
<label style="display: block;">
<input disabled type="checkbox" ?checked=${this.programmer}>
Programmer
</label>
<label>
<input disabled type="checkbox" ?checked=${this.isCool}>
Is Cool
</label>
`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script type="module" src="./my-wallet.js"></script>

<my-wallet></my-wallet>
Original file line number Diff line number Diff line change
@@ -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`
<id-card .name=${"Steven"} .age=${27} ?programmer=${true} .isCool=${true}></id-card>
<!-- It can also convert attributes into properties -->
<id-card name="Elliott" age="30" programmer></id-card>
<!--
NOTE: boolean-attribute="false" will be true, because the default
boolean attribute converter uses .hasAttribute()
-->
<id-card name="Dan" age="35" programmer is-cool></id-card>
`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "/samples/v3-base.json",
"files": {
"id-card.ts": {},
"my-wallet.ts": {},
"index.html": {}
},
"previewHeight": "400px"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script type="module" src="./my-element.js"></script>

<my-element></my-element>
Original file line number Diff line number Diff line change
@@ -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`
<div>
${min ? `${min}:${sec}.${hun}` : `${sec}.${hun}`}
</div>
<div>
${this._isPlaying ?
html`<button @click=${this.pause}>Pause</button>` :
html`<button @click=${this.start}>Play</button>`
}
<button @click=${this.reset}>Reset</button>
</div>
`;
}

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 */
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "/samples/v3-base.json",
"files": {
"my-element.ts": {},
"index.html": {}
},
"previewHeight": "100px"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script type="module" src="./my-element.js"></script>

<my-element></my-element>
Original file line number Diff line number Diff line change
@@ -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`
<section>
Request Update Array: [${this._requestUpdateArray.join(', ')}]
<div>
<button @click=${this._addToRequestUpdateArray}>Add Element</button>
</div>
</section>
<section>
New Reference Array: [${this._newReferenceArray.join(', ')}]
<div>
<button @click=${this._addToNewReferenceArray}>Add Element</button>
</div>
</section>
`;
}

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,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "/samples/v3-base.json",
"files": {
"my-element.ts": {},
"index.html": {}
},
"previewHeight": "120px"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"my-heading.ts": {},
"level-context.ts": {},
"index.html": {}
}
},
"previewHeight": "120px"
}
113 changes: 113 additions & 0 deletions packages/lit-dev-content/site/articles/article/lit-cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ts-js><span slot="ts"><code>@property</code> decorator</span><span slot="js"><code>static properties = { propertyName: {...}}</code> code block and initializing them in the <code>constructor()</code></span></ts-js>.


{% 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 <ts-js><span slot="ts"><code>@state</code> decorator</span><span slot="js"><code>static properties = { propertyName: {state: true, ...}}</code> code block and setting the <code>state: true</code> flag in the property's info. You can initialize them in the <code>constructor()</code></span></ts-js>.

{% 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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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)

0 comments on commit cf5f9c1

Please sign in to comment.