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

f[eat|ix](select): support values added by ngValue #13962

Merged
merged 4 commits into from
Jul 1, 2016

Conversation

Narretz
Copy link
Contributor

@Narretz Narretz commented Feb 6, 2016

select elements with ngModel will now set ngModel to option values added by ngValue.
This allows setting values of any type without the use of ngOptions.

Interpolations inside attributes can only be strings, but the ngValue directive uses attrs.$set,
which does not follow any type restriction. Any $observe on the value attribute will therefore receive
the original value (result of ngValue expression). However, when a user selects an option, the browser
sets the select value to the actual option's value attribute, which is still always a string.
For that reason, when option are added by ngValue, we set the hashed value of the original value in
the value attribute and store the actual value in an extra map. When the select value changes, we
read access the actual value via the hashed select value.

Closes #9842
Closes #6297

  • Test that it takes precedence over interpolated option / option text
  • Test for multiple selections
  • Fix failing Chrome tests
  • Docs

Talking points:

  • The implementation works reasonably well, although it's a bit hackyspecial
  • With ngValue, option values are hash values, not the real values (which makes sense, but might be confusing)
  • afaik angular 2 suffers from the same problem as ng1 right now

scope.selected = 'UPDATEDVALUE';
scope.$digest();

// expect(element.find('option').eq(0).prop('selected')).toBe(true); not selected in Chrome?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to fix this, but I'm not sure it's necessary. I don't think browsers have to set selected when the value of the select changes. It's strange though that is set before changing the select value via model update.

@petebacondarwin
Copy link
Contributor

This test fails:

      it('should work update the value as a result of changes to the options', function() {
        var A = { name: 'A'}, B = { name: 'B'}, C = { name: 'C'};
        scope.options = [A, B, C];
        scope.obj = {};
        compile(
          '<select ng-model="obj.value">' +
            '<option ng-repeat="option in options" ng-value="option">{{$index}}</option>' +
          '</select>'
        );

        var optionElements = element.find('option');
        expect(optionElements.length).toEqual(4);
        browserTrigger(optionElements.eq(0));

        optionElements = element.find('option');
        expect(optionElements.length).toEqual(3);
        expect(scope.obj.value).toBe(A);

        scope.options.shift();
        scope.$digest();

        optionElements = element.find('option');
        expect(optionElements.length).toEqual(3);
        expect(scope.obj.value).toBe(null);
      });

@Narretz
Copy link
Contributor Author

Narretz commented May 18, 2016

Have you tried if that works with regular interpolated options? Because I don't think it does

@petebacondarwin
Copy link
Contributor

It certainly works with ngOptions, which is what we are trying to emulate, right?

@Narretz Narretz force-pushed the feat-select-ngvalue branch 2 times, most recently from 518a8a6 to 7270826 Compare May 24, 2016 18:37
@Narretz
Copy link
Contributor Author

Narretz commented May 24, 2016

@petebacondarwin I've updated the PR again with the changes we talked about (support for disabled options, and model updates when the options change / are removed / get disabled / get added).

handleMultipleChanges = false;
self.ngModelCtrl.$setViewValue(self.readValue());
if (renderAfter) self.ngModelCtrl.$render();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

This pattern of running something once in the postDigest keeps appearing. I wonder if we should abstract it out into $scope?

@petebacondarwin
Copy link
Contributor

I think you might have nailed it but it is the kind of change that I want to spend some time thinking about corner cases...

@Narretz Narretz force-pushed the feat-select-ngvalue branch 2 times, most recently from abafb21 to 7bdc6b8 Compare June 7, 2016 14:33
@Narretz Narretz force-pushed the feat-select-ngvalue branch 2 times, most recently from 738af0b to 06e905e Compare June 20, 2016 14:35
@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm.

1 similar comment
@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm.

@petebacondarwin
Copy link
Contributor

I did author some of these commits and I am OK with that :-)

