diff --git a/docs/content/error/$compile/missingattr.ngdoc b/docs/content/error/$compile/missingattr.ngdoc new file mode 100644 index 000000000000..1fb2a346b4a2 --- /dev/null +++ b/docs/content/error/$compile/missingattr.ngdoc @@ -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. diff --git a/src/ng/compile.js b/src/ng/compile.js index 44b641bd3f17..615e42516fc1 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -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; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 4fa14d2daff0..e6036555627c 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -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: '' }; }); + directive('prototypeMethodNameAsScopeVarD', function() { + return { + scope: { + 'constructor': '' + }; + }); 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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($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( + '
' + )($rootScope); + }; + expect(func).not.toThrow(); + }); + }); + it('should not throw exception when using "watch" as binding in Firefox', inject( function($rootScope, $compile) { $rootScope.watch = 'watch';