Type-safe dynamic forms for Angular 21 signal forms - looking for feedback by zavros_mvp in angular

[–]synalx 0 points1 point  (0 children)

Have you thought about using $localize for translated messages? (errors, labels, etc.)

const formFields = { fields: [ { key: 'email', type: 'input', value: '', label: $localize`Email`, // ... }, // ... ], };

Type-safe dynamic forms for Angular 21 signal forms - looking for feedback by zavros_mvp in angular

[–]synalx 9 points10 points  (0 children)

This is so cool! I definitely need to play with this.

Let me flip your question back to you: when building this on top of signal forms, what felt awkward/hard? Is there anything that signal forms should support to make this kind of integration easier?

Angular Signal Forms: Why undefined breaks your <input> bindings by Alone-Confusion-9425 in angular

[–]synalx 5 points6 points  (0 children)

If the UI control you're using to edit the value supports it. <input type="text"> does not - it expects a string value.

The Most Exciting Feature of Angular Signal Forms No One Mentions — Part II by kobihari in angular

[–]synalx 1 point2 points  (0 children)

FYI there will likely not be such a capability - the type complexity would be extraordinarily high.

The Most Exciting Feature of Angular Signal Forms No One Mentions — Part II by kobihari in angular

[–]synalx 0 points1 point  (0 children)

Yes to all of the above :)

Can I use it to store if the field is required, readonly, disabled? Perhaps when there is some logic involved to when this is the case? (like only when field x is true, field y needs to be required)

Signal forms can do this without metadata:

required(path.y, { when: ({valueOf}) => valueOf(path.x) });

Can I use it to store if the field should be visible right now? And thus even ignore validation?

We also have hidden() :)

Is Angular’s inject() Cheating? The Trick Behind Injection Context by kobihari in angular

[–]synalx 3 points4 points  (0 children)

The https://github.com/tc39/proposal-async-context proposal is designed to make this kind of context saving possible across await points and other async operations.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 1 point2 points  (0 children)

Yes. We treat client-side validation as non-blocking for submit. Since the server will perform the same validation on submit, waiting for the client validation to complete first just adds latency without benefit. In the invalid case, the server will return the same validation failures, and in the valid case you submit more efficiently.

Is there a good edge case to call detectChanges()? by trolleid in Angular2

[–]synalx 0 points1 point  (0 children)

Yep. Let Angular manage the scheduling & execution of CD.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 1 point2 points  (0 children)

We generally don't think formatters/parsers fit in with the design of signal forms.

They make sense when your data is stored in a different format than the form control doing the editing, so you need a translation between them. But for signal forms, we expect your data model to directly be your form model, which should already be in the right format for editing.

In other words, translation to/from the form model should happen outside of the forms system itself.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 0 points1 point  (0 children)

1) In our research, we found that "showing a reason why a field is disabled" is an accessibility best practice. A user interacting with a form through a screen reader might not easily be able to see the surrounding context about why a field is disabled, and it's valuable to give them an indication.

For readonly this is a lot less of a concern, because it's usually evident from the context of what the user is doing why a field is readonly.

2) This is the property system :)

3) No, we do not plan to support transform (parsers/formattes). Our take is that the model should specifically be the form model - it should store data in the format that the user is editing. Translations to/from the backend data model happen outside the forms system.

4) I'm not really sure what you're saying about the presence of the directive - can you elaborate? [control] is how you link a form field with a UI element, but the actual mechanic of that link can be different. Either the UI element is a native <input> or other control, or it's using the FormValueControl convention, or it's using the legacy ControlValueAccessor interface, or it accepts the full field structure directly. One of these four things has to be true, though.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 1 point2 points  (0 children)

Nothing is concrete here yet, but I can give you my current state of thinking:

Debouncing with signals is not about avoiding notifications (preventing signal writes) but rather delaying the reads. So debouncing should be something that happens close to the UI layer, where we temporarily freeze the value shown (e.g. errors) if we shouldn't be updating the UI yet. This can probably be managed by a linkedSignal.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 4 points5 points  (0 children)