@@ -14,10 +14,15 @@ var SelectController =
['$element', '$scope', function($element, $scope) {

var self = this,
optionsMap = new HashMap();
optionsMap = new HashMap(),
handleMultipleDestroy = false; // Flag to run an update to the model after selected options
Copy link
Contributor

Choose a reason for hiding this comment

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

no longer needed?

@Narretz
Copy link
Contributor Author

Narretz commented Jun 23, 2016

I'll change the tests and then add some docs. Should I mentiond that performance in IE is going to be very bad with 3000+ options?

@petebacondarwin
Copy link
Contributor

Why not

@Narretz
Copy link
Contributor Author

Narretz commented Jun 29, 2016

While fixing up the PR, I found a behavior in ngOptions and select that I find a bit confusing: You have a select that is multiple and the model contains two values, one of which is matched by an option, while the other isn't.
What happens is that the matching option gets selected, and the model keeps both values. Imo, I would expect no option to be selected, as the model isn't completely matched by the select options. Thoughts?

@Narretz
Copy link
Contributor Author

Narretz commented Jun 29, 2016

So in #13172 we decided that if no option at all can be selected, then the select should be invalid (required).

Doesn't say anything about a partial mismatch, though. We could decide not to select anything when not all options are matched, but I'm not convinced that this is a good idea.

Theoretically, setting the model to match the available options is the developers responsibility, so if that happens it's your own fault. However, it's not very easy to check if a select's options will match the model. We could expose the selectCtrl.hasOption function and make it work with ngOptions, so that users could manually check if a specific $viewValue is matched by their select, see (#12915)

@Narretz
Copy link
Contributor Author

Narretz commented Jun 30, 2016

I'm also fine with making a decision on this later. As far as I can tell, the described behavior hasn't bothered anyone yet.
@petebacondarwin I need your perf commit as part of the commit that handles multiple selects correctly. Because otherwise, the current behavior doesn't work as expected. If the model has two values set, but only one is matched by options, then without the "perf" fix, the option will set the model to the one value only. Delaying the update after render handles this.

@Narretz Narretz force-pushed the feat-select-ngvalue branch 2 times, most recently from 128f6e8 to 91cafff Compare July 1, 2016 10:29
Narretz and others added 4 commits July 1, 2016 13:00
select elements with ngModel will now set ngModel to option values added by ngValue.
This allows setting values of any type (not only strings) without the use of ngOptions.

Interpolations inside attributes can only be strings, but the ngValue directive uses attrs.$set,
which does not have any type restriction. Any $observe on the value attribute will therefore receive
the original value (result of ngValue expression). However, when a user selects an option, the browser
sets the select value to the actual option's value attribute, which is still always a string.
For that reason, when options are added by ngValue, we set the hashed value of the original value in
the value attribute and store the actual value in an extra map. When the select value changes, we
read access the actual value via the hashed select value.

Since we only use a hashed value for ngValue, we will have extra checks for the hashed values:
- when options are read, for both single and multiple select
- when options are written, for multiple select

I don't expect this to have a performance impact, but it should be kept in mind.

Closes angular#9842
Closes angular#6297

BREAKING CHANGE:

`<option>` elements added to `<select ng-model>` via `ngValue` now add their values in hash form, i.e.
`<option ng-value="myString">` becomes `<option ng-value="myString" value="string:myString">`.

This is done to support binding options with values of any type to selects.

This should rarely affect applications, as the values of options are usually not relevant to the
application logic, but it's possible that option values are checked in tests.
These rules follow ngOptions behavior:

- when an option that is currently selected, is removed or its value changes, the model
is set to null.
- when an an option is added or its value changes to match the currently selected model,
this option is selected.
- when an option is disabled, the model is set to null.
- when the model value changes to a value that matches a disabled option,
this option is selected (analogue to ngOptions)
@Narretz Narretz force-pushed the feat-select-ngvalue branch from 91cafff to 7b50f49 Compare July 1, 2016 11:03
@Narretz Narretz merged commit 7b50f49 into angular:master Jul 1, 2016
@gkalpak
Copy link
Member

gkalpak commented Jul 1, 2016

✨ 🎉 ✨

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.

5 participants