-
Notifications
You must be signed in to change notification settings - Fork 27.5k
feat($compile): add support for arbitrary property and event bindings #16614
Conversation
There was a problem hiding this 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 🎉
src/ng/compile.js
Outdated
* | ||
* Defines the security context for HTML properties bound by ng-prop-* | ||
* | ||
* @param {string} the context type |
There was a problem hiding this comment.
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.)
src/ng/compile.js
Outdated
*/ | ||
(function setupPropertyContexts() { | ||
function registerContext(ctx, values) { | ||
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; }); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
src/ng/compile.js
Outdated
* @param {string} the element name or '*' to match any element | ||
* @param {string} the property name | ||
*/ | ||
this.addPropertyContext = function(ctx, elementName, propertyName) { |
There was a problem hiding this comment.
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 😁
There was a problem hiding this comment.
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
src/ng/compile.js
Outdated
'object|codebase', 'object|data', | ||
'script|src', | ||
]); | ||
})(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...?
There was a problem hiding this comment.
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...).
There was a problem hiding this comment.
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.
src/ng/compile.js
Outdated
'ng- versions (such as ng-click or ng-on-click instead of ng-prop-onclick) instead.'); | ||
} | ||
|
||
var nodeName = nodeName_(node); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done (jbedard@bd63689)
src/ng/compile.js
Outdated
scope.$watch(fn, function propertyWatchActionFn(value) { | ||
if (value) { | ||
// Sanitize img[srcset] + source[srcset] values. | ||
if ((nodeName === 'img' || nodeName === 'source') && propName === 'srcset') { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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...
src/ng/directive/ngEventDirs.js
Outdated
function createEventDirective($parse, $rootScope, attrName, eventName, forceAsync) { | ||
return { | ||
restrict: 'A', | ||
priority: 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the explicit 0
?
81d1529
to
bd63689
Compare
test/ng/compileSpec.js
Outdated
$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'); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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?
test/ng/compileSpec.js
Outdated
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();'); |
There was a problem hiding this comment.
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...
test/ng/compileSpec.js
Outdated
expect(element.prop('src')).toEqual('someuntrustedthing:foo();'); | ||
})); | ||
|
||
it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) { |
There was a problem hiding this comment.
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...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete?
There was a problem hiding this comment.
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?
src/ng/compile.js
Outdated
} | ||
|
||
if (isNgProp) { | ||
attrs[ngAttrName] = value; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems right to me.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
test/ng/compileSpec.js
Outdated
}); | ||
}); | ||
|
||
it('should use $$sanitizeUri on concatenated trusted values', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete?
test/ng/compileSpec.js
Outdated
expect(element.prop('src')).toEqual('someuntrustedthing:foo();'); | ||
})); | ||
|
||
it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) { |
There was a problem hiding this comment.
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?
test/ng/compileSpec.js
Outdated
expect(element.prop('src')).toEqual('unsafe:untrusted:foo();untrusted:foo();'); | ||
})); | ||
|
||
it('should not sanitize other properties', inject(function($compile, $rootScope) { |
There was a problem hiding this comment.
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.
test/ng/compileSpec.js
Outdated
}); | ||
|
||
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) { | ||
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope); |
There was a problem hiding this comment.
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"
?
src/ng/compile.js
Outdated
} | ||
|
||
if (isNgProp) { | ||
attrs[ngAttrName] = value; |
There was a problem hiding this comment.
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.
There was a problem hiding this 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. |
There was a problem hiding this comment.
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 | ||
``` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which docs specifically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addPropertyContext
docs 😁
There was a problem hiding this comment.
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.
src/ng/compile.js
Outdated
* @name $compileProvider#addPropertyContext | ||
* @description | ||
* | ||
* Defines the security context for HTML properties bound by ng-prop-* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about SVG properties 😁
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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 🤓).
There was a problem hiding this comment.
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?
src/ng/compile.js
Outdated
* | ||
* @param {string} elementName the element name or '*' to match any element | ||
* @param {string} propertyName the property name | ||
* @param {string} ctx the context type |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
.
src/ng/compile.js
Outdated
'object|codebase', 'object|data', | ||
'script|src', | ||
]); | ||
})(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the IIFE?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 😛
There was a problem hiding this comment.
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 😛
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/ng/compile.js
Outdated
ngAttrName = directiveNormalize(name); | ||
isNgAttr = NG_ATTR_BINDING.test(ngAttrName); | ||
if (isNgAttr) { | ||
if (ngPrefixMatch = ngAttrName.match(NG_PREFIX_BINDING)) { |
There was a problem hiding this comment.
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?
src/ng/compile.js
Outdated
} | ||
|
||
if (isNgProp) { | ||
attrs[ngAttrName] = value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems right to me.
src/ng/compile.js
Outdated
|
||
function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { | ||
var trustedContext = getTrustedContext(node, name); | ||
var tag = nodeName_(node); |
There was a problem hiding this comment.
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
😁
There was a problem hiding this comment.
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
...
src/ng/compile.js
Outdated
return { | ||
pre: function ngPropPreLinkFn(scope, $element) { | ||
scope.$watch(fn, function propertyWatchActionFn(value) { | ||
$element.prop(propName, value && sanitizer(value)); |
There was a problem hiding this comment.
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).
test/ng/compileSpec.js
Outdated
expect($rootScope.name).toBe('Misko3'); | ||
})); | ||
|
||
it("should use angular.element(x).on() API to add listener", inject(function($compile, $rootScope) { |
There was a problem hiding this comment.
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
😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤣
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. |
WRT performance I am not too concerned. The only changes effecting existing apps should be
I don't think this is worth a flag IMO, at least not just for performance reasons (based on my assumptions that is). |
src/ng/compile.js
Outdated
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; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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.)
src/ng/compile.js
Outdated
|
||
/** | ||
* @ngdoc method | ||
* @name $compileProvider#addPropertyContext |
There was a problem hiding this comment.
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?
src/ng/compile.js
Outdated
* - STYLE => CSS | ||
* - various URL => MEDIA_URL | ||
*/ | ||
(function setupPropertyContexts() { |
There was a problem hiding this comment.
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?
src/ng/compile.js
Outdated
|
||
if (isNgProp) { | ||
attrs[ngAttrName] = value; | ||
addPropertyDirective(node, directives, ngAttrName, name); |
There was a problem hiding this comment.
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?
src/ng/compile.js
Outdated
directives.push({ | ||
priority: 100, | ||
compile: function ngPropCompileFn(_, attr) { | ||
var fn = $parse(attr[attrName]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 $parse
ing of ng-event
and ng-on-*
which all read the attribute at compile time (although interpolation then reads it again at link time :|).
src/ng/directive/ngEventDirs.js
Outdated
}]; | ||
} | ||
); | ||
|
||
function createEventDirective($parse, $rootScope, attrName, eventName, forceAsync) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
src/ng/compile.js
Outdated
addPropertyDirective(node, directives, ngAttrName, name); | ||
} else if (isNgEvent) { | ||
attrs[ngAttrName] = value; | ||
addEventDirective(directives, ngAttrName, name); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
Why? What is the problem with having attributes reflected on |
Similar to what @gkalpak said, I think I think this means that |
Well I never! OK so I relent on my campaign to remove it. I'll take a look at the security stuff and tests later today. |
Added a few more fixups including a fix for the |
if (isNgAttr || !attrs.hasOwnProperty(nName)) { | ||
if (isNgProp || isNgEvent) { | ||
attrs[nName] = value; | ||
attrsMap[nName] = attr.name; |
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
- https://github.com/angular/angular.js/blob/v1.7.2/src/ng/compile.js#L2258 - on the raw attribute name
- https://github.com/angular/angular.js/blob/v1.7.2/src/ng/compile.js#L2274 - on the no-prefix attribute name, but in all lower case. This lower casing causes the mapping to be
myFormaction
instead ofmyFormAction
Is this a bug? Or a feature and we should be copying for ng-prop
? 🤔
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #16624
test/ng/ngPropSpec.js
Outdated
describe('*[style]', function() { | ||
// Support: ?? | ||
// Some browsers throw when assignging to HTMLElement.style | ||
function canAssingStyleProp() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: canAssingStyleProp
test/ng/ngPropSpec.js
Outdated
}); | ||
|
||
describe('*[style]', function() { | ||
// Support: ?? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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...
0a5c935
to
9c461b3
Compare
…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
I've addressed the typo + support-?? issues, updated the commit message, rebased and squashed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯 x 🎉
There is a typo in the commit message:
The following also sounds a little confusing (since there are no capital letters in the attribute to put the underscore before 😁):
|
9c461b3
to
6795251
Compare
…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
6795251
to
84eb9f8
Compare
…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
84eb9f8
to
7f7ab85
Compare
…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
I've broken up the commit message into <100 char lines. Fixed the events sentance.
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. |
I also noticed that some double-quotes in the commit message are fancier than others ( I would expand a bit on the "binding to camel case" thing. Maybe something like:
|
7f7ab85
to
06ba791
Compare
…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
I've updated the message again |
I still see at least on fancy quote: |
06ba791
to
129d5dc
Compare
…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
Fixed the quotes again |
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
129d5dc
to
dedb10c
Compare
…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
Here's the initial work for arbitrary property and event bindings. I think
ng-attr-*
andng-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
TODO:
updategetTrustedAttrContext
to the"el|attr"
style (likegetTrustedPropContext
) to reuse code?add ability to add new attribute contexts?addPropertySecurityContext
(fae5066)ngProp
to do preassign in prelink (13bab70)ng-on-*
docs (docs(ngProp, ngOn) #16627)ng-prop-*
docs (make note of feat($compile): add support for arbitrary property and event bindings #16614 (comment)) (docs(ngProp, ngOn) #16627)*|style
(04224c7)<source ng-prop-srcset="">
tests similar to<img ng-prop-srcset="">
(d163243)source[srcset]
attribute change (ef07536)ng-attr-*
that are interpolate specific (1e6b068)ng-attr-broken_camels
issue (ng-attr-* entry in$attrs
has incorrect casing #16624, referenced in tests 58d4c16)reduce size of sce prop IIFEseems unnecessary, see feat($compile): add support for arbitrary property and event bindings #16614 (comment)