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

feat($compile): add support for arbitrary property and event bindings #16614

Merged
merged 2 commits into from
Aug 2, 2018

Conversation

jbedard
Copy link
Contributor

@jbedard jbedard commented Jun 26, 2018

Here's the initial work for arbitrary property and event bindings. I think ng-attr-* and ng-on-* are essentially ready. The main thing to do is the $sce logic which I'm very unfamiliar with (I've never used it!), so any help/feedback in that area would be great. Also docs...

Could also think about doing the ng-bindon-* as @gkalpak has been calling it (something equivalent to [(*)] in Angular).

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

TODO:

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 haven't looked at the tests, but the rest looks reasonable 🎉

*
* Defines the security context for HTML properties bound by ng-prop-*
*
* @param {string} the context type
Copy link
Member

Choose a reason for hiding this comment

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

{string} the context type --> {string} ctx the context type
(Same below.)

*/
(function setupPropertyContexts() {
function registerContext(ctx, values) {
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; });
Copy link
Member

Choose a reason for hiding this comment

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

Since this is an internal function, we can avoid the .toLowerCase() (and ensure values are lowercased).

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'd prefer keeping it so the properties below can have the standard property casing. This also makes the list of properties almost the exact same as https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58

* @param {string} the element name or '*' to match any element
* @param {string} the property name
*/
this.addPropertyContext = function(ctx, elementName, propertyName) {
Copy link
Member

Choose a reason for hiding this comment

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

If you really want this to be an add operation, you need to ensure that the key doesn not already exists 😁

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 added the ctxoverride error for this

'object|codebase', 'object|data',
'script|src',
]);
})();
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it is worth avoiding the size increase in core and move these to a separated module.

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'd say since we already have attribute sanitizing built in we should probably do the same for properties?

Copy link
Member

Choose a reason for hiding this comment

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

I wonder how much this affects the overall size. Since these are strings, they can't be minified, but there are less of them than I thought there would be and they probably compress fine. So, I don't think this is a problem, but am still curious about the size increase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a quick (and inaccurate) test, this setup method on it's own is 1.2kb and 0.7kb minfied. Should minify a bit better when not isolated and then gzip would help a bit more...

Copy link
Member

Choose a reason for hiding this comment

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

Size is a bit of a concern, especially because there might be very limited take up of this feature.
What if we put the core changes into angular.js but always throw an exception if the ngProp module is not loaded. Then we could move as much of this sanitization (and anything else auxiliary) into that module and keep the core changes to a minimum?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does that mean ngProp can not be used at all without that extra module though?

Copy link
Member

Choose a reason for hiding this comment

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

Yes. That would be the case. I think that might be reasonable since this is a specialised scenario that many AngularJS applications would not use...?

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'd really prefer if it was usable without adding another file/module, especially since that file will just be this property list... all the actual functionality is baked into $compile and will already be there. Although I guess the ng-on-* is the one that will be used more, so that one is more important to me to be baked in.

Is it mainly just the size?

I could probably make this a bit smaller (concatenate all the strings instead of arrays of strings...).

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 mentioned I'd try to make this minify better... looks like the closure compiler already does what I was thinking of (convert ['a', 'b', 'c'] to 'a b c'.split(' ')).

When minifying only this IIFE it is 371 bytes gzipped. When minifying the full library, then measuring just this IIFE it is 348 bytes gzipped (however it might have been minified to gzip better along with the rest of the app?).

So basically ~350 bytes is a good estimate.

'ng- versions (such as ng-click or ng-on-click instead of ng-prop-onclick) instead.');
}

var nodeName = nodeName_(node);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Since we are computing the name here, we could pass it to getTrustedPropContext() directly.

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 keep the signatures of getTrustedPropContext and getTrustedAttrContext to be the same. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

However it looks like getTrustedAttrContext is also called right beside a nodeName_(node) call like this, so I could change both...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

