Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

model() should support a narrower output type #55754

Closed
Harpush opened this issue May 10, 2024 · 10 comments
Closed

model() should support a narrower output type #55754

Harpush opened this issue May 10, 2024 · 10 comments
Labels
area: core Issues related to the framework runtime core: inputs / outputs cross-cutting: signals feature Issue that requests a new feature
Milestone

Comments

@Harpush
Copy link

Harpush commented May 10, 2024

Which @angular/* package(s) are relevant/related to the feature request?

core

Description

Currently input supports transform but model doesn't although it can be very handy.

Proposed solution

Allow model(2, {transform: fn})
For example if I can accept number | '${number}' from outside but emit only number.
It allows the user to either use:

  1. modelName="8" without two way binding
  2. [(modelName)]="value" with two way binding while value is only number

Alternatives considered

Probably use model<number | '${number}'>(2). But then inside I need to run the transform function myself

@alxhub
Copy link
Member

alxhub commented May 10, 2024

This is a duplicate of #55166 which has the previous discussion (which concluded that model() should not support transform).

@alxhub alxhub changed the title Signal model with transform like input model() should support a narrower output type May 10, 2024
@alxhub
Copy link
Member

alxhub commented May 10, 2024

I'm going to steal this issue and refocus it a bit. transform support isn't necessary to address the problem you're talking about. If we had a way to declare that model() will emit a narrower type than it accepts, then it could support two-way binding to a WritableSignal<number> since Signal<number> is assignable to Signal<number|string> and we'd never try to write a string back.

@alxhub alxhub added feature Issue that requests a new feature area: core Issues related to the framework runtime core: inputs / outputs core: reactivity Work related to fine-grained reactivity in the core framework cross-cutting: signals labels May 10, 2024
@ngbot ngbot bot added this to the Backlog milestone May 10, 2024
@alxhub alxhub removed the core: reactivity Work related to fine-grained reactivity in the core framework label May 10, 2024
@Harpush
Copy link
Author

Harpush commented May 10, 2024

I'm going to steal this issue and refocus it a bit. transform support isn't necessary to address the problem you're talking about. If we had a way to declare that model() will emit a narrower type than it accepts, then it could support two-way binding to a WritableSignal<number> since Signal<number> is assignable to Signal<number|string> and we'd never try to write a string back.

But how would you transform the string to the number for input values? At the end the component internally only want number and will always emit number. Only the outside can provide strings.

@alxhub
Copy link
Member

alxhub commented May 11, 2024

But how would you transform the string to the number for input values? At the end the component internally only want number and will always emit number. Only the outside can provide strings.

You can use a computed to go from number|string to just number.

@Harpush
Copy link
Author

Harpush commented May 11, 2024

But how would you transform the string to the number for input values? At the end the component internally only want number and will always emit number. Only the outside can provide strings.

You can use a computed to go from number|string to just number.

But wouldn't something like model(2, {inputTansform: fn}) makes more sense? Especially if we look at it as input and output - a model built from input with transform and output with actual type. The output type must be assigned to the input type.

@Harpush
Copy link
Author

Harpush commented May 17, 2024

@alxhub Just adding that using input and output we can do:

value = input(2, { transform: fn });
valueChange = output<number>();

The only downside is we can't change it from inside and the outside must implement the output and input or two way binding.
Hence the suggested:

value = model(2, {inputTansform: fn});

Which does exactly the same but with model added features.

@alxhub
Copy link
Member

alxhub commented May 29, 2024

After some discussion, we've decided that it's not really feasible to implement this feature.

One, we (still) don't want to add transform to model. The intention of the model input is that it offers synchronization between the parent component and the model, and that synchronization breaks if a one-way transformation is introduced. This could be interrupted

Narrowing the setter type wouldn't work, as it would break the assignability of model inputs to the WritableSignal interface which has no such restrictions.

Given that both options produce tradeoffs that we don't feel strike the right balance, we don't think a change here makes sense.

@alxhub alxhub closed this as not planned Won't fix, can't repro, duplicate, stale May 29, 2024
@Harpush
Copy link
Author

Harpush commented May 29, 2024

@alxhub so how do you suggest to implement it instead? Using input and output requires the consumer to use two way binding for it to work (btw isn't it the same as what was suggested here but without internal control?)

@alxhub
Copy link
Member

alxhub commented May 29, 2024

Falling back to input & output is I think is a good workaround.

// Input with transform: signal of the internal value.
state = input(..., {transform: ...}); // InputSignal<Wide, Narrow>

// Output side of the two-way binding contract.
stateChange = output<Narrow>();

// Computed to project new input values to a WritableSignal, allowing internal mutations.
stateMutable = computed(() => signal(this.state())); // Signal<WritableSignal<Narrow>>;

// Actual value to use internally (unwrap the nested WritableSignal).
stateInternal = computed(() => this.stateMutable()()); // Signal<Narrow>

// Helper method to update the state (equivalent of `model.set()`).
setState(value: Narrow): void {
  this.stateMutable().set(value);
  this.stateChange(value);
}

@Harpush
Copy link
Author

Harpush commented May 29, 2024

Falling back to input & output is I think is a good workaround.

// Input with transform: signal of the internal value.
state = input(..., {transform: ...}); // InputSignal<Wide, Narrow>

// Output side of the two-way binding contract.
stateChange = output<Narrow>();

// Computed to project new input values to a WritableSignal, allowing internal mutations.
stateMutable = computed(() => signal(this.state())); // Signal<WritableSignal<Narrow>>;

// Actual value to use internally (unwrap the nested WritableSignal).
stateInternal = computed(() => this.stateMutable()()); // Signal<Narrow>

// Helper method to update the state (equivalent of `model.set()`).
setState(value: Narrow): void {
  this.stateMutable().set(value);
  this.stateChange(value);
}

Thanks that's helpful! Just a shame that such a pattern can't be integrated in angular for easier implementation. Especially if there are multiple of those inputs. Models are such a clean way to do it.
I guess one can create a wrapper so it ends up with: input,output,internal wrapper

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: inputs / outputs cross-cutting: signals feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests

2 participants