neutron
Define typed custom elements with effect-based lifecycles — props, events, and compose without rewriting the Custom Elements boilerplate.
Neutron is the element factory of the Nucleus Stack: Neutron({ tag, props }) returns a builder you chain lifecycles onto, then define(). Every NucleusKit element is a Neutron element, and so is every element you write yourself.
Features
- Declarative factory
Neutron({ tag, props }).… .define() - Typed props Primitives and
TokenListreflect to dashed attributes; objects / arrays / elements / promises stay on the instance - Effect returns Lifecycles / methods return a POJO (or an array of them) that sets props, emits, listens, calls methods, and styles
- Fine-grained reactions
onPropSet/Unset/Changed/onEffect - Events & broadcasts Tag-prefixed custom events, cancelable default actions, channel broadcasts
- Commands
onCommand("--verb")handles the HTML Command API —<button command commandfor>needs no custom element - Listener cleanup Listeners added through effects are removed on disconnect and restored on reconnect
- Compose Combine builders (
Neutron.compose) for mixin-style packages - Recompose Import a package's raw builder, add / remove lifecycles and methods, then
define()it yourself - DevTools
Neutron.attachDevtools()hooks the Nucleus DevTools extension
Installation
This package is available in the
<script src="https://unpkg.com/@excom/kit-utils@0.3.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/neutron@0.2.0/dist/index.umd.min.js"></script>npm install @excom/neutronimport { /* … */ } from "@excom/neutron";Peer dependencies (0)
Packages a consumer must install alongside this one. Workspace deps are bundled.
| Package | Version |
|---|---|
|
Usage
Beta disclaimer: Neutron automatically defines your element's Typescript types based upon your element config. This is done via complicated internal typing that has a few known issues. These issues will be resolved in the first stable release.
An element owns its own state (attributes) and announces changes (events). It never renders children or reaches into siblings — coordination belongs to Quark. The rules these examples follow are collected in
import { Neutron } from "@excom/neutron";
export const PressTracker = Neutron({
tag: "press-tracker",
props: {
pressCount: { type: Number, defaultValue: () => 0 }, // reflects ↔ `press-count`
},
})
.onEvent("click", ({ pressCount }) => ({
// effects are declarative instructions, not imperative mutations
pressCount: pressCount + 1,
emit: ["press-tracker-press", { detail: { pressCount: pressCount + 1 } }],
}));
PressTracker.define();import { Neutron } from "@excom/neutron";
export const PressTracker = Neutron({
tag: "press-tracker",
props: {
pressCount: { type: Number, defaultValue: () => 0 }, // reflects ↔ `press-count`
},
})
.onEvent("click", ({ pressCount }) => ({
// effects are declarative instructions, not imperative mutations
pressCount: pressCount + 1,
emit: ["press-tracker-press", { detail: { pressCount: pressCount + 1 } }],
}));
PressTracker.define();<press-tracker press-count="0">
<button>Press</button>
</press-tracker>
<!-- `press-tracker[press-count="3"]` is now a CSS / Quark selector --><press-tracker press-count="0">
<button>Press</button>
</press-tracker>
<!-- `press-tracker[press-count="3"]` is now a CSS / Quark selector -->Documentation
Defining elements
Props — typed props, reflection,TokenList, naming rulesProvision — the one property for published rich dataTypeScript — global element types,ConstructorType
Behavior
Lifecycles —onConnected& co., destructuring, async pitfallsEffects — the object a handler returnsProp reactions —onPropSet/Unset/Changed/onEffectMethods — methods as effectorsEvents — emits, default actions, broadcasts, listener cleanupCommands —onCommandfor--verbcommands, thecommandeffectPromise props —onPromiseResolved/Rejected
Composition
Compose — stack builders into mixin-style packagesRecompose — edit a packaged element before defining it
Runtime
Define —define()and class introspectionDebug — DevTools hook, loop guard
Examples
State on connect
Neutron({ tag: "ready-flag", props: { isReady: Boolean } })
.onConnected(() => ({ isReady: true, emit: ["ready-flag-ready"] }))
.define();Neutron({ tag: "ready-flag", props: { isReady: Boolean } })
.onConnected(() => ({ isReady: true, emit: ["ready-flag-ready"] }))
.define();Child element effect across handlers
Assign an element prop, then react to it with a nested effect. Listener callbacks that return effects must be defineMethods methods:
Neutron({
tag: "focus-host",
props: {
inputEl: { type: HTMLInputElement, store: "weak" },
isFocused: Boolean,
},
})
.defineMethods({
handleFocus: () => ({ isFocused: true, emit: ["focus-host-focus"] }),
handleBlur: () => ({ isFocused: false }),
})
.onConnected((el) => ({
inputEl: el.querySelector("input"),
}))
.onPropSet("inputEl", ({ handleFocus, handleBlur }) => ({
inputEl: {
addListeners: [
["focus", handleFocus],
["blur", handleBlur],
],
},
}));Neutron({
tag: "focus-host",
props: {
inputEl: { type: HTMLInputElement, store: "weak" },
isFocused: Boolean,
},
})
.defineMethods({
handleFocus: () => ({ isFocused: true, emit: ["focus-host-focus"] }),
handleBlur: () => ({ isFocused: false }),
})
.onConnected((el) => ({
inputEl: el.querySelector("input"),
}))
.onPropSet("inputEl", ({ handleFocus, handleBlur }) => ({
inputEl: {
addListeners: [
["focus", handleFocus],
["blur", handleBlur],
],
},
}));Attributes
Role —option is configurable, state
is managed by the element (read-only), or hybrid which is both.
Provision
Thisprovision property lives on the DOM node.
Read / watch it with Quark’s prop("provision"), or listen
for the neutron-provision event from app JS.
Events
Type
command event (HTML Command API): from a
<button command commandfor>, an <event-handler
command-name>, or a CommandEvent.
Recognized Elements
Child or descendant elements are recognized by and relevant to its functionality.Styles
@custom-selector synonyms so custom elements can share
semantic styles. Element aliases match the host tag / .tag-* class; state
aliases match attributes (nested under the element alias).
Release notes (4)
0.2.0
- Add hydration of prerendered pages: an element takes its
provisionfrom the page's hydration island before it mounts, and deferred event defaults keep hydration open - Add
no-ssrandNeutron({ ssr: false })to keep elements out of a prerender: the attribute keeps every Neutron element on or inside its element unmounted until the browser, and the option does the same for one tag's own instances - Fix an issue where a value assigned to a declared prop before the element's definition loaded was dropped: it is now applied at upgrade
- Fix an issue where
Neutron.compose()lostreflectDefaultPropswhen a later builder left it out: an option a builder leaves out is inherited, and the last builder that states it wins - Fix an error thrown when a
windowordocumentlistener fires after its element was removed and garbage-collected: the listener now does nothing - Fix an issue where a reflected lifecycle attribute (
reflectDefaultProps) was read back: anis-mountedin markup or on a clone no longer runsonConnectedon an element that is not mounted, and the attribute returns to the element's real state
0.1.3
- Fix the published type declarations:
index.d.tspointed at a folder that is not in the tarball, so consumers gotanyfor named re-exports and missing-member errors forexport *instead of the real types
0.1.2
- Fix an issue where an effect naming a property the element does not have was skipped silently under test runners: it throws
NeutronErrorthere too, as in a browser
0.1.1
- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
Release notes
e.preventDefault() is not
synchronously called on the event.
all: revert-layer.
Valence.css ships this as .unstyled and .unstyled-all for the entire tree.