@fluffjs/fluff 0.4.4 → 0.5.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.
Files changed (77) hide show
  1. package/bundle.min.js +1 -1
  2. package/decorators/Directive.d.ts +16 -0
  3. package/decorators/Directive.js +23 -0
  4. package/decorators/HostElement.d.ts +1 -0
  5. package/decorators/HostElement.js +4 -0
  6. package/index.d.ts +4 -0
  7. package/index.js +3 -0
  8. package/interfaces/ElementWithDirectives.d.ts +4 -0
  9. package/interfaces/FluffHostElement.d.ts +3 -10
  10. package/package.json +1 -1
  11. package/runtime/BreakController.d.ts +2 -3
  12. package/runtime/BreakController.js +0 -3
  13. package/runtime/FluffBase.d.ts +50 -42
  14. package/runtime/FluffBase.js +185 -242
  15. package/runtime/FluffDirective.d.ts +9 -0
  16. package/runtime/FluffDirective.js +36 -0
  17. package/runtime/FluffElement.d.ts +0 -1
  18. package/runtime/FluffElementImpl.d.ts +5 -10
  19. package/runtime/FluffElementImpl.js +59 -77
  20. package/runtime/FluffMarkers.d.ts +0 -1
  21. package/runtime/FluffMarkers.js +0 -1
  22. package/runtime/ForController.d.ts +2 -5
  23. package/runtime/ForController.js +7 -9
  24. package/runtime/IfController.d.ts +2 -5
  25. package/runtime/IfController.js +12 -13
  26. package/runtime/MarkerController.d.ts +10 -12
  27. package/runtime/MarkerController.js +7 -4
  28. package/runtime/MarkerManager.d.ts +2 -5
  29. package/runtime/MarkerManager.js +11 -50
  30. package/runtime/MarkerManagerInterface.d.ts +5 -2
  31. package/runtime/SwitchController.d.ts +2 -5
  32. package/runtime/SwitchController.js +10 -13
  33. package/runtime/TextController.d.ts +2 -4
  34. package/runtime/TextController.js +8 -16
  35. package/runtime/tests/DirectOutputParent.js +4 -1
  36. package/runtime/tests/TestChildTasksListComponent.js +4 -1
  37. package/runtime/tests/TestForComponent.js +4 -1
  38. package/runtime/tests/TestForReinsertBindsInputParentComponent.js +7 -2
  39. package/runtime/tests/TestForTextMarkerCollisionNoTrackParentComponent.js +5 -2
  40. package/runtime/tests/TestForTextMarkerCollisionParentComponent.js +5 -10
  41. package/runtime/tests/TestForUnsubscribeNestedParentComponent.js +7 -2
  42. package/runtime/tests/TestGetterReactivityComponent.js +4 -1
  43. package/runtime/tests/TestHarness.d.ts +1 -1
  44. package/runtime/tests/TestHarness.js +3 -3
  45. package/runtime/tests/TestIfReinsertBindsInputChildComponent.js +4 -1
  46. package/runtime/tests/TestIfReinsertBindsInputParentComponent.js +7 -2
  47. package/runtime/tests/TestIfUnsubscribeNestedParentComponent.js +7 -2
  48. package/runtime/tests/TestInterpolationNestedPropertyComponentBase.js +4 -1
  49. package/runtime/tests/TestLateDefineForComponent.js +4 -1
  50. package/runtime/tests/TestNullInputTextComponent.js +5 -2
  51. package/runtime/tests/TestOutputBindingChildComponent.js +4 -1
  52. package/runtime/tests/TestOutputBindingParentComponent.js +7 -2
  53. package/runtime/tests/TestParentBindsTasksComponent.js +4 -1
  54. package/runtime/tests/TestSwitchReinsertBindsInputChildComponent.js +4 -1
  55. package/runtime/tests/TestSwitchReinsertBindsInputParentComponent.js +8 -10
  56. package/runtime/tests/TestSwitchUnsubscribeNestedParentComponent.js +7 -9
  57. package/runtime/tests/TestTemplateNestedMarkersComponent.js +5 -2
  58. package/runtime/tests/TestUnsubscribeNestedChildComponent.js +4 -1
  59. package/runtime/tests/TestUnsubscribeNestedGrandchildComponent.js +4 -1
  60. package/runtime/tests/createPipeUnwrapTestComponent.js +4 -1
  61. package/runtime/tests/createPropBindParentComponent.js +4 -1
  62. package/runtime/tests/createTestInterpolationPipeComponent.js +4 -1
  63. package/runtime/tests/createTestInterpolationPipeWithArgsComponent.js +4 -1
  64. package/interfaces/BreakMarkerConfig.d.ts +0 -3
  65. package/interfaces/ForMarkerConfig.d.ts +0 -8
  66. package/interfaces/ForMarkerConfig.js +0 -1
  67. package/interfaces/IfMarkerConfig.d.ts +0 -7
  68. package/interfaces/IfMarkerConfig.js +0 -1
  69. package/interfaces/MarkerConfig.d.ts +0 -6
  70. package/interfaces/MarkerConfig.js +0 -1
  71. package/interfaces/SwitchMarkerConfig.d.ts +0 -10
  72. package/interfaces/SwitchMarkerConfig.js +0 -1
  73. package/interfaces/TextMarkerConfig.d.ts +0 -9
  74. package/interfaces/TextMarkerConfig.js +0 -1
  75. package/runtime/MarkerConfigGuards.d.ts +0 -13
  76. package/runtime/MarkerConfigGuards.js +0 -17
  77. /package/interfaces/{BreakMarkerConfig.js → ElementWithDirectives.js} +0 -0
