Skip to content

Commit eefe76d

Browse files
committed
Add beam-init directive and directives registry (v0.5.4)
- beam-init: runs a JS expression once after the state scope is fully initialized (useful for setInterval autoplay, timers, derived values) - src/directives.ts: typed registry of all beam reactivity directives with descriptions and usage examples; exports BEAM_EXACT_NAMES, BEAM_PREFIX_NAMES, and isValidBeamReactivityDirective() helper - package.json: add ./directives export entry
1 parent 6732a83 commit eefe76d

8 files changed

Lines changed: 346 additions & 3 deletions

File tree

dist/directives.d.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Beam Reactivity Directives Registry
3+
*
4+
* Authoritative list of all valid beam-* attributes for the reactivity system.
5+
* Import from '@benqoder/beam/directives' to use in validators or tooling.
6+
*
7+
* Note: This covers reactivity directives only (reactivity.ts).
8+
* Server-action directives (beam-action, beam-poll, etc.) are in client.ts.
9+
*/
10+
export type DirectiveCategory = 'state' | 'binding' | 'event' | 'display' | 'form';
11+
export interface BeamDirective {
12+
/** The attribute name (exact) or prefix (when isPrefix is true) */
13+
name: string;
14+
/**
15+
* True when the directive is a prefix pattern.
16+
* e.g. 'beam-attr-' matches beam-attr-href, beam-attr-style, etc.
17+
*/
18+
isPrefix?: boolean;
19+
/** Human-readable description of what this directive does */
20+
description: string;
21+
/** Short JSX/HTML usage snippet */
22+
example: string;
23+
/** Semantic category */
24+
category: DirectiveCategory;
25+
}
26+
/**
27+
* All valid beam reactivity directives.
28+
* Use `BEAM_REACTIVITY_DIRECTIVES` for the full registry object.
29+
* Use `BEAM_EXACT_NAMES` and `BEAM_PREFIX_NAMES` for fast lookup sets.
30+
*/
31+
export declare const BEAM_REACTIVITY_DIRECTIVES: BeamDirective[];
32+
/** Set of exact directive names for O(1) lookup */
33+
export declare const BEAM_EXACT_NAMES: ReadonlySet<string>;
34+
/** Prefix strings for wildcard directives (e.g. 'beam-attr-') */
35+
export declare const BEAM_PREFIX_NAMES: readonly string[];
36+
/**
37+
* Check if a given beam-* attribute name is a valid reactivity directive.
38+
* @param attr Full attribute name, e.g. 'beam-attr-style' or 'beam-show'
39+
*/
40+
export declare function isValidBeamReactivityDirective(attr: string): boolean;
41+
//# sourceMappingURL=directives.d.ts.map

