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

Align jqLite's attr method with jQuery #15181

Merged
merged 10 commits into from
Oct 6, 2016
Merged

Align jqLite's attr method with jQuery #15181

merged 10 commits into from
Oct 6, 2016

Conversation

mgol
Copy link
Member

@mgol mgol commented Sep 23, 2016

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Refactors & breaking API changes.

What is the current behavior? (You can also link to an open issue here)
Boolean attribute getters take properties into account and setters modify them. All falsy values for boolean attribute setters remove the attribute and set the property to false. The null value in a setter (for all attributes, not only boolean ones) sets the attribute value to the string "null" instead of removing it.

What is the new behavior (if this is a feature change)?
Boolean attribute getters/setters don't touch properties. The null value in a setter (for all attributes, not only boolean ones) removes the attribute. The false value removes it only for boolean attributes. Other defined values for boolean attributes set the value to the lowercased attribute name.

Does this PR introduce a breaking change?
Yes.

Please check if the PR fulfills these requirements

Other information:

@Narretz
Copy link
Contributor

Narretz commented Sep 26, 2016

There are a few failures when running the tests with jquery 2.2, which is probably expected. How do we handle this?

@mgol mgol force-pushed the jquery branch 2 times, most recently from 6eeb9fa to bd0d5a5 Compare September 28, 2016 12:21
@mgol
Copy link
Member Author

mgol commented Sep 28, 2016

@Narretz I fixed the test: I'm just skipping that one in jQuery 2.x as this was a jQuery 3.0 change.

BTW, most other changes here are aligning even with older jQuery versions, this is not just about 3.0 changes.

@mgol
Copy link
Member Author

mgol commented Sep 29, 2016

Tests pass, PR ready for review.

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

I have left a couple of (mostly minor) comments.

One thing that I believe is not pointed out explicitly enough in the BC notices is the fact that boolean attribute getters do not rely on the value of the corresponding property (as was previously the case).

From the commit message:

Previously, all boolean attributes were reflected into properties in a setter and from a property in a getter, even on elements that don't treat those attributes in a special way. Now it's only happening for the elements where browsers do the conversion automatically.

This sounds to me like boolean attribute getters will still take the property into account, but only on browsers that do the conversion automatically (which is not the case).

@@ -635,6 +635,56 @@ describe('jqLite', function() {
expect(select.attr('multiple')).toBe('multiple');
});

it('should set respective properties when getting/setting boolean attributes', function() {
Copy link
Member

@gkalpak gkalpak Sep 30, 2016

Choose a reason for hiding this comment

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

As far as I understand, it is actually the browser that does that, not jqLite/.attr().
We should either remove the test (if we don't consider it to be jqLite's job), or make sure jqLite always sets the properties (instead of relying on the browsers to do it).

Copy link
Member

Choose a reason for hiding this comment

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

Also, getting seems not related here.

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO it often does make sense to test things like that; in a way that we should have had a test like that in 1.5.x and it should remain passing after we remove our own reflection. In this way we'd know if a buggy browser broke it and we need to patch it ourselves.

But maybe here we wouldn't do the patching either way so I'd be fine either way.

Copy link
Member

@gkalpak gkalpak Sep 30, 2016

Choose a reason for hiding this comment

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

I was under the impression that we wouldn't do the patching even if a browser/version failed to do the right thing. If the plan is to do the patching in that case, then I would rather be on the afe side and set the properties anyway. (But if jQuery doesn't do it, I think it is better for jqLite not to do it either.)

I don't feel strongly about keeping the test around, but a different description that makes it clear we expect the browser to do it would probably make things a little easier for our future selves, if something breaks 😃

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I removed the test.

expect(div.attr('multiple')).toBe('multiple');
});

