@a11d/lit 0.13.3 โ†’ 0.14.0

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.0 (2026-10-07)
2
+ - **๐Ÿš€ Feature**: Make hydration write the attributes the browser renders differently from the server ([58d6b39](https://github.com/a11delavar/lit/commit/58d6b397782bce86749001431dbc56b10a6cfce7))
3
+ - **๐Ÿš€ Feature**: Add `Component.hydrating`, true during a server-rendered component's first render in the browser ([58d6b39](https://github.com/a11delavar/lit/commit/58d6b397782bce86749001431dbc56b10a6cfce7))
4
+
5
+ ## 0.13.4 (2026-10-06)
6
+ - **๐Ÿงน Chore**: Improve the package description and feature summaries ([285feac](https://github.com/a11delavar/lit/commit/285feac0f6a030647973e572344b168379227eb6))
7
+
1
8
  ## 0.13.3 (2026-09-28)
2
9
  - **๐Ÿงน Chore**: Ship the changelog and README with the package ([745c88f](https://github.com/a11delavar/lit/commit/745c88f9457ccbe3ad9ea927cbf2b48c3b106918))
3
10
 
@@ -96,4 +103,4 @@
96
103
  - **๐Ÿงน Chore**: Upgrade to Lit 3 ([f655b7f](https://github.com/a11delavar/lit/commit/f655b7ffcc83e69e06f4b336271093971fc35748))
97
104
 
98
105
  ## 0.4.0 (2023-10-10)
99
- - **๐Ÿš€ Feature**: Add the nothing constant to the html template string literal function ([fea9a9b](https://github.com/a11delavar/lit/commit/fea9a9b6a7d9e220496b432b9f08589ae564af8b))
106
+ - **๐Ÿš€ Feature**: Add the nothing constant to the html template string literal function ([fea9a9b](https://github.com/a11delavar/lit/commit/fea9a9b6a7d9e220496b432b9f08589ae564af8b))
@@ -1,12 +1,13 @@
1
1
  # `Component` class
2
2
 
3
- The `Component` class is the base class for all components.
3
+ The base class for components, extending `LitElement` with a `template` getter and additional lifecycle callbacks.
4
4
 
5
5
  In addition to [Lit's standard lifecycle](https://lit.dev/docs/components/lifecycle/), `Component` provides:
6
6
  - `template` getter - Define the component's template
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.
@@ -1,6 +1,6 @@
1
1
  # `ComponentPart` class
2
2
 
3
- A `ComponentPart` is a part of a component, extracted into a class of its own **without introducing a component boundary**.
3
+ A part of a component, extracted into a class of its own **without introducing a component boundary**.
4
4
 
5
5
  A part contributes a `template` to its host and may declare its own state, queries, events and event listeners with the very same decorators a component uses. As a part shares the update lifecycle of its host, changing the state of a part re-renders the host as a whole โ€” no properties have to be passed down and no updates have to be propagated by hand.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # `Controller` class
2
2
 
3
- A base class for [reactive controllers](https://lit.dev/docs/composition/controllers/) which registers itself with its host, so implementations only define the callbacks they are interested in.
3
+ A base class for [reactive controllers](https://lit.dev/docs/composition/controllers/) that registers itself with its host. Implementations only define the callbacks they are interested in.
4
4
 
5
5
  ```ts
6
6
  import { Controller } from '@a11d/lit'
@@ -1,6 +1,6 @@
1
1
  # `ElementRef` / `ElementRefs` classes
2
2
 
3
- The element โ€” or the elements โ€” a template designates, with whatever it declares about them. Code gets the elements it works with from the template instead of querying for them.
3
+ The element or elements a template designates, with whatever it declares about them. Code gets the elements it works with from the template instead of querying for them.
4
4
 
5
5
  ```ts
6
6
  import { Component, component, ElementRef, html } from '@a11d/lit'
package/README.md CHANGED
@@ -13,10 +13,10 @@ npm install @a11d/lit
13
13
  <!-- features -->
14
14
  ## Features
15
15
 
16
- - **[`Component` class](https://github.com/a11delavar/lit/tree/main/packages/Lit/Component)** - The `Component` class is the base class for all components.
17
- - **[`ComponentPart` class](https://github.com/a11delavar/lit/tree/main/packages/Lit/ComponentPart)** - A `ComponentPart` is a part of a component, extracted into a class of its own **without introducing a component boundary**.
18
- - **[`Controller` class](https://github.com/a11delavar/lit/tree/main/packages/Lit/Controller)** - A base class for [reactive controllers](https://lit.dev/docs/composition/controllers/) which registers itself with its host, so implementations only define the callbacks they are interested in.
19
- - **[`ElementRef` / `ElementRefs` classes](https://github.com/a11delavar/lit/tree/main/packages/Lit/ElementRef)** - The element โ€” or the elements โ€” a template designates, with whatever it declares about them.
16
+ - **[`Component` class](https://github.com/a11delavar/lit/tree/main/packages/Lit/Component)** - The base class for components, extending `LitElement` with a `template` getter and additional lifecycle callbacks.
17
+ - **[`ComponentPart` class](https://github.com/a11delavar/lit/tree/main/packages/Lit/ComponentPart)** - A part of a component, extracted into a class of its own **without introducing a component boundary**.
18
+ - **[`Controller` class](https://github.com/a11delavar/lit/tree/main/packages/Lit/Controller)** - A base class for [reactive controllers](https://lit.dev/docs/composition/controllers/) that registers itself with its host.
19
+ - **[`ElementRef` / `ElementRefs` classes](https://github.com/a11delavar/lit/tree/main/packages/Lit/ElementRef)** - The element or elements a template designates, with whatever it declares about them.
20
20
  - **[`style` Directive](https://github.com/a11delavar/lit/tree/main/packages/Lit/style)** - Apply inline styles to elements with proper typing and reactivity.
21
21
  - **[`updated` Decorator](https://github.com/a11delavar/lit/tree/main/packages/Lit/updated)** - React to property changes with callbacks.
22
22
  - **[`eventListener` Decorator](https://github.com/a11delavar/lit/tree/main/packages/Lit/eventListener)** - Declaratively register event listeners on methods.
@@ -67,4 +67,4 @@ npm install @a11d/lit
67
67
  | `state` | const | |
68
68
  | `ElementRefLifecycle` | interface | The lifecycle of the element or elements a reference holds. |
69
69
  | `BindSource` | type | The source a binding can be established on. |
70
- <!-- /exports -->
70
+ <!-- /exports -->
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,7 +1,7 @@
1
1
  export const bindingIntegrations = new Set();
2
2
  export const bindingIntegration = () => {
3
3
  return (BindingIntegrationConstructor) => {
4
- bindingIntegrations.add(new BindingIntegrationConstructor);
4
+ bindingIntegrations.add(new BindingIntegrationConstructor());
5
5
  };
6
6
  };
7
7
  export class BindingIntegration {
@@ -13,7 +13,7 @@ export function event(options) {
13
13
  return this[`$${propertyKey}Event$`] ??= !isServer && element instanceof HTMLElement
14
14
  ? new HTMLElementEventDispatcher(element, options?.type ?? propertyKey, options)
15
15
  : new PureEventDispatcher();
16
- }
16
+ },
17
17
  });
18
18
  };
19
19
  }
@@ -29,7 +29,7 @@ export const eventListener = (...eventListenerOptions) => {
29
29
  ? descriptor.get
30
30
  : descriptor.value).call(context, event);
31
31
  }
32
- };
32
+ }();
33
33
  });
34
34
  };
35
35
  };
@@ -4,7 +4,7 @@ export const query = (selector) => {
4
4
  Object.defineProperty(prototype, propertyKey, {
5
5
  get() {
6
6
  return this[host]?.renderRoot?.querySelector(selector) ?? undefined;
7
- }
7
+ },
8
8
  });
9
9
  };
10
10
  };
@@ -4,7 +4,7 @@ export const queryAll = (selector) => {
4
4
  Object.defineProperty(prototype, propertyKey, {
5
5
  get() {
6
6
  return [...this[host]?.renderRoot?.querySelectorAll(selector) ?? []];
7
- }
7
+ },
8
8
  });
9
9
  };
10
10
  };
@@ -18,7 +18,7 @@ export function queryConnectedInstances() {
18
18
  Object.defineProperty(constructor, propertyName, { value: new Set() });
19
19
  constructor.addInitializer(element => element.addController({
20
20
  hostConnected: () => element.constructor[propertyName].add(element),
21
- hostDisconnected: () => element.constructor[propertyName].delete(element)
21
+ hostDisconnected: () => element.constructor[propertyName].delete(element),
22
22
  }));
23
23
  Object.defineProperty(constructor, propertyKey, {
24
24
  configurable: false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a11d/lit",
3
- "version": "0.13.3",
4
- "description": "A thin wrapper around the Lit library",
3
+ "version": "0.14.0",
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",
7
7
  "url": "git+https://github.com/a11delavar/lit.git",