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

feat(ngList): support whitespace control through ngTrim #8216

Closed
wants to merge 3 commits 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
132 changes: 81 additions & 51 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -2349,62 +2349,92 @@ var minlengthDirective = function() {
* @name ngList
*
* @description
* Text input that converts between a delimited string and an array of strings. The delimiter
* can be a fixed string (by default a comma) or a regular expression.
* Text input that converts between a delimited string and an array of strings. The default
* delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
* delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
*
* @element input
* @param {string=} ngList optional delimiter that should be used to split the value. If
* specified in form `/something/` then the value will be converted into a regular expression.
* The behaviour of the directive is affected by the use of the `ngTrim` attribute.
* * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
* list item is respected. This implies that the user of the directive is responsible for
* dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
* tab or newline character.
* * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
* when joining the list items back together) and whitespace around each list item is stripped
* before it is added to the model.
*
* @example
<example name="ngList-directive" module="listExample">
<file name="index.html">
<script>
angular.module('listExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.names = ['igor', 'misko', 'vojta'];
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
List: <input name="namesInput" ng-model="names" ng-list required>
<span class="error" ng-show="myForm.namesInput.$error.required">
Required!</span>
<br>
<tt>names = {{names}}</tt><br/>
<tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
<tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
var listInput = element(by.model('names'));
var names = element(by.binding('{{names}}'));
var valid = element(by.binding('myForm.namesInput.$valid'));
var error = element(by.css('span.error'));

it('should initialize to model', function() {
expect(names.getText()).toContain('["igor","misko","vojta"]');
expect(valid.getText()).toContain('true');
expect(error.getCssValue('display')).toBe('none');
});

it('should be invalid if empty', function() {
listInput.clear();
listInput.sendKeys('');

expect(names.getText()).toContain('');
expect(valid.getText()).toContain('false');
expect(error.getCssValue('display')).not.toBe('none'); });
</file>
</example>
* ### Example with Validation
*
* <example name="ngList-directive" module="listExample">
* <file name="app.js">
* angular.module('listExample', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.names = ['morpheus', 'neo', 'trinity'];
* }]);
* </file>
* <file name="index.html">
* <form name="myForm" ng-controller="ExampleController">
* List: <input name="namesInput" ng-model="names" ng-list required>
* <span class="error" ng-show="myForm.namesInput.$error.required">
* Required!</span>
* <br>
* <tt>names = {{names}}</tt><br/>
* <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
* <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
* <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
* <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
* </form>
* </file>
* <file name="protractor.js" type="protractor">
* var listInput = element(by.model('names'));
* var names = element(by.binding('{{names}}'));
* var valid = element(by.binding('myForm.namesInput.$valid'));
* var error = element(by.css('span.error'));
*
* it('should initialize to model', function() {
* expect(names.getText()).toContain('["morpheus","neo","trinity"]');
* expect(valid.getText()).toContain('true');
* expect(error.getCssValue('display')).toBe('none');
* });
*
* it('should be invalid if empty', function() {
* listInput.clear();
* listInput.sendKeys('');
*
* expect(names.getText()).toContain('');
* expect(valid.getText()).toContain('false');
* expect(error.getCssValue('display')).not.toBe('none');
* });
* </file>
* </example>
*
* ### Example - splitting on whitespace
* <example name="ngList-directive-newlines">
* <file name="index.html">
* <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
* <pre>{{ list | json }}</pre>
* </file>
* <file name="protractor.js" type="protractor">
* it("should split the text by newlines", function() {
* var listInput = element(by.model('list'));
* var output = element(by.binding('{{ list | json }}'));
* listInput.sendKeys('abc\ndef\nghi');
* expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
* });
* </file>
* </example>
*
* @element input
* @param {string=} ngList optional delimiter that should be used to split the value.
*/
var ngListDirective = function() {
return {
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
var match = /\/(.*)\//.exec(attr.ngList),
separator = match && new RegExp(match[1]) || attr.ngList || ',';
// We want to control whitespace trimming so we use this convoluted approach
// to access the ngList attribute, which doesn't pre-trim the attribute
var ngList = element.attr(attr.$attr.ngList) || ', ';
var trimValues = attr.ngTrim !== 'false';
var separator = trimValues ? trim(ngList) : ngList;

var parse = function(viewValue) {
// If the viewValue is invalid (say required but empty) it will be `undefined`
Expand All @@ -2414,7 +2444,7 @@ var ngListDirective = function() {

if (viewValue) {
forEach(viewValue.split(separator), function(value) {
if (value) list.push(trim(value));
if (value) list.push(trimValues ? trim(value) : value);
});
}

Expand All @@ -2424,7 +2454,7 @@ var ngListDirective = function() {
ctrl.$parsers.push(parse);
ctrl.$formatters.push(function(value) {
if (isArray(value)) {
return value.join(', ');
return value.join(ngList);
}

return undefined;
Expand Down
69 changes: 56 additions & 13 deletions test/ng/directive/inputSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2633,7 +2633,7 @@ describe('input', function() {
it("should not clobber text if model changes due to itself", function() {
// When the user types 'a,b' the 'a,' stage parses to ['a'] but if the
// $parseModel function runs it will change to 'a', in essence preventing
// the user from ever typying ','.
// the user from ever typing ','.
compileInput('<input type="text" ng-model="list" ng-list />');

changeInputValueTo('a ');
Expand Down Expand Up @@ -2671,26 +2671,69 @@ describe('input', function() {
expect(inputElm).toBeValid();
});

describe('with a custom separator', function() {
it('should split on the custom separator', function() {
compileInput('<input type="text" ng-model="list" ng-list=":" />');

it('should allow custom separator', function() {
compileInput('<input type="text" ng-model="list" ng-list=":" />');
changeInputValueTo('a,a');
expect(scope.list).toEqual(['a,a']);

changeInputValueTo('a,a');
expect(scope.list).toEqual(['a,a']);
changeInputValueTo('a:b');
expect(scope.list).toEqual(['a', 'b']);
});

changeInputValueTo('a:b');
expect(scope.list).toEqual(['a', 'b']);

it("should join the list back together with the custom separator", function() {
compileInput('<input type="text" ng-model="list" ng-list=" : " />');

scope.$apply(function() {
scope.list = ['x', 'y', 'z'];
});
expect(inputElm.val()).toBe('x : y : z');
});
});

describe('(with ngTrim undefined or true)', function() {

it('should allow regexp as a separator', function() {
compileInput('<input type="text" ng-model="list" ng-list="/:|,/" />');
it('should ignore separator whitespace when splitting', function() {
compileInput('<input type="text" ng-model="list" ng-list=" | " />');

changeInputValueTo('a,b');
expect(scope.list).toEqual(['a', 'b']);
changeInputValueTo('a|b');
expect(scope.list).toEqual(['a', 'b']);
});

it('should trim whitespace from each list item', function() {
compileInput('<input type="text" ng-model="list" ng-list="|" />');

changeInputValueTo('a,b: c');
expect(scope.list).toEqual(['a', 'b', 'c']);
changeInputValueTo('a | b');
expect(scope.list).toEqual(['a', 'b']);
});
});

describe('(with ngTrim set to false)', function() {

it('should use separator whitespace when splitting', function() {
compileInput('<input type="text" ng-model="list" ng-trim="false" ng-list=" | " />');

changeInputValueTo('a|b');
expect(scope.list).toEqual(['a|b']);

changeInputValueTo('a | b');
expect(scope.list).toEqual(['a','b']);

});

it("should not trim whitespace from each list item", function() {
compileInput('<input type="text" ng-model="list" ng-trim="false" ng-list="|" />');
changeInputValueTo('a | b');
expect(scope.list).toEqual(['a ',' b']);
});

it("should support splitting on newlines", function() {
compileInput('<textarea type="text" ng-model="list" ng-trim="false" ng-list="&#10;"></textarea');
changeInputValueTo('a\nb');
expect(scope.list).toEqual(['a','b']);
});
});
});

Expand Down