GithubHelp home page GithubHelp logo

Add "transform" to model() about angular HOT 8 CLOSED

e-oz avatar e-oz commented on May 12, 2024
Add "transform" to model()

from angular.

Comments (8)

sonukapoor avatar sonukapoor commented on May 12, 2024 1

That is correct. The same information is provided on angular.dev.

https://angular.dev/guide/signals/model#customizing-model-inputs

Please use angular.dev for future reference as angular.io is not updated anymore:

from angular.

alxhub avatar alxhub commented on May 12, 2024 1

Does

export class Box {
  position = model<Vector3|[number, number, number]>(new Vector3(0, 0, 0));
  private vecPosition = computed(() => {
    if (Array.isArray(position)) {
      return new Vector3(position[0], position[1], position[2]);
    } else {
      return position;
    }
  }, equal: (a, b) => a.equals(b));
}

not address this use case?

In your first example, the positionChange event is defined via toObservable of the signal.

 public readonly positionChange = outputFromObservable(toObservable(this.$position));

This is effectively doing (2) from my original message, and emitting positionChange on any change of the position input. This is unexpected from a 2-way binding - the change event should only be emitted for internal changes, not for changes originating from the input itself. This can have unwanted consequences around ExpressionChanged or when a listener doesn't expect to see a change event from a binding change.

This actually highlights a benefit of model() over direct implementation of the two-way binding contract - it's hard to get these subtle things right.

from angular.

RobbyRabbitman avatar RobbyRabbitman commented on May 12, 2024

The docs say model() does not support input transformation, but it does not specify, whether its by design or simply not implemented at the moment: The why is missing, maybe that could be added in the docs.

https://angular.io/guide/model-inputs#customizing-model-inputs

from angular.

JelleBruisten avatar JelleBruisten commented on May 12, 2024

But if model would transform a incoming value and therefore adjusting the value of the model, it does update by 2 way binding the value of the parent?

Seems like a unwanted interaction going on here

from angular.

JeanMeche avatar JeanMeche commented on May 12, 2024

I haven't the details in mind but it is likely by design. In signal input document it says

Do not use transforms if they change the meaning of the input, or if they are impure.

This is to say that transform is mostly here to coerce the input to a type (name unkown to string, unknown to boolean). Since model is dedicated to double binding and that the binding of output is typed, it don't make much sense to coerce a value since it have a defined emitted type.

We'll need a more concrete usecase from @e-oz to confirm that, but the feature request is probably linked to a misuse of the transform feature.

from angular.

alxhub avatar alxhub commented on May 12, 2024

model() not supporting transform is by design.

Model properties are designed for synchronization - keeping an outside copy of the data in sync with the value of the model. This is expressed via the two-way binding sugar [(model)]="outsideData". With this statement, outsideData and the model should stay in sync via change detection (data -> model) and the modelChange event (model -> data).

Because this synchronization is bi-directional, it'd be confusing to support a transformation during the data -> model operation. That would result in the value in the model being different than the value of the outside data. Either:

  1. We accept this discrepancy as how things work.

We don't think this option is a good fit, because it makes model more difficult to reason about if the internal value can be different than the external value when its entire purpose is to keep them in sync.

This is also equivalent to declaring a computed that applies the transform, which is straightforward to achieve already.

  1. We make model emit a modelChange event after the transform changes the value.

This would keep the outside data in sync with the model, but has some major issues. For one, it would cause ExpressionChanged errors if used with a non-signal two way binding. Even with signals, it could result in infinite looping change detection if the transform always produces a new value, which is easy to do accidentally.

Given that it's straightforward to work around this by using a computed (which makes it obvious to anyone looking at the code that the transform is internal-only), we don't plan on adding any support for transform to model. However, we would be interested in collecting use cases when people feel like transform is necessary, or that the additional boilerplate of a computed is too great.

from angular.

e-oz avatar e-oz commented on May 12, 2024

Component <vos-box> accepts position coordinates in 2 formats:

<vos-box [position]="[0, 1, - 1]"/>
<vos-box [position]="item.$position()" (positionChange)="item.$position.set($event)"/>

I would like to replace this:

@Component({
  selector: 'vos-box',  
})
export class Box {
 private readonly $position = signal<Vector3>(new Vector3(0, 0, 0), { equal: (a, b) => a.equals(b)});

 @Input() set position(position: Vector3 | [number, number, number]) {
     if (Array.isArray(position)) {
        this.$position.set(new Vector3(position[0], position[1], position[2]));
     } else {
        this.$position.set(position);
     }
  }
 
 public readonly positionChange = outputFromObservable(toObservable(this.$position));
}

with this:

@Component({
  selector: 'vos-box',  
})
export class Box {
 public readonly position = model<Vector3, [number, number, number] | Vector3>(new Vector3(0, 0, 0), { 
    transform: (position: Vector3 | [number, number, number]) => {
      if (Array.isArray(position)) {
        return new Vector3(position[0], position[1], position[2]);
      } else {
         return position;
      }
    },
    equal: (a, b) => a.equals(b),
 });
}

In this example, transform() is a pure function without side effects. It is not difficult to create other examples where transform() will also be pure.

I believe adding transform() here would simply make the API more consistent. While there are ways to misuse allowSignalWrites, we still have it available.

from angular.

e-oz avatar e-oz commented on May 12, 2024

@alxhub

Agreed, it's too easy to cause infinite loops with transform() in this scenario. The only workaround I can currently think of is to require equal when transform is set.

That would result in the value in the model being different than the value of the outside data.

Here, I respectfully disagree. The data that arrives at the input isn't always the direct representation of our model. It's merely a consequence of the fact that two-way binding in Angular serves as syntax sugar. There's no guarantee, nor should there be, that the (modelChange) part will always be utilized or implemented, or that synchronization will occur at all. model() should function without depending on this guarantee. Therefore, the only aspect that a component can refer to as the "value" of a certain model is the value of the field defined by model(), rather than relying on external data.

My initial rationale behind incorporating transform() into model() was to enforce this logic: "users may input any data into this field, but the model should consistently maintain accurate data. Unfiltered data or format disparities can potentially lead to issues." A straightforward example of this scenario is a time/date input field (where we have multiple formats for the same thing).

position = model<Vector3|[number, number, number]>(new Vector3(0, 0, 0));

Here output type is also Vector3|[number, number, number], not just Vector3.

Nonetheless, I concur that the inclusion of transform() raises the risk of infinite loops significantly. Even if we mandate users to specify equal when declaring transform, we cannot assure that a custom equal will invariably prevent endless loops. While it may be straightforward to verify in the example I provided, in practical scenarios, this may not always be the case. I don't want to be responsible for all the possible CPU-warming pages, so I'm closing this issue 😉

I believe this particular use case is rare enough to justify dedicating one additional field instead of relying on transform() 😎

from angular.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.