Skip to content
This repository has been archived by the owner on Aug 29, 2023. It is now read-only.

Commit

Permalink
fix(select): Fix empty option stlying issue.
Browse files Browse the repository at this point in the history
Previously, if you used `ng-value` on your `md-option` and attempted
to have an empty option without a value
(i.e. `<md-option>Select...</md-option>`), the component would still
be in a "has selected value" state causing the floating label to
appear among other issues.

Add a new `md-option-empty` attribute to `<md-option>` which allows
the user to specify the empty option. Additionally, automatically
add this attribute for the most common use cases.

Update demo/docs to show usage.

Fixes #6851.

Closes #8907
  • Loading branch information
topherfangio authored and ThomasBurleson committed Jul 10, 2016
1 parent 87aa4cf commit fcd42df
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/components/select/demoBasicUsage/index.html
Expand Up @@ -29,7 +29,8 @@
<md-input-container>
<label>State</label>
<md-select ng-model="ctrl.userState">
<md-option ng-repeat="state in ctrl.states" value="{{state.abbrev}}" ng-disabled="$index === 1">
<md-option><em>None</em></md-option>
<md-option ng-repeat="state in ctrl.states" ng-value="state.abbrev" ng-disabled="$index === 1">
{{state.abbrev}}
</md-option>
</md-select>
Expand Down
43 changes: 43 additions & 0 deletions src/components/select/select.js
Expand Up @@ -40,6 +40,32 @@ angular.module('material.components.select', [
* By default, the select will display with an underline to match other form elements. This can be
* disabled by applying the `md-no-underline` CSS class.
*
* ### Option Params
*
* When applied, `md-option-empty` will mark the option as "empty" allowing the option to clear the
* select and put it back in it's default state. You may supply this attribute on any option you
* wish, however, it is automatically applied to an option whose `value` or `ng-value` are not
* defined.
*
* **Automatically Applied**
*
* - `<md-option>`
* - `<md-option value>`
* - `<md-option value="">`
* - `<md-option ng-value>`
* - `<md-option ng-value="">`
*
* **NOT Automatically Applied**
*
* - `<md-option ng-value="1">`
* - `<md-option ng-value="''">`
* - `<md-option ng-value="undefined">`
* - `<md-option value="undefined">` (this evaluates to the string `"undefined"`)
* - <code ng-non-bindable>&lt;md-option ng-value="{{someValueThatMightBeUndefined}}"&gt;</code>
*
* **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this
* attribute) since you may wish this to be your "Not Available" or "None" option.
*
* @param {expression} ng-model The model!
* @param {boolean=} multiple Whether it's multiple.
* @param {expression=} md-on-close Expression to be evaluated when the select is closed.
Expand Down Expand Up @@ -723,6 +749,11 @@ function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
// Map the given element to its innerHTML string. If the element has a child ripple
// container remove it from the HTML string, before returning the string.
mapFn = function(el) {
// If we do not have a `value` or `ng-value`, assume it is an empty option which clears the select
if (el.hasAttribute('md-option-empty')) {
return '';
}

var html = el.innerHTML;

// Remove the ripple container from the selected option, copying it would cause a CSP violation.
Expand Down Expand Up @@ -856,9 +887,21 @@ function OptionDirective($mdButtonInkRipple, $mdUtil) {
element.append(angular.element('<div class="_md-text">').append(element.contents()));

element.attr('tabindex', attr.tabindex || '0');

if (!hasDefinedValue(attr)) {
element.attr('md-option-empty', '');
}

return postLink;
}

function hasDefinedValue(attr) {
var value = attr.value;
var ngValue = attr.ngValue;

return value || ngValue;
}

function postLink(scope, element, attr, ctrls) {
var optionCtrl = ctrls[0];
var selectCtrl = ctrls[1];
Expand Down
54 changes: 54 additions & 0 deletions src/components/select/select.spec.js
Expand Up @@ -255,6 +255,60 @@ describe('<md-select>', function() {
expect(el).not.toHaveClass('md-input-has-value');
}));

it('should add has-value class on container for option ng-value="undefined"', inject(function($rootScope) {
var el = setupSelect('ng-model="$root.value"',
'<md-option ng-value="undefined"></md-option><md-option ng-value="1">1</md-option>'
);
var select = el.find('md-select');

document.body.appendChild(el[0]);

openSelect(select);
waitForSelectOpen();
clickOption(select, 0);
expect(el).toHaveClass('md-input-has-value');

openSelect(select);
waitForSelectOpen();
clickOption(select, 1);
expect(el).toHaveClass('md-input-has-value');

el.remove();
}));

it('should unset has-value class on container for empty value option', inject(function($rootScope) {
var templates = [
'<md-option></md-option>',
'<md-option value></md-option>',
'<md-option value>None</md-option>',
'<md-option ng-value></md-option>',
'<md-option ng-value>None</md-option>',
'<md-option value=""></md-option>',
'<md-option ng-value=""></md-option>',
];

templates.forEach(function(template) {
var el = setupSelect('ng-model="$root.value"',
template + '<md-option ng-value="1">1</md-option>'
);
var select = el.find('md-select');

document.body.appendChild(el[0]);

openSelect(select);
waitForSelectOpen();
clickOption(select, 1);
expect(el).toHaveClass('md-input-has-value');

openSelect(select);
waitForSelectOpen();
clickOption(select, 0);
expect(el).not.toHaveClass('md-input-has-value');

el.remove();
});
}));

it('should match label to given input id', function() {
var el = setupSelect('ng-model="$root.value" id="foo"');
expect(el.find('label').attr('for')).toBe('foo');
Expand Down

0 comments on commit fcd42df

Please sign in to comment.