ezfw-core 1.0.16 → 1.0.17

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.
@@ -14,6 +14,7 @@ declare const ez: {
14
14
  getDeepValue(obj: unknown, path: string[]): unknown;
15
15
  setDeepValue(obj: unknown, path: string[], value: unknown): void;
16
16
  _createElement(config: EzComponentConfig): Promise<HTMLElement>;
17
+ hasStyles(name: string): boolean;
17
18
  };
18
19
 
19
20
  interface EzController {
@@ -41,6 +42,8 @@ interface BindConfig {
41
42
  visible?: string;
42
43
  cls?: string | (() => string);
43
44
  html?: string | (() => string);
45
+ style?: Record<string, string> | (() => Record<string, string>);
46
+ text?: string | (() => string);
44
47
  }
45
48
 
46
49
  export interface EzComponentConfig {
@@ -218,6 +221,8 @@ export class EzBaseComponent {
218
221
  this._applyDataBind(el, bind, ctrl);
219
222
  this._applyVisibleBind(el, bind);
220
223
  this._applyClsBind(el, bind);
224
+ this._applyStyleBind(el, bind);
225
+ this._applyTextBind(el, bind);
221
226
  this._applyHtmlBind(el, bind);
222
227
  }
223
228
  }
@@ -290,7 +295,13 @@ export class EzBaseComponent {
290
295
  const stop = effect(() => {
291
296
  (async () => {
292
297
  const itemFn = this.config.itemRender;
293
- let data = ez.getDeepValue(ez.getController(activeCtrl!)?.state, props!) as unknown[] | unknown;
298
+ const controller = ez.getController(activeCtrl!);
299
+ let data = ez.getDeepValue(controller?.state, props!) as unknown[] | unknown;
300
+
301
+ // If not found in state, check computed properties
302
+ if (data == null && props?.length === 1 && controller?._computed?.[props[0]]) {
303
+ data = controller._computed[props[0]].value;
304
+ }
294
305
 
295
306
  if (data == null) {
296
307
  return;
@@ -327,7 +338,10 @@ export class EzBaseComponent {
327
338
  if (!childCfg.controller) {
328
339
  childCfg.controller = activeCtrl;
329
340
  }
330
- if (!childCfg.css && this.config.css) {
341
+ // Only inherit CSS if child doesn't have its own CSS module
342
+ const childHasOwnCss = childCfg.css ||
343
+ (childCfg.eztype && ez.hasStyles(childCfg.eztype));
344
+ if (!childHasOwnCss && this.config.css) {
331
345
  childCfg.css = this.config.css;
332
346
  childCfg._styleModule = this.config._styleModule;
333
347
  }
@@ -472,6 +486,60 @@ export class EzBaseComponent {
472
486
  this._effects!.push(stop);
473
487
  }
474
488
 
489
+ private _applyStyleBind(el: HTMLElement, bind: BindConfig): void {
490
+ if (!bind.style) return;
491
+
492
+ let prevStyles: Record<string, string> = {};
493
+
494
+ const stop = effect(() => {
495
+ let next = bind.style;
496
+
497
+ if (typeof next === 'function') {
498
+ next = next();
499
+ }
500
+
501
+ if (typeof next !== 'object' || next === null) {
502
+ next = {};
503
+ }
504
+
505
+ // Remove previous styles that are not in the new object
506
+ for (const key of Object.keys(prevStyles)) {
507
+ if (!(key in next)) {
508
+ (el.style as unknown as Record<string, string>)[key] = '';
509
+ }
510
+ }
511
+
512
+ // Apply new styles (supports camelCase like 'backgroundColor')
513
+ for (const [key, value] of Object.entries(next)) {
514
+ (el.style as unknown as Record<string, string>)[key] = value;
515
+ }
516
+
517
+ prevStyles = { ...next };
518
+ });
519
+
520
+ this._effects!.push(stop);
521
+ }
522
+
523
+ private _applyTextBind(el: HTMLElement, bind: BindConfig): void {
524
+ if (!bind.text) return;
525
+
526
+ const stop = effect(() => {
527
+ let next = bind.text;
528
+
529
+ if (typeof next === 'function') {
530
+ next = next();
531
+ }
532
+
533
+ if (typeof next !== 'string') {
534
+ next = String(next ?? '');
535
+ }
536
+
537
+ el.textContent = next;
538
+ });
539
+
540
+ this._effects!.push(stop);
541
+ }
542
+
475
543
  applyStyles(el: HTMLElement): void {
476
544
  if (this.config.style && typeof this.config.style === 'object') {
477
545
  Object.assign(el.style, this.config.style);
@@ -8,6 +8,7 @@ export interface EzCheckboxConfig extends EzBaseComponentConfig {
8
8
  size?: 'sm' | 'lg';
9
9
  disabled?: boolean;
10
10
  value?: boolean;
11
+ checked?: boolean;
11
12
  name?: string;
12
13
  formData?: string;
13
14
  label?: string;
@@ -33,7 +34,7 @@ export class EzCheckbox extends EzBaseComponent {
33
34
  const input = document.createElement('input');
34
35
  input.type = 'checkbox';
35
36
  input.className = cls('input');
36
- input.checked = !!cfg.value;
37
+ input.checked = !!(cfg.checked ?? cfg.value);
37
38
 
38
39
  if (cfg.disabled) input.disabled = true;
39
40
  if (cfg.name) input.name = cfg.name;
@@ -0,0 +1,168 @@
1
+ import styles from './EzInput.module.scss';
2
+ import { cx } from '../../utils/cssModules.js';
3
+ import { EzBaseComponent, EzBaseComponentConfig } from '../EzBaseComponent.js';
4
+
5
+ const cls = cx(styles);
6
+
7
+ declare const ez: {
8
+ _createElement(config: unknown): Promise<HTMLElement>;
9
+ };
10
+
11
+ export interface EzInputConfig extends EzBaseComponentConfig {
12
+ size?: 'sm' | 'lg';
13
+ variant?: 'filled' | 'underline';
14
+ block?: boolean;
15
+ disabled?: boolean;
16
+ readonly?: boolean;
17
+ icon?: string;
18
+ inputType?: string;
19
+ placeholder?: string;
20
+ value?: string | number;
21
+ name?: string;
22
+ formData?: string;
23
+ onKeydown?: ((e: KeyboardEvent) => void) | string;
24
+ onKeyup?: ((e: KeyboardEvent) => void) | string;
25
+ onEnter?: ((value: string) => void) | string;
26
+ }
27
+
28
+ export class EzInput extends EzBaseComponent {
29
+ declare config: EzInputConfig;
30
+ declare el: HTMLDivElement;
31
+
32
+ private _input: HTMLInputElement | null = null;
33
+ private _error: HTMLDivElement | null = null;
34
+
35
+ async render(): Promise<HTMLDivElement> {
36
+ const cfg = this.config;
37
+
38
+ // Build input config
39
+ const inputConfig: Record<string, unknown> = {
40
+ eztype: 'input',
41
+ cls: cls('input'),
42
+ type: cfg.inputType || 'text',
43
+ placeholder: cfg.placeholder || '',
44
+ disabled: cfg.disabled,
45
+ readonly: cfg.readonly,
46
+ value: cfg.value !== undefined ? String(cfg.value) : undefined,
47
+ style: cfg.style
48
+ };
49
+
50
+ // Handle form binding
51
+ if (cfg.name && cfg.formData) {
52
+ this.config.bind = `${cfg.formData}.${cfg.name}`;
53
+ }
54
+
55
+ if (cfg.name) {
56
+ inputConfig['data-ez-field'] = cfg.name;
57
+ }
58
+
59
+ // Handle keyboard events
60
+ if (cfg.onKeydown && typeof cfg.onKeydown === 'function') {
61
+ inputConfig.onKeyDown = cfg.onKeydown;
62
+ }
63
+
64
+ if (cfg.onKeyup && typeof cfg.onKeyup === 'function') {
65
+ inputConfig.onKeyUp = cfg.onKeyup;
66
+ }
67
+
68
+ // Build items array
69
+ const rowItems: unknown[] = [];
70
+
71
+ if (cfg.icon) {
72
+ rowItems.push({
73
+ eztype: 'i',
74
+ cls: [cls('icon'), `fa fa-${cfg.icon}`].join(' ')
75
+ });
76
+ }
77
+
78
+ rowItems.push(inputConfig);
79
+
80
+ // Create wrapper using ez._createElement
81
+ const wrapper = await ez._createElement({
82
+ eztype: 'div',
83
+ cls: this._buildClasses(),
84
+ items: [
85
+ {
86
+ eztype: 'div',
87
+ cls: cls('fieldError')
88
+ },
89
+ {
90
+ eztype: 'div',
91
+ cls: cls('inputRow'),
92
+ items: rowItems
93
+ }
94
+ ]
95
+ }) as HTMLDivElement;
96
+
97
+ // Get references
98
+ this._error = wrapper.querySelector(`.${cls('fieldError')}`) as HTMLDivElement;
99
+ this._input = wrapper.querySelector('input') as HTMLInputElement;
100
+
101
+ // Apply bindings and onChange handler
102
+ if (this._input) {
103
+ this.applyCommonBindings(this._input);
104
+
105
+ const onChange = this._createOnChangeHandler();
106
+ if (onChange) {
107
+ this._input.addEventListener('input', e => {
108
+ onChange((e.target as HTMLInputElement).value);
109
+ });
110
+ }
111
+
112
+ // Handle onEnter
113
+ if (cfg.onEnter && typeof cfg.onEnter === 'function') {
114
+ const onEnterFn = cfg.onEnter;
115
+ this._input.addEventListener('keydown', e => {
116
+ if (e.key === 'Enter') {
117
+ onEnterFn(this._input!.value);
118
+ }
119
+ });
120
+ }
121
+ }
122
+
123
+ this.el = wrapper;
124
+ return wrapper;
125
+ }
126
+
127
+ private _buildClasses(): string {
128
+ const cfg = this.config;
129
+ const classes: string[] = [cls('inputGroup')];
130
+
131
+ if (cfg.size) classes.push(cls(cfg.size));
132
+ if (cfg.variant) classes.push(cls(cfg.variant));
133
+ if (cfg.block) classes.push(cls('block'));
134
+ if (cfg.disabled) classes.push(cls('disabled'));
135
+
136
+ return classes.join(' ');
137
+ }
138
+
139
+ showError(message: string): void {
140
+ if (this.el && this._error) {
141
+ this.el.classList.add(cls('hasError'));
142
+ this._error.classList.add(cls('fieldErrorVisible'));
143
+ this._error.textContent = message;
144
+ }
145
+ }
146
+
147
+ clearError(): void {
148
+ if (this.el && this._error) {
149
+ this.el.classList.remove(cls('hasError'));
150
+ this._error.classList.remove(cls('fieldErrorVisible'));
151
+ this._error.textContent = '';
152
+ }
153
+ }
154
+
155
+ getValue(): string {
156
+ return this._input?.value || '';
157
+ }
158
+
159
+ setValue(value: string): void {
160
+ if (this._input) {
161
+ this._input.value = value;
162
+ }
163
+ }
164
+
165
+ focus(): void {
166
+ this._input?.focus();
167
+ }
168
+ }
package/core/ez.ts CHANGED
@@ -346,6 +346,10 @@ const ez: EzFramework = {
346
346
  });
347
347
  },
348
348
 
349
+ getStylesSync(name: string): Record<string, string> | null {
350
+ return this._stylesCache[name] || null;
351
+ },
352
+
349
353
  async resolveStyles(name: string): Promise<Record<string, string> | null> {
350
354
  if (this._stylesCache[name]) {
351
355
  return this._stylesCache[name];
package/core/loader.ts CHANGED
@@ -273,6 +273,11 @@ export class EzLoader {
273
273
  }
274
274
 
275
275
  this.ez.registerComponent(eztype, exported);
276
+
277
+ // Pre-resolve CSS module if it exists (will be cached for sync access later)
278
+ if (this.ez.hasStyles(eztype)) {
279
+ await this.ez.resolveStyles(eztype);
280
+ }
276
281
  }
277
282
 
278
283
  async resolveController(name: string | object | null): Promise<unknown> {
package/core/renderer.ts CHANGED
@@ -634,10 +634,12 @@ export class EzRenderer {
634
634
 
635
635
  let merged: ComponentConfig = { ...Registered as ComponentConfig, ...rest };
636
636
 
637
+ // Auto-detect CSS module if component has one
637
638
  if (!merged.css && this.ez.hasStyles(eztype)) {
638
639
  merged.css = eztype;
639
640
  }
640
641
 
642
+ // Load style module if css is set but module not loaded yet
641
643
  if (merged.css && !merged._styleModule) {
642
644
  merged._styleModule = (await this.ez.resolveStyles(merged.css)) || undefined;
643
645
  }
@@ -663,8 +665,12 @@ export class EzRenderer {
663
665
  if (typeof merged.template === 'function' && !targetIsClassComponent) {
664
666
  const inheritedCss = merged.css;
665
667
  const inheritedStyleModule = merged._styleModule;
668
+ const inheritedCls = merged.cls;
669
+ const inheritedLayout = merged.layout;
670
+ const inheritedEztype = merged.eztype;
671
+ const inheritedStyle = merged.style;
666
672
 
667
- const tpl = merged.template(config.props || {}, this.ez._controllers[activeController], controllerState);
673
+ const tpl = merged.template(merged, this.ez._controllers[activeController], controllerState);
668
674
  if (!tpl || typeof tpl !== 'object') {
669
675
  throw new EzError({
670
676
  code: 'EZ_TEMPLATE_001',
@@ -681,6 +687,18 @@ export class EzRenderer {
681
687
  merged.css = inheritedCss;
682
688
  merged._styleModule = inheritedStyleModule;
683
689
  }
690
+ if (inheritedCls && !merged.cls) {
691
+ merged.cls = inheritedCls;
692
+ }
693
+ if (inheritedLayout && !merged.layout) {
694
+ merged.layout = inheritedLayout;
695
+ }
696
+ if (inheritedEztype && !merged.eztype) {
697
+ merged.eztype = inheritedEztype;
698
+ }
699
+ if (inheritedStyle && !merged.style) {
700
+ merged.style = inheritedStyle;
701
+ }
684
702
  }
685
703
 
686
704
  el = await this.createElement(merged, activeController, controllerState);
@@ -766,7 +784,10 @@ export class EzRenderer {
766
784
  localItem._ezSourcePath = parentSource || undefined;
767
785
  }
768
786
 
769
- if (parentCss && !localItem.css) {
787
+ // Only inherit CSS if child doesn't have its own CSS module
788
+ const childHasOwnCss = localItem.css ||
789
+ (localItem.eztype && this.ez.hasStyles(localItem.eztype));
790
+ if (parentCss && !childHasOwnCss) {
770
791
  localItem.css = parentCss;
771
792
  }
772
793
 
package/core/state.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { deepSignal } from 'deepsignal';
2
+ import { computed, type ReadonlySignal } from '@preact/signals';
2
3
  import { EzError } from './EzError.js';
3
4
 
4
5
  export interface ControllerDefinition {
5
6
  state?: Record<string, unknown>;
7
+ computed?: Record<string, () => unknown>;
8
+ _computed?: Record<string, ReadonlySignal<unknown>>;
6
9
  getState?: (key?: string) => unknown;
10
+ getComputed?: (key: string) => unknown;
7
11
  [key: string]: unknown;
8
12
  }
9
13
 
@@ -48,6 +52,25 @@ export class EzState {
48
52
  return (definition.state as Record<string, unknown>)[key];
49
53
  };
50
54
 
55
+ // Process computed properties
56
+ if (definition.computed) {
57
+ definition._computed = {};
58
+ for (const [key, fn] of Object.entries(definition.computed)) {
59
+ const computedSignal = computed(() => fn.call(definition));
60
+ definition._computed[key] = computedSignal;
61
+
62
+ // Make computed accessible directly on controller (e.g., ctrl.filteredTodos)
63
+ Object.defineProperty(definition, key, {
64
+ get: () => computedSignal.value,
65
+ enumerable: true
66
+ });
67
+ }
68
+
69
+ definition.getComputed = (key: string) => {
70
+ return definition._computed?.[key]?.value;
71
+ };
72
+ }
73
+
51
74
  this.ez._controllers[name] = definition;
52
75
  }
53
76
 
package/modules.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  export const frameworkModules: Record<string, () => Promise<unknown>> = {
5
5
  // Core components
6
6
  './components/EzComponent.ts': () => import('./components/EzComponent.js'),
7
- './components/EzInput.ts': () => import('./components/EzInput.js'),
7
+ './components/EzInput.ts': () => import('./components/input/EzInput.js'),
8
8
  './components/EzLabel.ts': () => import('./components/EzLabel.js'),
9
9
  './components/EzOutlet.ts': () => import('./components/EzOutlet.js'),
10
10
  './components/EzBaseComponent.ts': () => import('./components/EzBaseComponent.js'),
@@ -82,7 +82,7 @@ export const frameworkModules: Record<string, () => Promise<unknown>> = {
82
82
  };
83
83
 
84
84
  export const frameworkStyles: Record<string, () => Promise<unknown>> = {
85
- './components/EzInput.module.scss': () => import('./components/EzInput.module.scss'),
85
+ './components/EzInput.module.scss': () => import('./components/input/EzInput.module.scss'),
86
86
  './components/button/EzButton.module.scss': () => import('./components/button/EzButton.module.scss'),
87
87
  './components/card/EzCard.module.scss': () => import('./components/card/EzCard.module.scss'),
88
88
  './components/avatar/EzAvatar.module.scss': () => import('./components/avatar/EzAvatar.module.scss'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezfw-core",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Ez Framework - A declarative component framework for building modern web applications",
5
5
  "type": "module",
6
6
  "main": "./core/ez.ts",
@@ -1,132 +0,0 @@
1
- import styles from './EzInput.module.scss';
2
- import { cx } from '../utils/cssModules.js';
3
- import { EzBaseComponent, EzBaseComponentConfig } from './EzBaseComponent.js';
4
-
5
- const cls = cx(styles);
6
-
7
- export interface EzInputConfig extends EzBaseComponentConfig {
8
- size?: 'sm' | 'lg';
9
- variant?: 'filled' | 'underline';
10
- block?: boolean;
11
- disabled?: boolean;
12
- readonly?: boolean;
13
- icon?: string;
14
- inputType?: string;
15
- placeholder?: string;
16
- value?: string | number;
17
- name?: string;
18
- formData?: string;
19
- onKeydown?: (e: KeyboardEvent, ctrl: unknown) => void;
20
- onKeyup?: (e: KeyboardEvent, ctrl: unknown) => void;
21
- onEnter?: (value: string, ctrl: unknown) => void;
22
- }
23
-
24
- export class EzInput extends EzBaseComponent {
25
- declare config: EzInputConfig;
26
-
27
- private _wrapper: HTMLDivElement | null = null;
28
- private _error: HTMLDivElement | null = null;
29
-
30
- render(): HTMLDivElement {
31
- const cfg = this.config;
32
-
33
- const wrapper = document.createElement('div');
34
- wrapper.className = cls(
35
- 'inputGroup',
36
- cfg.size,
37
- cfg.variant,
38
- cfg.block && 'block',
39
- cfg.disabled && 'disabled'
40
- );
41
-
42
- const error = document.createElement('div');
43
- error.className = cls('fieldError');
44
- wrapper.appendChild(error);
45
-
46
- const row = document.createElement('div');
47
- row.className = cls('inputRow');
48
-
49
- if (cfg.icon) {
50
- const icon = document.createElement('i');
51
- icon.className = cls('icon', `fa fa-${cfg.icon}`);
52
- row.appendChild(icon);
53
- }
54
-
55
- const input = document.createElement('input');
56
- input.className = cls('input');
57
- input.type = cfg.inputType || 'text';
58
- input.placeholder = cfg.placeholder || '';
59
- if (cfg.value !== undefined) input.value = String(cfg.value);
60
-
61
- if (cfg.name && cfg.formData) {
62
- this.config.bind = `${cfg.formData}.${cfg.name}`;
63
- }
64
-
65
- if (cfg.name) {
66
- input.setAttribute('data-ez-field', cfg.name);
67
- }
68
-
69
- if (cfg.disabled) input.disabled = true;
70
- if (cfg.readonly) input.readOnly = true;
71
-
72
- this.applyCommonBindings(input);
73
- this.applyStyles(input);
74
-
75
- const onChange = this._createOnChangeHandler();
76
-
77
- input.addEventListener('input', e => {
78
- if (onChange) {
79
- onChange((e.target as HTMLInputElement).value);
80
- }
81
- });
82
-
83
- const getController = () => {
84
- const ctrlName = this.config.controller;
85
- return ctrlName ? ez.getControllerSync(ctrlName) : null;
86
- };
87
-
88
- if (cfg.onKeydown) {
89
- input.addEventListener('keydown', e => {
90
- cfg.onKeydown!(e, getController());
91
- });
92
- }
93
-
94
- if (cfg.onKeyup) {
95
- input.addEventListener('keyup', e => {
96
- cfg.onKeyup!(e, getController());
97
- });
98
- }
99
-
100
- if (cfg.onEnter) {
101
- input.addEventListener('keydown', e => {
102
- if (e.key === 'Enter') {
103
- cfg.onEnter!(input.value, getController());
104
- }
105
- });
106
- }
107
-
108
- row.appendChild(input);
109
- wrapper.appendChild(row);
110
-
111
- this._wrapper = wrapper;
112
- this._error = error;
113
-
114
- return wrapper;
115
- }
116
-
117
- showError(message: string): void {
118
- if (this._wrapper && this._error) {
119
- this._wrapper.classList.add(cls('hasError'));
120
- this._error.classList.add(cls('fieldErrorVisible'));
121
- this._error.textContent = message;
122
- }
123
- }
124
-
125
- clearError(): void {
126
- if (this._wrapper && this._error) {
127
- this._wrapper.classList.remove(cls('hasError'));
128
- this._error.classList.remove(cls('fieldErrorVisible'));
129
- this._error.textContent = '';
130
- }
131
- }
132
- }