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

feat(directive): ngName #6569

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
ngModelOptionsDirective,
ngAttributeAliasDirectives,
ngEventDirectives,
ngNameDirective,

$AnchorScrollProvider,
$AnimateProvider,
Expand Down Expand Up @@ -197,7 +198,8 @@ function publishExternalAPI(angular){
maxlength: maxlengthDirective,
ngMaxlength: maxlengthDirective,
ngValue: ngValueDirective,
ngModelOptions: ngModelOptionsDirective
ngModelOptions: ngModelOptionsDirective,
ngName: ngNameDirective
}).
directive({
ngInclude: ngIncludeFillContentDirective
Expand Down
110 changes: 110 additions & 0 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2510,3 +2510,113 @@ var ngModelOptionsDirective = function() {
}]
};
};

/**
* @ngdoc directive
* @name ngName
*
* @priority 100
*
* @description
* The `ng-name` directive allows you the ability to evaluate scope expressions on the name attribute.
* This directive does not react to the scope expression. It merely evaluates the expression, and sets the
* {@link ngModel.NgModelController NgModelController}'s `$name` property and the `<input>` element's
* `name` attribute.
*
* <div class="alert alert-info">
* This is particularly useful when building forms while looping through data with `ng-repeat`,
* allowing you evaluate expressions such as as control names.
* </div>
*
* <div class="alert alert-warning">
* This is <strong>NOT</strong> a data binding, in the sense that the attribute is not observed nor is the scope
* expression watched. The ngName directive's link function runs after the ngModelController but before ngModel's
* link function. This allows the evaluated result to be updated to the $name property prior to the
* {@link form.FormController FormController}'s `$addControl` function being called.
* </div>
*
* @element input
* @param {expression} ngName {@link guide/expression Expression} to evaluate.
*
* @example
<example name="ngName-directive">
<file name="index.html">
<div ng-controller="Ctrl">
<form name="candyForm">
<h2>Enter the amount of candy you want.</h2>
<div ng-repeat="c in candy">
<label for="{{ c.type }}">{{ c.type }}</label>
<input type="text"
ng-model="c.qty"
ng-name="c.type + 'Qty'"
id="{{ c.type }}"
ng-pattern="/^([1-9][0-9]*|0)$/"
required>
</div>
</form>
<br>
<div ng-repeat="c in candy">
<div><b>{{ c.type }}</b></div>
<div>candyForm.{{ c.type + 'Qty' }}.$valid = <b ng-bind="candyForm.{{ c.type + 'Qty' }}.$valid"></b></div>
<div>Quantity = <b>{{ c.qty }}</b></div>
<br>
</div>
</div>
</file>
<file name="script.js">
function Ctrl($scope) {
$scope.candy = [
{
type: 'chocolates',
qty: null
},
{
type: 'peppermints',
qty: null
},
{
type: 'lollipops',
qty: null
}
];
}
</file>
<file name="protractor.js" type="protractor">
var chocolatesInput = element(by.id('chocolates'));
var chocolatesValid = element(by.binding('candyForm.chocolatesQty.$valid'));
var peppermintsInput = element(by.id('peppermints'));
var peppermintsValid = element(by.binding('candyForm.peppermintsQty.$valid'));
var lollipopsInput = element(by.id('lollipops'));
var lollipopsValid = element(by.binding('candyForm.lollipopsQty.$valid'));

it('should initialize controls properly', function() {
expect(chocolatesValid.getText()).toBe('false');
expect(peppermintsValid.getText()).toBe('false');
expect(lollipopsValid.getText()).toBe('false');
});

it('should be valid when entering n >= 0', function() {
chocolatesInput.sendKeys('5');
peppermintsInput.sendKeys('55');
lollipopsInput.sendKeys('555');

expect(chocolatesValid.getText()).toBe('true');
expect(peppermintsValid.getText()).toBe('true');
expect(lollipopsValid.getText()).toBe('true');
});
</file>
</example>
*/
var ngNameDirective = function() {
return {
priority: 100,
restrict: 'A',
require: 'ngModel',
link: {
pre: function ngNameLinkFn(scope, elem, attrs, ctrl) {
ctrl.$name = scope.$eval(attrs.ngName);
attrs.$set('name', ctrl.$name);
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 believe this will work in certain versions of IE

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@petebacondarwin Wouldn't the Selenium Tests caught that? What versions do you think are affected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

<carried from outdated diff>

@petebacondarwin said: I don't believe this will work in certain versions of IE

@sjbarker said: @petebacondarwin Wouldn't the Selenium Tests <catch> that? What versions do you think are affected?

}
}
};
};
142 changes: 142 additions & 0 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2884,6 +2884,148 @@ describe('input', function() {
expect(scope.items[0].selected).toBe(false);
});
});