dist/directives.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/directives.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Beam Reactivity Directives Registry
3+
*
4+
* Authoritative list of all valid beam-* attributes for the reactivity system.
5+
* Import from '@benqoder/beam/directives' to use in validators or tooling.
6+
*
7+
* Note: This covers reactivity directives only (reactivity.ts).
8+
* Server-action directives (beam-action, beam-poll, etc.) are in client.ts.
9+
*/
10+
/**
11+
* All valid beam reactivity directives.
12+
* Use `BEAM_REACTIVITY_DIRECTIVES` for the full registry object.
13+
* Use `BEAM_EXACT_NAMES` and `BEAM_PREFIX_NAMES` for fast lookup sets.
14+
*/
15+
export const BEAM_REACTIVITY_DIRECTIVES = [
16+
{
17+
name: 'beam-state',
18+
description: 'Declares reactive state on an element. All descendant directives share this scope. ' +
19+
'Accepts JSON objects, semicolon-separated key-value pairs, or a simple primitive value (requires beam-id).',
20+
example: '<div beam-state="open: false; tab: 0">\n ...\n</div>',
21+
category: 'state',
22+
},
23+
{
24+
name: 'beam-id',
25+
description: 'Names the state scope for cross-scope access via beam-state-ref. ' +
26+
'Also required when beam-state holds a simple primitive value. ' +
27+
'Must be camelCase — dashes break JS expression evaluation.',
28+
example: '<div beam-state="false" beam-id="menu">\n ...\n</div>',
29+
category: 'state',
30+
},
31+
{
32+
name: 'beam-state-ref',
33+
description: 'References a named state scope (set via beam-id) from outside its container. ' +
34+
'Allows elements anywhere on the page to read or mutate that state.',
35+
example: '<button beam-state-ref="menu" beam-click="open = !open">Toggle menu</button>',
36+
category: 'state',
37+
},
38+
{
39+
name: 'beam-init',
40+
description: 'Runs a JavaScript expression once after the state scope is fully initialized. ' +
41+
'Ideal for setting up intervals (auto-play carousels), timers, or derived initial values. ' +
42+
'Has full access to the reactive state via the scope context.',
43+
example: '<div beam-state="index: 0; total: 4"\n' +
44+
' beam-init="setInterval(() => { index = (index + 1) % total }, 3000)">\n' +
45+
' ...\n' +
46+
'</div>',
47+
category: 'state',
48+
},
49+
{
50+
name: 'beam-click',
51+
description: 'Evaluates a JS expression when the element is clicked. ' +
52+
'Has access to reactive state and receives the native click event as `$event`. ' +
53+
'Use semicolons to chain multiple statements.',
54+
example: '<button beam-click="count++">Increment</button>',
55+
category: 'event',
56+
},
57+
{
58+
name: 'beam-state-toggle',
59+
description: 'Toggles a boolean state property on click. ' +
60+
'Optionally force-sets to a value with the "prop=value" syntax. ' +
61+
'Automatically manages aria-pressed and aria-expanded attributes.',
62+
example: '<button beam-state-toggle="open">Toggle\n' +
63+
'<!-- Force open: beam-state-toggle="open=true" -->\n' +
64+
'<!-- Force close: beam-state-toggle="open=false" -->',
65+
category: 'event',
66+
},
67+
{
68+
name: 'beam-show',
69+
description: 'Controls element visibility reactively. Sets display:none when the expression is falsy. ' +
70+
'Re-evaluates automatically whenever referenced state changes.',
71+
example: '<div beam-show="open">Visible when open is truthy</div>',
72+
category: 'display',
73+
},
74+
{
75+
name: 'beam-class',
76+
description: 'Conditionally applies CSS classes. ' +
77+
'Accepts "className: condition" pairs (semicolon-separated) or a JSON object. ' +
78+
'Quote class names that contain spaces or hyphens.',
79+
example: '<button beam-class="active: tab === 0; \'font-bold\': isSelected">Tab 1</button>',
80+
category: 'display',
81+
},
82+
{
83+
name: 'beam-text',
84+
description: 'Reactively sets the text content of an element to the result of a JS expression. ' +
85+
'The element\'s existing text children are replaced.',
86+
example: '<span beam-text="count + \' items selected\'"></span>',
87+
category: 'binding',
88+
},
89+
{
90+
name: 'beam-attr-',
91+
isPrefix: true,
92+
description: 'Reactively binds any HTML attribute. Append the target attribute name after the prefix. ' +
93+
'Setting to false or null removes the attribute; true sets an empty attribute.',
94+
example: '<!-- Animated slide -->\n' +
95+
'<div beam-attr-style="`transform: translateX(${index * 100}%)`"></div>\n' +
96+
'<!-- Conditional disabled -->\n' +
97+
'<button beam-attr-disabled="!isReady"></button>',
98+
category: 'binding',
99+
},
100+
{
101+
name: 'beam-model',
102+
description: 'Creates a two-way binding between a form input and a state property. ' +
103+
'The input value reflects state and updates state on user input/change. ' +
104+
'Supports text, number, checkbox, radio, and select elements.',
105+
example: '<input beam-model="query" type="text" placeholder="Search..." />',
106+
category: 'form',
107+
},
108+
];
109+
/** Set of exact directive names for O(1) lookup */
110+
export const BEAM_EXACT_NAMES = new Set(BEAM_REACTIVITY_DIRECTIVES.filter(d => !d.isPrefix).map(d => d.name));
111+
/** Prefix strings for wildcard directives (e.g. 'beam-attr-') */
112+
export const BEAM_PREFIX_NAMES = BEAM_REACTIVITY_DIRECTIVES
113+
.filter(d => d.isPrefix)
114+
.map(d => d.name);
115+
/**
116+
* Check if a given beam-* attribute name is a valid reactivity directive.
117+
* @param attr Full attribute name, e.g. 'beam-attr-style' or 'beam-show'
118+
*/
119+
export function isValidBeamReactivityDirective(attr) {
120+
if (BEAM_EXACT_NAMES.has(attr))
121+
return true;
122+
return BEAM_PREFIX_NAMES.some(prefix => attr.startsWith(prefix));
123+
}