scope.$watch(fn, function propertyWatchActionFn(value) {
if (value) {
// Sanitize img[srcset] + source[srcset] values.
if ((nodeName === 'img' || nodeName === 'source') && propName === 'srcset') {
Copy link
Member

Choose a reason for hiding this comment

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

Since these values are available beforehand, we could create different watchActionsFns and avoid checking them over and over.

Copy link
Contributor Author

@jbedard jbedard Jun 28, 2018

Choose a reason for hiding this comment

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

I'll try this, but will also do it for attributes since the code is the exact same. Should have a fixup commit doing this shortly...

EDIT: nevermind attributes don't do the exact same (this condition is in $attr.set instead), so I'll make this change only here 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, but instead of different watchActionFns I created different sanitizer methods that a common watchActionFn invokes...

function createEventDirective($parse, $rootScope, attrName, eventName, forceAsync) {
return {
restrict: 'A',
priority: 0,
Copy link
Member

Choose a reason for hiding this comment

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

Why the explicit 0?

$rootScope.name = 'angular';
$rootScope.$apply();
log('digest=' + element.prop('myName'));
expect(log).toEqual('compile=undefined; preLinkP101=undefined; preLinkP0=undefined; postLink=undefined; postCompile=undefined; digest=angular');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's one I wasn't sure about...

With ng-attr-* the attribute is evaluated on pre-link so it is available to other link functions before the first $watch applies. I have not done that for ng-prop-*. Should we?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that's very important for properties. Listening to attributes is / was quite important in AngularJS, but with props that's different.

Copy link
Member

Choose a reason for hiding this comment

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

Just make sure it is documented, so that people don't assume it works the same as ngAttr.

Copy link
Member

Choose a reason for hiding this comment

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

But why not make it the same as ngAttr?

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 think for $attr it is important because properties are put into $attrs which is part of the directive compile/link API. But there is no equivalent API for properties. Unless you consider $element.prop/attr the API...

I think I'd vote to make it the same as ngAttr just for consistency. Unless someone can think of a case where assigning to a DOM property one extra time (init + $watch) would be an issue?

element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('someuntrustedthing:foo();');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that for the ng-attr- version of this test this string was someUnstrustedThing:foo(). But it seems like for properties the browser (at least some) change the protocol to lowercase when assigned to the property, so the expect below failed (which also has the mixed case for the attribute version). I changed both this line and the expect to be all lower case to avoid this since I don't think it matters here...

expect(element.prop('src')).toEqual('someuntrustedthing:foo();');
}));

it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this test and the next correct for expressions like this? I'm not sure if expressions vs interpolation are or should be handled different...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Delete?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think so. Was the consensus yesterday that ngProp can / should only consider the result of all expressions?

}

if (isNgProp) {
attrs[ngAttrName] = value;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this correct? Which attribute should have the `value? If any?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is attrs the attrs object that gets passed to the link fn / controller? I don't think ngProp should set the attribute here.

Copy link
Member

Choose a reason for hiding this comment

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

Seems right to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this attrs is what gets passed to the compile / link / controller.

Note that here we are assigning attrs.ngPropFoo, not attrs.foo like ng-attr-foo would. Also the value is the expression that will later get $parsed. While I agree it's weird putting anything prop related on attrs, I did this so we can...
a) use the existing createEventDirective from ngEventDirs.js
b) follow the same pattern as ngEventDirs.js and ng-attr-* where the value that gets $parse-ed (or $interpolate-ed for ng-attr) is read of the attribute in the compile step of the directive

I'm not sure how to achieve both of those without putting the expression onto attrs...

Ideas? Opinions?

Copy link
Member

Choose a reason for hiding this comment

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

In my comment above, I meant it seems fine to me to have attrs[ngAttrName] = value.
Even if ngProp* assigns a property, it is still an attribute on the DOM element, so I think it should be reflected on $attrs.

});
});

it('should use $$sanitizeUri on concatenated trusted values', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Delete?

expect(element.prop('src')).toEqual('someuntrustedthing:foo();');
}));

it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think so. Was the consensus yesterday that ngProp can / should only consider the result of all expressions?

expect(element.prop('src')).toEqual('unsafe:untrusted:foo();untrusted:foo();');
}));

it('should not sanitize other properties', inject(function($compile, $rootScope) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this is obsolete, too. It doesn't make sense in ngProp context at least.

});

it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be ng-prop-srcset="testUrl"?

}

if (isNgProp) {
attrs[ngAttrName] = value;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is attrs the attrs object that gets passed to the link fn / controller? I don't think ngProp should set the attribute here.

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.

A couple of thoughts:

  • We also need to document how people can bind to case-sensitive properties using case-insensitive attributes (e.g. ng-prop-inner_h_t_m_l) 😁

  • It would be interesting to see how the compiler performance is affected by the changes.

@fullName Context Override
@description

This error occurs when the security context for an attribute is defined multiple times under different security contexts.
Copy link
Member

Choose a reason for hiding this comment

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

attribute --> property (?)

```
$compileProvider.addPropertyContext("my-element", "src", "mediaUrl");
$compileProvider.addPropertyContext("my-element", "src", "resourceUrl"); //throws
```
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be useful to have a link to the docs somewhere in this error message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Which docs specifically?

Copy link
Member

Choose a reason for hiding this comment

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

The addPropertyContext docs 😁

Copy link
Contributor

Choose a reason for hiding this comment

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

We could and probably should create directive doc entries for ngProp and ngOn even though they are not real directives. NgAttr is mentioned in the attribute guide but that isn't so great either.

* @name $compileProvider#addPropertyContext
* @description
*
* Defines the security context for HTML properties bound by ng-prop-*
Copy link
Member

Choose a reason for hiding this comment

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

What about SVG properties 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like we normally say "HTML/SVG"?

Copy link
Member

Choose a reason for hiding this comment

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

I don't know. Maybe 😁
But ngProp should also work on SVG elements (which are first-class citizen in AngularJS 🤓).

Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is HTML at all, it should be "DOM properties" no?

*
* @param {string} elementName the element name or '*' to match any element
* @param {string} propertyName the property name
* @param {string} ctx the context type
Copy link
Member

Choose a reason for hiding this comment

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

It might be useful to link to $sce here (or else people would have no idea what contexts are supported).

Copy link
Member

Choose a reason for hiding this comment

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

The description of this parameter could be a bit more descriptive. This is what is on the $sce.trustAs method for a similar parameter:

The context in which this value is safe for use, e.g. $sce.URL,
$sce.RESOURCE_URL, $sce.HTML, $sce.JS or $sce.CSS.

'object|codebase', 'object|data',
'script|src',
]);
})();
Copy link
Member

Choose a reason for hiding this comment

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

Why the IIFE?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It hides the registerContext helper method, and I guess the main reason is to isolate the block of code (mostly) copied from Angular.

Copy link
Member

Choose a reason for hiding this comment

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

Hide from who? We don't this IIRC.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hide from you!

I guess I just like keeping the stuff copied from Angular in one place. Prevents people from using it or moving the helper method elsewhere etc.

I can drop the IIFE if people don't like it though...

Copy link
Member

@gkalpak gkalpak Jun 29, 2018

Choose a reason for hiding this comment

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

You keep talking about this registerContext helper method, but I don't see it 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perfect, the IIFE is hiding it well then 😛

Copy link
Member

Choose a reason for hiding this comment

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

Does the IIFE help the closure compiler to reduce the code size?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the IIFE help the closure compiler to reduce the code size?

I think since the full library is already in an IIFE this one doesn't help, or might even make it larger since the closure compiler probably doesn't remove this one... That is a valid concern that I agree might be worth removing the IIFE for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned in a different comment... the IIFE probably adds 5-10 bytes ("function(){}()".length + gzipping). The closure compiler does not remove it like I was hoping, I guess because it would cause some vars/functions to be exposed a level higher.

ngAttrName = directiveNormalize(name);
isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
if (isNgAttr) {
if (ngPrefixMatch = ngAttrName.match(NG_PREFIX_BINDING)) {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't there be an extra pair of parenthesis here, to indicate the assignment is intentional?

}

if (isNgProp) {
attrs[ngAttrName] = value;
Copy link
Member

Choose a reason for hiding this comment

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

Seems right to me.


function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
var trustedContext = getTrustedContext(node, name);
var tag = nodeName_(node);
Copy link
Member

Choose a reason for hiding this comment

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

Why not nodeName for consistency with addPropertyDirective 😁

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 think because getTrustedAttrContext used tag, but I agree and will update this one to nodeName...

return {
pre: function ngPropPreLinkFn(scope, $element) {
scope.$watch(fn, function propertyWatchActionFn(value) {
$element.prop(propName, value && sanitizer(value));
Copy link
Member

@gkalpak gkalpak Jun 29, 2018

Choose a reason for hiding this comment

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

The value && check is redundant. .prop(propName, sanitizer(value)) should be enough (and gives sanitizers a chance to decide whether a falsy value is safe or not).

expect($rootScope.name).toBe('Misko3');
}));

it("should use angular.element(x).on() API to add listener", inject(function($compile, $rootScope) {
Copy link
Member

Choose a reason for hiding this comment

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

...to add listener --> ...to add listener, so that Jason can hook his ngUpgrade hacks into it 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤣

@Narretz
Copy link
Contributor

Narretz commented Jun 29, 2018

Wrt compiler performance - I think we should put ngProp and ngOn behind a flag, similar to css class / comment directives. This way, there's no hit if you don't need it.

@jbedard
Copy link
Contributor Author

jbedard commented Jun 29, 2018

WRT performance I am not too concerned. The only changes effecting existing apps should be

  • the ng-attr-* regex is a bit more complicated (this runs on every attribute!)
  • if (isNgProp) { ... } else if (isNgOn) { ... } else { ... old code ... } was added, so basically adds 2 if (variable) statements

I don't think this is worth a flag IMO, at least not just for performance reasons (based on my assumptions that is).

var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
// support *-start / *-end multi element directives
} else if ((multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) {
attrStartName = name;
attrEndName = name.substr(0, name.length - 5) + 'end';
name = name.substr(0, name.length - 6);
}

nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this right for ng-on/prop? I don't think we'd want to "map" ng-on-foo to the foo attribute... Would we want to map it at all? To ngAttrName (still containing ng-on/prop prefix) instead of name?

Copy link
Member

Choose a reason for hiding this comment

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

It is fine imo. We should indeed not reflect the property/event as attribute, but keeping track of the ngProp/On prefixed attribute sounds reasonable and consistent (since the attribute is there).

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 think your comment is true regarding the attrs[ngAttrName] = value; a few lines down. But what about the "mapping" on this line?

This line creates the ngPropFoo => foo mapping for https://docs.angularjs.org/api/ng/type/$compile.directive.Attributes#$attr ...

Copy link
Member

Choose a reason for hiding this comment

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

Oh, you are right. I don't think we want to map foo to ngPropFoo.
(Mapping ng-prop-foo to ngPropFoo sounds right (for consistency), although it would probably have no effect.)


/**
* @ngdoc method
* @name $compileProvider#addPropertyContext
Copy link
Member

Choose a reason for hiding this comment

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

I think this method needs a more descriptive name. Context is a pretty ambiguous term. Perhaps it could be addPropertyTrustContext or addPropertySecurityContext or something?

* - STYLE => CSS
* - various URL => MEDIA_URL
*/
(function setupPropertyContexts() {
Copy link
Member

Choose a reason for hiding this comment

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

Tempting to name this function registerNativePropertyContexts or something similar?


if (isNgProp) {
attrs[ngAttrName] = value;
addPropertyDirective(node, directives, ngAttrName, name);
Copy link
Member

Choose a reason for hiding this comment

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

Why not pass value in here since we know it and it must be a static template string? That way you don't need to add the value to the attrs map?

directives.push({
priority: 100,
compile: function ngPropCompileFn(_, attr) {
var fn = $parse(attr[attrName]);
Copy link
Member

Choose a reason for hiding this comment

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

We could receive this value as a parameter to the addPropertyDirective and avoid adding it to the attrs map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that I wanted to read the attribute here at compile time so it is equivalent to attribute interpolation and $parseing of ng-event and ng-on-* which all read the attribute at compile time (although interpolation then reads it again at link time :|).

}];
}
);

function createEventDirective($parse, $rootScope, attrName, eventName, forceAsync) {
Copy link
Member

Choose a reason for hiding this comment

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

We could have a value param on this function which is used rather than attr[attrName] if it is defined.
That way you can avoid adding the value to the attrs map in the case of ng-on-, no?

Copy link
Contributor Author

@jbedard jbedard Jul 3, 2018

Choose a reason for hiding this comment

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

I wanted to avoid making changes to the existing event directives, and I thought making ng-on-* the same would also be good. In all those cases the attr[attrName] is not read until compile time, and those attributes do exist in the attrs map...

addPropertyDirective(node, directives, ngAttrName, name);
} else if (isNgEvent) {
attrs[ngAttrName] = value;
addEventDirective(directives, ngAttrName, name);
Copy link
Member

Choose a reason for hiding this comment

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

We could pass the value here as an extra param, and this function would know to use that rather than reading off the attrs map.

Copy link
Member

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

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

Maybe I am missing something but I feel really uncomfortable about re-using the $attr stuff for these property directives. I believe that a bit of refactoring would allow us to avoid putting these values on that map and give us tighter control over how these special directives are handled.

@gkalpak
Copy link
Member

gkalpak commented Jul 2, 2018

Why? What is the problem with having attributes reflected on $attrs? Why is it OK for $attrs.ngClick to be present, but not for $attrs.ngOnFooEvent?

@jbedard
Copy link
Contributor Author

jbedard commented Jul 3, 2018

Similar to what @gkalpak said, I think ngClick and ngOnClick should be equivalent other then the extra "on" in the attribute name. Similar for properties... ngPropDisabled and ngDisabled should be equivalent, except for interpolation vs expression (and the fact that ngDisabled does some to-boolean casting).

I think this means that $attrs will contain ngOnClick and ngPropDisabled, but will not contain click or disabled or anything like that...

@petebacondarwin
Copy link
Member

Well I never! ngClick does get added to the attrs. That is interesting...

OK so I relent on my campaign to remove it. I'll take a look at the security stuff and tests later today.

@jbedard
Copy link
Contributor Author

jbedard commented Jul 4, 2018

Added a few more fixups including a fix for the attr.$attr mappings (4995d0e) along with some tests that failed before it (the ng-attr test never failed, but seemed good to add at the same time).

if (isNgAttr || !attrs.hasOwnProperty(nName)) {
if (isNgProp || isNgEvent) {
attrs[nName] = value;
attrsMap[nName] = attr.name;
Copy link
Member

Choose a reason for hiding this comment

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

For reference, this is the difference between how ngAttr and ngProp attributes get mapped (and it looks reasonable to me):

// HTML: x-ng-attr-my-form_action
attrsMap['myFormaction'] = 'my-formAction'

// HTML: x-ng-prop-my-form_action
attrsMap['ngPropMyForm_action'] = 'x-ng-prop-my-form_action'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should the _s not behave the same?

I'll try to add some tests with _s for both ngAttr and ngProp

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've added a commit c032255 (or two 7835fc3) to test this.

The result is a bit different then you assumed for the ngProp case:

// HTML: x-ng-attr-my-form_action
attrsMap['myFormaction'] = 'my-formAction'

// HTML: x-ng-prop-my-form_action
attrsMap['ngPropMyFormAction'] = 'x-ng-prop-my-form_action'

Notice the capital A for the ng-prop one... I think that is actually more correct then the ng-attr one. This is because we invoke directiveNormalize in two different ways:

Is this a bug? Or a feature and we should be copying for ng-prop? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

I think that behaviour for ngAttr looks like a bit of a bug to me... especially given this test:

    it('should work if they are prefixed with x- or data-', inject(function($compile, $rootScope) {
      $rootScope.dimensions = '0 0 0 0';
      $rootScope.number = 0.42;
      $rootScope.scale = 1;
      element = $compile('<svg data-ng-attr-view_box="{{dimensions}}">' +
        '<filter x-ng-attr-filter_units="{{number}}">' +
        '<feDiffuseLighting data-ng:attr_surface_scale="{{scale}}">' +
        '</feDiffuseLighting>' +
        '<feSpecularLighting x-ng:attr_surface_scale="{{scale}}">' +
        '</feSpecularLighting></filter></svg>')($rootScope);
      expect(element.attr('viewBox')).toBeUndefined();
      $rootScope.$digest();
      expect(element.attr('viewBox')).toBe('0 0 0 0');
      expect(element.find('filter').attr('filterUnits')).toBe('0.42');
      expect(element.find('feDiffuseLighting').attr('surfaceScale')).toBe('1');
      expect(element.find('feSpecularLighting').attr('surfaceScale')).toBe('1');
    }));

suggests that element.attr('...') expects the attribute to be capitalised in the way we expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah... the element.attr('...') and the real attribute name are correct, it's only the nName variable which is what is used on $attrs that is wrong.

Should we fix it though? If anyone is depending on these bad names it would be hard to find+fix if we fixed the bug. However it would be nice to fix the bug...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created #16624

describe('*[style]', function() {
// Support: ??
// Some browsers throw when assignging to HTMLElement.style
function canAssingStyleProp() {
Copy link
Contributor

@Narretz Narretz Jul 26, 2018

Choose a reason for hiding this comment

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

typo: canAssingStyleProp

});

describe('*[style]', function() {
// Support: ??
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 we know which browser is this? IE9?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where I was looking all the browsers test output as randomly output mixed together so I couldn't tell. Are there per-browser logs somewhere?

See https://travis-ci.org/angular/angular.js/jobs/406066641#L750

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Although I would guess it is IE9 because 2 tests failed in IE9 and that error ("Attempted to assign to readonly property") occurred 2 times...

jbedard added a commit to jbedard/angular.js that referenced this pull request Jul 30, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as `my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM properties. For example `ng-prop-foo=”x”` will assign the value of the expression `x` to the `foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using such as `ng-click`, `ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example `ng-on-bar=”barOccured($event)”` will add a listener to the “bar” event and invoke the `barOccured($event)` expression.

For properties or events using mixed case underscores can be used before capital letters. For example `ng-prop-my_prop="x"` will bind `x` to the `myProp` property, and `ng-on-my_custom_event="x()"` will invoke `x()` on the `myCustomEvent` event.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
@jbedard
Copy link
Contributor Author

jbedard commented Jul 30, 2018

I've addressed the typo + support-?? issues, updated the commit message, rebased and squashed.

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.

💯 x 🎉

@gkalpak
Copy link
Member

gkalpak commented Jul 30, 2018

There is a typo in the commit message:

[...] events could be bound using such as ng-click [...]

The following also sounds a little confusing (since there are no capital letters in the attribute to put the underscore before 😁):

For properties or events using mixed case underscores can be used before capital letters.

jbedard added a commit to jbedard/angular.js that referenced this pull request Jul 30, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo=”x”` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using such as `ng-click`, `ng-blur` etc.
`ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar=”barOccured($event)”` will add a listener to the “bar” event and invoke the
`barOccured($event)` expression.

For properties or events using mixed case underscores can be used before capital letters. For
example `ng-prop-my_prop="x"` will bind `x` to the `myProp` property, and
`ng-on-my_custom_event="x()"` will invoke `x()` on the `myCustomEvent` event.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
jbedard added a commit to jbedard/angular.js that referenced this pull request Jul 30, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo=”x”` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound such as `ng-click`, `ng-blur` etc.
`ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar=”barOccured($event)”` will add a listener to the “bar” event and invoke the
`barOccured($event)` expression.

For properties or events using mixed case, underscores can be used before capital letters. For
example `ng-prop-my_prop="x"` will bind `x` to the `myProp` property, and
`ng-on-my_custom_event="x()"` will invoke `x()` on the `myCustomEvent` event.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
jbedard added a commit to jbedard/angular.js that referenced this pull request Jul 30, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo=”x”` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar=”barOccured($event)”` will add a listener to the “bar” event and invoke the
`barOccured($event)` expression.

For properties or events using mixed case, underscores can be used before capital letters. For
example `ng-prop-my_prop="x"` will bind `x` to the `myProp` property, and
`ng-on-my_custom_event="x()"` will invoke `x()` on the `myCustomEvent` event.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
@jbedard
Copy link
Contributor Author

jbedard commented Jul 30, 2018

I've broken up the commit message into <100 char lines. Fixed the events sentance.

The following also sounds a little confusing (since there are no capital letters in the attribute to put the underscore before 😁):

I added a comma in there. Is that clearer?

Thanks for reviewing the commit message. Let me know if you find any other issues or things that could be clearer or added.

@gkalpak
Copy link
Member

gkalpak commented Jul 31, 2018

I also noticed that some double-quotes in the commit message are fancier than others ( vs "). It's good to have non-fancy quotes everywhere.

I would expand a bit on the "binding to camel case" thing. Maybe something like:

Since HTML attributes are case-insensitive, you need to convert property and event names to snake_case for ng-prop-* and ng-on-*. For example, to bind to the fooBar property use ng-prop-foo_bar...

jbedard added a commit to jbedard/angular.js that referenced this pull request Jul 31, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the “bar" event and invoke the
`barOccured($event)` expression.

Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
@jbedard
Copy link
Contributor Author

jbedard commented Jul 31, 2018

I've updated the message again

@gkalpak
Copy link
Member

gkalpak commented Jul 31, 2018

I still see at least on fancy quote: “bar" 😁
Other than that looks great 💯
:shipit:

jbedard added a commit to jbedard/angular.js that referenced this pull request Jul 31, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the "bar" event and invoke the
`barOccured($event)` expression.

Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
@jbedard
Copy link
Contributor Author

jbedard commented Aug 1, 2018

Fixed the quotes again

@Narretz
Copy link
Contributor

Narretz commented Aug 1, 2018

Travis is green, looks like we're good to go!

…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the "bar" event and invoke the
`barOccured($event)` expression.

Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.

Fixes angular#16428
Fixes angular#16235
Closes angular#16614
@jbedard jbedard merged commit dedb10c into angular:master Aug 2, 2018
Narretz pushed a commit that referenced this pull request Aug 3, 2018
…ings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the "bar" event and invoke the
`barOccured($event)` expression.

Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.

Fixes #16428
Fixes #16235
Closes #16614
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.

None yet

6 participants