describe('ngName', function() {

it('should set name attribute and property on element', function() {
scope.controlNamespace = 'test';
scope.something = 'modelVal';
compileInput('<input type="text" ng-model="something" ng-name="controlNamespace + \'Control\'">');
expect(inputElm[0].name).toBe('testControl');
expect(inputElm[0].getAttribute('name')).toBe('testControl');
});

it('should set the $name property on the ngModelController', function() {
scope.controlName = 'test';
scope.something = 'modelVal';
compileInput('<input type="text" ng-model="something" ng-name="controlName">');

var ctrl = inputElm['data']('$ngModelController');
expect(ctrl.$name).toBe('test');
});

it('should set have set the controller $name prior to adding the control to the form controller', function() {
scope.controlNamespace = 'test';
scope.something = 'modelVal';
compileInput('<input type="text" ng-model="something" ng-name="controlNamespace + \'Control\'">');

expect(scope.form.testControl).toBeDefined();
});

it('should work inside ngRepeat', function() {
compileInput('<div ng-repeat="p in packages">' +
'<input type="checkbox" ng-model="p.isDelivered" ng-name="\'isDelivered\' + p.id">' +
'</div>');

scope.$apply(function() {
scope.packages = [
{
id: 0,
isDelivered: false
},
{
id: 1,
isDelivered: false
},
{
id: 2,
isDelivered: false
}
];
});

inputElm = formElm.find('input');
var ctrls = [];
forEach(inputElm, function(val) {
ctrls.push(jqLite(val)['data']('$ngModelController'));
});

expect(inputElm[0].name).toBe('isDelivered0');
expect(inputElm[0].getAttribute('name')).toBe('isDelivered0');
expect(ctrls[0].$name).toBe('isDelivered0');
expect(scope.form.isDelivered0).toBeDefined();

expect(inputElm[1].name).toBe('isDelivered1');
expect(inputElm[1].getAttribute('name')).toBe('isDelivered1');
expect(ctrls[1].$name).toBe('isDelivered1');
expect(scope.form.isDelivered1).toBeDefined();

expect(inputElm[2].name).toBe('isDelivered2');
expect(inputElm[2].getAttribute('name')).toBe('isDelivered2');
expect(ctrls[2].$name).toBe('isDelivered2');
expect(scope.form.isDelivered2).toBeDefined();

scope.$apply(function() {
scope.packages = [
{
id: 0,
isDelivered: false
},
{
id: 2,
isDelivered: false
}
];
});

inputElm = formElm.find('input');
ctrls = [];
forEach(inputElm, function(val) {
ctrls.push(jqLite(val)['data']('$ngModelController'));
});

expect(inputElm[0].name).toBe('isDelivered0');
expect(inputElm[0].getAttribute('name')).toBe('isDelivered0');
expect(ctrls[0].$name).toBe('isDelivered0');
expect(scope.form.isDelivered0).toBeDefined();

expect(inputElm[1].name).toBe('isDelivered2');
expect(inputElm[1].getAttribute('name')).toBe('isDelivered2');
expect(ctrls[1].$name).toBe('isDelivered2');
expect(scope.form.isDelivered2).toBeDefined();

expect(scope.form.isDelivered1).toBeUndefined();
});

it('should not affect the control or form when the evaluated result changes', function() {
compileInput('<div ng-repeat="p in packages">' +
'<input type="checkbox" ng-model="p.isDelivered" ng-name="\'isDelivered\' + p.id">' +
'</div>');

scope.$apply(function() {
scope.packages = [
{
id: 0,
isDelivered: false
},
{
id: 1,
isDelivered: false
}
];
});

inputElm = formElm.find('input');
var ctrls = [];
forEach(inputElm, function(val) {
ctrls.push(jqLite(val)['data']('$ngModelController'));
});

expect(ctrls.length).toBe(2);
expect(ctrls[0].$name).toBe('isDelivered0');
expect(scope.form.isDelivered0).toBeDefined();

scope.$apply(function() {
scope.packages[0].id = 10;
});

expect(ctrls.length).toBe(2);
expect(ctrls[0].$name).toBe('isDelivered0');
expect(scope.form.isDelivered0).toBeDefined();
expect(scope.form.isDelivered10).toBeUndefined();
});

});
});

describe('NgModel animations', function() {
Expand Down