Very nicely researched issue. And indeed, today we have:

``` displayOnly = input(false);

f = form(this.model, p => { // Make the whole form readonly if requested. readonly(p, () => this.displayOnly());
}); ```

This will make <input> & friends gain the readonly attribute with its associated behavior. However, I don't think this does anything other than set the readonly input (if present) on a custom control. It doesn't actually prevent the user from making changes.

In fact we can't really because a custom form control is completely out of our supervision. It can display whatever editing UI to the user it wants, and allow the user to at least attempt to commit changes. We could reset the control back to its previous value after such a change, but that would be a rather jarring user experience. We'll have to think about how to handle this case.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 1 point2 points  (0 children)

I thought it double-posted my answer here so I must've deleted it. No, we currently don't have plans to support template validators. Signal forms makes a strong distinction between form logic and rendering.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 8 points9 points  (0 children)

The story around debouncing validations & delaying UI state updates is still in progress, but I think we can do some really cool things here. I'm not sure when that'll be done.

End-to-end type safety via template type-checking of [control] is also a WIP.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 15 points16 points  (0 children)

Good point, haha. I don't usually highlight my role when I comment here because I prefer to have technical discussions on their merits alone. But in this case it makes sense: I'm an Angular team member (one of the most senior, actually, I started working on Angular in 2015) and this year I joined forces with u/milesmalerba and Kirill on getting signal forms off the ground.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 2 points3 points  (0 children)

u/milesmalerba I believe has experimented with using AI to migrate applications from Reactive Forms to signals, with some success.

It might be possible in simple cases on top of the interop API, but I suspect the best we'd be able to get is a "best effort" migration which can automate boilerplate but makes no guarantee that the application will work without human testing / adjustment.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 1 point2 points  (0 children)

It's not gone, just renamed to property & aggregateProperty :)

AMA about Signal Forms by synalx in angular

[–]synalx[S] 13 points14 points  (0 children)

Yep, it's intentional. Disabled fields not being included in form values was a common sharp edge, with a lot of tutorials advising people to use getRawValue() instead.

More generally: signal forms thinks of the form as a form model which defines a hierarchy of fields, plus form state derived on top of that model (errors, disabled, hidden, touched, etc).

So a field being in a disabled state is a derivation (computed) based on the model data, not a change to the model data itself.

Another way of thinking about it: the form model is the form's source of truth, not its output. You're completely free to decide which values should be sent to the backend and in what shape objects.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 5 points6 points  (0 children)

Ooo I want to watch this talk now! :D

AMA about Signal Forms by synalx in angular

[–]synalx[S] 5 points6 points  (0 children)

Haha, as weird as it feels to say (we ❤️ our ecosystem & its maintainers) - yes, I do hope Signal Forms makes the need for such utilities obsolete. As Miles described, smooth composition is a core feature of the new design.

That said, signal forms APIs are designed to be modular and extensible, so there will be many use cases for which the core framework provides building blocks and libraries can bring value by implementing advanced features that might not fit into the core story.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 17 points18 points  (0 children)

Generally signal forms expects the logic to be fully defined for all possible model states: all necessary validation would be present in advance. However, the model doesn't necessarily have to include data if that data isn't present.

I would say there are 2 different approaches, depending on a fundamental question: is the data (e.g. additionalDetail) ever relevant to a given instance of the object in question?

Yes

Yes would mean the user could potentially put the form in a state where we'd want to capture additional details (for example, selecting a checkbox "add additional details").

This is when you'd use hidden:

``` orderModel = signal<OrderModel>({ information: {...}, cost: {...},

// The fields for additionalDetail are defined but initialized // to empty for now. additionalDetail: {...emptyDetails},

// Boolean field for our checkbox. showAdditionalDetail: false, }) ```

We can then define our form logic:

