A brief history of Directives
The “directive” — a function attached to a DOM element to add cross-cutting behavior — is one of the oldest abstractions in JavaScript UI work. It predates JSX by years, predates React entirely, and has been quietly rediscovered every framework cycle since 2009. This page traces the pattern honestly: what each contributor brought, what didn’t survive, and where Phaze fits.
Timeline at a glance
Section titled “Timeline at a glance”| year | framework | shape |
|---|---|---|
| 2009 | AngularJS | module.directive(name, factory) — link(scope, el, attrs) — directives ARE components in early Angular |
| 2010 | Knockout.js | ko.bindingHandlers.name = { init, update } — custom data-bind attributes |
| 2014 | Vue 1.x | v-name="value" — bind / update / unbind |
| 2016 | Vue 2 | v-name — bind / inserted / update / componentUpdated / unbind |
| 2016 | Svelte 1 → 3 | use:action={value} — (node, params) => { update?, destroy? } |
| 2018 | Solid | use:directive={value} — borrowed from Svelte, JSX-namespaced |
| 2019 | Lit | directive(class) — render-helpers, different category |
| 2019 | Alpine.js | x-name="..." — directives as the entire authoring surface |
| 2020 | Vue 3 | v-name — mounted / updated / beforeUnmount / unmounted (lifecycle rename) |
| 2024 | Vue 3 Vapor | v-name retained in compiled mode |
| 2026 | Phaze | use:name={value} — (el, () => value) => void | (() => void); plus phaze:attr={expr} (reactive attributes) and on:event={fn} (native events) — three namespaces, one job each |
AngularJS (2009)
Section titled “AngularJS (2009)”The original directive system. Released October 2010. The first time the word “directive” was load-bearing in a JS framework.
What it brought
module.directive('myThing', factory)— register a directive globally.- The
link(scope, element, attrs, controller)function — the attachment hook. restrict: 'A' | 'E' | 'C' | 'M'— directives could be attributes, elements, classes, or HTML comments.transclude,template,templateUrl— directives carried template machinery.compilevslink— a two-phase model that nobody else copied.
What didn’t survive
- The whole AngularJS framework, mostly. Angular 2 (2016) was a rewrite with a completely different mental model.
- The directive-as-component conflation. Modern Angular separates
@Componentfrom@Directivecleanly. restrict: 'M'(HTML-comment directives). Nobody used them.- The
compile/linktwo-phase split. - Two-way scope binding through
=— replaced by explicit input/output binding.
What Phaze carries forward The word “directive” and the idea that some behavior is element-shaped, not component-shaped. The implementation has nothing in common — Phaze’s directive is a 4-line function, not a registry entry with 12 configuration knobs.
Knockout.js (2010)
Section titled “Knockout.js (2010)”The same idea, smaller surface. Knockout’s “custom binding handlers” predated Vue’s directives by four years and were structurally identical.
What it brought
ko.bindingHandlers.myBinding = { init, update }— register a binding by name.init(element, valueAccessor, allBindings, ...)— runs once on element creation.update(element, valueAccessor, ...)— runs whenever the bound observable changes.- Used as
data-bind="myBinding: someValue".
What didn’t survive
- The
data-bind="..."HTML attribute syntax — JSX won. - The
init+updatelifecycle split — frameworks consolidated to “one mount, plus reactive primitives for re-running.” - Knockout’s MVVM framing — component-based UI ate it.
What Phaze carries forward
Conceptually quite a lot. Knockout’s init(el, valueAccessor) is one rename and one parameter shuffle from Phaze’s directive(el, value). Both attach behavior to an element by name. Both pass a reactive value accessor. Knockout split mount and update into two callbacks; Phaze unifies them — write effect(() => { ... value() ... }) inside the mount and you get the update callback for free, with auto-cleanup.
Vue 1 → Vue 2 → Vue 3 (2014 → 2020)
Section titled “Vue 1 → Vue 2 → Vue 3 (2014 → 2020)”The mainstream-legitimating directive API. Vue made
v-namepart of every Vue developer’s daily vocabulary.
Vue 1.x (2014) — bind / update / unbind. Directives accepted a value, a string argument (v-on:click), and a string of modifiers (v-on:click.prevent.stop).
Vue 2 (2016) — five hooks: bind (initial setup), inserted (DOM insertion), update (vnode updated), componentUpdated (children resolved), unbind (teardown). Most directives needed only two; the rest existed for niche cases.
Vue 3 (2020) — renamed to align with component lifecycle: created / beforeMount / mounted / beforeUpdate / updated / beforeUnmount / unmounted. Same five-ish concepts, longer names.
What didn’t survive (across the Vue evolution)
- The five-hook lifecycle. Phaze, Svelte, and Solid all collapsed it to “one function, with cleanup.”
componentUpdatedas a separate hook fromupdated— even Vue dropped this in v3.- The
v-on:click.prevent.stopmodifier syntax — neat but not portable; nothing else picked it up. - Strict directive registration (
Vue.directive('name', def)). Vue 3 supports inline directives via setup; Phaze and Svelte don’t require any registration step.
What Phaze carries forward
The name="value" attribute shape (Phaze writes it use:name={value}, but the visual structure is the same). The idea that directives have an argument (e.g. v-on:click) led naturally to on:click and phaze:class in Phaze — same colon-separated namespace.
Svelte (2016 → 2019)
Section titled “Svelte (2016 → 2019)”The cleanest version. Svelte’s
use:actionis the closest ancestor to Phaze’s directive shape — and to Solid’suse:.
What it brought
use:action={value}— JSX-attribute-shaped, value-passed.- The action function:
function action(node, parameters) {return {update(newParams) { /* runs when parameters change */ },destroy() { /* teardown */ }}}
- Plain JS, no registration. Import the function; reference it in the markup.
- Svelte’s compiler emits the call site after node creation, exactly like Phaze does today.
What didn’t survive
- The
update/destroyobject-return shape. Svelte 5 still supports it for back-compat, but the modern pattern uses signals/runes inside the action body and an effect-style return. - Returning the object literal — a small ceremony Phaze trims by giving the directive a thunk-shaped value argument that re-runs naturally.
What Phaze carries forward
Almost everything. Phaze’s use:name={value} is structurally identical to Svelte’s use:action={parameters}. The differences:
- Phaze’s value argument is
() => T(a thunk). Svelte’s wasTplus anupdate(T)callback. Phaze unifies the two — callvalue()anywhere inside the directive body to read the current value; wrap ineffect(() => ...)to react to changes; cleanup runs throughcleanup()from the active scope. - Phaze relies on the runtime’s auto-cleanup (
AbortSignal-driven listener removal,cleanup()-based teardown) instead of adestroyreturn. - Phaze adds JSX type augmentation (
JSX.Directives) souse:autofocus={42}is a type error when the directive expects aboolean.
The contract is clean enough that Svelte → Phaze migration is mechanical. The naming (use:) is identical because Solid borrowed it from Svelte and Phaze borrowed it from Solid.
Solid (2018)
Section titled “Solid (2018)”The bridge from Svelte’s
use:to JSX. Solid took Svelte’s directive idea and showed it works in a JSX-based runtime.
What it brought
use:directive={value}JSX namespace — same shape as Svelte, transplanted into JSX.- A typed
Directivesinterface for declaration merging — Phaze copied this. - A compiled call site equivalent to
directive(node, () => value)— Phaze’s transform is structurally identical.
What didn’t survive (or didn’t propagate)
- Nothing significant. Solid’s directive API is small and stable; Phaze’s mainly clones it. The differences are in the surrounding ecosystem (DSL aliases, the canonical
@madenowhere/phaze-directivespackage, the TS language-service plugin), not in the contract itself.
What Phaze carries forward
The JSX namespace use:. The thunk-shaped second argument. The declaration-merging type augmentation. The post-creation IIFE compile target. Phaze’s design owes more to Solid’s directive surface than to any other ancestor on this page.
Lit (2019)
Section titled “Lit (2019)”A different category. Lit “directives” are render-helpers, not behavior attachers. Worth flagging because the naming overlaps and the role doesn’t.
What it brought
class MyDirective extends Directive { update(part, args) { ... } }— a class that controls aPart(a render hole in a template).- Used inline inside a tagged template literal —
htmltemplate +${myDirective(args)}interpolation. - Built-ins like
until,repeat,cache,classMap,styleMap.
What’s different from Phaze/Vue/Svelte directives
Lit directives produce content into a render slot. They’re closer to render functions or React’s HOC pattern than to Vue’s v-name. Lit also has “async directives” for streaming results.
What Phaze carries forward Nothing directly. Phaze’s directives are not render-helpers; they’re behavior-attachers. The two solve different problems and the names being identical is mostly an accident.
If you’re coming from Lit and need its repeat / cache / classMap patterns: Phaze’s <For> is the keyed-list answer; reactive class={() => ...} covers classMap; there is no cache because the runtime doesn’t re-render trees.
Alpine.js (2019)
Section titled “Alpine.js (2019)”The framework where directives are the entire surface. Alpine builds the whole authoring experience around
x-data/x-bind/x-on/x-show/ etc.
What it brought
x-data="{ count: 0 }"— reactive scope on an element.x-bind:class="..."andx-on:click="..."— Vue-shaped directives but server-render-friendly.- A philosophy: small enough to drop in via
<script>, no build step, no JSX.
What didn’t survive (in the broader directive lineage)
Alpine’s full x- directive set is specific to Alpine and hasn’t propagated. The x-bind: / x-on: namespaces share DNA with Vue’s : / @ shortcuts, but the runtime expression-string evaluation (x-show="user.isAdmin") is bound to Alpine.
What Phaze carries forward Nothing directly — Phaze ships JSX, not HTML-attribute expressions. The relationship is generational: Alpine proved that a directive-first authoring model could work in 2019, which contributed to the broader rehabilitation of directives as a first-class authoring tool.
Where Phaze fits
Section titled “Where Phaze fits”Phaze isn’t claiming originality on directives. The pattern is older than React. What Phaze contributes is a particular synthesis of three earlier ideas plus one new one:
| Carried forward from | What |
|---|---|
| Vue 1 (2014) | The name:value colon-separated namespace shape (Phaze: use:name, phaze:attr, on:event) |
| Knockout (2010) | The “register behavior by name, accept a reactive value accessor” core idea |
| Svelte (2016) | The plain-function-no-registration model; use: keyword |
| Solid (2018) | The JSX namespace; JSX.Directives declaration-merging type pattern |
What’s distinctly Phaze:
- Three namespaces, three jobs, no overlap.
phaze:attris for reactive attributes only.on:eventis for native events only.use:directiveis for behavior attachments only. Vue’sv-namecarries arguments and modifiers and conditionally bound classes; Phaze splits those into separate, purpose-built namespaces. Each namespace’s contract is one sentence long. - Thunk-shaped value argument unifies one-time and reactive use. Svelte 1’s
update(newParams)callback and Vue’scomponentUpdatedlifecycle are both replaced byvalue()inside the body. Read once for static use; wrap ineffect(() => value())for reactive use. One signature, both modes. - Auto-cleanup by default.
listen(el, ...)registers the listener via the active scope’sAbortController;cleanup(fn)registers a teardown with the same scope. Nodestroyreturn value to remember; nothing leaks if you forget. - Type-safe directive props.
JSX.Directivesdeclaration merging meansuse:autofocus={42}errors at the call site when the directive expects aboolean. Solid pioneered this; Phaze ships the canonical types in@madenowhere/phaze-directivesso importing the package activates them. - A canonical directive package, curated to keep components clean while still composing with signals.
@madenowhere/phaze-directivesships the boring set (autofocus,tooltip,longpress,clickOutside) at ~20–30 lines each. The shortlist is deliberate — these are the cross-cutting behaviors that, if written inline, would clutter every component withuseEffect-shaped boilerplate (focus management, listener wiring, observer setup, document-level click tracking). Pulling them into directives lets the JSX read as structure-and-state-only while the directive body still callseffect(() => value())to react to a passed signal. Components stay focused on what they render; directives absorb the imperative DOM glue. Vue’s official directive ecosystem is fragmented across third-party packages with inconsistent contracts; Phaze treats this curated set as a stdlib and rejects directives that don’t earn their keep on this exact axis. - A TS language-service plugin for the IDE-only false positive.
use:directiveNamedoesn’t read like a normal reference, so TS marks the import as unused.@madenowhere/phaze-tspluginfilters the diagnostic. Vue 3 sidesteps this by registering directives globally; Phaze keeps the import explicit and patches the IDE story instead.
What didn’t survive (across the whole lineage)
Section titled “What didn’t survive (across the whole lineage)”A list of patterns that were tried and shed:
- Multi-phase directive lifecycle (AngularJS
compile/link, Vue 2’s five hooks). Modern frameworks unify on “one function, plus reactive primitives for re-running.” - Global directive registration (AngularJS
module.directive(...), Vue 1/2Vue.directive(...)). Svelte/Solid/Phaze just import a function. - The
init+updatecallback split (Knockout, Vue 1). Replaced by closure + reactive primitives. - HTML-attribute expression strings (Knockout
data-bind, Alpinex-show="..."). Lost to JSX where the value is a real JS expression. restrict: 'M'HTML-comment directives (AngularJS). Nobody used them.return { update, destroy }object literals (Svelte 1–4). Replaced by closure + auto-cleanup.- Modifiers in the directive name (Vue’s
v-on:click.prevent.stop). Neat, not portable; nothing else picked it up. - Two-way binding via directive (Vue’s
v-model, AngularJS’s=). Phaze deliberately defers this — see Decisions for the rationale.
This isn’t framework triumphalism — every one of those patterns made sense in its time. They turned out to be replaceable as the broader pattern matured.
References
Section titled “References”- AngularJS Directive guide — https://docs.angularjs.org/guide/directive
- Knockout custom bindings — https://knockoutjs.com/documentation/custom-bindings.html
- Vue 3 custom directives — https://vuejs.org/guide/reusability/custom-directives.html
- Svelte actions (
use:) — https://svelte.dev/docs/svelte/use - SolidJS directives (
use:) — https://docs.solidjs.com/concepts/refs#directives - Lit directives — https://lit.dev/docs/templates/directives/
- Alpine.js directive list — https://alpinejs.dev/directives/data