@radix-ng/primitives 1.0.10 → 1.1.1
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.
- package/README.md +26 -5
- package/fesm2022/radix-ng-primitives-autocomplete.mjs +88 -21
- package/fesm2022/radix-ng-primitives-autocomplete.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-checkbox.mjs +199 -129
- package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-combobox.mjs +203 -52
- package/fesm2022/radix-ng-primitives-combobox.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-composite.mjs +221 -36
- package/fesm2022/radix-ng-primitives-composite.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-core.mjs +352 -24
- package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-date-field.mjs +11 -5
- package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-field.mjs +127 -23
- package/fesm2022/radix-ng-primitives-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-form.mjs +35 -4
- package/fesm2022/radix-ng-primitives-form.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-input.mjs +87 -15
- package/fesm2022/radix-ng-primitives-input.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-menu.mjs +34 -7
- package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-number-field.mjs +38 -7
- package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-popper.mjs +80 -11
- package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-radio.mjs +49 -2
- package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-scroll-area.mjs +6 -3
- package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-select.mjs +70 -14
- package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-signal-forms.mjs +27 -11
- package/fesm2022/radix-ng-primitives-signal-forms.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-slider.mjs +17 -2
- package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-switch.mjs +60 -7
- package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-time-field.mjs +12 -6
- package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
- package/fesm2022/radix-ng-primitives-toggle-group.mjs +25 -3
- package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
- package/package.json +8 -3
- package/types/radix-ng-primitives-autocomplete.d.ts +16 -2
- package/types/radix-ng-primitives-checkbox.d.ts +66 -44
- package/types/radix-ng-primitives-combobox.d.ts +74 -3
- package/types/radix-ng-primitives-composite.d.ts +48 -2
- package/types/radix-ng-primitives-core.d.ts +212 -96
- package/types/radix-ng-primitives-date-field.d.ts +7 -4
- package/types/radix-ng-primitives-field.d.ts +55 -8
- package/types/radix-ng-primitives-form.d.ts +9 -0
- package/types/radix-ng-primitives-input.d.ts +10 -2
- package/types/radix-ng-primitives-menu.d.ts +45 -4
- package/types/radix-ng-primitives-navigation-menu.d.ts +2 -2
- package/types/radix-ng-primitives-number-field.d.ts +12 -5
- package/types/radix-ng-primitives-popover.d.ts +2 -2
- package/types/radix-ng-primitives-popper.d.ts +60 -15
- package/types/radix-ng-primitives-preview-card.d.ts +2 -2
- package/types/radix-ng-primitives-radio.d.ts +6 -1
- package/types/radix-ng-primitives-select.d.ts +25 -4
- package/types/radix-ng-primitives-signal-forms.d.ts +22 -8
- package/types/radix-ng-primitives-slider.d.ts +2 -1
- package/types/radix-ng-primitives-switch.d.ts +19 -3
- package/types/radix-ng-primitives-time-field.d.ts +9 -6
- package/types/radix-ng-primitives-toggle-group.d.ts +5 -1
- package/types/radix-ng-primitives-tooltip.d.ts +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { forwardRef, input, booleanAttribute, output, linkedSignal, untracked, Directive, inject, isDevMode, HOST_TAG_NAME, ElementRef, InjectionToken,
|
|
3
|
-
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
2
|
+
import { forwardRef, input, booleanAttribute, output, linkedSignal, untracked, Directive, inject, Injector, DestroyRef, signal, computed, isDevMode, HOST_TAG_NAME, ElementRef, InjectionToken, APP_ID, Injectable, DOCUMENT, PLATFORM_ID, model, effect, afterNextRender, assertInInjectionContext } from '@angular/core';
|
|
3
|
+
import { NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
|
|
4
4
|
import { getLocalTimeZone, CalendarDateTime, ZonedDateTime, getDayOfWeek, DateFormatter, createCalendar, toCalendar, CalendarDate, Time, startOfMonth, endOfMonth, today } from '@internationalized/date';
|
|
5
5
|
import { isPlatformBrowser, DOCUMENT as DOCUMENT$1 } from '@angular/common';
|
|
6
6
|
|
|
@@ -113,6 +113,103 @@ function injectControlValueAccessor() {
|
|
|
113
113
|
return inject(RdxControlValueAccessor);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function toValidationErrors(errors) {
|
|
117
|
+
if (!errors) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return Object.entries(errors).map(([kind, details]) => {
|
|
121
|
+
const message = typeof details === 'string'
|
|
122
|
+
? details
|
|
123
|
+
: details && typeof details === 'object' && typeof details['message'] === 'string'
|
|
124
|
+
? details['message']
|
|
125
|
+
: undefined;
|
|
126
|
+
return message === undefined ? { kind } : { kind, message };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Lazily connects to the same-host `NgControl` without creating a circular dependency while Angular
|
|
131
|
+
* is resolving its `ControlValueAccessor`. The unified `AbstractControl.events` stream mirrors
|
|
132
|
+
* programmatic value, status, validation, and interaction-state changes as well as user-driven ones.
|
|
133
|
+
*
|
|
134
|
+
* Must be called in an injection context.
|
|
135
|
+
*
|
|
136
|
+
* @ignore
|
|
137
|
+
*/
|
|
138
|
+
function injectNgControlState() {
|
|
139
|
+
const injector = inject(Injector);
|
|
140
|
+
const destroyRef = inject(DestroyRef);
|
|
141
|
+
const connected = signal(false, { debugName: 'RdxNgControlState.connected' });
|
|
142
|
+
const name = signal(undefined, { debugName: 'RdxNgControlState.name' });
|
|
143
|
+
const value = signal(undefined, { debugName: 'RdxNgControlState.value' });
|
|
144
|
+
const valid = signal(false, { debugName: 'RdxNgControlState.valid' });
|
|
145
|
+
const invalid = signal(false, { debugName: 'RdxNgControlState.invalid' });
|
|
146
|
+
const pending = signal(false, { debugName: 'RdxNgControlState.pending' });
|
|
147
|
+
const disabled = signal(false, { debugName: 'RdxNgControlState.disabled' });
|
|
148
|
+
const rawErrors = signal(null, { debugName: 'RdxNgControlState.rawErrors' });
|
|
149
|
+
const errors = computed(() => toValidationErrors(rawErrors()), {
|
|
150
|
+
debugName: 'RdxNgControlState.errors'
|
|
151
|
+
});
|
|
152
|
+
const dirty = signal(false, { debugName: 'RdxNgControlState.dirty' });
|
|
153
|
+
const touched = signal(false, { debugName: 'RdxNgControlState.touched' });
|
|
154
|
+
let destroyed = false;
|
|
155
|
+
let unsubscribe;
|
|
156
|
+
let resetControl;
|
|
157
|
+
const connect = () => {
|
|
158
|
+
if (destroyed) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const ngControl = injector.get(NgControl, null, { self: true, optional: true });
|
|
162
|
+
const control = ngControl?.control;
|
|
163
|
+
// Signal Forms' `FormField` is also discoverable through Angular's forms DI integration, but
|
|
164
|
+
// it is not an `AbstractControl` and owns state through signals instead of `control.events`.
|
|
165
|
+
if (!control || typeof control.events?.subscribe !== 'function') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
resetControl = (nextValue) => control.reset(nextValue);
|
|
169
|
+
const sync = () => {
|
|
170
|
+
name.set(ngControl.name == null ? undefined : String(ngControl.name));
|
|
171
|
+
value.set(control.value);
|
|
172
|
+
valid.set(control.valid);
|
|
173
|
+
invalid.set(control.invalid);
|
|
174
|
+
pending.set(control.pending);
|
|
175
|
+
disabled.set(control.disabled);
|
|
176
|
+
rawErrors.set(control.errors);
|
|
177
|
+
dirty.set(control.dirty);
|
|
178
|
+
touched.set(control.touched);
|
|
179
|
+
};
|
|
180
|
+
sync();
|
|
181
|
+
const subscription = control.events.subscribe(sync);
|
|
182
|
+
unsubscribe = () => subscription.unsubscribe();
|
|
183
|
+
connected.set(true);
|
|
184
|
+
};
|
|
185
|
+
// `NgControl` resolves its value accessor from this same element. Defer the lookup until every
|
|
186
|
+
// directive on the host has finished constructing to avoid a DI cycle.
|
|
187
|
+
queueMicrotask(connect);
|
|
188
|
+
destroyRef.onDestroy(() => {
|
|
189
|
+
destroyed = true;
|
|
190
|
+
unsubscribe?.();
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
connected: connected.asReadonly(),
|
|
194
|
+
name: name.asReadonly(),
|
|
195
|
+
value: value.asReadonly(),
|
|
196
|
+
valid: valid.asReadonly(),
|
|
197
|
+
invalid: invalid.asReadonly(),
|
|
198
|
+
pending: pending.asReadonly(),
|
|
199
|
+
disabled: disabled.asReadonly(),
|
|
200
|
+
errors,
|
|
201
|
+
dirty: dirty.asReadonly(),
|
|
202
|
+
touched: touched.asReadonly(),
|
|
203
|
+
reset: (nextValue) => {
|
|
204
|
+
if (!resetControl) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
resetControl(nextValue);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
116
213
|
/**
|
|
117
214
|
* The `clamp` function restricts a number within a specified range by returning the value itself if it
|
|
118
215
|
* falls within the range, or the closest boundary value if it exceeds the range.
|
|
@@ -1405,7 +1502,7 @@ function useDateField(props) {
|
|
|
1405
1502
|
return prevValue;
|
|
1406
1503
|
const str = prevValue.toString();
|
|
1407
1504
|
if (str.length === 1) {
|
|
1408
|
-
props.modelValue.set(
|
|
1505
|
+
props.modelValue.set(null);
|
|
1409
1506
|
return null;
|
|
1410
1507
|
}
|
|
1411
1508
|
return Number.parseInt(str.slice(0, -1));
|
|
@@ -1899,11 +1996,30 @@ function itemToStringLabel(value) {
|
|
|
1899
1996
|
return String(value);
|
|
1900
1997
|
}
|
|
1901
1998
|
/**
|
|
1902
|
-
* Converts an item value to the string used for form serialization.
|
|
1903
|
-
*
|
|
1999
|
+
* Converts an item value to the string used for form serialization. A conventional `{ value, label }`
|
|
2000
|
+
* item serializes its `value` member; other non-string values use Base UI-compatible JSON serialization
|
|
2001
|
+
* with `String()` as a fallback. Kept separate so a primitive can diverge label vs. value.
|
|
1904
2002
|
*/
|
|
1905
2003
|
function itemToStringValue(value) {
|
|
1906
|
-
|
|
2004
|
+
if (value !== null && typeof value === 'object' && 'value' in value && 'label' in value) {
|
|
2005
|
+
return serializeValue(value.value);
|
|
2006
|
+
}
|
|
2007
|
+
return serializeValue(value);
|
|
2008
|
+
}
|
|
2009
|
+
/** Base UI-compatible serialization fallback for native form values. */
|
|
2010
|
+
function serializeValue(value) {
|
|
2011
|
+
if (isNullish(value)) {
|
|
2012
|
+
return '';
|
|
2013
|
+
}
|
|
2014
|
+
if (typeof value === 'string') {
|
|
2015
|
+
return value;
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
return JSON.stringify(value) ?? String(value);
|
|
2019
|
+
}
|
|
2020
|
+
catch {
|
|
2021
|
+
return String(value);
|
|
2022
|
+
}
|
|
1907
2023
|
}
|
|
1908
2024
|
/**
|
|
1909
2025
|
* Compares two item values for equality using an optional {@link ItemValueComparator}.
|
|
@@ -2035,8 +2151,8 @@ function provideExistingToken(token, type) {
|
|
|
2035
2151
|
|
|
2036
2152
|
/**
|
|
2037
2153
|
* Builds the shared form-UI state and its mutators from a control's input signals, removing the
|
|
2038
|
-
* per-control copy-paste of the
|
|
2039
|
-
*
|
|
2154
|
+
* per-control copy-paste of the validation/interaction computeds and the `markAsTouched`/`markDirty`
|
|
2155
|
+
* logic.
|
|
2040
2156
|
*
|
|
2041
2157
|
* **Why the inputs stay on the control (not in here):** Angular's compiler only discovers
|
|
2042
2158
|
* `input()`/`model()` declared as field initializers, and Signal Forms binds form-written state
|
|
@@ -2049,22 +2165,58 @@ function provideExistingToken(token, type) {
|
|
|
2049
2165
|
function createFormUiState(options) {
|
|
2050
2166
|
const dirtyValue = signal(false, /* @ts-ignore */
|
|
2051
2167
|
...(ngDevMode ? [{ debugName: "dirtyValue" }] : /* istanbul ignore next */ []));
|
|
2168
|
+
const errorsState = computed(() => {
|
|
2169
|
+
const ownErrors = options.errors() ?? [];
|
|
2170
|
+
const ngControlErrors = options.ngControlState?.connected() ? options.ngControlState.errors() : [];
|
|
2171
|
+
return ngControlErrors.length > 0 ? [...ownErrors, ...ngControlErrors] : ownErrors;
|
|
2172
|
+
}, /* @ts-ignore */
|
|
2173
|
+
...(ngDevMode ? [{ debugName: "errorsState" }] : /* istanbul ignore next */ []));
|
|
2174
|
+
const invalidState = computed(() => options.invalid() ||
|
|
2175
|
+
errorsState().length > 0 ||
|
|
2176
|
+
Boolean(options.ngControlState?.connected() && options.ngControlState.invalid()), /* @ts-ignore */
|
|
2177
|
+
...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
|
|
2178
|
+
const pendingState = computed(() => (options.pending?.() ?? false) ||
|
|
2179
|
+
Boolean(options.ngControlState?.connected() && options.ngControlState.pending()), /* @ts-ignore */
|
|
2180
|
+
...(ngDevMode ? [{ debugName: "pendingState" }] : /* istanbul ignore next */ []));
|
|
2181
|
+
const validState = computed(() => {
|
|
2182
|
+
if (pendingState()) {
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
if (invalidState()) {
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
if (options.ngControlState?.connected() && options.ngControlState.disabled()) {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
return true;
|
|
2192
|
+
}, /* @ts-ignore */
|
|
2193
|
+
...(ngDevMode ? [{ debugName: "validState" }] : /* istanbul ignore next */ []));
|
|
2052
2194
|
return {
|
|
2053
|
-
invalidState
|
|
2054
|
-
|
|
2055
|
-
|
|
2195
|
+
invalidState,
|
|
2196
|
+
pendingState,
|
|
2197
|
+
validState,
|
|
2198
|
+
errorsState,
|
|
2199
|
+
touchedState: computed(() => options.ngControlState?.connected() ? options.ngControlState.touched() : options.touched()),
|
|
2200
|
+
dirtyState: computed(() => options.ngControlState?.connected() ? options.ngControlState.dirty() : options.dirty() || dirtyValue()),
|
|
2056
2201
|
markDirty: () => dirtyValue.set(true),
|
|
2057
2202
|
markAsTouched: () => {
|
|
2058
2203
|
options.cva?.markAsTouched();
|
|
2059
2204
|
options.touched.set(true);
|
|
2060
2205
|
options.touch.emit();
|
|
2061
|
-
}
|
|
2206
|
+
},
|
|
2207
|
+
resetInteractionState: () => {
|
|
2208
|
+
options.touched.set(false);
|
|
2209
|
+
dirtyValue.set(false);
|
|
2210
|
+
},
|
|
2211
|
+
resetDirtyState: () => dirtyValue.set(false)
|
|
2062
2212
|
};
|
|
2063
2213
|
}
|
|
2064
2214
|
/** Spread helper to wire {@link RdxFormUiStateContext} fields from an {@link RdxFormUiState} into a context factory. */
|
|
2065
2215
|
function formUiStateContext(state) {
|
|
2066
2216
|
return {
|
|
2067
2217
|
invalidState: state.invalidState,
|
|
2218
|
+
pendingState: state.pendingState,
|
|
2219
|
+
validState: state.validState,
|
|
2068
2220
|
touchedState: state.touchedState,
|
|
2069
2221
|
dirtyState: state.dirtyState,
|
|
2070
2222
|
markAsTouched: () => state.markAsTouched()
|
|
@@ -2084,15 +2236,21 @@ const RDX_FIELD_VALIDITY = new InjectionToken('RdxFieldValidity');
|
|
|
2084
2236
|
/**
|
|
2085
2237
|
* Tri-state display validity: when inside a `Field` the field's gated `validState` is the **single
|
|
2086
2238
|
* source** (the control reflects it, including its `validationMode` neutral state); standalone, the
|
|
2087
|
-
* control's own
|
|
2239
|
+
* control's own tri-state validity (pending is neutral).
|
|
2088
2240
|
*
|
|
2089
2241
|
* **Contract:** inside a `Field` a control's own `invalid` / `errors` inputs are **not** displayed — the
|
|
2090
2242
|
* Field owns displayed validity. Drive validity through the Field instead: bind `rdxSignalField`
|
|
2091
2243
|
* (Signal Forms), or set `[invalid]` on `rdxFieldRoot`. The control's own `invalid`/`errors` are for
|
|
2092
2244
|
* standalone use.
|
|
2093
2245
|
*/
|
|
2094
|
-
function resolveDisplayValid(fieldValidity, ownInvalid) {
|
|
2095
|
-
|
|
2246
|
+
function resolveDisplayValid(fieldValidity, ownInvalid, ownPending) {
|
|
2247
|
+
if (fieldValidity) {
|
|
2248
|
+
return fieldValidity();
|
|
2249
|
+
}
|
|
2250
|
+
if (ownPending?.()) {
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
return ownInvalid() ? false : true;
|
|
2096
2254
|
}
|
|
2097
2255
|
/**
|
|
2098
2256
|
* Provide a control's {@link RdxFormUiState} on its host element so {@link RdxFormUiStateHost} can
|
|
@@ -2129,7 +2287,7 @@ class RdxFormUiStateHost {
|
|
|
2129
2287
|
this.formUi = inject(RDX_FORM_UI_STATE);
|
|
2130
2288
|
this.fieldValidity = inject(RDX_FIELD_VALIDITY, { optional: true });
|
|
2131
2289
|
/** @ignore Tri-state display validity (enclosing Field's gated state, else own invalidity). */
|
|
2132
|
-
this.displayValid = computed(() =>
|
|
2290
|
+
this.displayValid = computed(() => this.fieldValidity ? this.fieldValidity() : this.formUi.validState(), /* @ts-ignore */
|
|
2133
2291
|
...(ngDevMode ? [{ debugName: "displayValid" }] : /* istanbul ignore next */ []));
|
|
2134
2292
|
}
|
|
2135
2293
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxFormUiStateHost, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -2152,7 +2310,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
|
|
|
2152
2310
|
}] });
|
|
2153
2311
|
/**
|
|
2154
2312
|
* Abstract base that declares the optional `FormUiControl` state inputs
|
|
2155
|
-
* (`invalid`/`errors`/`touched`/`dirty` + the `touch` output) once and builds the control's
|
|
2313
|
+
* (`invalid`/`pending`/`errors`/`touched`/`dirty` + the `touch` output) once and builds the control's
|
|
2156
2314
|
* {@link RdxFormUiState} from them, so a control directive can inherit the whole surface with a
|
|
2157
2315
|
* single `extends` instead of re-declaring it.
|
|
2158
2316
|
*
|
|
@@ -2170,6 +2328,8 @@ class RdxFormUiControlBase {
|
|
|
2170
2328
|
constructor() {
|
|
2171
2329
|
/** Whether the control is invalid. A non-empty {@link errors} list also marks it invalid. */
|
|
2172
2330
|
this.invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2331
|
+
/** Whether async validation is pending. Pending controls publish neither `data-valid` nor `data-invalid`. */
|
|
2332
|
+
this.pending = input(false, { ...(ngDevMode ? { debugName: "pending" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
2173
2333
|
/** Whether the control has been touched. A `model()` so Signal Forms can write it; set on blur/focus-out. */
|
|
2174
2334
|
this.touched = model(false, /* @ts-ignore */
|
|
2175
2335
|
...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
@@ -2180,15 +2340,20 @@ class RdxFormUiControlBase {
|
|
|
2180
2340
|
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
2181
2341
|
/** Emits when the control is touched, notifying Signal Forms (stable Angular 22 contract). */
|
|
2182
2342
|
this.touch = output();
|
|
2343
|
+
this.ngControlState = injectNgControlState();
|
|
2183
2344
|
/** The shared form-UI state derived from the inputs above. Call `formUi.markDirty()` on value change. */
|
|
2184
2345
|
this.formUi = createFormUiState({
|
|
2185
2346
|
invalid: this.invalid,
|
|
2347
|
+
pending: this.pending,
|
|
2186
2348
|
errors: this.errors,
|
|
2187
2349
|
touched: this.touched,
|
|
2188
2350
|
touch: this.touch,
|
|
2189
2351
|
dirty: this.dirty,
|
|
2352
|
+
ngControlState: this.ngControlState,
|
|
2190
2353
|
cva: this.formUiTouchTarget()
|
|
2191
2354
|
});
|
|
2355
|
+
/** Validation errors from the control inputs and a same-host Reactive/template-driven form control. */
|
|
2356
|
+
this.validationErrors = this.formUi.errorsState;
|
|
2192
2357
|
/** The enclosing Field's tri-state display validity, if any. `protected` so a control whose own
|
|
2193
2358
|
* invalidity is richer than `formUi.invalidState` (e.g. date/time-field add a parse check) can build
|
|
2194
2359
|
* its own `displayValid` from it. */
|
|
@@ -2196,27 +2361,58 @@ class RdxFormUiControlBase {
|
|
|
2196
2361
|
/**
|
|
2197
2362
|
* Tri-state *displayed* validity for controls that bind their own host attributes (radio, switch,
|
|
2198
2363
|
* number-field): the enclosing Field's gated state when inside a `rdxFieldRoot`, else this control's
|
|
2199
|
-
* own
|
|
2364
|
+
* own validity (`formUi.pendingState` is neutral; otherwise `formUi.invalidState` is binary). Bind
|
|
2365
|
+
* `data-valid`/`data-invalid`/`aria-invalid` to
|
|
2200
2366
|
* this so a neutral field shows neither. Controls whose `invalidState` is richer than
|
|
2201
|
-
* `formUi.invalidState` override this with
|
|
2367
|
+
* `formUi.invalidState` override this with
|
|
2368
|
+
* `resolveDisplayValid(this.fieldValidity, this.invalidState, this.formUi.pendingState)`.
|
|
2202
2369
|
*/
|
|
2203
|
-
this.displayValid = computed(() =>
|
|
2370
|
+
this.displayValid = computed(() => this.fieldValidity ? this.fieldValidity() : this.formUi.validState(), /* @ts-ignore */
|
|
2204
2371
|
...(ngDevMode ? [{ debugName: "displayValid" }] : /* istanbul ignore next */ []));
|
|
2372
|
+
// Reactive/template-driven forms own their NgControl interaction state. Signal Forms instead
|
|
2373
|
+
// writes the public touched/dirty members, so standalone controls retain the existing fallback.
|
|
2374
|
+
effect(() => {
|
|
2375
|
+
if (this.ngControlState.connected()) {
|
|
2376
|
+
this.touched.set(this.ngControlState.touched());
|
|
2377
|
+
if (!this.ngControlState.dirty()) {
|
|
2378
|
+
this.formUi.resetDirtyState?.();
|
|
2379
|
+
}
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (!this.dirty()) {
|
|
2383
|
+
this.formUi.resetDirtyState?.();
|
|
2384
|
+
}
|
|
2385
|
+
}, { debugName: 'RdxFormUiControlBase.syncInteractionState' });
|
|
2205
2386
|
}
|
|
2206
2387
|
/**
|
|
2207
2388
|
* Override to bridge the control's `ControlValueAccessor` into `markAsTouched` (dual controls —
|
|
2208
2389
|
* return `injectControlValueAccessor()` or a `{ markAsTouched }` adapter). Default: no CVA
|
|
2209
|
-
* (Signal-Forms-only controls such as
|
|
2390
|
+
* (Signal-Forms-only controls such as date-field).
|
|
2210
2391
|
*/
|
|
2211
2392
|
formUiTouchTarget() {
|
|
2212
2393
|
return null;
|
|
2213
2394
|
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Resets a same-host Reactive/template-driven control without reporting the reset as a user edit.
|
|
2397
|
+
* Returns `false` when no Angular `NgControl` is connected, so a control can fall back to its CVA
|
|
2398
|
+
* change callback for an unusually early native reset.
|
|
2399
|
+
*/
|
|
2400
|
+
resetNgControl(value) {
|
|
2401
|
+
return this.ngControlState.reset(value);
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Reset control-owned interaction state. Angular Signal Forms calls this optional custom-control
|
|
2405
|
+
* hook from `FieldState.reset()` after restoring the field value.
|
|
2406
|
+
*/
|
|
2407
|
+
reset() {
|
|
2408
|
+
this.formUi.resetInteractionState?.();
|
|
2409
|
+
}
|
|
2214
2410
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxFormUiControlBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2215
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxFormUiControlBase, isStandalone: true, inputs: { invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", touch: "touch" }, ngImport: i0 }); }
|
|
2411
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: RdxFormUiControlBase, isStandalone: true, inputs: { invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, pending: { classPropertyName: "pending", publicName: "pending", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", touch: "touch" }, ngImport: i0 }); }
|
|
2216
2412
|
}
|
|
2217
2413
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RdxFormUiControlBase, decorators: [{
|
|
2218
2414
|
type: Directive
|
|
2219
|
-
}], propDecorators: { invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
|
|
2415
|
+
}], ctorParameters: () => [], propDecorators: { invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], pending: [{ type: i0.Input, args: [{ isSignal: true, alias: "pending", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
|
|
2220
2416
|
|
|
2221
2417
|
/** The default validation-display mode when neither `rdxFieldRoot` nor `rdxFormRoot` sets one. */
|
|
2222
2418
|
const RDX_DEFAULT_VALIDATION_MODE = 'onBlur';
|
|
@@ -3207,6 +3403,138 @@ function getActiveElement(root = document) {
|
|
|
3207
3403
|
return activeElement;
|
|
3208
3404
|
}
|
|
3209
3405
|
|
|
3406
|
+
/**
|
|
3407
|
+
* Converts a scalar or array model to HTML form values. `null` and `undefined` represent no
|
|
3408
|
+
* successful control; arrays deliberately become repeated entries under the same field name.
|
|
3409
|
+
*/
|
|
3410
|
+
function serializeNativeFormValue(value, itemToStringValue$1 = itemToStringValue) {
|
|
3411
|
+
const values = Array.isArray(value) ? value : [value];
|
|
3412
|
+
return values.flatMap((entry) => (entry == null ? [] : [itemToStringValue$1(entry)]));
|
|
3413
|
+
}
|
|
3414
|
+
const NOOP_NATIVE_FORM_CONTROL = {
|
|
3415
|
+
ownerForm: () => null,
|
|
3416
|
+
sync: () => undefined,
|
|
3417
|
+
requestSubmit: () => undefined
|
|
3418
|
+
};
|
|
3419
|
+
/**
|
|
3420
|
+
* Adds native form semantics to a composite control, rendering hidden inputs only when the control
|
|
3421
|
+
* has no native successful control of its own.
|
|
3422
|
+
*
|
|
3423
|
+
* A hidden input is necessary for a headless root to enter the platform's successful-controls tree
|
|
3424
|
+
* (including older engines that do not dispatch `formdata` for `new FormData(form)`). One input is
|
|
3425
|
+
* created per serialized value, preserving repeated-field semantics; controls with their own native
|
|
3426
|
+
* input use this solely for reset and imperative-submit coordination.
|
|
3427
|
+
*
|
|
3428
|
+
* Generated inputs are browser-only. Rendering imperative sibling nodes on the server would put DOM
|
|
3429
|
+
* outside Angular's hydration graph; the client creates them after its view has been claimed instead.
|
|
3430
|
+
*/
|
|
3431
|
+
function useNativeFormControl(options) {
|
|
3432
|
+
const host = inject(ElementRef).nativeElement;
|
|
3433
|
+
const ownerDocument = host.ownerDocument;
|
|
3434
|
+
const destroyRef = inject(DestroyRef);
|
|
3435
|
+
if (!ownerDocument || !isPlatformBrowser(inject(PLATFORM_ID))) {
|
|
3436
|
+
return NOOP_NATIVE_FORM_CONTROL;
|
|
3437
|
+
}
|
|
3438
|
+
const inputs = [];
|
|
3439
|
+
let defaultValue;
|
|
3440
|
+
let defaultValues = [];
|
|
3441
|
+
let hasDefaultValue = false;
|
|
3442
|
+
let alive = true;
|
|
3443
|
+
let viewClaimed = false;
|
|
3444
|
+
const ownerForm = () => {
|
|
3445
|
+
const formId = options.form();
|
|
3446
|
+
const candidate = formId
|
|
3447
|
+
? ownerDocument.getElementById(formId)
|
|
3448
|
+
: typeof host.closest === 'function'
|
|
3449
|
+
? host.closest('form')
|
|
3450
|
+
: host.parentElement?.closest('form');
|
|
3451
|
+
return candidate?.localName === 'form' ? candidate : null;
|
|
3452
|
+
};
|
|
3453
|
+
const sync = () => {
|
|
3454
|
+
options.syncNativeControl?.();
|
|
3455
|
+
const name = options.name();
|
|
3456
|
+
const form = options.form();
|
|
3457
|
+
const disabled = options.disabled();
|
|
3458
|
+
const nativeControlOwnsValue = options.hasNativeControl?.() ?? false;
|
|
3459
|
+
const values = options.value && options.serialize ? options.serialize(options.value()) : [];
|
|
3460
|
+
// During hydration Angular must claim every server-rendered sibling before an imperative input
|
|
3461
|
+
// can be inserted. The after-render callback below performs the first DOM synchronization.
|
|
3462
|
+
if (!viewClaimed) {
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
const shouldRender = Boolean(name && options.serialize && !nativeControlOwnsValue);
|
|
3466
|
+
const count = shouldRender ? values.length : 0;
|
|
3467
|
+
while (inputs.length > count) {
|
|
3468
|
+
inputs.pop()?.remove();
|
|
3469
|
+
}
|
|
3470
|
+
while (inputs.length < count) {
|
|
3471
|
+
const input = ownerDocument.createElement('input');
|
|
3472
|
+
input.type = 'hidden';
|
|
3473
|
+
const previous = inputs.at(-1);
|
|
3474
|
+
host.parentNode?.insertBefore(input, previous ? previous.nextSibling : host.nextSibling);
|
|
3475
|
+
inputs.push(input);
|
|
3476
|
+
}
|
|
3477
|
+
for (let index = 0; index < inputs.length; index++) {
|
|
3478
|
+
const input = inputs[index];
|
|
3479
|
+
input.name = name;
|
|
3480
|
+
input.value = values[index];
|
|
3481
|
+
input.disabled = disabled;
|
|
3482
|
+
if (form) {
|
|
3483
|
+
input.setAttribute('form', form);
|
|
3484
|
+
}
|
|
3485
|
+
else {
|
|
3486
|
+
input.removeAttribute('form');
|
|
3487
|
+
}
|
|
3488
|
+
// Native reset reads `defaultValue`; retain the original default rather than accidentally
|
|
3489
|
+
// promoting later user edits to the reset baseline.
|
|
3490
|
+
if (!input.hasAttribute('data-rdx-native-form-control')) {
|
|
3491
|
+
input.defaultValue = defaultValues[index] ?? values[index];
|
|
3492
|
+
input.setAttribute('data-rdx-native-form-control', '');
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
effect(() => {
|
|
3497
|
+
if (!hasDefaultValue && options.defaultValue) {
|
|
3498
|
+
defaultValue = options.defaultValue();
|
|
3499
|
+
defaultValues = options.serialize ? options.serialize(defaultValue) : [];
|
|
3500
|
+
hasDefaultValue = true;
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
effect(sync);
|
|
3504
|
+
afterNextRender(() => {
|
|
3505
|
+
viewClaimed = true;
|
|
3506
|
+
untracked(sync);
|
|
3507
|
+
});
|
|
3508
|
+
const onReset = (event) => {
|
|
3509
|
+
if (event.target !== ownerForm() || !options.onReset || !hasDefaultValue) {
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
// Native controls reset after the event. Check `defaultPrevented` in the microtask so a later
|
|
3513
|
+
// bubble listener can still cancel the reset before the model follows it.
|
|
3514
|
+
queueMicrotask(() => {
|
|
3515
|
+
if (alive && !event.defaultPrevented) {
|
|
3516
|
+
untracked(() => options.onReset(defaultValue));
|
|
3517
|
+
}
|
|
3518
|
+
});
|
|
3519
|
+
};
|
|
3520
|
+
ownerDocument.addEventListener('reset', onReset, true);
|
|
3521
|
+
destroyRef.onDestroy(() => {
|
|
3522
|
+
alive = false;
|
|
3523
|
+
ownerDocument.removeEventListener('reset', onReset, true);
|
|
3524
|
+
for (const input of inputs) {
|
|
3525
|
+
input.remove();
|
|
3526
|
+
}
|
|
3527
|
+
});
|
|
3528
|
+
return {
|
|
3529
|
+
ownerForm,
|
|
3530
|
+
sync,
|
|
3531
|
+
requestSubmit: () => {
|
|
3532
|
+
untracked(sync);
|
|
3533
|
+
ownerForm()?.requestSubmit();
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3210
3538
|
/**
|
|
3211
3539
|
* Creates a resize observer effect for element
|
|
3212
3540
|
*
|
|
@@ -4284,5 +4612,5 @@ var RdxPositionAlign;
|
|
|
4284
4612
|
* Generated bundle index. Do not edit.
|
|
4285
4613
|
*/
|
|
4286
4614
|
|
|
4287
|
-
export { A, ALT, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ASTERISK, BACKSPACE, CAPS_LOCK, CONTROL, CTRL, DELETE, DOCS_BASE_URL, END, ENTER, ESCAPE, F1, F10, F11, F12, F2, F3, F4, F5, F6, F7, F8, F9, HOME, META, P, PAGE_DOWN, PAGE_UP, RDX_DEFAULT_VALIDATION_MODE, RDX_FIELD_VALIDITY, RDX_FLOATING_REGISTRATION, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_TREE, RDX_FORM_UI_STATE, RDX_INTERNAL_BACKDROP_ATTR, RDX_SCROLL_LOCKED_ATTR, RdxControlValueAccessor, RdxFloatingNode, RdxFloatingNodeRegistration, RdxFloatingRegistrationContext, RdxFloatingRootContext, RdxFloatingTree, RdxFormUiControlBase, RdxFormUiStateHost, RdxIdGenerator, RdxLiveAnnouncer, RdxPositionAlign, RdxPositionSide, RdxTriggerRegistry, SHIFT, SPACE, SPACE_CODE, TAB, TIME_GRANULARITIES, a, areAllDaysBetweenValid, clamp, createCancelableChangeEventDetails, createContent, createContext, createFloatingEvents, createFloatingRootContext, createFormUiState, createFormatter, createMonth, createMonths, docsUrl, elementSize, formUiStateContext, getActiveElement, getDaysBetween, getDaysInMonth, getDefaultDate, getDefaultTime, getLastFirstDayOfWeek, getMaxTransitionDuration, getNextLastDayOfWeek, getOptsByGranularity, getPlaceholder, getSegmentElements, getWeekNumber, handleAndDispatchCustomEvent, handleCalendarInitialFocus, hasTime, initializeSegmentValues, injectControlValueAccessor, injectDocument, injectFloatingRootContext, injectId, isAcceptableSegmentKey, isAfter, isAfterOrSame, isBefore, isBeforeOrSame, isBetween, isBetweenInclusive, isCalendarDateTime, isEqual, isItemEqualToValue, isNullish, isNumberString, isSegmentNavigationKey, isValidationRevealed, isZonedDateTime, itemToStringLabel, itemToStringValue, j, k, n, normalizeDateStep, normalizeHour12, normalizeHourCycle, p, provideExistingToken, provideFloatingRegistration, provideFloatingRootContext, provideFloatingTree, provideFormUiState, provideValueAccessor, rdxCheckLabelElement, rdxCheckTriggerElement, rdxDevError, rdxDevWarning, resetRdxDevWarnings, resizeEffect, resolveDisplayValid, resolveFloatingTree, roundToStepPrecision, segmentBuilders, setupInternalBackdrop, snapValueToStep, syncSegmentValues, syncTimeSegmentValues, toDate, useAnchoredScrollLock, useArrowNavigation, useDateField, useFilter, useGraceArea, useListHighlight, usePointerDrag, useScrollLock, useTransitionStatus, watch };
|
|
4615
|
+
export { A, ALT, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, ARROW_UP, ASTERISK, BACKSPACE, CAPS_LOCK, CONTROL, CTRL, DELETE, DOCS_BASE_URL, END, ENTER, ESCAPE, F1, F10, F11, F12, F2, F3, F4, F5, F6, F7, F8, F9, HOME, META, P, PAGE_DOWN, PAGE_UP, RDX_DEFAULT_VALIDATION_MODE, RDX_FIELD_VALIDITY, RDX_FLOATING_REGISTRATION, RDX_FLOATING_ROOT_CONTEXT, RDX_FLOATING_TREE, RDX_FORM_UI_STATE, RDX_INTERNAL_BACKDROP_ATTR, RDX_SCROLL_LOCKED_ATTR, RdxControlValueAccessor, RdxFloatingNode, RdxFloatingNodeRegistration, RdxFloatingRegistrationContext, RdxFloatingRootContext, RdxFloatingTree, RdxFormUiControlBase, RdxFormUiStateHost, RdxIdGenerator, RdxLiveAnnouncer, RdxPositionAlign, RdxPositionSide, RdxTriggerRegistry, SHIFT, SPACE, SPACE_CODE, TAB, TIME_GRANULARITIES, a, areAllDaysBetweenValid, clamp, createCancelableChangeEventDetails, createContent, createContext, createFloatingEvents, createFloatingRootContext, createFormUiState, createFormatter, createMonth, createMonths, docsUrl, elementSize, formUiStateContext, getActiveElement, getDaysBetween, getDaysInMonth, getDefaultDate, getDefaultTime, getLastFirstDayOfWeek, getMaxTransitionDuration, getNextLastDayOfWeek, getOptsByGranularity, getPlaceholder, getSegmentElements, getWeekNumber, handleAndDispatchCustomEvent, handleCalendarInitialFocus, hasTime, initializeSegmentValues, injectControlValueAccessor, injectDocument, injectFloatingRootContext, injectId, injectNgControlState, isAcceptableSegmentKey, isAfter, isAfterOrSame, isBefore, isBeforeOrSame, isBetween, isBetweenInclusive, isCalendarDateTime, isEqual, isItemEqualToValue, isNullish, isNumberString, isSegmentNavigationKey, isValidationRevealed, isZonedDateTime, itemToStringLabel, itemToStringValue, j, k, n, normalizeDateStep, normalizeHour12, normalizeHourCycle, p, provideExistingToken, provideFloatingRegistration, provideFloatingRootContext, provideFloatingTree, provideFormUiState, provideValueAccessor, rdxCheckLabelElement, rdxCheckTriggerElement, rdxDevError, rdxDevWarning, resetRdxDevWarnings, resizeEffect, resolveDisplayValid, resolveFloatingTree, roundToStepPrecision, segmentBuilders, serializeNativeFormValue, setupInternalBackdrop, snapValueToStep, syncSegmentValues, syncTimeSegmentValues, toDate, useAnchoredScrollLock, useArrowNavigation, useDateField, useFilter, useGraceArea, useListHighlight, useNativeFormControl, usePointerDrag, useScrollLock, useTransitionStatus, watch };
|
|
4288
4616
|
//# sourceMappingURL=radix-ng-primitives-core.mjs.map
|