@angular/forms 21.2.0-next.1 → 21.2.0-next.2

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.
@@ -1,12 +1,13 @@
1
1
  /**
2
- * @license Angular v21.2.0-next.1
2
+ * @license Angular v21.2.0-next.2
3
3
  * (c) 2010-2026 Google LLC. https://angular.dev/
4
4
  * License: MIT
5
5
  */
6
6
 
7
- import { FieldNode, getInjectorFromOptions, FieldNodeState, FieldNodeStructure, calculateValidationSelfStatus, BasicFieldAdapter, form, normalizeFormArgs } from './_structure-chunk.mjs';
8
- import { linkedSignal, untracked, runInInjectionContext, computed, signal, ɵRuntimeError as _RuntimeError } from '@angular/core';
9
- import { FormGroup, FormArray, AbstractControl } from '@angular/forms';
7
+ import { FieldNode, getInjectorFromOptions, FieldNodeState, FieldNodeStructure, calculateValidationSelfStatus, extractNestedReactiveErrors, BasicFieldAdapter, form, normalizeFormArgs, signalErrorsToValidationErrors } from './_structure-chunk.mjs';
8
+ export { CompatValidationError } from './_structure-chunk.mjs';
9
+ import { linkedSignal, untracked, runInInjectionContext, computed, signal, ɵRuntimeError as _RuntimeError, EventEmitter, inject, Injector, effect } from '@angular/core';
10
+ import { AbstractControl, ValueChangeEvent, StatusChangeEvent, TouchedChangeEvent, PristineChangeEvent, FormResetEvent } from '@angular/forms';
10
11
  import { toSignal } from '@angular/core/rxjs-interop';
11
12
  import { ReplaySubject } from 'rxjs';
12
13
  import { map, takeUntil } from 'rxjs/operators';
@@ -152,48 +153,6 @@ class CompatStructure extends FieldNodeStructure {
152
153
  }
153
154
  }
154
155
 
155
- class CompatValidationError {
156
- kind = 'compat';
157
- control;
158
- fieldTree;
159
- context;
160
- message;
161
- constructor({
162
- context,
163
- kind,
164
- control
165
- }) {
166
- this.context = context;
167
- this.kind = kind;
168
- this.control = control;
169
- }
170
- }
171
-
172
- function reactiveErrorsToSignalErrors(errors, control) {
173
- if (errors === null) {
174
- return [];
175
- }
176
- return Object.entries(errors).map(([kind, context]) => {
177
- return new CompatValidationError({
178
- context,
179
- kind,
180
- control
181
- });
182
- });
183
- }
184
- function extractNestedReactiveErrors(control) {
185
- const errors = [];
186
- if (control.errors) {
187
- errors.push(...reactiveErrorsToSignalErrors(control.errors, control));
188
- }
189
- if (control instanceof FormGroup || control instanceof FormArray) {
190
- for (const c of Object.values(control.controls)) {
191
- errors.push(...extractNestedReactiveErrors(c));
192
- }
193
- }
194
- return errors;
195
- }
196
-
197
156
  const EMPTY_ARRAY_SIGNAL = computed(() => [], ...(ngDevMode ? [{
198
157
  debugName: "EMPTY_ARRAY_SIGNAL"
199
158
  }] : []));
@@ -318,5 +277,385 @@ const NG_STATUS_CLASSES = {
318
277
  }) => state().pending()
319
278
  };
320
279
 
