feat($compile): add strictComponentBindingsEnabled() method

Closes #16129
This commit is contained in:
Zita Nemeckova
2017-07-25 12:31:07 +02:00
committed by Georgios Kalpakas
parent 01d6a47e91
commit f1d01bbc74
3 changed files with 272 additions and 0 deletions
@@ -0,0 +1,8 @@
@ngdoc error
@name $compile:missingattr
@fullName Missing required attribute
@description
This error may occur only when `$compileProvider.strictComponentBindingsEnabled` is set to `true`.
Then all attributes mentioned in `bindings` without `?` must be set. If one or more aren't set,
the first one will throw an error.
+41
View File
@@ -1403,6 +1403,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
/**
* @ngdoc method
* @name $compileProvider#strictComponentBindingsEnabled
*
* @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, otherwise just return the
* current strictComponentBindingsEnabled state
* @returns {*} current value if used as getter or itself (chaining) if used as setter
*
* @kind function
*
* @description
* Call this method to enable/disable strict component bindings check. If enabled, the compiler will enforce that
* for all bindings of a component that are not set as optional with `?`, an attribute needs to be provided
* on the component's HTML tag.
*
* The default value is false.
*/
var strictComponentBindingsEnabled = false;
this.strictComponentBindingsEnabled = function(enabled) {
if (isDefined(enabled)) {
strictComponentBindingsEnabled = enabled;
return this;
}
return strictComponentBindingsEnabled;
};
var TTL = 10;
/**
* @ngdoc method
@@ -3413,12 +3439,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
function strictBindingsCheck(attrName, directiveName) {
if (strictComponentBindingsEnabled) {
throw $compileMinErr('missingattr',
'Attribute \'{0}\' of \'{1}\' is non-optional and must be set!',
attrName, directiveName);
}
}
// Set up $watches for isolate scope and controller bindings.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
var initialChanges = {};
var changes;
forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -3430,7 +3464,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
case '@':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
strictBindingsCheck(attrName, directive.name);
destination[scopeName] = attrs[attrName] = undefined;
}
removeWatch = attrs.$observe(attrName, function(value) {
if (isString(value) || isBoolean(value)) {
@@ -3457,6 +3493,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
case '=':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
strictBindingsCheck(attrName, directive.name);
attrs[attrName] = undefined;
}
if (optional && !attrs[attrName]) break;
@@ -3501,6 +3538,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
case '<':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
strictBindingsCheck(attrName, directive.name);
attrs[attrName] = undefined;
}
if (optional && !attrs[attrName]) break;
@@ -3526,6 +3564,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
case '&':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
strictBindingsCheck(attrName, directive.name);
}
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
+223
View File
@@ -169,6 +169,15 @@ describe('$compile', function() {
inject();
});
it('should allow strictComponentBindingsEnabled to be configured', function() {
module(function($compileProvider) {
expect($compileProvider.strictComponentBindingsEnabled()).toBe(false); // the default
$compileProvider.strictComponentBindingsEnabled(true);
expect($compileProvider.strictComponentBindingsEnabled()).toBe(true);
});
inject();
});
it('should allow onChangesTtl to be configured', function() {
module(function($compileProvider) {
expect($compileProvider.onChangesTtl()).toBe(10); // the default
@@ -2546,6 +2555,16 @@ describe('$compile', function() {
template: '<span></span>'
};
});
directive('prototypeMethodNameAsScopeVarD', function() {
return {
scope: {
'constructor': '<?',
'valueOf': '<'
},
restrict: 'AE',
template: '<span></span>'
};
});
directive('watchAsScopeVar', function() {
return {
scope: {
@@ -2854,6 +2873,57 @@ describe('$compile', function() {
})
);
it('should throw an error for undefined non-optional "=" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-a></div>'
)($rootScope);
};
expect(func).toThrowMinErr('$compile',
'missingattr',
'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
'ScopeVarA\' is non-optional and must be set!');
});
});
it('should not throw an error for set non-optional "=" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-a constructor="constructor" value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should not throw an error for undefined optional "=" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-a value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should handle "@" bindings with same method names in Object.prototype correctly when not present', inject(
function($rootScope, $compile) {
var func = function() {
@@ -2891,6 +2961,57 @@ describe('$compile', function() {
})
);
it('should throw an error for undefined non-optional "@" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-b></div>'
)($rootScope);
};
expect(func).toThrowMinErr('$compile',
'missingattr',
'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
'ScopeVarB\' is non-optional and must be set!');
});
});
it('should not throw an error for set non-optional "@" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-b constructor="constructor" value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should not throw an error for undefined optional "@" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-b value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should handle "&" bindings with same method names in Object.prototype correctly when not present', inject(
function($rootScope, $compile) {
var func = function() {
@@ -2923,6 +3044,108 @@ describe('$compile', function() {
})
);
it('should throw an error for undefined non-optional "&" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-c></div>'
)($rootScope);
};
expect(func).toThrowMinErr('$compile',
'missingattr',
'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
'ScopeVarC\' is non-optional and must be set!');
});
});
it('should not throw an error for set non-optional "&" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-c constructor="constructor" value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should not throw an error for undefined optional "&" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-c value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should throw an error for undefined non-optional "<" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-d></div>'
)($rootScope);
};
expect(func).toThrowMinErr('$compile',
'missingattr',
'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
'ScopeVarD\' is non-optional and must be set!');
});
});
it('should not throw an error for set non-optional "<" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-d constructor="constructor" value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should not throw an error for undefined optional "<" bindings when ' +
'strictComponentBindingsEnabled is true', function() {
module(function($compileProvider) {
$compileProvider.strictComponentBindingsEnabled(true);
});
inject(
function($rootScope, $compile) {
var func = function() {
element = $compile(
'<div prototype-method-name-as-scope-var-d value-of="valueOf"></div>'
)($rootScope);
};
expect(func).not.toThrow();
});
});
it('should not throw exception when using "watch" as binding in Firefox', inject(
function($rootScope, $compile) {
$rootScope.watch = 'watch';