dist/reactivity.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/reactivity.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,12 @@ function setupReactiveScope(el) {
461461
}
462462
reactiveElStates.set(el, state);
463463
processReactiveBindings(el, state);
464+
// beam-init: run expression once after state scope is fully initialized.
465+
// Useful for setInterval (auto-play), setTimeout, or computing derived values.
466+
const initExpr = el.getAttribute('beam-init');
467+
if (initExpr) {
468+
evalReactiveExpr(initExpr, state);
469+
}
464470
}
465471
// --- Setup standalone ref elements (elements with beam-state-ref outside any beam-state scope) ---
466472
function setupRefElements() {

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@benqoder/beam",
3-
"version": "0.5.3",
3+
"version": "0.5.4",
44
"type": "module",
55
"publishConfig": {
66
"registry": "https://registry.npmjs.org",
@@ -23,6 +23,10 @@
2323
"types": "./dist/reactivity.d.ts",
2424
"import": "./dist/reactivity.js"
2525
},
26+
"./directives": {
27+
"types": "./dist/directives.d.ts",
28+
"import": "./dist/directives.js"
29+
},
2630
"./vite": {
2731
"types": "./dist/vite.d.ts",
2832
"import": "./dist/vite.js"
@@ -61,4 +65,4 @@
6165
"typescript": "^5.0.0",
6266
"vite": "^6.0.0"
6367
}
64-
}
68+
}