321
- export { CompatValidationError, NG_STATUS_CLASSES, compatForm };
280
+ class SignalFormControl extends AbstractControl {
281
+ fieldTree;
282
+ sourceValue;
283
+ fieldState;
284
+ initialValue;
285
+ pendingParentNotifications = 0;
286
+ onChangeCallbacks = [];
287
+ onDisabledChangeCallbacks = [];
288
+ valueChanges = new EventEmitter();
289
+ statusChanges = new EventEmitter();
290
+ constructor(value, schemaOrOptions, options) {
291
+ super(null, null);
292
+ const [model, schema, opts] = normalizeFormArgs([signal(value), schemaOrOptions, options]);
293
+ this.sourceValue = model;
294
+ this.initialValue = value;
295
+ const injector = opts?.injector ?? inject(Injector);
296
+ const rawTree = schema ? compatForm(this.sourceValue, schema, {
297
+ injector
298
+ }) : compatForm(this.sourceValue, {
299
+ injector
300
+ });
301
+ this.fieldTree = wrapFieldTreeForSyncUpdates(rawTree, () => this.parent?.updateValueAndValidity({
302
+ sourceControl: this
303
+ }));
304
+ this.fieldState = this.fieldTree();
305
+ this.defineCompatProperties();
306
+ effect(() => {
307
+ const value = this.sourceValue();
308
+ untracked(() => {
309
+ this.notifyParentUnlessPending();
310
+ this.valueChanges.emit(value);
311
+ this.emitControlEvent(new ValueChangeEvent(value, this));
312
+ });
313
+ }, {
314
+ injector
315
+ });
316
+ effect(() => {
317
+ const status = this.status;
318
+ untracked(() => {
319
+ this.statusChanges.emit(status);
320
+ });
321
+ this.emitControlEvent(new StatusChangeEvent(status, this));
322
+ }, {
323
+ injector
324
+ });
325
+ effect(() => {
326
+ const isDisabled = this.disabled;
327
+ untracked(() => {
328
+ for (const fn of this.onDisabledChangeCallbacks) {
329
+ fn(isDisabled);
330
+ }
331
+ });
332
+ }, {
333
+ injector
334
+ });
335
+ effect(() => {
336
+ const isTouched = this.fieldState.touched();
337
+ this.emitControlEvent(new TouchedChangeEvent(isTouched, this));
338
+ const parent = this.parent;
339
+ if (!parent) {
340
+ return;
341
+ }
342
+ if (!isTouched) {
343
+ parent.markAsUntouched();
344
+ } else {
345
+ parent.markAsTouched();
346
+ }
347
+ }, {
348
+ injector
349
+ });
350
+ effect(() => {
351
+ const isDirty = this.fieldState.dirty();
352
+ this.emitControlEvent(new PristineChangeEvent(!isDirty, this));
353
+ const parent = this.parent;
354
+ if (!parent) {
355
+ return;
356
+ }
357
+ if (isDirty) {
358
+ parent.markAsDirty();
359
+ } else {
360
+ parent.markAsPristine();
361
+ }
362
+ }, {
363
+ injector
364
+ });
365
+ }
366
+ defineCompatProperties() {
367
+ const valueProp = getClosureSafeProperty({
368
+ value: getClosureSafeProperty
369
+ });
370
+ Object.defineProperty(this, valueProp, {
371
+ get: () => this.sourceValue()
372
+ });
373
+ const errorsProp = getClosureSafeProperty({
374
+ errors: getClosureSafeProperty
375
+ });
376
+ Object.defineProperty(this, errorsProp, {
377
+ get: () => signalErrorsToValidationErrors(this.fieldState.errors())
378
+ });
379
+ }
380
+ emitControlEvent(event) {
381
+ untracked(() => {
382
+ this._events.next(event);
383
+ });
384
+ }
385
+ setValue(value, options) {
386
+ this.updateValue(value, options);
387
+ }
388
+ patchValue(value, options) {
389
+ this.updateValue(value, options);
390
+ }
391
+ updateValue(value, options) {
392
+ const parent = this.scheduleParentUpdate(options);
393
+ this.sourceValue.set(value);
394
+ if (parent) {
395
+ this.updateParentValueAndValidity(parent, options?.emitEvent);
396
+ }
397
+ if (options?.emitModelToViewChange !== false) {
398
+ for (const fn of this.onChangeCallbacks) {
399
+ fn(value, true);
400
+ }
401
+ }
402
+ }
403
+ registerOnChange(fn) {
404
+ this.onChangeCallbacks.push(fn);
405
+ }
406
+ _unregisterOnChange(fn) {
407
+ removeListItem(this.onChangeCallbacks, fn);
408
+ }
409
+ registerOnDisabledChange(fn) {
410
+ this.onDisabledChangeCallbacks.push(fn);
411
+ }
412
+ _unregisterOnDisabledChange(fn) {
413
+ removeListItem(this.onDisabledChangeCallbacks, fn);
414
+ }
415
+ getRawValue() {
416
+ return this.value;
417
+ }
418
+ reset(value, options) {
419
+ if (isFormControlState(value)) {
420
+ throw unsupportedDisableEnableError();
421
+ }
422
+ const resetValue = value ?? this.initialValue;
423
+ this.fieldState.reset(resetValue);
424
+ if (value !== undefined) {
425
+ this.updateValue(value, options);
426
+ } else if (!options?.onlySelf) {
427
+ const parent = this.parent;
428
+ if (parent) {
429
+ this.updateParentValueAndValidity(parent, options?.emitEvent);
430
+ }
431
+ }
432
+ if (options?.emitEvent !== false) {
433
+ this.emitControlEvent(new FormResetEvent(this));
434
+ }
435
+ }
436
+ scheduleParentUpdate(options) {
437
+ const parent = options?.onlySelf ? null : this.parent;
438
+ if (options?.onlySelf || parent) {
439
+ this.pendingParentNotifications++;
440
+ }
441
+ return parent;
442
+ }
443
+ notifyParentUnlessPending() {
444
+ if (this.pendingParentNotifications > 0) {
445
+ this.pendingParentNotifications--;
446
+ return;
447
+ }
448
+ const parent = this.parent;
449
+ if (parent) {
450
+ this.updateParentValueAndValidity(parent);
451
+ }
452
+ }
453
+ updateParentValueAndValidity(parent, emitEvent) {
454
+ parent.updateValueAndValidity({
455
+ emitEvent,
456
+ sourceControl: this
457
+ });
458
+ }
459
+ propagateToParent(opts, fn) {
460
+ const parent = this.parent;
461
+ if (parent && !opts?.onlySelf) {
462
+ fn(parent);
463
+ }
464
+ }
465
+ get status() {
466
+ if (this.fieldState.disabled()) {
467
+ return 'DISABLED';
468
+ }
469
+ if (this.fieldState.valid()) {
470
+ return 'VALID';
471
+ }
472
+ if (this.fieldState.invalid()) {
473
+ return 'INVALID';
474
+ }
475
+ return 'PENDING';
476
+ }
477
+ get valid() {
478
+ return this.fieldState.valid();
479
+ }
480
+ get invalid() {
481
+ return this.fieldState.invalid();
482
+ }
483
+ get pending() {
484
+ return this.fieldState.pending();
485
+ }
486
+ get disabled() {
487
+ return this.fieldState.disabled();
488
+ }
489
+ get enabled() {
490
+ return !this.disabled;
491
+ }
492
+ get dirty() {
493
+ return this.fieldState.dirty();
494
+ }
495
+ set dirty(_) {
496
+ throw unsupportedFeatureError('Setting dirty directly is not supported. Instead use markAsDirty().');
497
+ }
498
+ get pristine() {
499
+ return !this.dirty;
500
+ }
501
+ set pristine(_) {
502
+ throw unsupportedFeatureError('Setting pristine directly is not supported. Instead use reset().');
503
+ }
504
+ get touched() {
505
+ return this.fieldState.touched();
506
+ }
507
+ set touched(_) {
508
+ throw unsupportedFeatureError('Setting touched directly is not supported. Instead use markAsTouched() or reset().');
509
+ }
510
+ get untouched() {
511
+ return !this.touched;
512
+ }
513
+ set untouched(_) {
514
+ throw unsupportedFeatureError('Setting untouched directly is not supported. Instead use reset().');
515
+ }
516
+ markAsTouched(opts) {
517
+ this.fieldState.markAsTouched();
518
+ this.propagateToParent(opts, parent => parent.markAsTouched(opts));
519
+ }
520
+ markAsDirty(opts) {
521
+ this.fieldState.markAsDirty();
522
+ this.propagateToParent(opts, parent => parent.markAsDirty(opts));
523
+ }
524
+ markAsPristine(opts) {
525
+ this.fieldState.markAsPristine();
526
+ this.propagateToParent(opts, parent => parent.markAsPristine(opts));
527
+ }
528
+ markAsUntouched(opts) {
529
+ this.fieldState.markAsUntouched();
530
+ this.propagateToParent(opts, parent => parent.markAsUntouched(opts));
531
+ }
532
+ updateValueAndValidity(_opts) {}
533
+ _updateValue() {}
534
+ _forEachChild(_cb) {}
535
+ _anyControls(_condition) {
536
+ return false;
537
+ }
538
+ _allControlsDisabled() {
539
+ return this.disabled;
540
+ }
541
+ _syncPendingControls() {
542
+ return false;
543
+ }
544
+ disable(_opts) {
545
+ throw unsupportedDisableEnableError();
546
+ }
547
+ enable(_opts) {
548
+ throw unsupportedDisableEnableError();
549
+ }
550
+ setValidators(_validators) {
551
+ throw unsupportedValidatorsError();
552
+ }
553
+ setAsyncValidators(_validators) {
554
+ throw unsupportedValidatorsError();
555
+ }
556
+ addValidators(_validators) {
557
+ throw unsupportedValidatorsError();
558
+ }
559
+ addAsyncValidators(_validators) {
560
+ throw unsupportedValidatorsError();
561
+ }
562
+ removeValidators(_validators) {
563
+ throw unsupportedValidatorsError();
564
+ }
565
+ removeAsyncValidators(_validators) {
566
+ throw unsupportedValidatorsError();
567
+ }
568
+ clearValidators() {
569
+ throw unsupportedValidatorsError();
570
+ }
571
+ clearAsyncValidators() {
572
+ throw unsupportedValidatorsError();
573
+ }
574
+ setErrors(_errors, _opts) {
575
+ throw unsupportedFeatureError(ngDevMode && 'Imperatively setting errors is not supported in signal forms. Errors are derived from validation rules.');
576
+ }
577
+ markAsPending(_opts) {
578
+ throw unsupportedFeatureError(ngDevMode && 'Imperatively marking as pending is not supported in signal forms. Pending state is derived from async validation status.');
579
+ }
580
+ }
581
+ class CachingWeakMap {
582
+ map = new WeakMap();
583
+ getOrCreate(key, create) {
584
+ const cached = this.map.get(key);
585
+ if (cached) {
586
+ return cached;
587
+ }
588
+ const value = create();
589
+ this.map.set(key, value);
590
+ return value;
591
+ }
592
+ }
593
+ function wrapFieldTreeForSyncUpdates(tree, onUpdate) {
594
+ const treeCache = new CachingWeakMap();
595
+ const stateCache = new CachingWeakMap();
596
+ const wrapState = state => {
597
+ const {
598
+ value
599
+ } = state;
600
+ const wrappedValue = Object.assign((...a) => value(...a), {
601
+ set: v => {
602
+ value.set(v);
603
+ onUpdate();
604
+ },
605
+ update: fn => {
606
+ value.update(fn);
607
+ onUpdate();
608
+ }
609
+ });
610
+ return Object.create(state, {
611
+ value: {
612
+ get: () => wrappedValue
613
+ }
614
+ });
615
+ };
616
+ const wrapTree = t => {
617
+ return treeCache.getOrCreate(t, () => {
618
+ return new Proxy(t, {
619
+ get(target, prop, receiver) {
620
+ const val = Reflect.get(target, prop, receiver);
621
+ if (typeof val === 'function' && typeof prop === 'string') {
622
+ return wrapTree(val);
623
+ }
624
+ return val;
625
+ },
626
+ apply(target, _, args) {
627
+ const state = target(...args);
628
+ return stateCache.getOrCreate(state, () => wrapState(state));
629
+ }
630
+ });
631
+ });
632
+ };
633
+ return wrapTree(tree);
634
+ }
635
+ function isFormControlState(formState) {
636
+ return typeof formState === 'object' && formState !== null && Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState;
637
+ }
638
+ function unsupportedFeatureError(message) {
639
+ return new _RuntimeError(1920, message ?? false);
640
+ }
641
+ function unsupportedDisableEnableError() {
642
+ return unsupportedFeatureError(ngDevMode && 'Imperatively changing enabled/disabled status in form control is not supported in signal forms. Instead use a "disabled" rule to derive the disabled status from a signal.');
643
+ }
644
+ function unsupportedValidatorsError() {
645
+ return unsupportedFeatureError(ngDevMode && 'Dynamically adding and removing validators is not supported in signal forms. Instead use the "applyWhen" rule to conditionally apply validators based on a signal.');
646
+ }
647
+ function removeListItem(list, el) {
648
+ const index = list.indexOf(el);
649
+ if (index > -1) list.splice(index, 1);
650
+ }
651
+ function getClosureSafeProperty(objWithPropertyToExtract) {
652
+ for (let key in objWithPropertyToExtract) {
653
+ if (objWithPropertyToExtract[key] === getClosureSafeProperty) {
654
+ return key;
655
+ }
656
+ }
657
+ throw Error(typeof ngDevMode === 'undefined' || ngDevMode ? 'Could not find renamed property on target object.' : '');
658
+ }
659
+
660
+ export { NG_STATUS_CLASSES, SignalFormControl, compatForm };
322
661
  //# sourceMappingURL=signals-compat.mjs.map