@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 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
 
@@ -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. Hydration assumes an element's first render in the browser to equal the server's, so an element binding whose value changes what the element renders leaves it as the server rendered it. Server-rendered templates therefore bind the default property by name:
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;IAIzB,aAAa;cACM,YAAY,CAAC,KAAK,EAAE,cAAc;IAKrD,aAAa;IACJ,iBAAiB;IAK1B,aAAa;IACJ,oBAAoB;CAI7B"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hydrateAttributes.d.ts.map
@@ -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
+ }
@@ -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").ElementPart | import("lit-html").AttributePart | import("lit-html").PropertyPart | import("lit-html").BooleanAttributePart>;
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").ElementPart | import("lit-html").AttributePart | import("lit-html").PropertyPart | import("lit-html").BooleanAttributePart, parameters: import("./BindDirective.js").BindDirectiveParameters<BindSource, "requestUpdate">): unknown;
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;AAepD,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
+ {"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 originalUpdate = ReactiveElement.prototype['update'];
7
- ReactiveElement.prototype['update'] = function (changedProperties) {
8
- this[changedPropertiesKey] = changedProperties;
9
- return originalUpdate.call(this, changedProperties);
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]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a11d/lit",
3
- "version": "0.13.4",
3
+ "version": "0.14.1",
4
4
  "description": "A layer over Lit with a component base class, controllers, element references, two-way binding and typed events.",
5
5
  "repository": {
6
6
  "type": "git",