@a11d/lit 0.13.4 โ 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/Component/README.md +15 -1
- package/bind/README.md +1 -1
- package/dist/Component/Component.d.ts +9 -0
- package/dist/Component/Component.d.ts.map +1 -1
- package/dist/Component/Component.js +18 -0
- package/dist/Component/Hydration.d.ts +16 -0
- package/dist/Component/Hydration.d.ts.map +1 -0
- package/dist/Component/Hydration.js +29 -0
- package/dist/Component/hydrateAttributes.d.ts +2 -0
- package/dist/Component/hydrateAttributes.d.ts.map +1 -0
- package/dist/Component/hydrateAttributes.js +43 -0
- package/dist/bind/Binder.d.ts +2 -2
- package/dist/updated/UpdatedController.d.ts.map +1 -1
- package/dist/updated/UpdatedController.js +10 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 0.14.1 (2026-10-07)
|
|
2
|
+
- **๐ Fix**: Run `@property({ updated })` hooks with Lit's hydration support loaded ([52b35bc](https://github.com/a11delavar/lit/commit/52b35bc93974f87e7e5e3237e86dd51a0a75df63))
|
|
3
|
+
|
|
4
|
+
## 0.14.0 (2026-10-07)
|
|
5
|
+
- **๐ Feature**: Make hydration write the attributes the browser renders differently from the server ([58d6b39](https://github.com/a11delavar/lit/commit/58d6b397782bce86749001431dbc56b10a6cfce7))
|
|
6
|
+
- **๐ Feature**: Add `Component.hydrating`, true during a server-rendered component's first render in the browser ([58d6b39](https://github.com/a11delavar/lit/commit/58d6b397782bce86749001431dbc56b10a6cfce7))
|
|
7
|
+
|
|
1
8
|
## 0.13.4 (2026-10-06)
|
|
2
9
|
- **๐งน Chore**: Improve the package description and feature summaries ([285feac](https://github.com/a11delavar/lit/commit/285feac0f6a030647973e572344b168379227eb6))
|
|
3
10
|
|
package/Component/README.md
CHANGED
|
@@ -7,6 +7,7 @@ In addition to [Lit's standard lifecycle](https://lit.dev/docs/components/lifecy
|
|
|
7
7
|
- `initialized()` - Called once after the component is constructed
|
|
8
8
|
- `connected()` - Called each time the component is connected to the DOM
|
|
9
9
|
- `disconnected()` - Called each time the component is disconnected from the DOM
|
|
10
|
+
- `hydrating` getter - Whether the first render in the browser must reproduce the server's
|
|
10
11
|
|
|
11
12
|
```ts
|
|
12
13
|
import { component, Component, html, property } from '@a11d/lit'
|
|
@@ -35,4 +36,17 @@ class CustomButton extends Component {
|
|
|
35
36
|
`
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
```
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Server-side rendering
|
|
42
|
+
|
|
43
|
+
A component rendered by [Lit SSR](https://lit.dev/docs/ssr/overview/) hydrates in the browser once `@lit-labs/ssr-client/lit-element-hydrate-support.js` is imported ahead of it. Hydration keeps the server's text and template choices, so the first render in the browser must give what the server gave, and the server saw no light DOM. `hydrating` is `true` during that render. Once it is done, the component updates once more, and every slot with content signals a `slotchange`, which the browser did not for server-rendered content. A template can therefore answer as the server did while hydrating and catch up right after:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
protected override get template() {
|
|
47
|
+
const panes = isServer || this.hydrating ? [] : [...this.children]
|
|
48
|
+
return html`${panes.map(pane => html`<div part='pane'><slot name=${pane.slot}></slot></div>`)}`
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Attributes need no such care: Lit's hydration alone would keep the server's value of an attribute the browser renders differently, and `Component` makes it write those. This applies to every Lit template on a page that hydrates, not only to components, as it changes how Lit's attribute parts record their first value, and to no page that does not.
|
package/bind/README.md
CHANGED
|
@@ -172,7 +172,7 @@ class MyParentComponent extends Component {
|
|
|
172
172
|
|
|
173
173
|
### Server-Side Rendering
|
|
174
174
|
|
|
175
|
-
A server renders attribute, boolean attribute and property bindings with the source's value, but no element bindings, as Lit renders no element directives on a server.
|
|
175
|
+
A server renders attribute, boolean attribute and property bindings with the source's value, but no element bindings, as Lit renders no element directives on a server. In the browser, an element binding runs while the parent hydrates, before the element does, so the element's first render has the value the server's did not. That is fine as long as the value changes only the element's attributes, which hydration writes; where it changes text or a template choice, bind the default property by name, which the server renders too:
|
|
176
176
|
|
|
177
177
|
```ts
|
|
178
178
|
html`<my-component .value=${bind(this, 'value')}></my-component>`
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import './hydrateAttributes.js';
|
|
2
3
|
export declare const component: (tagName: string) => import("lit/decorators.js").CustomElementDecorator;
|
|
3
4
|
export declare abstract class Component extends LitElement {
|
|
5
|
+
private readonly hydration;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the component renders its first update in the browser on top of the server's render, which it must reproduce,
|
|
8
|
+
* as hydration keeps the server's text and template choices and the server saw no light DOM.
|
|
9
|
+
* Another update follows at once, and every slot with content signals a `slotchange` then, which the browser did not.
|
|
10
|
+
*/
|
|
11
|
+
get hydrating(): boolean;
|
|
4
12
|
/** Invoked after first update i.e. render is completed */
|
|
5
13
|
protected initialized(): void;
|
|
6
14
|
/** Invoked every time the component is connected to the Document Object Model (DOM) */
|
|
@@ -11,6 +19,7 @@ export declare abstract class Component extends LitElement {
|
|
|
11
19
|
protected get template(): import("lit-html").HTMLTemplateResult;
|
|
12
20
|
/** @final */
|
|
13
21
|
protected render(): import("lit-html").HTMLTemplateResult;
|
|
22
|
+
protected update(props: PropertyValues): void;
|
|
14
23
|
/** @final */
|
|
15
24
|
protected firstUpdated(props: PropertyValues): void;
|
|
16
25
|
/** @final */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Component.d.ts","sourceRoot":"","sources":["../../Component/Component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAIrD,eAAO,MAAM,SAAS,yEAAgB,CAAA;AAEtC,8BAAsB,SAAU,SAAQ,UAAU;IACjD,0DAA0D;IAC1D,SAAS,CAAC,WAAW;IAErB,uFAAuF;IACvF,SAAS,CAAC,SAAS;IAEnB,4FAA4F;IAC5F,SAAS,CAAC,YAAY;IAEtB,gGAAgG;IAChG,SAAS,KAAK,QAAQ,0CAErB;IAED,aAAa;cACM,MAAM;
|
|
1
|
+
{"version":3,"file":"Component.d.ts","sourceRoot":"","sources":["../../Component/Component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAIrD,OAAO,wBAAwB,CAAA;AAE/B,eAAO,MAAM,SAAS,yEAAgB,CAAA;AAEtC,8BAAsB,SAAU,SAAQ,UAAU;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAEhD;;;;OAIG;IACH,IAAI,SAAS,YAEZ;IAED,0DAA0D;IAC1D,SAAS,CAAC,WAAW;IAErB,uFAAuF;IACvF,SAAS,CAAC,SAAS;IAEnB,4FAA4F;IAC5F,SAAS,CAAC,YAAY;IAEtB,gGAAgG;IAChG,SAAS,KAAK,QAAQ,0CAErB;IAED,aAAa;cACM,MAAM;cAIN,MAAM,CAAC,KAAK,EAAE,cAAc;IAK/C,aAAa;cACM,YAAY,CAAC,KAAK,EAAE,cAAc;IAKrD,aAAa;IACJ,iBAAiB;IAK1B,aAAa;IACJ,oBAAoB;CAI7B"}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { LitElement } from 'lit';
|
|
2
2
|
import { customElement } from 'lit/decorators.js';
|
|
3
3
|
import { html } from './html.js';
|
|
4
|
+
import { Hydration } from './Hydration.js';
|
|
5
|
+
import './hydrateAttributes.js';
|
|
4
6
|
export const component = customElement;
|
|
5
7
|
export class Component extends LitElement {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.hydration = new Hydration(this);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether the component renders its first update in the browser on top of the server's render, which it must reproduce,
|
|
14
|
+
* as hydration keeps the server's text and template choices and the server saw no light DOM.
|
|
15
|
+
* Another update follows at once, and every slot with content signals a `slotchange` then, which the browser did not.
|
|
16
|
+
*/
|
|
17
|
+
get hydrating() {
|
|
18
|
+
return this.hydration.hydrating;
|
|
19
|
+
}
|
|
6
20
|
/** Invoked after first update i.e. render is completed */
|
|
7
21
|
initialized() { }
|
|
8
22
|
/** Invoked every time the component is connected to the Document Object Model (DOM) */
|
|
@@ -17,6 +31,10 @@ export class Component extends LitElement {
|
|
|
17
31
|
render() {
|
|
18
32
|
return this.template;
|
|
19
33
|
}
|
|
34
|
+
update(props) {
|
|
35
|
+
super.update(props);
|
|
36
|
+
this.hydration.hostUpdate();
|
|
37
|
+
}
|
|
20
38
|
/** @final */
|
|
21
39
|
firstUpdated(props) {
|
|
22
40
|
super.firstUpdated(props);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactiveControllerHost } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Tracks the first update of a server-rendered host, which must give what the server gave, and lets it catch up right after:
|
|
4
|
+
* with another update, and a `slotchange` from every slot with content, which the browser signals only for content it assigned itself.
|
|
5
|
+
*
|
|
6
|
+
* The host drives it from `update()` rather than registering it as a controller, so that it acts before any controller of the host
|
|
7
|
+
* reads the light DOM in `hostUpdated`, and is not counted among the host's controllers.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Hydration {
|
|
10
|
+
private readonly host;
|
|
11
|
+
hydrating: boolean;
|
|
12
|
+
constructor(host: ReactiveControllerHost & Element);
|
|
13
|
+
hostUpdate(): void;
|
|
14
|
+
private hydrated;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=Hydration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Hydration.d.ts","sourceRoot":"","sources":["../../Component/Hydration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,sBAAsB,EAAE,MAAM,KAAK,CAAA;AAE3D;;;;;;GAMG;AACH,qBAAa,SAAS;IAGT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,SAAS,EAAE,OAAO,CAAA;gBAEW,IAAI,EAAE,sBAAsB,GAAG,OAAO;IAKnE,UAAU;IAOV,OAAO,CAAC,QAAQ;CAQhB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isServer } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* Tracks the first update of a server-rendered host, which must give what the server gave, and lets it catch up right after:
|
|
4
|
+
* with another update, and a `slotchange` from every slot with content, which the browser signals only for content it assigned itself.
|
|
5
|
+
*
|
|
6
|
+
* The host drives it from `update()` rather than registering it as a controller, so that it acts before any controller of the host
|
|
7
|
+
* reads the light DOM in `hostUpdated`, and is not counted among the host's controllers.
|
|
8
|
+
*/
|
|
9
|
+
export class Hydration {
|
|
10
|
+
constructor(host) {
|
|
11
|
+
this.host = host;
|
|
12
|
+
// A shadow root at construction is the server's, as Lit attaches one only on connection
|
|
13
|
+
this.hydrating = !isServer && !!host.shadowRoot;
|
|
14
|
+
}
|
|
15
|
+
hostUpdate() {
|
|
16
|
+
if (this.hydrating) {
|
|
17
|
+
this.hydrating = false;
|
|
18
|
+
this.host.updateComplete.then(() => this.hydrated());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
hydrated() {
|
|
22
|
+
this.host.requestUpdate();
|
|
23
|
+
for (const slot of this.host.shadowRoot?.querySelectorAll('slot') ?? []) {
|
|
24
|
+
if (slot.assignedNodes().length > 0) {
|
|
25
|
+
slot.dispatchEvent(new Event('slotchange', { bubbles: true }));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydrateAttributes.d.ts","sourceRoot":"","sources":["../../Component/hydrateAttributes.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isServer, nothing } from 'lit';
|
|
2
|
+
import { _$LH } from 'lit-html/private-ssr-support.js';
|
|
3
|
+
// Lit's hydration support announces itself through this global ahead of any element, so a page which never hydrates is left alone
|
|
4
|
+
if (!isServer && 'litElementHydrateSupport' in globalThis) {
|
|
5
|
+
const { AttributePart, BooleanAttributePart } = _$LH;
|
|
6
|
+
// Lit's production build mangles these two names to stable ones, by which its hydration package calls them as well
|
|
7
|
+
const setValueKey = ['_$setValue', '_$AI'].find(key => key in AttributePart.prototype);
|
|
8
|
+
if (!setValueKey) {
|
|
9
|
+
throw new Error('Hydration cannot write attributes, as this version of lit-html has renamed AttributePart.prototype._$setValue');
|
|
10
|
+
}
|
|
11
|
+
const committedValueKey = setValueKey === '_$setValue' ? '_$committedValue' : '_$AH';
|
|
12
|
+
const prototype = AttributePart.prototype;
|
|
13
|
+
const recorded = (part) => part[committedValueKey];
|
|
14
|
+
const setValue = prototype[setValueKey];
|
|
15
|
+
/** Writes the value as a render would, sparing the writes which would change nothing. */
|
|
16
|
+
const commit = (part, value) => {
|
|
17
|
+
const { element, name } = part;
|
|
18
|
+
if (part instanceof BooleanAttributePart) {
|
|
19
|
+
element.toggleAttribute(name, !!value && value !== nothing);
|
|
20
|
+
}
|
|
21
|
+
else if (value === nothing) {
|
|
22
|
+
element.removeAttribute(name);
|
|
23
|
+
}
|
|
24
|
+
else if (element.getAttribute(name) !== String(value ?? '')) {
|
|
25
|
+
element.setAttribute(name, String(value ?? ''));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
prototype[setValueKey] = function (value, directiveParent, valueIndex, noCommit) {
|
|
29
|
+
if (noCommit !== true) {
|
|
30
|
+
return setValue.call(this, value, directiveParent, valueIndex, noCommit);
|
|
31
|
+
}
|
|
32
|
+
// Hydration passes "noCommit" to only record what a render would write. A render writes no initial "nothing", so the
|
|
33
|
+
// server's attribute stays, as it may be the element's own reflected default; an interpolation always records.
|
|
34
|
+
const { strings } = this;
|
|
35
|
+
const before = recorded(this);
|
|
36
|
+
setValue.call(this, value, directiveParent, valueIndex, noCommit);
|
|
37
|
+
const after = recorded(this);
|
|
38
|
+
if (strings || after !== before) {
|
|
39
|
+
const values = after;
|
|
40
|
+
commit(this, !strings ? after : values.includes(nothing) ? nothing : strings.reduce((text, string, i) => `${text}${values[i - 1] ?? ''}${string}`));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/bind/Binder.d.ts
CHANGED
|
@@ -25,9 +25,9 @@ export declare class Binder<T> {
|
|
|
25
25
|
constructor(host: BindSource, key: string);
|
|
26
26
|
bind: (...[parameter]: BinderParameters<T>) => import("lit-html/directive.js").DirectiveResult<{
|
|
27
27
|
new (partInfo: import("lit-html/directive.js").PartInfo): {
|
|
28
|
-
"__#private@#valueBinder"?: import("./ValueBinder.js").ValueBinder<import("lit-html").
|
|
28
|
+
"__#private@#valueBinder"?: import("./ValueBinder.js").ValueBinder<import("lit-html").AttributePart | import("lit-html").BooleanAttributePart | import("lit-html").ElementPart | import("lit-html").PropertyPart>;
|
|
29
29
|
render(component: BindSource, property: "requestUpdate", options?: BindDirectiveParametersOptions<(name?: PropertyKey, oldValue?: unknown, options?: import("lit").PropertyDeclaration, useNewValue?: boolean, newValue?: unknown) => void> | undefined): any;
|
|
30
|
-
update(part: import("lit-html").
|
|
30
|
+
update(part: import("lit-html").AttributePart | import("lit-html").BooleanAttributePart | import("lit-html").ElementPart | import("lit-html").PropertyPart, parameters: import("./BindDirective.js").BindDirectiveParameters<BindSource, "requestUpdate">): unknown;
|
|
31
31
|
disconnected(): void;
|
|
32
32
|
reconnected(): void;
|
|
33
33
|
isConnected: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdatedController.d.ts","sourceRoot":"","sources":["../../updated/UpdatedController.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAQ,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"UpdatedController.d.ts","sourceRoot":"","sources":["../../updated/UpdatedController.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAQ,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAqBpD,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;AAEhE,qBAAa,iBAAiB,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,MAAM,CAAC,CAAE,SAAQ,UAAU;IAG/E,QAAQ,CAAC,OAAO,EAAE,CAAC;IAAE,QAAQ,CAAC,WAAW,EAAE,CAAC;IAAE,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAFlG,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAa;gBAE3B,OAAO,EAAE,CAAC,EAAW,WAAW,EAAE,CAAC,EAAW,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAKlG,OAAO,KAAK,KAAK,GAA4C;IAEpD,WAAW;CAMpB"}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { ReactiveElement } from 'lit';
|
|
1
|
+
import { LitElement, ReactiveElement } from 'lit';
|
|
2
2
|
import { Controller } from '../Controller/index.js';
|
|
3
3
|
import { host } from '../host.js';
|
|
4
4
|
import { getChangedPropertyKey } from './getChangedPropertyKey.js';
|
|
5
5
|
const changedPropertiesKey = Symbol('changedProperties');
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const recordChangedProperties = (prototype) => {
|
|
7
|
+
const update = prototype['update'];
|
|
8
|
+
prototype['update'] = function (changedProperties) {
|
|
9
|
+
this[changedPropertiesKey] = changedProperties;
|
|
10
|
+
return update.call(this, changedProperties);
|
|
11
|
+
};
|
|
10
12
|
};
|
|
13
|
+
recordChangedProperties(ReactiveElement.prototype);
|
|
14
|
+
// Lit's hydration support replaces LitElement's update with one calling the ReactiveElement update it captured before this module ran
|
|
15
|
+
recordChangedProperties(LitElement.prototype);
|
|
11
16
|
export class UpdatedController extends Controller {
|
|
12
17
|
constructor(context, propertyKey, callback) {
|
|
13
18
|
super(context[host]);
|