@tachui/forms 0.8.15 → 0.8.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.
@@ -47,6 +47,7 @@ export const TextField: Component<TextFieldProps> = props => {
47
47
  autocomplete,
48
48
  spellcheck = true,
49
49
  disabled = false,
50
+ readOnly = false,
50
51
  required = false,
51
52
  validation,
52
53
  value: controlledValue,
@@ -74,6 +75,7 @@ export const TextField: Component<TextFieldProps> = props => {
74
75
  text: textSignal,
75
76
  placeholderSignal,
76
77
  disabledSignal,
78
+ readOnlySignal,
77
79
 
78
80
  ...restProps
79
81
  } = props
@@ -96,6 +98,17 @@ export const TextField: Component<TextFieldProps> = props => {
96
98
  const [currentText, setCurrentText] = createSignal('')
97
99
  const [currentPlaceholder, setCurrentPlaceholder] = createSignal('')
98
100
  const [currentDisabled, setCurrentDisabled] = createSignal(false)
101
+ const [currentReadOnly, setCurrentReadOnly] = createSignal(false)
102
+
103
+ if (
104
+ disabledSignal !== undefined &&
105
+ typeof process !== 'undefined' &&
106
+ process.env.NODE_ENV === 'development'
107
+ ) {
108
+ console.warn(
109
+ 'TextField: `disabledSignal` is deprecated. Prefer `disabled` with a Signal<boolean>.'
110
+ )
111
+ }
99
112
 
100
113
  // Set up reactive updates for signal-based props
101
114
  createEffect(() => {
@@ -120,6 +133,12 @@ export const TextField: Component<TextFieldProps> = props => {
120
133
  }
121
134
  })
122
135
 
136
+ createEffect(() => {
137
+ if (readOnlySignal) {
138
+ setCurrentReadOnly(resolveValue(readOnlySignal, false))
139
+ }
140
+ })
141
+
123
142
  // Sync with controlled value
124
143
  if (controlledValue !== undefined) {
125
144
  createEffect(() => {
@@ -241,7 +260,16 @@ export const TextField: Component<TextFieldProps> = props => {
241
260
  const currentPlaceholderValue = placeholderSignal
242
261
  ? currentPlaceholder()
243
262
  : placeholder
244
- const isDisabled = disabledSignal ? currentDisabled() : disabled
263
+ const disabledBinding = disabledSignal
264
+ ? currentDisabled
265
+ : typeof disabled === 'function'
266
+ ? (disabled as () => boolean)
267
+ : () => disabled
268
+ const readOnlyBinding = readOnlySignal
269
+ ? currentReadOnly
270
+ : typeof readOnly === 'function'
271
+ ? (readOnly as () => boolean)
272
+ : () => readOnly
245
273
  const displayValue = textSignal ? currentText() : field.value() || ''
246
274
  const formattedDisplayValue = formatValue(displayValue)
247
275
 
@@ -251,7 +279,8 @@ export const TextField: Component<TextFieldProps> = props => {
251
279
  name,
252
280
  value: formattedDisplayValue,
253
281
  placeholder: currentPlaceholderValue,
254
- disabled: isDisabled,
282
+ disabled: disabledBinding,
283
+ readOnly: readOnlyBinding,
255
284
  required,
256
285
  minlength: minLength,
257
286
  maxlength: maxLength,
@@ -1,4 +1,4 @@
1
- import { isSignal } from '@tachui/core'
1
+ import { createEffect, isComputed, isSignal } from '@tachui/core'
2
2
  import type { Signal } from '@tachui/core'
3
3
  import type {
4
4
  Modifier,
@@ -22,6 +22,7 @@ function resolvePlaceholder(value: PlaceholderValue): string {
22
22
  }
23
23
 
24
24
  const placeholderHandlers = new WeakMap<Element, () => void>()
25
+ const placeholderDisposers = new WeakMap<Element, () => void>()
25
26
 
26
27
  function applyPlaceholder(
27
28
  element: Element,
@@ -59,10 +60,21 @@ function createPlaceholderModifier(value: PlaceholderValue): Modifier {
59
60
  element.removeEventListener('input', previous)
60
61
  }
61
62
 
63
+ const previousDispose = placeholderDisposers.get(element)
64
+ if (previousDispose) {
65
+ previousDispose()
66
+ placeholderDisposers.delete(element)
67
+ }
68
+
62
69
  const handler = () => applyPlaceholder(element, value)
63
70
  element.addEventListener('input', handler)
64
71
  placeholderHandlers.set(element, handler)
65
72
 
73
+ if (isSignal(value) || isComputed(value)) {
74
+ const effect = createEffect(update)
75
+ placeholderDisposers.set(element, () => effect.dispose())
76
+ }
77
+
66
78
  return node
67
79
  },
68
80
  }
@@ -1,16 +1,31 @@
1
1
  import type { Modifier, ModifierContext } from '@tachui/core/modifiers/types'
2
2
  import type { ModifierRegistry, PluginInfo } from '@tachui/registry'
3
3
  import { registerModifierWithMetadata } from '@tachui/core/modifiers'
4
+ import { createEffect, isComputed, isSignal, type Signal } from '@tachui/core'
4
5
 
5
6
  const requiredPriority = 72
6
7
 
7
8
  interface RequiredModifierOptions {
9
+ message?: string | Signal<string>
10
+ enabled?: boolean | Signal<boolean>
11
+ }
12
+
13
+ type RequiredInput = boolean | string | RequiredModifierOptions
14
+ interface ResolvedRequiredOptions {
8
15
  message?: string
9
16
  enabled?: boolean
10
17
  }
18
+ const requiredDisposers = new WeakMap<Element, () => void>()
19
+
20
+ function resolveReactive<T>(value: T | Signal<T>): T {
21
+ if (isSignal(value) || isComputed(value)) {
22
+ return (value as Signal<T>)()
23
+ }
24
+ return value as T
25
+ }
11
26
 
12
27
  function normalizeOptions(
13
- value?: boolean | string | RequiredModifierOptions,
28
+ value?: RequiredInput,
14
29
  ): RequiredModifierOptions {
15
30
  if (typeof value === 'boolean') {
16
31
  return { enabled: value }
@@ -27,9 +42,36 @@ function normalizeOptions(
27
42
  return normalized
28
43
  }
29
44
 
45
+ function resolveOptions(
46
+ value?: RequiredInput,
47
+ ): ResolvedRequiredOptions {
48
+ const normalized = normalizeOptions(value)
49
+ return {
50
+ enabled:
51
+ normalized.enabled === undefined
52
+ ? undefined
53
+ : resolveReactive(normalized.enabled),
54
+ message:
55
+ normalized.message === undefined
56
+ ? undefined
57
+ : resolveReactive(normalized.message),
58
+ }
59
+ }
60
+
61
+ function hasReactiveOptions(value?: RequiredInput): boolean {
62
+ if (isSignal(value) || isComputed(value)) return true
63
+ if (!value || typeof value !== 'object') return false
64
+ return (
65
+ isSignal(value.enabled) ||
66
+ isComputed(value.enabled) ||
67
+ isSignal(value.message) ||
68
+ isComputed(value.message)
69
+ )
70
+ }
71
+
30
72
  function applyRequired(
31
73
  element: Element,
32
- { enabled, message }: RequiredModifierOptions,
74
+ { enabled, message }: ResolvedRequiredOptions,
33
75
  ): void {
34
76
  if (!(element instanceof HTMLElement)) return
35
77
 
@@ -57,18 +99,30 @@ function applyRequired(
57
99
  }
58
100
 
59
101
  function createRequiredModifier(
60
- options?: boolean | string | RequiredModifierOptions,
102
+ options?: RequiredInput,
61
103
  ): Modifier {
62
- const normalized = normalizeOptions(options)
63
104
  return {
64
105
  type: 'forms:required',
65
106
  priority: requiredPriority,
66
- properties: normalized,
107
+ properties: normalizeOptions(options),
67
108
  apply(node: any, context: ModifierContext) {
68
109
  const element = (context.element ?? node) as Element | undefined
69
110
  if (!element) return node
70
111
 
71
- applyRequired(element, normalized)
112
+ const applyCurrent = () => applyRequired(element, resolveOptions(options))
113
+
114
+ const previousDispose = requiredDisposers.get(element)
115
+ if (previousDispose) {
116
+ previousDispose()
117
+ requiredDisposers.delete(element)
118
+ }
119
+
120
+ applyCurrent()
121
+
122
+ if (hasReactiveOptions(options)) {
123
+ const effect = createEffect(applyCurrent)
124
+ requiredDisposers.set(element, () => effect.dispose())
125
+ }
72
126
  return node
73
127
  },
74
128
  }
@@ -83,7 +137,7 @@ const REQUIRED_METADATA = {
83
137
  }
84
138
 
85
139
  export function required(
86
- options?: boolean | string | RequiredModifierOptions,
140
+ options?: RequiredInput,
87
141
  ): Modifier {
88
142
  return createRequiredModifier(options)
89
143
  }
@@ -92,7 +146,7 @@ export function registerRequiredModifier(
92
146
  registry?: ModifierRegistry,
93
147
  plugin?: PluginInfo,
94
148
  ): void {
95
- const factory = (options?: boolean | string | RequiredModifierOptions) =>
149
+ const factory = (options?: RequiredInput) =>
96
150
  createRequiredModifier(options)
97
151
 
98
152
  registerModifierWithMetadata(
@@ -1,6 +1,7 @@
1
1
  import type { Modifier, ModifierContext } from '@tachui/core/modifiers/types'
2
2
  import type { ModifierRegistry, PluginInfo } from '@tachui/registry'
3
3
  import { registerModifierWithMetadata } from '@tachui/core/modifiers'
4
+ import { createEffect, isComputed, isSignal, type Signal } from '@tachui/core'
4
5
  import { validateValue } from '../validation'
5
6
  import type { ValidationResult, ValidationRule } from '../types'
6
7
 
@@ -9,25 +10,40 @@ const validationPriority = 74
9
10
  type ValidationArgs =
10
11
  | ValidationRule[]
11
12
  | ValidationRule
13
+ | Signal<ValidationRule[]>
14
+ | Signal<ValidationRule>
15
+ | Signal<ValidationRule[] | ValidationRule>
12
16
 
13
17
  interface ValidationProperties {
14
18
  rules: ValidationRule[]
15
19
  }
16
20
 
17
21
  const validationHandlers = new WeakMap<Element, (event?: Event) => void>()
22
+ const validationDisposers = new WeakMap<Element, () => void>()
23
+
24
+ function isReactiveRuleInput(
25
+ value: ValidationArgs
26
+ ): value is Signal<ValidationRule[] | ValidationRule> {
27
+ return isSignal(value) || isComputed(value)
28
+ }
18
29
 
19
30
  function normalizeRules(input: ValidationArgs[]): ValidationRule[] {
20
31
  const flattened: ValidationRule[] = []
21
32
  input.forEach(entry => {
22
- if (Array.isArray(entry)) {
23
- flattened.push(...entry)
33
+ const resolvedEntry = isReactiveRuleInput(entry) ? entry() : entry
34
+ if (Array.isArray(resolvedEntry)) {
35
+ flattened.push(...resolvedEntry)
24
36
  } else {
25
- flattened.push(entry)
37
+ flattened.push(resolvedEntry)
26
38
  }
27
39
  })
28
40
  return flattened
29
41
  }
30
42
 
43
+ function hasReactiveRuleInput(input: ValidationArgs[]): boolean {
44
+ return input.some(entry => isReactiveRuleInput(entry))
45
+ }
46
+
31
47
  function applyValidationResult(
32
48
  element: Element,
33
49
  result: ValidationResult,
@@ -73,27 +89,42 @@ function createValidationHandler(
73
89
  }
74
90
 
75
91
  function createValidationModifier(
76
- rules: ValidationRule[],
92
+ rulesInput: ValidationArgs[],
77
93
  ): Modifier {
78
- const normalizedRules = rules.length
79
- ? rules
80
- : (['required'] as ValidationRule[])
81
-
82
94
  return {
83
95
  type: 'forms:validation',
84
96
  priority: validationPriority,
85
- properties: { rules: normalizedRules } satisfies ValidationProperties,
97
+ properties: {
98
+ get rules() {
99
+ const resolved = normalizeRules(rulesInput)
100
+ return resolved.length ? resolved : (['required'] as ValidationRule[])
101
+ },
102
+ } as ValidationProperties,
86
103
  apply(node: any, context: ModifierContext) {
87
104
  const element = (context.element ?? node) as Element | undefined
88
105
  if (!element) return node
89
106
 
90
- const handler = createValidationHandler(element, normalizedRules)
107
+ const buildRules = (): ValidationRule[] => {
108
+ const resolved = normalizeRules(rulesInput)
109
+ return resolved.length ? resolved : (['required'] as ValidationRule[])
110
+ }
111
+
112
+ const handler = () => {
113
+ const currentRules = buildRules()
114
+ const run = createValidationHandler(element, currentRules)
115
+ run()
116
+ }
91
117
 
92
118
  const existing = validationHandlers.get(element)
93
119
  if (existing) {
94
120
  element.removeEventListener('blur', existing)
95
121
  element.removeEventListener('input', existing)
96
122
  }
123
+ const existingDispose = validationDisposers.get(element)
124
+ if (existingDispose) {
125
+ existingDispose()
126
+ validationDisposers.delete(element)
127
+ }
97
128
 
98
129
  element.addEventListener('blur', handler)
99
130
  element.addEventListener('input', handler)
@@ -101,6 +132,11 @@ function createValidationModifier(
101
132
 
102
133
  handler()
103
134
 
135
+ if (hasReactiveRuleInput(rulesInput)) {
136
+ const effect = createEffect(handler)
137
+ validationDisposers.set(element, () => effect.dispose())
138
+ }
139
+
104
140
  return node
105
141
  },
106
142
  }
@@ -117,8 +153,7 @@ const VALIDATION_METADATA = {
117
153
  export function validation(
118
154
  ...rules: ValidationArgs[]
119
155
  ): Modifier {
120
- const normalized = normalizeRules(rules)
121
- return createValidationModifier(normalized)
156
+ return createValidationModifier(rules)
122
157
  }
123
158
 
124
159
  export function registerValidationModifier(
@@ -126,7 +161,7 @@ export function registerValidationModifier(
126
161
  plugin?: PluginInfo,
127
162
  ): void {
128
163
  const factory = (...rules: ValidationArgs[]) =>
129
- createValidationModifier(normalizeRules(rules))
164
+ createValidationModifier(rules)
130
165
 
131
166
  registerModifierWithMetadata(
132
167
  'validation',
@@ -9,6 +9,7 @@ import type {
9
9
  ComponentChildren,
10
10
  ComponentInstance,
11
11
  ComponentProps,
12
+ Signal,
12
13
  } from '@tachui/core'
13
14
 
14
15
  /**
@@ -125,7 +126,8 @@ export interface BaseFieldProps extends ComponentProps {
125
126
  name: string
126
127
  label?: string
127
128
  placeholder?: string
128
- disabled?: boolean
129
+ disabled?: boolean | Signal<boolean>
130
+ readOnly?: boolean | Signal<boolean>
129
131
  required?: boolean
130
132
  validation?: FieldValidation
131
133
  value?: any
@@ -241,6 +243,7 @@ export interface TextFieldProps extends BaseFieldProps {
241
243
  text?: string | (() => string) // Signal support
242
244
  placeholderSignal?: string | (() => string) // Signal support
243
245
  disabledSignal?: boolean | (() => boolean) // Signal support
246
+ readOnlySignal?: boolean | (() => boolean) // Signal support
244
247
  }
245
248
 
246
249
  /**