@@ -1,23 +1,5 @@
1
1
  import { Property } from '../utils/Property.js';
2
2
  import { Publisher } from '../utils/Publisher.js';
3
- const BINDING_TYPES = ['property', 'event', 'two-way', 'class', 'style', 'ref'];
4
- /**
5
- * Compact Marker Config Format (Decoder)
6
- *
7
- * Marker configs use the same string table as bindings. Type is numeric:
8
- * 0=if, 1=for, 2=text, 3=switch, 4=break
9
- *
10
- * Format varies by type:
11
- * - if: [0, branches[]] where branch = [exprId?, deps?]
12
- * - for: [1, iteratorIdx, iterableExprId, hasEmpty, deps?, trackByIdx?]
13
- * - text: [2, exprId, deps?, pipes?]
14
- * - switch: [3, expressionExprId, deps?, cases[]]
15
- * - break: [4]
16
- *
17
- * deps are interned as CompactDep[] (same as bindings)
18
- * pipes are [pipeNameIdx, argExprIds[]][]
19
- */
20
- const MARKER_TYPES = ['if', 'for', 'text', 'switch', 'break'];
21
3
  export class FluffBase extends HTMLElement {
22
4
  static __e = [];
23
5
  static __h = [];
@@ -37,9 +19,6 @@ export class FluffBase extends HTMLElement {
37
19
  callback();
38
20
  }
39
21
  }
