Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

fix(input): don't invoke viewChangeListeners before real user input #9913

Closed
wants to merge 1 commit into from

Conversation

caitp
Copy link
Contributor

@caitp caitp commented Nov 4, 2014

Closes #9867

// String input types can transform a value into a string without touching it.
if (oldVal != null && !isNaN(oldVal) &&
ctrl.$modelValue === oldVal.toString()) {
return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we still change the scope value, the only difference is we don't invoke change listeners. @petebacondarwin you get to review this one and decide if it's worth fixing.

@Narretz
Copy link
Contributor

Narretz commented Nov 5, 2014

I think it's worth fixing. :)
Although I'd like to explore the possibility of fixing all the issues showing the same problem (#9063, #9867, #9918) with one change. Might be problematic, because they seem to be subtly different.

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

#9918 is totally unrelated, and we need to see a repro before dealing with that. The other issue looks related, but not easy to fix in one CL, i'll open a second one for that. Updating the model value during validation was always a terrible idea and I have no idea why we feel the need to do that.

@Narretz
Copy link
Contributor

Narretz commented Nov 5, 2014

For #9063 I have a PR: #9890 Would be great if you could review it. I am not terribly happy with it, but it works.

@petebacondarwin
Copy link
Contributor

@caitp and @Narretz - I'll take a look at these tomorrow morning if that is OK with you.

@petebacondarwin petebacondarwin added this to the 1.3.2 milestone Nov 5, 2014
@petebacondarwin
Copy link
Contributor

This fix doesn't seem right to me - but I can't put my finger on it. It seems like we shouldn't be permanently converting the value to/from a string only to validate.

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

we shouldn't, but that's what we do --- we also can't throw it away because we don't know if we did it or if a user did it. Best we can do is just not notify change listeners (if the model is basically the same)

@petebacondarwin
Copy link
Contributor

OK, so I think what is happening is...

Without a validator
The model is a number
The model gets rendered to the control as a string
If the user changes the value of the input then it triggers the model synchronisation process which calls the ng-change handler

With a validator
The model is a number
The model gets rendered to the control as a string
There is a validator so the $$parseAndValidate() method gets called
This reads the input and retrieves the view value as a string, the model is updated to a string
The ng-change handler is called

So I am thinking, perhaps the workaround is to put in place a parser that converts the input to a number?

@petebacondarwin
Copy link
Contributor

Yep, so if the input is type="number" then we don't get the problem.

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

That doesn't really make sense --- we shouldn't require people to set up formatters/parsers, and there's no way for us to know if that's what they would want in the first place, so we can't do it in core.

I'm pretty sure the thing to do is just not fire view change listeners if the model is essentially unchanged

@petebacondarwin
Copy link
Contributor

If the user puts a number into a model that is attached to a text input box then that input box will convert the value to a string, which effectively changes the model and so triggers ng-change.

I think we just need to give guidance that they should ensure that the type of the model data is not modified by the input if they are bothered about change events by doing one of the following:

  • converting to an appropriate type before binding to the input (e.g. converting those numbers to strings in the response from the server)
  • using an appropriate input control (e.g. a type="number" if the model is going to be a number)
  • providing appropriate formatters/parsers on the input control to ensure that the model going in and the model coming out are a consistent type.

If anything the bug is that we don't do a round trip triggering the ng-change if there are no validators.

@petebacondarwin
Copy link
Contributor

The trouble with this fix is that we are assuming that the only conversion is to and from a string. If there are formatters/parsers in the NgModelController that do other kinds of conversion (such as two and from Date objects) then you would still get the same problem.

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

If the user puts a number into a model that is attached to a text input box then that input box will convert the value to a string, which effectively changes the model and so triggers ng-change.

I understand what we're doing. But this is crap, we should not be doing this.

We can't "not translate the model", we need the model to be a string for other reasons --- but we should not be shooting off the change listeners when there's no good reason to.

Lets stop coming up with non-solutions, because they're just going to cause the framework to continue to be absolute crap

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

We are in agreement that the forms story in Angular has become complete nonsense, but there are things we can't change, and there are things we can --- this is one of the things we can change without clobbering the rest of the system

@petebacondarwin
Copy link
Contributor

We can't "not translate the model", we need the model to be a string for other reasons --- but we should not be shooting off the change listeners when there's no good reason to.

Can you explain what you mean by "we need the model to be a string for other reasons"? Am I missing something here?

As I see it the problem is that the stringBasedInputType is not symmetric - it has a formatter that converts the model to a string but no corresponding parser. If we intend to pass some non-string to such an input we ought to provide this corresponding parser.

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

Can you explain what you mean by "we need the model to be a string for other reasons"? Am I missing something here?

We format string input types to a string value so that we can be sure all of the builtin validators work correctly for them. Not formatting breaks them there --- and requires us to use really awful hacks that sometimes work and sometimes don't.

It's not clear why the modelValue is being set to this, though.

@Narretz
Copy link
Contributor

Narretz commented Nov 5, 2014

We should find the exact reasons for having the string formatter in text input types. The commit doesn't tell us why: 1eda183

As far as I remember, here's why: In #7968, we decided to use the $viewValue for the length validation. However, if the model is set programatically to a number, or $setViewValue is called with a number, validating the $viewValue for length doesn't work.
The pragmatic solution I propose is to convert the value to string inside the validator, and remove the string formatter. There is no other known use to it.

Regarding the $viewChangeListener - from its name it's called when the view / control changes. But this is not correct - it only changes when the viewValue and the modelValue change. And I think we should keep it that way: fire it only when the change in the modelValue is driven from the input (user). If the change is model -> view -> validator ???, don't fire it.

@caitp
Copy link
Contributor Author

caitp commented Nov 5, 2014

We should find the exact reasons for having the string formatter in text input types. The commit doesn't tell us why: 1eda183

We know why --- we need them for a variety of the builtin validator attributes for string input types.

What doesn't make sense, is why the model value is getting updated this way. View value is fine, but the model value should not be getting stringified

@Narretz
Copy link
Contributor

Narretz commented Nov 6, 2014

for a variety of the builtin validator attributes for string input types

This is too vague. I linked to the issue where the length validation is discussed; no other validation operations are mentioned. I really don't want to make decisions based on these vague statements.

Here's how the modelValue becomes string:

  • model -> value sets viewValue = modelValue (model)
  • formatters are run: for text, $viewValue = $$lastCommittedViewValue = string(viewValue)
  • $$runValidators: this does not set the model or anything, so the string doesn't matter
  • after that, the $observer in the maxlength directive is fired, which calls $validate
  • that calls $$parseAndValidate which takes the $$lastCommittedViewValue as starting viewValue
  • this viewValue is then run through parser to become the modelValue
  • however, the check for model change is done with modelValue !== ctrl.$modelValue
    -> ctrl.$modelValue at this point is still a number, but the parse modelValue is a string
    -> update

Now why this only happens if the input is in an ng-repeat? It looks like the ng-repeat changes the timing or the initialization order of the directives. And because $validate before the model is initialized does nothing, the whole parsing shenanigans never happens.

The order of function calls with ng-repeat:

model -> value
$$run type: number
observe
validate type: number
actual validate type: number
$$parse type: string
$$run type: string
change fired

The order without ng-repeat

observe
validate type: number
model -> value
$$run type: number

see: http://plnkr.co/edit/EIgrUCxN9Sglr5qkfD7G?p=preview
So, looks like we have plenty to work with :) Though I have to catch some sleep now ...

