Add SignalFormControl for bottom-up migration to Signal Forms#66614
Add SignalFormControl for bottom-up migration to Signal Forms#66614leonsenft merged 20 commits intoangular:mainfrom
Conversation
| : compatForm(this.sourceValue, {injector}); | ||
| this.fieldState = this.fieldTree(); | ||
|
|
||
| Object.defineProperty(this, 'value', { |
There was a problem hiding this comment.
why are these defined like this instead of just a normal getter property?
There was a problem hiding this comment.
Can we land #66672 first and update this PR accordingly?
| } | ||
|
|
||
| override getRawValue(): T { | ||
| return this.value; |
There was a problem hiding this comment.
I think we should either throw an error or properly filter out disabled fields for this
There was a problem hiding this comment.
I don't understand the question here. What's the issue?
There was a problem hiding this comment.
I think in reactive forms getRawValue was supposed to be the value with disabled fields filtered out? (At least that's what I remember from the previous meeting where this was discussed). So if that's the case it feels like a lie to just return the full value without filtering out the disabled fields
There was a problem hiding this comment.
We discussed that, and since this is a control, not a group, we should not take disabled into account
There was a problem hiding this comment.
We can remove the TODO comment now.
|
Deployed adev-preview for bf6c3c5 to: https://ng-dev-previews-fw--pr-angular-angular-66614-adev-prev-5cqju58f.web.app Note: As new commits are pushed to this pull request, this link is updated after the preview is rebuilt. |
| constructor(value: T, schemaOrOptions?: SchemaFn<T> | FormOptions, options?: FormOptions) { | ||
| super(null, null); | ||
|
|
||
| const [model, schema, opts] = normalizeFormArgs<T>([signal(value), schemaOrOptions, options]); |
There was a problem hiding this comment.
Why do we take a value–from which we create a signal–rather than a signal like the form() function?
There was a problem hiding this comment.
This is because we need to immediately update the value instead of using effect (hence patching).
I added a TODO to document it better in the markdown.
There was a problem hiding this comment.
I'm sorry I don't follow. Where are we immediately updating the value?
There was a problem hiding this comment.
Because we own the value, we can wrap it with wrapFieldTreeForSyncUpdates below, which does it
There was a problem hiding this comment.
We pass rawTree to wrapFieldTreeForSyncUpdates, which is derived from sourceValue which is signal(value). Why can't sourceValue just be a signal that the user provides? Does something break? Or is it a conceptual issue of ownership?
There was a problem hiding this comment.
The idea is to never expose the source signal, this way people would only be able to change values through the form, where we'd react to changes immediately.
If we took a signal, we woudn't be able to react to changes
| } | ||
|
|
||
| override getRawValue(): T { | ||
| return this.value; |
There was a problem hiding this comment.
I don't understand the question here. What's the issue?
e87c138 to
35e7840
Compare
| : compatForm(this.sourceValue, {injector}); | ||
| this.fieldState = this.fieldTree(); | ||
|
|
||
| Object.defineProperty(this, 'value', { |
There was a problem hiding this comment.
Can we land #66672 first and update this PR accordingly?
| constructor(value: T, schemaOrOptions?: SchemaFn<T> | FormOptions, options?: FormOptions) { | ||
| super(null, null); | ||
|
|
||
| const [model, schema, opts] = normalizeFormArgs<T>([signal(value), schemaOrOptions, options]); |
There was a problem hiding this comment.
I'm sorry I don't follow. Where are we immediately updating the value?
| } | ||
|
|
||
| override getRawValue(): T { | ||
| return this.value; |
There was a problem hiding this comment.
We can remove the TODO comment now.
| ``` | ||
|
|
||
| <!-- TODO: include some high level usage comment about how people should mostly interact with this via the signal forms API exposed on .fieldTree, not via the reactive forms methods. --> | ||
| <!-- TODO: Elaborate on why the value taken is not a signal. --> |
There was a problem hiding this comment.
Are these TODOs for this PR or the future? If not this PR, should they be added as items to our project?
There was a problem hiding this comment.
That would be the next thing i'd do, I just want to get the big parts in first
90aa71c to
a0c5a05
Compare
- Add dirty, pristine, touched, untouched getters - Add markAsTouched, markAsDirty methods - Add markAsPristine, markAsUntouched methods (preserve other state)
- Add reset() method with optional value parameter
- Support FormControlState unboxing ({value, disabled} format)
- Add isFormControlState helper function
- Add valueChanges EventEmitter that emits when source signal changes - Add statusChanges EventEmitter that emits when status changes - Set up effects to emit to observables
- Emit ValueChangeEvent, StatusChangeEvent on changes - Emit TouchedChangeEvent, PristineChangeEvent on status changes - Emit FormResetEvent on reset() - Add emitControlEvent helper method
- Add ValueUpdateOptions type with onlySelf, emitEvent options - Add parent notification on value changes via effect - Add parent notification helpers: scheduleParentUpdate, notifyParentUnlessPending - Propagate dirty/touched/pristine/untouched to parent - Support onlySelf option to prevent parent propagation
- Add CachingWeakMap utility for memoization - Add wrapFieldTreeForSyncUpdates Proxy wrapper - Intercept fieldTree().value.set() calls to sync parent immediately - Cache wrapped trees and states to preserve identity
- Add registerOnChange, _unregisterOnChange for value change callbacks - Add registerOnDisabledChange, _unregisterOnDisabledChange for disabled callbacks - Add disabled changes effect to notify registered callbacks - Call onChange callbacks from updateValue with emitModelEvent flag
- Add disable, enable methods that throw with helpful messages - Add validator methods (set/add/remove/clear) that throw - Add setErrors, markAsPending methods that throw - Add setters for dirty/pristine/touched/untouched that throw - Add JSDoc with @usageNotes examples - Add comprehensive unit tests for SignalFormControl - Add FormGroup/FormArray integration tests - Add web tests for CVA directive lifecycle - Update migration docs with SignalFormControl usage
This make things cleaner
Consolidate everything related to converting errors in one place
Make them tree-shakeable and fix the naming
- Add more comments and docs - In signalErrorsToValidationErrors return null for empty object - Drop messages in prod mode
Add some TODOs for future things
Document that injectors are optional
Untrack callbacks, so they are not called when signals change
Make the way reset works for it to be more consistent
Clean up tests, drop old todos
We have to do this because Abstract control doesn't allow us to have value as a getter type-wise
leonsenft
left a comment
There was a problem hiding this comment.
reviewed-for: public-api
|
This PR was merged into the repository. The changes were merged into the following branches:
|
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This would allow create something the looks like a FormControl and can be integrated with a FormGroup.
Note: I asked a robot to break this code in sizeable commits
That said, I'm very happy to finally introduce at least some kind of SFCs to Angular 🤣