it('should not set properties when settting respective boolean attributes', function() {
Copy link
Member

Choose a reason for hiding this comment

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

Too much t in setttings 😃

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

@@ -638,33 +638,32 @@ forEach({
},

attr: function(element, name, value) {
var ret;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I prefer declaring vars in the block in which they are used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Then we have a conflict as I prefer them where they're really declared by the browsers. :P

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer small functions so that there is not much difference between the two approaches :-P

// setter

if (value === null || (value === false && isBooleanAttr)) {
element.removeAttribute(name);
Copy link
Member

Choose a reason for hiding this comment

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

Previously, for boolean attributes, we used the lowercasedName. Does it make a difference?

Copy link
Member Author

Choose a reason for hiding this comment

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

It shouldn't. Also, jQuery removes name, not lowercasedName.

} else {
// getter

ret = element.getAttribute(name);
Copy link
Member

Choose a reason for hiding this comment

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

Is there any chance getAttribute(name) could return undefined (on any browser)?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so but if that happened then what do you propose? jQuery only checks for null to convert it to undefined, if a browser returned undefined we'd already have what we want.

Copy link
Member

Choose a reason for hiding this comment

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

This line was using a != null check, so I assumed undefined is a possible return value. If that is the case, then we shouldn't set ret = lowercasedName when ret === undefined, but afaict getAttribute() shouldn't ever return undefined (in which case we are fine).

}
} else if (isDefined(value)) {
element.setAttribute(name, value);
} else if (element.getAttribute) {
Copy link
Member

Choose a reason for hiding this comment

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

Previously, there was this element.getAttribute check, which is now removed.
Was it necessary for IE<9 browsers only? It would be useful to have a comment (e.g. in the PR) explaining why it is not necessary any more.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. It's needed for window doesn't have getAttribute. In such cases jQuery falls back from attr to prop; should we do the same here?

Copy link
Member

Choose a reason for hiding this comment

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

SGTM

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Copy link
Member Author

Choose a reason for hiding this comment

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

It was still failing in a setter which jQuery doesn't so I fixed that as well. I decided to not fall back to prop for now, though, but just return as this feature is not very well tested & documented in jQuery.

} else if (element.getAttribute) {
// the extra argument "2" is to get the right thing for a.href in IE, see jQuery code
// some elements (e.g. Document) don't have get attribute, so return undefined
var ret = element.getAttribute(name, 2);
Copy link
Member

Choose a reason for hiding this comment

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

Same as above: It would be useful to have a comment (e.g. in the PR) explaining why this is not necessary any more (assuming it isn't 😛).

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -56,7 +56,7 @@
* - [`after()`](http://api.jquery.com/after/)
* - [`append()`](http://api.jquery.com/append/)
* - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
* - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
* - (_deprecated_, use `on()`) [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
Copy link
Member Author

Choose a reason for hiding this comment

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

Is that a good way to mark methods as deprecated? Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's how it looks like:
screen shot 2016-10-05 at 12 42 29

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps put the deprecated after the method name?

  • bind() (deprecated, use on()) - ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good. It looked better in the code but in the screenshot my order looks a bit off, I agree.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed.

Copy link
Member

Choose a reason for hiding this comment

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

We should alse:

  1. Document deprecations with the release in which they will be removed (usually the next minor release).
  2. Place an issue in the appropriate milestone to remind us to do so.

@@ -56,7 +56,7 @@
* - [`after()`](http://api.jquery.com/after/)
* - [`append()`](http://api.jquery.com/append/)
* - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
* - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
* - (_deprecated_, use `on()`) [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
Copy link
Member

Choose a reason for hiding this comment

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

We should alse:

  1. Document deprecations with the release in which they will be removed (usually the next minor release).
  2. Place an issue in the appropriate milestone to remind us to do so.

@@ -2256,4 +2334,19 @@ describe('jqLite', function() {
expect(onLoadCallback).toHaveBeenCalledOnce();
});
});


describe('bind()/unbind()', function() {
Copy link
Member

Choose a reason for hiding this comment

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

Super-nit: Other descriptions don't have parenthesis 😁

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't worry about it

Copy link
Member Author

Choose a reason for hiding this comment

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

Already changed locally! :D

@petebacondarwin
Copy link
Contributor

LGTM

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

LGTM
(Don't forget the issue for removing `bind/unbind' 😃)

@gkalpak
Copy link
Member

gkalpak commented Oct 5, 2016

(Also, feel free to tweak the commit message as per #15181 (review) 😃.)

@petebacondarwin
Copy link
Contributor

@mgol - feel free to tweak as suggested by @gkalpak and then merge. Thanks for your work on this.

@mgol
Copy link
Member Author

mgol commented Oct 6, 2016

Removal issue: #15221.

mgol added 10 commits October 6, 2016 12:15
…butes

jQuery supports removing multiple attributes in one go, jqLite doesn't.
This is now documented.
This is done automatically by browsers in cases where it's needed; the
workaround was only needed for IE<9. The new behavior means boolean attributes
will not be reflected on elements where browsers don't reflect them.

This change aligns jqLite with jQuery 3.

Fixes angular#14126

BREAKING CHANGE: Previously, all boolean attributes were reflected into
properties in a setter and from a property in a getter, even on elements that
don't treat those attributes in a special way. Now Angular doesn't do it
by itself but relies on browsers to know when to reflect the property. Note that
this browser-level conversions differs between browsers; if you need to change
dynamic state of an element you should modify the property, not the attribute.

See https://jquery.com/upgrade-guide/1.9/#attr-versus-prop- for a more detailed
description about a related change in jQuery 1.9.

To migrate the code follow the example below:

Before:

CSS:
input[checked="checked"] { ... }

JS:
elem1.attr('checked', 'checked');
elem2.attr('checked', false);

After:

CSS:
input:checked { ... }

JS:
elem1.prop('checked', true);
elem2.prop('checked', false);
This change aligns jqLite with jQuery.

Also, the extra `2` second parameter to `setAttribute` has been removed;
it was only needed for IE<9 and latest jQuery doesn't pass it either.

Ref angular#15126

BREAKING CHANGE: Invoking `elem.attr(attributeName, null)` would set the
`attributeName` atribute value to a string `"null"`, now it removes the
attribute instead.

To migrate the code follow the example below:

Before:

elem.attr(attributeName, null);

After:

elem.attr(attributeName, "null");
This change aligns jqLite with jQuery.

Ref angular#15126

BREAKING CHANGE: Before, using the `attr` method with an empty string as a value
would remove the boolean attribute. Now it sets it to its lowercase name as
was happening for every non-empty string so far. The only two values that remove
the boolean attribute are now null & false, just like in jQuery.

To migrate the code follow the example below:

Before:

elem.attr(booleanAttrName, '');

After:

elem.attr(booleanAttrName, false);

or:

elem.attr(booleanAttrName, null);
The attr method was refactored to be divided into setter & getter parts
and to handle boolean attributes in each one of them separately instead of
dividing into boolean & non-boolean ones and then handling setter & getter
in both of them. This is because handling boolean & non-boolean attributes
has common parts; in particular handling of the `null` value or using
getAttribute to get the value in the getter.
The bind/unbind aliases to on/off were being assinged in every iteration
of the function assigning traversal methods to the prototype. Now it happens
only once.
The on/off aliases have been available since Angular 1.2. bind/unbind have
been deprecated in jQuery 3.0 so we're following suit in jqLite.
jQuery falls back to prop here but this feature is not very well tested
& documented so let's just skip it here.

Closes angular#15181
@mgol mgol merged commit 4e6c14d into angular:master Oct 6, 2016
@mgol mgol deleted the jquery branch October 6, 2016 10:48
ellimist pushed a commit to ellimist/angular.js that referenced this pull request Mar 15, 2017
jQuery falls back to prop here but this feature is not very well tested
& documented so let's just skip it here.

Closes angular#15181
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