From 07c3d13ff51e9594567bc7360fabedfa52a4e141 Mon Sep 17 00:00:00 2001 From: Robert Messerle Date: Fri, 1 May 2015 15:34:27 -0700 Subject: [PATCH] feat(autocomplete): adds support for ng-messages BREAKING CHANGE: `name` attribute has been changed to `input-name` to more clearly show its purpose --- src/components/autocomplete/autocomplete.scss | 16 +-- .../autocomplete/autocomplete.spec.js | 40 +++++- .../autocomplete/demoFloatingLabel/index.html | 22 --- .../index.html | 33 +++++ .../script.js | 2 +- .../autocomplete/js/autocompleteController.js | 4 +- .../autocomplete/js/autocompleteDirective.js | 134 +++++++++++++----- src/components/autocomplete/js/parentScope.js | 6 +- 8 files changed, 185 insertions(+), 72 deletions(-) delete mode 100644 src/components/autocomplete/demoFloatingLabel/index.html create mode 100644 src/components/autocomplete/demoFloatingLabelWithValidation/index.html rename src/components/autocomplete/{demoFloatingLabel => demoFloatingLabelWithValidation}/script.js (95%) diff --git a/src/components/autocomplete/autocomplete.scss b/src/components/autocomplete/autocomplete.scss index 676a906945f..1dcf856da4b 100644 --- a/src/components/autocomplete/autocomplete.scss +++ b/src/components/autocomplete/autocomplete.scss @@ -1,6 +1,4 @@ $autocomplete-option-height: 48px; -$input-container-padding: 2px !default; -$input-error-height: 24px !default; @keyframes md-autocomplete-list-out { 0% { @@ -40,21 +38,13 @@ md-autocomplete { overflow: visible; min-width: 190px; &[md-floating-label] { - padding-bottom: $input-container-padding + $input-error-height; box-shadow: none; border-radius: 0; background: transparent; height: auto; - md-input-container { - padding-bottom: 0; - } md-autocomplete-wrap { height: auto; } - button { - top: auto; - bottom: 5px; - } } md-autocomplete-wrap { display: block; @@ -105,6 +95,12 @@ md-autocomplete { display: none; } } + .md-autocomplete-button-wrapper { + position: relative; + flex: 1 1 auto; + order: 1; + transform: translateY(-10px); + } button { position: absolute; top: 10px; diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js index 84afc9f1080..9549e7871d5 100644 --- a/src/components/autocomplete/autocomplete.spec.js +++ b/src/components/autocomplete/autocomplete.spec.js @@ -28,7 +28,7 @@ describe('', function() { } describe('basic functionality', function () { - it('should fail', inject(function($timeout, $mdConstant, $rootElement) { + it('should update selected item and search text', inject(function($timeout, $mdConstant, $rootElement) { var scope = createScope(); var template = '\ ', function() { })); }); + describe('basic functionality with template', function () { + it('should update selected item and search text', inject(function($timeout, $mdConstant, $rootElement) { + var scope = createScope(); + var template = '\ + \ + \ + {{item.display}}\ + \ + '; + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + var ul = element.find('ul'); + + expect(scope.searchText).toBe(''); + expect(scope.selectedItem).toBe(null); + + element.scope().searchText = 'fo'; + ctrl.keydown({}); + element.scope().$apply(); + $timeout.flush(); + + expect(scope.searchText).toBe('fo'); + expect(scope.match(scope.searchText).length).toBe(1); + expect(ul.find('li').length).toBe(1); + + ctrl.keydown({ keyCode: $mdConstant.KEY_CODE.DOWN_ARROW, preventDefault: angular.noop }); + ctrl.keydown({ keyCode: $mdConstant.KEY_CODE.ENTER, preventDefault: angular.noop }); + scope.$apply(); + expect(scope.searchText).toBe('foo'); + expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]); + })); + }); + describe('API access', function() { it('should clear the selected item', inject(function($timeout, $mdConstant) { var scope = createScope(); diff --git a/src/components/autocomplete/demoFloatingLabel/index.html b/src/components/autocomplete/demoFloatingLabel/index.html deleted file mode 100644 index ee476c7a20e..00000000000 --- a/src/components/autocomplete/demoFloatingLabel/index.html +++ /dev/null @@ -1,22 +0,0 @@ -
- -
-

The following example demonstrates floating labels being used as a normal form element.

-
- - - - - - {{item.display}} - -
-
-
-
diff --git a/src/components/autocomplete/demoFloatingLabelWithValidation/index.html b/src/components/autocomplete/demoFloatingLabelWithValidation/index.html new file mode 100644 index 00000000000..13a9824c2b0 --- /dev/null +++ b/src/components/autocomplete/demoFloatingLabelWithValidation/index.html @@ -0,0 +1,33 @@ +
+ +
+

The following example demonstrates floating labels being used as a normal form element.

+
+ + + + + + + {{item.display}} + +
+
You must have a favorite state.
+
Your entry is not long enough.
+
Your entry is too long.
+
+
+
+
+
+
diff --git a/src/components/autocomplete/demoFloatingLabel/script.js b/src/components/autocomplete/demoFloatingLabelWithValidation/script.js similarity index 95% rename from src/components/autocomplete/demoFloatingLabel/script.js rename to src/components/autocomplete/demoFloatingLabelWithValidation/script.js index eeebf190e26..4c3f7d8f3bd 100644 --- a/src/components/autocomplete/demoFloatingLabel/script.js +++ b/src/components/autocomplete/demoFloatingLabelWithValidation/script.js @@ -1,7 +1,7 @@ (function () { 'use strict'; angular - .module('autocompleteFloatingLabelDemo', ['ngMaterial']) + .module('autocompleteFloatingLabelDemo', ['ngMaterial', 'ngMessages']) .controller('DemoCtrl', DemoCtrl); function DemoCtrl ($timeout, $q) { diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 8fac1211877..c76f70faa38 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -285,7 +285,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $timeout, $ } function isMinLengthMet () { - return $scope.searchText.length >= getMinLength(); + var minLength = getMinLength(); + if (!minLength) return true; + return $scope.searchText && $scope.searchText.length >= minLength; } //-- actions diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index cf694321a92..d0ba3efcf2b 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -18,6 +18,19 @@ angular * no matches were found. You can do this by wrapping your template in `md-item-template` and adding * a tag for `md-not-found`. An example of this is shown below. * + * ### Validation + * + * You can use `ng-messages` to include validation the same way that you would normally validate; + * however, if you want to replicate a standard input with a floating label, you will have to do the + * following: + * + * - Make sure that your template is wrapped in `md-item-template` + * - Add your `ng-messages` code inside of `md-autocomplete` + * - Add your validation properties to `md-autocomplete` (ie. `required`) + * - Add a `name` to `md-autocomplete` (to be used on the generated `input`) + * + * There is an example below of how this should look. + * * @param {expression} md-items An expression in the format of `item in items` to iterate over matches for your search. * @param {expression} md-selected-item-change An expression to be run each time a new item is selected * @param {expression} md-search-text-change An expression to be run each time the search text updates @@ -63,6 +76,29 @@ angular * * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different * parts that make up our component. + * + * ### Example with validation + * + *
+ * + * + * {{item.display}} + * + *
+ *
This field is required
+ *
+ *
+ *
+ *
+ * + * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the different + * parts that make up our component. */ function MdAutocomplete ($mdTheming, $mdUtil) { @@ -71,33 +107,51 @@ function MdAutocomplete ($mdTheming, $mdUtil) { controllerAs: '$mdAutocompleteCtrl', link: link, scope: { - name: '@', - searchText: '=?mdSearchText', - selectedItem: '=?mdSelectedItem', - itemsExpr: '@mdItems', - itemText: '&mdItemText', - placeholder: '@placeholder', - noCache: '=?mdNoCache', - itemChange: '&?mdSelectedItemChange', - textChange: '&?mdSearchTextChange', - minLength: '=?mdMinLength', - delay: '=?mdDelay', - autofocus: '=?mdAutofocus', - floatingLabel: '@?mdFloatingLabel', - autoselect: '=?mdAutoselect', - menuClass: '@?mdMenuClass' + inputName: '@', + inputMinlength: '@', + inputMaxlength: '@', + searchText: '=?mdSearchText', + selectedItem: '=?mdSelectedItem', + itemsExpr: '@mdItems', + itemText: '&mdItemText', + placeholder: '@placeholder', + noCache: '=?mdNoCache', + itemChange: '&?mdSelectedItemChange', + textChange: '&?mdSearchTextChange', + minLength: '=?mdMinLength', + delay: '=?mdDelay', + autofocus: '=?mdAutofocus', + floatingLabel: '@?mdFloatingLabel', + autoselect: '=?mdAutoselect', + menuClass: '@?mdMenuClass' }, template: function (element, attr) { - var itemTemplate = getItemTemplate(), - noItemsTemplate = getNoItemsTemplate(); + var noItemsTemplate = getNoItemsTemplate(), + itemTemplate = getItemTemplate(), + leftover = element.html(); + element.empty(); + return '\ \ \ \ +
\ + \ + \ + Clear\ + \ +
\ \ +
' + leftover + '
\
\ +
\ + \ + \ + Clear\ + \ +
\ \ - \ - \ - Clear\ - \ \ @@ -151,13 +209,14 @@ function MdAutocomplete ($mdTheming, $mdUtil) { md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\ ' + itemTemplate + '\ \ - ' + (function () { - return noItemsTemplate - ? '
  • ' + noItemsTemplate + '
  • ' - : ''; - })() + '\ + ' + + //-- Add "not found" template if available + (noItemsTemplate + ? '
  • ' + noItemsTemplate + '
  • ' + : '' ) + + '\ \
    \ '; function getItemTemplate () { - var templateTag = element.find('md-item-template').remove(); - return templateTag.length ? templateTag.html() : element.html(); + var templateTag = element.find('md-item-template').remove(), + html = templateTag.length ? templateTag.html() : element.html(); + if (!templateTag.length) element.empty(); + return html; } function getNoItemsTemplate () { @@ -181,6 +242,7 @@ function MdAutocomplete ($mdTheming, $mdUtil) { function link (scope, element, attr) { attr.$observe('disabled', function (value) { scope.isDisabled = value; }); + attr.$observe('required', function (value) { scope.isRequired = value !== null; }); $mdUtil.initOptionalProperties(scope, attr, {searchText:null, selectedItem:null} ); diff --git a/src/components/autocomplete/js/parentScope.js b/src/components/autocomplete/js/parentScope.js index 7af1094e190..07c009ea96c 100644 --- a/src/components/autocomplete/js/parentScope.js +++ b/src/components/autocomplete/js/parentScope.js @@ -10,7 +10,11 @@ function MdAutocompleteParentScope ($compile, $mdUtil) { scope: false }; function postLink (scope, element, attr) { - var ctrl = scope.$parent.$mdAutocompleteCtrl; + var ctrl = scope.$parent.$mdAutocompleteCtrl; $compile(element.contents())(ctrl.parent); + if (attr.hasOwnProperty('mdAutocompleteReplace')) { + element.after(element.contents()); + element.remove(); + } } }