@caitp
Copy link
Contributor Author

caitp commented Nov 6, 2014

It's really not very fun, the forms story has become incredibly stupid and frustrating, and we can't really fix it :/

@caitp caitp modified the milestones: 1.3.2, 1.3.3 Nov 7, 2014
@caitp caitp modified the milestones: 1.3.3, 1.3.2 Nov 7, 2014
Narretz added a commit to Narretz/angular.js that referenced this pull request Nov 13, 2014
Previously, $validate would execute the parsers to obtain a
modelValue for validation. This was necessary, because a validator
that is called outside of model / view update (e.g. from an observer)
otherwise might only an undefined modelValue, because a previous
view update has found a validation $error and set the model
to undefined (as is tradition in angular)

This is problematic as validators that are run immediately after
the ngModelController initializes would parse the modelValue
and replace the model, even though there had been no user input.

The solution is to go back to an older design: the ngModelController
will now internally record the $$rawModelValue. This means a model
or view update will store the set / parsed modelValue regardless
of validity, that is, it will never set it to undefined because of
validation errors.

When $validate is called, the $$rawModelValue will passed to the
validators. If the validity has changed, the usual behavior is kept:
if it became invalid, set the model to undefined, if valid,
restore the last available modelValue - the $$rawModelValue.

Additionally, $validate will only update the model when the validity
changed. This is to prevent setting initially invalid models other
than undefined to undefined (see angular#9063)

Fixes: angular#9063
Fixes: angular#9959
Fixes: angular#9996
Fixes: angular#10025

Closes: angular#9890
Closes: angular#9913
Closes: angular#9997
Narretz added a commit to Narretz/angular.js that referenced this pull request Nov 13, 2014
Previously, $validate would execute the parsers to obtain a
modelValue for validation. This was necessary, because a validator
that is called outside of model / view update (e.g. from an observer)
otherwise might only an undefined modelValue, because a previous
view update has found a validation $error and set the model
to undefined (as is tradition in angular)

This is problematic as validators that are run immediately after
the ngModelController initializes would parse the modelValue
and replace the model, even though there had been no user input.

The solution is to go back to an older design: the ngModelController
will now internally record the $$rawModelValue. This means a model
or view update will store the set / parsed modelValue regardless
of validity, that is, it will never set it to undefined because of
validation errors.

When $validate is called, the $$rawModelValue will passed to the
validators. If the validity has changed, the usual behavior is kept:
if it became invalid, set the model to undefined, if valid,
restore the last available modelValue - the $$rawModelValue.

Additionally, $validate will only update the model when the validity
changed. This is to prevent setting initially invalid models other
than undefined to undefined (see angular#9063)

Fixes: angular#9063
Fixes: angular#9959
Fixes: angular#9996
Fixes: angular#10025

Closes: angular#9890
Closes: angular#9913
Closes: angular#9997
Narretz added a commit to Narretz/angular.js that referenced this pull request Nov 13, 2014
Previously, $validate would execute the parsers to obtain a
modelValue for validation. This was necessary, because a validator
that is called outside of model / view update (e.g. from an observer)
otherwise might only an undefined modelValue, because a previous
view update has found a validation $error and set the model
to undefined (as is tradition in angular)

This is problematic as validators that are run immediately after
the ngModelController initializes would parse the modelValue
and replace the model, even though there had been no user input.

The solution is to go back to an older design: the ngModelController
will now internally record the $$rawModelValue. This means a model
or view update will store the set / parsed modelValue regardless
of validity, that is, it will never set it to undefined because of
validation errors.

When $validate is called, the $$rawModelValue will passed to the
validators. If the validity has changed, the usual behavior is kept:
if it became invalid, set the model to undefined, if valid,
restore the last available modelValue - the $$rawModelValue.

Additionally, $validate will only update the model when the validity
changed. This is to prevent setting initially invalid models other
than undefined to undefined (see angular#9063)

Fixes: angular#9063
Fixes: angular#9959
Fixes: angular#9996
Fixes: angular#10025

Closes: angular#9890
Closes: angular#9913
Closes: angular#9997
Narretz added a commit to Narretz/angular.js that referenced this pull request Nov 15, 2014
Previously, $validate would execute the parsers to obtain a
modelValue for validation. This was necessary, because a validator
that is called outside of model / view update (e.g. from an observer)
otherwise might only an undefined modelValue, because a previous
view update has found a validation $error and set the model
to undefined (as is tradition in angular)

This is problematic as validators that are run immediately after
the ngModelController initializes would parse the modelValue
and replace the model, even though there had been no user input.

The solution is to go back to an older design: the ngModelController
will now internally record the $$rawModelValue. This means a model
or view update will store the set / parsed modelValue regardless
of validity, that is, it will never set it to undefined because of
validation errors.

When $validate is called, the $$rawModelValue will passed to the
validators. If the validity has changed, the usual behavior is kept:
if it became invalid, set the model to undefined, if valid,
restore the last available modelValue - the $$rawModelValue.

Additionally, $validate will only update the model when the validity
changed. This is to prevent setting initially invalid models other
than undefined to undefined (see angular#9063)

Fixes: angular#9063
Fixes: angular#9959
Fixes: angular#9996
Fixes: angular#10025

Closes: angular#9890
Closes: angular#9913
Closes: angular#9997
Narretz added a commit to Narretz/angular.js that referenced this pull request Nov 17, 2014
Previously, $validate would execute the parsers to obtain a
modelValue for validation. This was necessary, because a validator
that is called outside of model / view update (e.g. from an observer)
otherwise might only an undefined modelValue, because a previous
view update has found a validation $error and set the model
to undefined (as is tradition in angular)

This is problematic as validators that are run immediately after
the ngModelController initializes would parse the modelValue
and replace the model, even though there had been no user input.

The solution is to go back to an older design: the ngModelController
will now internally record the $$rawModelValue. This means a model
or view update will store the set / parsed modelValue regardless
of validity, that is, it will never set it to undefined because of
validation errors.

When $validate is called, the $$rawModelValue will passed to the
validators. If the validity has changed, the usual behavior is kept:
if it became invalid, set the model to undefined, if valid,
restore the last available modelValue - the $$rawModelValue.

Additionally, $validate will only update the model when the validity
changed. This is to prevent setting initially invalid models other
than undefined to undefined (see angular#9063)

Fixes: angular#9063
Fixes: angular#9959
Fixes: angular#9996
Fixes: angular#10025

Closes: angular#9890
Closes: angular#9913
Closes: angular#9997
Closes: angular#10048
Narretz added a commit to Narretz/angular.js that referenced this pull request Nov 17, 2014
Previously, $validate would execute the parsers to obtain a
modelValue for validation. This was necessary, because a validator
that is called outside of model / view update (e.g. from an observer)
otherwise might only an undefined modelValue, because a previous
view update has found a validation $error and set the model
to undefined (as is tradition in angular)

This is problematic as validators that are run immediately after
the ngModelController initializes would parse the modelValue
and replace the model, even though there had been no user input.

The solution is to go back to an older design: the ngModelController
will now internally record the $$rawModelValue. This means a model
or view update will store the set / parsed modelValue regardless
of validity, that is, it will never set it to undefined because of
validation errors.

When $validate is called, the $$rawModelValue will passed to the
validators. If the validity has changed, the usual behavior is kept:
if it became invalid, set the model to undefined, if valid,
restore the last available modelValue - the $$rawModelValue.

Additionally, $validate will only update the model when the validity
changed. This is to prevent setting initially invalid models other
than undefined to undefined (see angular#9063)

Fixes: angular#9063
Fixes: angular#9959
Fixes: angular#9996
Fixes: angular#10025

Closes: angular#9890
Closes: angular#9913
Closes: angular#9997
Closes: angular#10048
@petebacondarwin petebacondarwin modified the milestones: 1.3.3, 1.3.4 Nov 17, 2014
@Narretz
Copy link
Contributor

Narretz commented Nov 23, 2014

Closed by e3764e3

@Narretz Narretz closed this Nov 23, 2014
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Function in ng-change is fired before input is touched.
4 participants