src/directives.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Beam Reactivity Directives Registry
3+
*
4+
* Authoritative list of all valid beam-* attributes for the reactivity system.
5+
* Import from '@benqoder/beam/directives' to use in validators or tooling.
6+
*
7+
* Note: This covers reactivity directives only (reactivity.ts).
8+
* Server-action directives (beam-action, beam-poll, etc.) are in client.ts.
9+
*/
10+
11+
export type DirectiveCategory = 'state' | 'binding' | 'event' | 'display' | 'form';
12+
13+
export interface BeamDirective {
14+
/** The attribute name (exact) or prefix (when isPrefix is true) */
15+
name: string;
16+
/**
17+
* True when the directive is a prefix pattern.
18+
* e.g. 'beam-attr-' matches beam-attr-href, beam-attr-style, etc.
19+
*/
20+
isPrefix?: boolean;
21+
/** Human-readable description of what this directive does */
22+
description: string;
23+
/** Short JSX/HTML usage snippet */
24+
example: string;
25+
/** Semantic category */
26+
category: DirectiveCategory;
27+
}
28+
29+
/**
30+
* All valid beam reactivity directives.
31+
* Use `BEAM_REACTIVITY_DIRECTIVES` for the full registry object.
32+
* Use `BEAM_EXACT_NAMES` and `BEAM_PREFIX_NAMES` for fast lookup sets.
33+
*/
34+
export const BEAM_REACTIVITY_DIRECTIVES: BeamDirective[] = [
35+
{
36+
name: 'beam-state',
37+
description:
38+
'Declares reactive state on an element. All descendant directives share this scope. ' +
39+
'Accepts JSON objects, semicolon-separated key-value pairs, or a simple primitive value (requires beam-id).',
40+
example: '<div beam-state="open: false; tab: 0">\n ...\n</div>',
41+
category: 'state',
42+
},
43+
{
44+
name: 'beam-id',
45+
description:
46+
'Names the state scope for cross-scope access via beam-state-ref. ' +
47+
'Also required when beam-state holds a simple primitive value. ' +
48+
'Must be camelCase — dashes break JS expression evaluation.',
49+
example: '<div beam-state="false" beam-id="menu">\n ...\n</div>',
50+
category: 'state',
51+
},
52+
{
53+
name: 'beam-state-ref',
54+
description:
55+
'References a named state scope (set via beam-id) from outside its container. ' +
56+
'Allows elements anywhere on the page to read or mutate that state.',
57+
example: '<button beam-state-ref="menu" beam-click="open = !open">Toggle menu</button>',
58+
category: 'state',
59+
},
60+
{
61+
name: 'beam-init',
62+
description:
63+
'Runs a JavaScript expression once after the state scope is fully initialized. ' +
64+
'Ideal for setting up intervals (auto-play carousels), timers, or derived initial values. ' +
65+
'Has full access to the reactive state via the scope context.',
66+
example:
67+
'<div beam-state="index: 0; total: 4"\n' +
68+
' beam-init="setInterval(() => { index = (index + 1) % total }, 3000)">\n' +
69+
' ...\n' +
70+
'</div>',
71+
category: 'state',
72+
},
73+
{
74+
name: 'beam-click',
75+
description:
76+
'Evaluates a JS expression when the element is clicked. ' +
77+
'Has access to reactive state and receives the native click event as `$event`. ' +
78+
'Use semicolons to chain multiple statements.',
79+
example: '<button beam-click="count++">Increment</button>',
80+
category: 'event',
81+
},
82+
{
83+
name: 'beam-state-toggle',
84+
description:
85+
'Toggles a boolean state property on click. ' +
86+
'Optionally force-sets to a value with the "prop=value" syntax. ' +
87+
'Automatically manages aria-pressed and aria-expanded attributes.',
88+
example:
89+
'<button beam-state-toggle="open">Toggle\n' +
90+
'<!-- Force open: beam-state-toggle="open=true" -->\n' +
91+
'<!-- Force close: beam-state-toggle="open=false" -->',
92+
category: 'event',
93+
},
94+
{
95+
name: 'beam-show',
96+
description:
97+
'Controls element visibility reactively. Sets display:none when the expression is falsy. ' +
98+
'Re-evaluates automatically whenever referenced state changes.',
99+
example: '<div beam-show="open">Visible when open is truthy</div>',
100+
category: 'display',
101+
},
102+
{
103+
name: 'beam-class',
104+
description:
105+
'Conditionally applies CSS classes. ' +
106+
'Accepts "className: condition" pairs (semicolon-separated) or a JSON object. ' +
107+
'Quote class names that contain spaces or hyphens.',
108+
example:
109+
'<button beam-class="active: tab === 0; \'font-bold\': isSelected">Tab 1</button>',
110+
category: 'display',
111+
},
112+
{
113+
name: 'beam-text',
114+
description:
115+
'Reactively sets the text content of an element to the result of a JS expression. ' +
116+
'The element\'s existing text children are replaced.',
117+
example: '<span beam-text="count + \' items selected\'"></span>',
118+
category: 'binding',
119+
},
120+
{
121+
name: 'beam-attr-',
122+
isPrefix: true,
123+
description:
124+
'Reactively binds any HTML attribute. Append the target attribute name after the prefix. ' +
125+
'Setting to false or null removes the attribute; true sets an empty attribute.',
126+
example:
127+
'<!-- Animated slide -->\n' +
128+
'<div beam-attr-style="`transform: translateX(${index * 100}%)`"></div>\n' +
129+
'<!-- Conditional disabled -->\n' +
130+
'<button beam-attr-disabled="!isReady"></button>',
131+
category: 'binding',
132+
},
133+
{
134+
name: 'beam-model',
135+
description:
136+
'Creates a two-way binding between a form input and a state property. ' +
137+
'The input value reflects state and updates state on user input/change. ' +
138+
'Supports text, number, checkbox, radio, and select elements.',
139+
example: '<input beam-model="query" type="text" placeholder="Search..." />',
140+
category: 'form',
141+
},
142+
];
143+
144+
/** Set of exact directive names for O(1) lookup */
145+
export const BEAM_EXACT_NAMES: ReadonlySet<string> = new Set(
146+
BEAM_REACTIVITY_DIRECTIVES.filter(d => !d.isPrefix).map(d => d.name),
147+
);
148+
149+
/** Prefix strings for wildcard directives (e.g. 'beam-attr-') */
150+
export const BEAM_PREFIX_NAMES: readonly string[] = BEAM_REACTIVITY_DIRECTIVES
151+
.filter(d => d.isPrefix)
152+
.map(d => d.name);
153+
154+
/**
155+
* Check if a given beam-* attribute name is a valid reactivity directive.
156+
* @param attr Full attribute name, e.g. 'beam-attr-style' or 'beam-show'
157+
*/
158+
export function isValidBeamReactivityDirective(attr: string): boolean {
159+
if (BEAM_EXACT_NAMES.has(attr)) return true;
160+
return BEAM_PREFIX_NAMES.some(prefix => attr.startsWith(prefix));
161+
}

src/reactivity.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,13 @@ function setupReactiveScope(el: HTMLElement): void {
495495

496496
reactiveElStates.set(el, state)
497497
processReactiveBindings(el, state)
498+
499+
// beam-init: run expression once after state scope is fully initialized.
500+
// Useful for setInterval (auto-play), setTimeout, or computing derived values.
501+
const initExpr = el.getAttribute('beam-init')
502+
if (initExpr) {
503+
evalReactiveExpr(initExpr, state)
504+
}
498505
}
499506

500507
// --- Setup standalone ref elements (elements with beam-state-ref outside any beam-state scope) ---

0 commit comments

Comments
 (0)