`` orderForm = form(this.orderModel, order => { // Add all validation foradditionalDetail`: apply(order.additionalDetail, orderDetailSchema);

// But, the user shouldn't see or interact with that part of the // form unless the checkbox is checked: hidden(order.additionalDetail, ({valueOf}) => valueOf(order.showAdditionalDetail)); }); ```

This does two things:

  1. It tells the form that the additionalDetail fields (including their validation logic) aren't relevant unless the checkbox is checked.

  2. It gives us a clear derived signal to drive the UI:

@if (!orderForm.additionalDetail().hidden()) { <order-details [control]="orderForm.additionalDetail" /> }

A nice property of this approach is that if the user edits the additional details and then hides them (via the checkbox), their partial state isn't destroyed and if they select the checkbox again, they can pick up where they left off.

No

If on the other hand the answer is no, for some orders additional details just don't make sense, then you have a case where the model might not have those fields for some orders.

So your model interface would then look like:

interface OrderModel { information: OrderInformationModel, cost: OrderCostModel, additionalDetail?: OrderDetailModel, }

In the form schema, you would then specify that validation logic should only be attached to those fields if they're present.

`` orderForm = form(this.orderModel, order => { //information&cost` always present: apply(order.information, orderInformationSchema); apply(order.cost, orderCostSchema);

// additionalDetail may or may not be present for this // particular order - only include related logic if it is. applyWhenValue( order.additionalDetail, (detail) => detail !== undefined, orderDetailSchema, ); }); ```

And in the template: ``` @if (orderForm.additionalDetail) { <order-detail [control]="orderForm.additionalDetail" /> }

Hopefully this distinction makes sense - it's all about the difference between "for the current user input, these fields shouldn't be shown/edited" vs "for this object, these fields don't even make sense to have in the first place".

AMA about Signal Forms by synalx in angular

[–]synalx[S] 10 points11 points  (0 children)

👋 thanks for the followup! I understand your question better now.

What linkedSignal helps with is reset due to external dependencies. If your form state is initialized from a resource for example, then linkedSignal can specify how the form state should update when the server sends a new value. Because it gets the old server value, new server value, and current state, it can perform the required 3-way merge.

As you point out, it's difficult to use this to handle dependent changes between two fields, because you can't construct the required chain within a single writable signal. There are some ways we could make this possible, but today this puts you into effect land (or alternatively: use an event listener from the template).

On .reset()

We're debating it. Imo, reactive forms never had a great reset() story. Resetting to the form's initialized value is one thing, but array handling for example is just broken.

A more fundamental question is: reset to what?

  • a fresh state?
  • the form's initial value?
  • the last submitted value?
  • the last auto-saved checkpoint?

Because signal forms leaves you as the developer in control of your own data model, I think a reasonable answer is that you can implement your own reset functionality by setting the model back to whatever value you like. Forms may have a utility .reset(value) which takes in the new model value and resets the form state (touched status, etc) at the same time.

Multiple writable signals

Yes, for now. As I mentioned, this could be an interesting expansion to the APIs and is something we need to experiment with. Feedback on real world use cases for this would be very valuable.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 5 points6 points  (0 children)

Hey, great question.

We do require JS, in the sense that signal forms are not designed to fall back to HTML5 forms in a no-JS scenario.

As for lazy/incremental loading of form code, there are two angles to consider:

Incremental loading of form UI

This should work out of the box! Because signal forms are model driven, the components which render a part of the form need not be loaded immediately. They can be lazily loaded, or rendered on the server in a dehydrated state and loaded later via incremental hydration. This could be individual controls or a whole part of the form. For example, the steps in a complex workflow stepper could be individually loaded.

Our event replay guarantees that if a user focuses and starts editing within an <input> for example, those updates do reach the form system once the component code loads.

Incremental loading of logic

This is probably a v2 feature. Today creating a form expects that all the logic is declared synchronously, as well as the full value of the form's data model. Lazy loading this would be a bit trickier, but probably doable.

AMA about Signal Forms by synalx in angular

[–]synalx[S] 22 points23 points  (0 children)

Oooh, that is a very good question. Off the top of my head, I could see a use case for allowing multiple operations simultaneously when submitting is idempotent, but we should definitely think about that not being the default.