@open-wc/lit-helpers
Spread directives and a read-only properties mixin that fill the gaps lit-html and LitElement leave in dynamic template binding.
Repository Health
Technical Analysis
@open-wc/lit-helpers is a small, focused utility package from the Open Web Components project that solves two recurring pain points when building web components with lit and lit-html: binding a large or dynamically-shaped set of attributes/properties/events to an element, and defining properties that outside consumers can read but never write.
The package ships three lit-html directives — spread, spreadProps, and spreadEvents — that let a template apply an entire object of bindings to an element in a single expression instead of enumerating each attribute, property, and event listener individually. It also exports ReadOnlyPropertiesMixin, a mixin for LitElement (or its UpdatingElement parent) that turns a readOnly: true property declaration into a getter-only public property with an internal setReadOnlyProperties escape hatch for the component itself to update the value.
Both utilities are narrow in scope by design: they exist to remove boilerplate that lit-html’s declarative binding syntax doesn’t handle well on its own, without pulling in a larger framework or state-management layer. The package is maintained as one of many small, independently versioned packages inside the open-wc monorepo, which itself provides guides, tools, and testing infrastructure for the web components ecosystem.
What You Get
- spread directive - applies a single object containing sigil-prefixed attributes (plain), boolean attributes (
?), properties (.), and events (@) to an element in one template binding. - spreadProps directive - binds an entire object as element properties without requiring sigils, for cases where only properties need to be set dynamically.
- spreadEvents directive - attaches multiple named event listeners from a single object, including support for binding the same event name from more than one spread source.
- ReadOnlyPropertiesMixin - wraps LitElement’s
createPropertyso any property flaggedreadOnly: truebecomes publicly read-only, settable only via the mixin’ssetReadOnlyPropertiesmethod. - Automatic cleanup of removed bindings - the spread directives track previous values internally and clear attributes/properties/listeners that are dropped from the source object on a later render, rather than leaving stale state behind.
- Decorator compatibility - ReadOnlyPropertiesMixin works with both the plain
static get properties()declaration style and the@propertydecorator.
Common Use Cases
- Dynamic design-system wrappers - a wrapper component that forwards an arbitrary, caller-supplied set of attributes/properties/events to an inner native or third-party element without hand-listing every possible binding.
- Higher-order component templates - templates generated programmatically (e.g. from a schema or config object) where the exact set of bindings isn’t known until render time.
- Read-only computed state on custom elements - exposing a timestamp, status flag, or derived value as a real reactive LitElement property that the component controls internally but consumers can only observe.
- Migrating imperative DOM code to lit-html - replacing manual
setAttribute/addEventListenercall sites with a single declarative spread binding during a framework migration.
Under The Hood
Architecture
The package is a single flat module (index.js) re-exporting from two small, independent source files: src/spread.ts (compiled to src/spread.js) and src/read-only-properties-mixin.js. The three spread directives form a class hierarchy — SpreadDirective extends SpreadEventsDirective extends SpreadPropsDirective, each overriding apply/groom to add attribute/boolean-attribute/event handling on top of the base property-setting behavior — so spread is effectively spreadProps plus spreadEvents plus raw/boolean attribute support. Each directive extends lit’s AsyncDirective and tracks prevData across renders to diff and clean up removed bindings, with SpreadEventsDirective additionally implementing disconnected/reconnected lifecycle hooks to detach and reattach listeners when the directive’s host element is disconnected from the DOM. ReadOnlyPropertiesMixin is a standalone functional mixin that overrides LitElement’s static createProperty to redefine the property with a getter backed by a per-instance Symbol-keyed private slot, and a setter that no-ops after first initialization.
Tech Stack
The spread directives are authored in TypeScript against lit’s directive/AsyncDirective/Part APIs (peer dependency lit ^2.0.0 || ^3.0.0) and published as compiled .js with hand-written .d.ts files; the mixin is authored directly in plain JS with a companion .d.ts. The package is part of the open-wc npm/Lerna-less workspace (root package.json name @open-wc/root), built with tsc --build for type declarations, tested with @open-web-components/testing + web-test-runner in the browser and mocha for Node, linted with ESLint (eslint-plugin-lit included) and Prettier, and released via Changesets (CHANGELOG.md is changeset-generated). GitHub Actions workflows cover linting, Node verification, canary releases, and preview builds.
Code Quality
Both directives and the mixin have dedicated browser tests (test-web/spread.test.js, test-web/read-only-properties-mixin.test.js) built on @open-wc/testing’s fixture/expect, covering not just the happy path but binding-order edge cases (spread before/after a direct property binding, spread appearing twice, properties removed between renders) that the directives’ own doc comments call out as tricky. Types are fully declared for the public API (peer-typed against lit), naming is consistent with lit-html’s own directive conventions, and the monorepo enforces ESLint plus TypeScript’s tsc --build project references across all packages including this one — there’s no evidence of untyped any-heavy code or swallowed errors in the reviewed source.
What Makes It Unique
Rather than reimplementing a general “spread props” utility from scratch, the package layers three progressively broader directives on a shared diffing base class, so consumers can pick the narrowest one (spreadProps or spreadEvents) that fits instead of always paying for full attribute/property/event sigil parsing. The read-only mixin’s approach — redefining createProperty itself rather than wrapping the whole class’s set/get — means read-only enforcement composes correctly with LitElement’s existing reactive-property machinery (dirty-checking, requestUpdate) instead of working around it.