40
- static __decodeString(idx) {
41
- return FluffBase.__s[idx];
42
- }
43
22
  static __decodeDep(dep) {
44
23
  if (Array.isArray(dep)) {
45
24
  return dep.map(idx => FluffBase.__s[idx]);
@@ -51,107 +30,6 @@ export class FluffBase extends HTMLElement {
51
30
  return undefined;
52
31
  return deps.map(d => FluffBase.__decodeDep(d));
53
32
  }
54
- static __decodeMarkerConfig(compact) {
55
- const [typeNum] = compact;
56
- const type = MARKER_TYPES[typeNum];
57
- switch (typeNum) {
58
- case 0: // if
59
- {
60
- const [, rawBranches] = compact;
61
- const branches = rawBranches.map(b => {
62
- if (b.length === 0)
63
- return {};
64
- const [branchExprId, branchDeps] = b;
65
- const result = {};
66
- if (branchExprId !== null)
67
- result.exprId = branchExprId;
68
- if (branchDeps)
69
- result.deps = FluffBase.__decodeDeps(branchDeps);
70
- return result;
71
- });
72
- return { type, branches };
73
- }
74
- case 1: // for
75
- {
76
- const [, iteratorIdx, iterableExprId, hasEmpty, deps, trackByIdx] = compact;
77
- const result = {
78
- type,
79
- iterator: FluffBase.__s[iteratorIdx],
80
- iterableExprId,
81
- hasEmpty
82
- };
83
- if (deps)
84
- result.deps = FluffBase.__decodeDeps(deps);
85
- if (trackByIdx !== null)
86
- result.trackBy = FluffBase.__s[trackByIdx];
87
- return result;
88
- }
89
- case 2: // text
90
- {
91
- const [, exprId, deps, pipes] = compact;
92
- const result = { type, exprId };
93
- if (deps)
94
- result.deps = FluffBase.__decodeDeps(deps);
95
- if (pipes) {
96
- result.pipes = pipes.map(([nameIdx, argExprIds]) => ({
97
- name: FluffBase.__s[nameIdx],
98
- argExprIds
99
- }));
100
- }
101
- return result;
102
- }
103
- case 3: // switch
104
- {
105
- const [, expressionExprId, deps, cases] = compact;
106
- const result = { type, expressionExprId };
107
- if (deps)
108
- result.deps = FluffBase.__decodeDeps(deps);
109
- result.cases = cases.map(([isDefault, fallthrough, valueExprId]) => {
110
- const c = { isDefault, fallthrough };
111
- if (valueExprId !== null)
112
- c.valueExprId = valueExprId;
113
- return c;
114
- });
115
- return result;
116
- }
117
- case 4: // break
118
- return { type };
119
- default:
120
- return { type: 'unknown' };
121
- }
122
- }
123
- static __decodeBinding(compact) {
124
- const [nameIdx, bType, deps, id, extras] = compact;
125
- const n = FluffBase.__s[nameIdx];
126
- const b = BINDING_TYPES[bType];
127
- const result = { n, b };
128
- if (deps) {
129
- result.d = deps.map(d => FluffBase.__decodeDep(d));
130
- }
131
- if (b === 'event') {
132
- if (id !== null) {
133
- result.h = id;
134
- }
135
- }
136
- else if (id !== null) {
137
- result.e = id;
138
- }
139
- if (extras) {
140
- if (extras.t) {
141
- result.t = extras.t;
142
- }
143
- if (extras.s) {
144
- result.s = extras.s;
145
- }
146
- if (extras.p) {
147
- result.p = extras.p.map(([pipeNameIdx, args]) => ({
148
- n: FluffBase.__s[pipeNameIdx],
149
- a: args
150
- }));
151
- }
152
- }
153
- return result;
154
- }
155
33
  static __areExpressionsReady() {
156
34
  return FluffBase.__expressionsReady;
157
35
  }
@@ -185,13 +63,14 @@ export class FluffBase extends HTMLElement {
185
63
  if (result instanceof Property) {
186
64
  result = result.getValue();
187
65
  }
188
- for (const pipe of pipes) {
189
- const pipeFn = this.__getPipeFn(pipe.name);
66
+ for (const [nameIdx, argExprIds] of pipes) {
67
+ const name = FluffBase.__s[nameIdx];
68
+ const pipeFn = this.__getPipeFn(name);
190
69
  if (!pipeFn) {
191
- console.warn(`Pipe "${pipe.name}" not found`);
70
+ console.warn(`Pipe "${name}" not found`);
192
71
  continue;
193
72
  }
194
- const args = pipe.argExprIds.map(id => this.__getCompiledExprFn(id)(this, locals));
73
+ const args = argExprIds.map(id => this.__getCompiledExprFn(id)(this, locals));
195
74
  result = pipeFn(result, ...args);
196
75
  }
197
76
  return result;
@@ -211,42 +90,37 @@ export class FluffBase extends HTMLElement {
211
90
  const ctor = this.constructor;
212
91
  if (typeof ctor === 'function') {
213
92
  const bindings = Reflect.get(ctor, '__bindings');
214
- if (this.__isBindingsMap(bindings)) {
93
+ if (this.__isRecord(bindings) && lid in bindings) {
215
94
  const lidBindings = bindings[lid];
216
- if (lidBindings) {
217
- return lidBindings.map(b => FluffBase.__decodeBindingAny(b));
95
+ if (this.__isCompactBindingArray(lidBindings)) {
96
+ return lidBindings;
218
97
  }
219
98
  }
220
99
  }
221
100
  return undefined;
222
101
  }
223
- static __decodeBindingAny(binding) {
224
- if (Array.isArray(binding)) {
225
- return FluffBase.__decodeBinding(binding);
226
- }
227
- return binding;
228
- }
229
- __isBindingsMap(value) {
230
- return !(!value || typeof value !== 'object');
102
+ __isCompactBindingArray(value) {
103
+ return Array.isArray(value);
231
104
  }
105
+ // CompactBinding[1] type dispatch: 0=property, 1=event, 2=two-way, 3=class, 4=style, 5=ref
232
106
  __applyBindingWithScope(el, binding, scope, subscriptions) {
233
- switch (binding.b) {
234
- case 'property':
107
+ switch (binding[1]) {
108
+ case 0:
235
109
  this.__applyPropertyBindingWithScope(el, binding, scope, subscriptions);
236
110
  break;
237
- case 'event':
111
+ case 1:
238
112
  this.__applyEventBindingWithScope(el, binding, scope);
239
113
  break;
240
- case 'two-way':
114
+ case 2:
241
115
  this.__applyTwoWayBindingWithScope(el, binding, scope, subscriptions);
242
116
  break;
243
- case 'class':
117
+ case 3:
244
118
  this.__applyClassBindingWithScope(el, binding, scope, subscriptions);
245
119
  break;
246
- case 'style':
120
+ case 4:
247
121
  this.__applyStyleBindingWithScope(el, binding, scope, subscriptions);
248
122
  break;
249
- case 'ref':
123
+ case 5:
250
124
  break;
251
125
  }
252
126
  }
@@ -264,9 +138,6 @@ export class FluffBase extends HTMLElement {
264
138
  }
265
139
  return fn;
266
140
  }
267
- __applyPipes(value, pipes, locals) {
268
- return this.__applyPipesForController(value, pipes.map(p => ({ name: p.n, argExprIds: p.a })), locals);
269
- }
270
141
  __getPipeFn(_name) {
271
142
  return undefined;
272
143
  }
@@ -302,34 +173,7 @@ export class FluffBase extends HTMLElement {
302
173
  const [first, ...rest] = chain;
303
174
  const reactiveProp = this.__getReactivePropFromScope(first, scope);
304
175
  if (reactiveProp) {
305
- if (rest.length === 0) {
306
- addSub(reactiveProp.onChange.subscribe(callback));
307
- }
308
- else {
309
- let nestedSubs = [];
310
- const resubscribeNested = () => {
311
- for (const sub of nestedSubs) {
312
- sub.unsubscribe();
313
- }
314
- nestedSubs = [];
315
- const currentValue = reactiveProp.getValue();
316
- if (currentValue !== null && currentValue !== undefined) {
317
- this.__subscribeToNestedChain(currentValue, rest, callback, (sub) => {
318
- nestedSubs.push(sub);
319
- addSub(sub);
320
- });
321
- }
322
- callback();
323
- };
324
- addSub(reactiveProp.onChange.subscribe(resubscribeNested));
325
- const currentValue = reactiveProp.getValue();
326
- if (currentValue !== null && currentValue !== undefined) {
327
- this.__subscribeToNestedChain(currentValue, rest, callback, (sub) => {
328
- nestedSubs.push(sub);
329
- addSub(sub);
330
- });
331
- }
332
- }
176
+ this.__subscribeToProp(reactiveProp, rest, callback, addSub);
333
177
  }
334
178
  else if (first in scope.locals) {
335
179
  const localValue = scope.locals[first];
@@ -341,40 +185,42 @@ export class FluffBase extends HTMLElement {
341
185
  console.warn(`Binding dependency "${first}" not found on component ${scope.host.constructor.name}`);
342
186
  }
343
187
  }
188
+ __subscribeToProp(prop, rest, callback, addSub) {
189
+ if (rest.length === 0) {
190
+ addSub(prop.onChange.subscribe(callback));
191
+ return;
192
+ }
193
+ let nestedSubs = [];
194
+ const resubscribeNested = () => {
195
+ for (const sub of nestedSubs) {
196
+ sub.unsubscribe();
197
+ }
198
+ nestedSubs = [];
199
+ const currentValue = prop.getValue();
200
+ if (currentValue !== null && currentValue !== undefined) {
201
+ this.__subscribeToNestedChain(currentValue, rest, callback, (sub) => {
202
+ nestedSubs.push(sub);
203
+ addSub(sub);
204
+ });
205
+ }
206
+ callback();
207
+ };
208
+ addSub(prop.onChange.subscribe(resubscribeNested));
209
+ const currentValue = prop.getValue();
210
+ if (currentValue !== null && currentValue !== undefined) {
211
+ this.__subscribeToNestedChain(currentValue, rest, callback, (sub) => {
212
+ nestedSubs.push(sub);
213
+ addSub(sub);
214
+ });
215
+ }
216
+ }
344
217
  __subscribeToNestedChain(obj, chain, callback, addSub) {
345
218
  if (chain.length === 0 || obj === null || obj === undefined)
346
219
  return;
347
220
  const [first, ...rest] = chain;
348
221
  const prop = Reflect.get(obj, first);
349
222
  if (prop instanceof Property) {
350
- if (rest.length === 0) {
351
- addSub(prop.onChange.subscribe(callback));
352
- }
353
- else {
354
- let nestedSubs = [];
355
- const resubscribeNested = () => {
356
- for (const sub of nestedSubs) {
357
- sub.unsubscribe();
358
- }
359
- nestedSubs = [];
360
- const currentValue = prop.getValue();
361
- if (currentValue !== null && currentValue !== undefined) {
362
- this.__subscribeToNestedChain(currentValue, rest, callback, (sub) => {
363
- nestedSubs.push(sub);
364
- addSub(sub);
365
- });
366
- }
367
- callback();
368
- };
369
- addSub(prop.onChange.subscribe(resubscribeNested));
370
- const currentValue = prop.getValue();
371
- if (currentValue !== null && currentValue !== undefined) {
372
- this.__subscribeToNestedChain(currentValue, rest, callback, (sub) => {
373
- nestedSubs.push(sub);
374
- addSub(sub);
375
- });
376
- }
377
- }
223
+ this.__subscribeToProp(prop, rest, callback, addSub);
378
224
  }
379
225
  else if (rest.length > 0 && prop !== null && prop !== undefined) {
380
226
  this.__subscribeToNestedChain(prop, rest, callback, addSub);
@@ -417,28 +263,32 @@ export class FluffBase extends HTMLElement {
417
263
  }
418
264
  return value;
419
265
  }
266
+ // CompactBinding: [nameIdx, type, deps, exprId, extras?]
420
267
  __applyPropertyBindingWithScope(el, binding, scope, subscriptions) {
268
+ const [nameIdx, , compactDeps, exprId, extras] = binding;
269
+ const name = FluffBase.__s[nameIdx];
270
+ const deps = FluffBase.__decodeDeps(compactDeps);
421
271
  const tagName = el.tagName.toLowerCase();
422
272
  const isCustomElement = customElements.get(tagName) !== undefined;
423
273
  const update = () => {
424
274
  try {
425
- if (typeof binding.e !== 'number') {
426
- throw new Error(`Binding for ${binding.n} is missing exprId`);
275
+ if (typeof exprId !== 'number') {
276
+ throw new Error(`Binding for ${name} is missing exprId`);
427
277
  }
428
- const fn = this.__getCompiledExprFn(binding.e);
278
+ const fn = this.__getCompiledExprFn(exprId);
429
279
  let value = fn(this, scope.locals);
430
- if (binding.p && binding.p.length > 0) {
431
- value = this.__applyPipes(value, binding.p, scope.locals);
280
+ if (extras?.p && extras.p.length > 0) {
281
+ value = this.__applyPipesForController(value, extras.p, scope.locals);
432
282
  }
433
- this.__setChildProperty(el, binding.n, value);
283
+ this.__setChildProperty(el, name, value);
434
284
  }
435
285
  catch (e) {
436
286
  console.error('Property binding error:', e);
437
287
  }
438
288
  };
439
- this.__subscribeToExpressionInScope(binding.d, scope, update, subscriptions);
440
- if (binding.s) {
441
- this.__subscribeToExpressionInScope([binding.s], scope, update, subscriptions);
289
+ this.__subscribeToExpressionInScope(deps, scope, update, subscriptions);
290
+ if (extras?.s) {
291
+ this.__subscribeToExpressionInScope([extras.s], scope, update, subscriptions);
442
292
  }
443
293
  if (isCustomElement) {
444
294
  if (el instanceof FluffBase) {
@@ -450,7 +300,7 @@ export class FluffBase extends HTMLElement {
450
300
  update();
451
301
  }
452
302
  else {
453
- console.warn(`Element <${tagName}> is not a FluffBase instance after whenDefined - binding for "${binding.n}" skipped`);
303
+ console.warn(`Element <${tagName}> is not a FluffBase instance after whenDefined - binding for "${name}" skipped`);
454
304
  }
455
305
  });
456
306
  }
@@ -459,20 +309,24 @@ export class FluffBase extends HTMLElement {
459
309
  update();
460
310
  }
461
311
  }
312
+ // CompactBinding: [nameIdx, type, deps, handlerId, extras?]
462
313
  __applyEventBindingWithScope(el, binding, scope) {
463
- const boundKey = `__fluff_event_${binding.n}`;
314
+ const [nameIdx, , , handlerId] = binding;
315
+ const name = FluffBase.__s[nameIdx];
316
+ const boundKey = `__fluff_event_${name}`;
464
317
  if (Reflect.has(el, boundKey))
465
318
  return;
466
319
  Reflect.set(el, boundKey, true);
467
- if (typeof binding.h !== 'number') {
468
- throw new Error(`Event binding for ${binding.n} is missing handlerId`);
320
+ if (typeof handlerId !== 'number') {
321
+ throw new Error(`Event binding for ${name} is missing handlerId`);
469
322
  }
470
- const handlerFn = this.__getCompiledHandlerFn(binding.h);
471
- if (el.hasAttribute('x-fluff-component')) {
472
- this.__applyOutputBinding(el, binding.n, handlerFn, scope);
323
+ const handlerFn = this.__getCompiledHandlerFn(handlerId);
324
+ const hasDirectives = el.hasAttribute('data-fluff-directives');
325
+ if (el.hasAttribute('x-fluff-component') || hasDirectives) {
326
+ this.__applyOutputBinding(el, name, handlerFn, scope);
473
327
  }
474
328
  else {
475
- el.addEventListener(binding.n, (event) => {
329
+ el.addEventListener(name, (event) => {
476
330
  try {
477
331
  handlerFn(this, scope.locals, event);
478
332
  }
@@ -483,8 +337,7 @@ export class FluffBase extends HTMLElement {
483
337
  }
484
338
  }
485
339
  __applyOutputBinding(el, outputName, handlerFn, scope) {
486
- const trySubscribe = () => {
487
- const publisher = Reflect.get(el, outputName);
340
+ const subscribeToPublisher = (publisher) => {
488
341
  if (publisher instanceof Publisher) {
489
342
  const sub = publisher.subscribe((value) => {
490
343
  try {
@@ -499,25 +352,48 @@ export class FluffBase extends HTMLElement {
499
352
  }
500
353
  return false;
501
354
  };
502
- if (trySubscribe()) {
503
- return;
355
+ const subscribeToElement = () => {
356
+ const publisher = Reflect.get(el, outputName);
357
+ return subscribeToPublisher(publisher);
358
+ };
359
+ const subscribeToDirectives = () => {
360
+ let subscribed = false;
361
+ const directives = el.__fluffDirectives;
362
+ if (directives) {
363
+ for (const directive of directives) {
364
+ const directivePublisher = Reflect.get(directive, outputName);
365
+ if (subscribeToPublisher(directivePublisher)) {
366
+ subscribed = true;
367
+ }
368
+ }
369
+ }
370
+ return subscribed;
371
+ };
372
+ subscribeToDirectives();
373
+ if (!subscribeToElement()) {
374
+ const tagName = el.tagName.toLowerCase();
375
+ if (tagName.includes('-')) {
376
+ this.__whenDefined(tagName, () => {
377
+ subscribeToElement();
378
+ });
379
+ }
504
380
  }
505
- this.__whenDefined(el.tagName.toLowerCase(), () => {
506
- trySubscribe();
507
- });
508
381
  }
509
382
  __whenDefined(tagName, callback) {
510
383
  customElements.whenDefined(tagName)
511
384
  .then(callback)
512
385
  .catch(console.error);
513
386
  }
387
+ // CompactBinding: [nameIdx, type, deps, exprId, extras?] — extras.t = targetProp for two-way
514
388
  __applyTwoWayBindingWithScope(el, binding, scope, subscriptions) {
515
389
  this.__applyPropertyBindingWithScope(el, binding, scope, subscriptions);
516
- if (typeof binding.t !== 'string' || binding.t.length === 0) {
517
- throw new Error(`Two-way binding for ${binding.n} is missing targetProp`);
390
+ const name = FluffBase.__s[binding[0]];
391
+ const targetProp = binding[4]?.t;
392
+ if (typeof targetProp !== 'string' || targetProp.length === 0) {
393
+ throw new Error(`Two-way binding for ${name} is missing targetProp`);
518
394
  }
519
- const reactiveProp = this.__getReactivePropFromScope(binding.t, scope);
520
- const childPropCandidate = Reflect.get(el, binding.n);
395
+ const reactiveProp = this.__getReactivePropFromScope(targetProp, scope);
396
+ const childPropCandidate = Reflect.get(el, name);
521
397
  if (reactiveProp && childPropCandidate instanceof Property) {
522
398
  const sub = childPropCandidate.onChange.subscribe((val) => {
523
399
  reactiveProp.setValue(val, true);
@@ -530,48 +406,115 @@ export class FluffBase extends HTMLElement {
530
406
  }
531
407
  }
532
408
  }
409
+ // CompactBinding: [nameIdx, type, deps, exprId]
533
410
  __applyClassBindingWithScope(el, binding, scope, subscriptions) {
411
+ const [nameIdx, , compactDeps, exprId] = binding;
412
+ const name = FluffBase.__s[nameIdx];
413
+ const deps = FluffBase.__decodeDeps(compactDeps);
534
414
  const update = () => {
535
415
  try {
536
- if (typeof binding.e !== 'number') {
537
- throw new Error(`Class binding for ${binding.n} is missing exprId`);
416
+ if (typeof exprId !== 'number') {
417
+ throw new Error(`Class binding for ${name} is missing exprId`);
538
418
  }
539
- const fn = this.__getCompiledExprFn(binding.e);
419
+ const fn = this.__getCompiledExprFn(exprId);
540
420
  const value = this.__unwrap(fn(this, scope.locals));
541
421
  if (value) {
542
- el.classList.add(binding.n);
422
+ el.classList.add(name);
543
423
  }
544
424
  else {
545
- el.classList.remove(binding.n);
425
+ el.classList.remove(name);
546
426
  }
547
427
  }
548
428
  catch (e) {
549
429
  console.error('Class binding error:', e);
550
430
  }
551
431
  };
552
- this.__subscribeToExpressionInScope(binding.d, scope, update, subscriptions);
432
+ this.__subscribeToExpressionInScope(deps, scope, update, subscriptions);
553
433
  update();
554
434
  }
435
+ // CompactBinding: [nameIdx, type, deps, exprId]
555
436
  __applyStyleBindingWithScope(el, binding, scope, subscriptions) {
437
+ const [nameIdx, , compactDeps, exprId] = binding;
438
+ const name = FluffBase.__s[nameIdx];
439
+ const deps = FluffBase.__decodeDeps(compactDeps);
556
440
  const update = () => {
557
441
  try {
558
- if (typeof binding.e !== 'number') {
559
- throw new Error(`Style binding for ${binding.n} is missing exprId`);
442
+ if (typeof exprId !== 'number') {
443
+ throw new Error(`Style binding for ${name} is missing exprId`);
560
444
  }
561
- const fn = this.__getCompiledExprFn(binding.e);
445
+ const fn = this.__getCompiledExprFn(exprId);
562
446
  const value = this.__unwrap(fn(this, scope.locals));
563
447
  if (this.__hasStyle(el)) {
564
- el.style.setProperty(binding.n, String(value));
448
+ el.style.setProperty(name, String(value));
565
449
  }
566
450
  }
567
451
  catch (e) {
568
452
  console.error('Style binding error:', e);
569
453
  }
570
454
  };
571
- this.__subscribeToExpressionInScope(binding.d, scope, update, subscriptions);
455
+ this.__subscribeToExpressionInScope(deps, scope, update, subscriptions);
572
456
  update();
573
457
  }
574
458
  __hasStyle(el) {
575
459
  return 'style' in el;
576
460
  }
461
+ __createProp(nameOrIdx, options) {
462
+ const name = typeof nameOrIdx === 'number' ? FluffBase.__s[nameOrIdx] : nameOrIdx;
463
+ const prop = new Property(options);
464
+ Object.defineProperty(this, name, {
465
+ get() {
466
+ return prop.getValue();
467
+ },
468
+ set(v) {
469
+ prop.setValue(v);
470
+ },
471
+ enumerable: true,
472
+ configurable: true
473
+ });
474
+ return prop;
475
+ }
476
+ __defineClassHostBinding(name, className, privateName) {
477
+ const host = this.__getHostElement();
478
+ Object.defineProperty(this, name, {
479
+ get: () => Boolean(Reflect.get(this, privateName)),
480
+ set: (v) => {
481
+ Reflect.set(this, privateName, v);
482
+ if (v) {
483
+ host.classList.add(className);
484
+ }
485
+ else {
486
+ host.classList.remove(className);
487
+ }
488
+ },
489
+ enumerable: true,
490
+ configurable: true
491
+ });
492
+ }
493
+ __applyPendingProps() {
494
+ const existing = Reflect.get(this, '__pendingProps');
495
+ if (!this.__isRecord(existing)) {
496
+ return;
497
+ }
498
+ for (const [propName, value] of Object.entries(existing)) {
499
+ const key = `__${propName}`;
500
+ if (key in this) {
501
+ const prop = Reflect.get(this, key);
502
+ if (prop instanceof Property) {
503
+ prop.setValue(value, true);
504
+ }
505
+ }
506
+ else if (propName in this) {
507
+ Reflect.set(this, propName, value);
508
+ }
509
+ }
510
+ Reflect.deleteProperty(this, '__pendingProps');
511
+ }
512
+ __isRecord(value) {
513
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
514
+ }
515
+ __getHostElement() {
516
+ return this;
517
+ }
518
+ __initHostBindings() {
519
+ }
577
520
  }
@@ -0,0 +1,9 @@
1
+ import { FluffBase } from './FluffBase.js';
2
+ export declare class FluffDirective extends FluffBase {
3
+ private _hostElement;
4
+ private _initialized;
5
+ __setHostElement(hostElement: HTMLElement): void;
6
+ initialize(): void;
7
+ destroy(): void;
8
+ protected __getHostElement(): HTMLElement;
9
+ }
@@ -0,0 +1,36 @@
1
+ import { FluffBase } from './FluffBase.js';
2
+ export class FluffDirective extends FluffBase {
3
+ _hostElement = null;
4
+ _initialized = false;
5
+ __setHostElement(hostElement) {
6
+ this._hostElement = hostElement;
7
+ if ('__assignHostElementProps' in this && typeof this.__assignHostElementProps === 'function') {
8
+ this.__assignHostElementProps(hostElement);
9
+ }
10
+ }
11
+ initialize() {
12
+ if (this._initialized)
13
+ return;
14
+ this._initialized = true;
15
+ this.__applyPendingProps();
16
+ this.__initHostBindings();
17
+ if ('onInit' in this && typeof this.onInit === 'function') {
18
+ this.onInit();
19
+ }
20
+ }
21
+ destroy() {
22
+ if ('onDestroy' in this && typeof this.onDestroy === 'function') {
23
+ this.onDestroy();
24
+ }
25
+ for (const sub of this.__baseSubscriptions) {
26
+ sub.unsubscribe();
27
+ }
28
+ this.__baseSubscriptions = [];
29
+ }
30
+ __getHostElement() {
31
+ if (!this._hostElement) {
32
+ throw new Error('Directive host element not set');
33
+ }
34
+ return this._hostElement;
35
+ }
36
+ }
@@ -1,5 +1,4 @@
1
1
  export { FluffBase } from './FluffBase.js';
2
- export type { BindingInfo } from './FluffBase.js';
3
2
  export { FluffElement } from './FluffElementImpl.js';
4
3
  export { getScope, registerScope, unregisterScope } from './ScopeRegistry.js';
5
4
  export type { Scope } from './ScopeRegistry.js';