mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(): md-input
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# mdInput
|
||||
|
||||
Inputs are the basic input component of Material 2. The spec can be found [here](https://www.google.com/design/spec/components/text-fields.html).
|
||||
|
||||
### Screenshots
|
||||
|
||||
|
||||
|
||||
## Type
|
||||
|
||||
At the time of writing this README, the `[type]` attribute is copied to the actual `<input>` element in the `<md-input>`.
|
||||
|
||||
The valid `type` attribute values are any supported by your browser, with the exception of `file`, `checkbox` and `radio`. File inputs aren't supported for now, while check boxes and radio buttons have their own components.
|
||||
|
||||
## Prefix and Suffix
|
||||
|
||||
You can include HTML before, and after the input tag, as prefix or suffix. It will be underlined as per the Material specification, and clicking it will focus the input.
|
||||
|
||||
To add a prefix, use the `md-prefix` attribute on the element. Similarly, to add a suffix, use the `md-suffix` attribute. For example, in a template:
|
||||
|
||||
```html
|
||||
<md-input type="number" placeholder="amount">
|
||||
<span md-prefix>$</span>
|
||||
<span md-suffix>.00</span>
|
||||
</md-input>
|
||||
```
|
||||
|
||||
Will result in this:
|
||||
|
||||
!!!! INSERT SCREENSHOT HERE.
|
||||
|
||||
|
||||
## Hint Labels
|
||||
|
||||
Hint labels are the labels that shows the underline. You can have up to two hint labels; one on the `start` of the line (left in an LTR language, right in RTL), or one on the `end`.
|
||||
|
||||
You specify a hint-label in one of two ways; either using the `hintLabel` attribute, or using an `<md-hint>` directive in the `<md-input>`, which takes an `align` attribute containing the side. The attribute version is assumed to be at the `start`.
|
||||
|
||||
Specifying a side twice will result in an exception during initialization.
|
||||
|
||||
## Divider Color
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<div class="md-input-wrapper" (click)="input.focus()">
|
||||
<div class="md-input-table">
|
||||
<div class="md-input-prefix"><ng-content select="[md-prefix]"></ng-content></div>
|
||||
|
||||
<label class="md-input-placeholder"
|
||||
[attr.for]="id"
|
||||
[class.md-empty]="empty"
|
||||
[class.md-focused]="focused"
|
||||
[class.md-float]="floatingPlaceholder"
|
||||
[class.md-accent]="dividerColor == 'accent'"
|
||||
*ngIf="hasPlaceholder()">
|
||||
<ng-content select="md-placeholder"></ng-content>
|
||||
{{placeholder}}
|
||||
<span class="md-placeholder-required" *ngIf="required">*</span>
|
||||
</label>
|
||||
|
||||
<input #input
|
||||
aria-target
|
||||
class="md-input-element"
|
||||
[class.md-end]="align == 'end'"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.aria-labelledby]="ariaLabelledBy"
|
||||
[attr.aria-disabled]="ariaDisabled"
|
||||
[attr.aria-required]="ariaRequired"
|
||||
[attr.aria-invalid]="ariaInvalid"
|
||||
[id]="id"
|
||||
[disabled]="disabled"
|
||||
[required]="required"
|
||||
[attr.maxlength]="maxLength"
|
||||
[type]="type"
|
||||
(focus)="onFocus()"
|
||||
(blur)="onBlur()"
|
||||
[(ngModel)]="value">
|
||||
|
||||
<div class="md-input-suffix"><ng-content select="[md-suffix]"></ng-content></div>
|
||||
</div>
|
||||
|
||||
<div class="md-input-underline"
|
||||
[class.md-disabled]="disabled">
|
||||
<span class="md-input-ripple"
|
||||
[class.md-focused]="focused"
|
||||
[class.md-accent]="dividerColor == 'accent'"></span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hintLabel != ''" class="md-hint">{{hintLabel}}</div>
|
||||
<ng-content select="md-hint"></ng-content>
|
||||
</div>
|
||||
@@ -0,0 +1,184 @@
|
||||
@import 'default-theme';
|
||||
@import 'mixins';
|
||||
@import 'variables';
|
||||
|
||||
|
||||
// Placeholder colors. Required is used for the `*` star shown in the placeholder.
|
||||
$md-input-placeholder-color: md-color($md-foreground, hint-text);
|
||||
$md-input-floating-placeholder-color: md-color($md-primary);
|
||||
$md-input-required-placeholder-color: md-color($md-accent);
|
||||
|
||||
// Underline colors.
|
||||
$md-input-underline-color: md-color($md-foreground, hint-text);
|
||||
$md-input-underline-color-accent: md-color($md-accent);
|
||||
$md-input-underline-disabled-color: md-color($md-foreground, hint-text);
|
||||
$md-input-underline-focused-color: md-color($md-primary);
|
||||
|
||||
// Gradient for showing the dashed line when the input is disabled.
|
||||
$md-input-underline-disabled-background-image: linear-gradient(to right,
|
||||
rgba(0,0,0,0.26) 0%, rgba(0,0,0,0.26) 33%, transparent 0%);
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-family: $md-font-family;
|
||||
|
||||
// Global wrapper. We need to apply margin to the element for spacing, but
|
||||
// cannot apply it to the host element directly.
|
||||
.md-input-wrapper {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
// We use a table layout to baseline align the prefix and suffix classes.
|
||||
// The underline is outside of it so it can cover all of the elements under
|
||||
// this table.
|
||||
// Flex does not respect the baseline. What we really want is akin to a table
|
||||
// as want an inline-block where elements don't wrap.
|
||||
.md-input-table {
|
||||
display: inline-table;
|
||||
flex-flow: column;
|
||||
vertical-align: bottom;
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
|
||||
// The Input element proper.
|
||||
.md-input-element {
|
||||
// Font needs to be inherited, because by default <input> has a system font.
|
||||
font: inherit;
|
||||
|
||||
// By default, <input> has a padding, border, outline and a default width.
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
&.md-end {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
// The placeholder label. This is invisible unless it is. The logic to show it is
|
||||
// basically `empty || (float && (!empty || focused))`. Float is dependent on the
|
||||
// `floatingPlaceholder` input.
|
||||
.md-input-placeholder {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
font-size: 100%;
|
||||
pointer-events: none; // We shouldn't catch mouse events (let them through).
|
||||
color: $md-input-placeholder-color;
|
||||
z-index: 1;
|
||||
|
||||
width: 100%;
|
||||
|
||||
transform: translateY(0);
|
||||
transform-origin: bottom left;
|
||||
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function,
|
||||
scale $swift-ease-out-duration $swift-ease-out-timing-function,
|
||||
color $swift-ease-out-duration $swift-ease-out-timing-function;
|
||||
|
||||
&.md-empty {
|
||||
visibility: visible;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
// Show the placeholder above the input when it's not empty, or focused.
|
||||
&.md-float:not(.md-empty), &.md-float.md-focused {
|
||||
visibility: visible;
|
||||
padding-bottom: 5px;
|
||||
transform: translateY(-100%) scale(0.75);
|
||||
|
||||
.md-placeholder-required {
|
||||
color: $md-input-required-placeholder-color;
|
||||
}
|
||||
}
|
||||
|
||||
// :focus is applied to the input, but we apply md-focused to the other elements
|
||||
// that need to listen to it.
|
||||
&.md-focused {
|
||||
color: $md-input-floating-placeholder-color;
|
||||
|
||||
&.md-accent {
|
||||
color: $md-input-underline-color-accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The underline is what's shown under the input, its prefix and its suffix.
|
||||
// The ripple is the blue animation coming on top of it.
|
||||
.md-input-underline {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid $md-input-underline-color;
|
||||
|
||||
&.md-disabled {
|
||||
border-top: 0;
|
||||
background-image: $md-input-underline-disabled-background-image;
|
||||
background-position: 0;
|
||||
background-size: 4px 1px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.md-input-ripple {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
z-index: 1;
|
||||
background-color: $md-input-underline-focused-color;
|
||||
top: -1px;
|
||||
width: 100%;
|
||||
transform-origin: top;
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function,
|
||||
opacity $swift-ease-out-duration $swift-ease-out-timing-function;
|
||||
|
||||
&.md-accent {
|
||||
background-color: $md-input-underline-color-accent;
|
||||
}
|
||||
|
||||
&.md-focused {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hint is shown below the underline. There can be more than one; one at the start
|
||||
// and one at the end.
|
||||
.md-hint {
|
||||
position: absolute;
|
||||
font-size: 75%;
|
||||
bottom: -0.5em;
|
||||
|
||||
&.md-right {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// RTL support.
|
||||
:host-context([dir="rtl"]) {
|
||||
.md-input-placeholder {
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
|
||||
.md-input-element.md-end {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.md-hint {
|
||||
right: 0;
|
||||
left: auto;
|
||||
|
||||
&.md-right {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import {
|
||||
fakeAsync,
|
||||
inject,
|
||||
ComponentFixture,
|
||||
TestComponentBuilder,
|
||||
injectAsync,
|
||||
tick,
|
||||
} from 'angular2/testing';
|
||||
import {Component} from 'angular2/core';
|
||||
import {By} from 'angular2/platform/browser';
|
||||
import {
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
} from '../../core/facade/testing';
|
||||
import {
|
||||
MdInput,
|
||||
MdInputDuplicatedHintException,
|
||||
MD_INPUT_DIRECTIVES,
|
||||
MdInputPlaceholderConflictException
|
||||
} from './input';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('MdInput', function () {
|
||||
var builder: TestComponentBuilder;
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) {
|
||||
builder = tcb;
|
||||
}));
|
||||
|
||||
it('creates a native <input> element', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputBaseTestController)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.query(By.css('input'))).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('support ngModel', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputBaseTestController)
|
||||
.then((fixture) => {
|
||||
fixture.detectChanges();
|
||||
fakeAsync(() => {
|
||||
let instance = fixture.componentInstance;
|
||||
let component = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
|
||||
let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
|
||||
instance.model = 'hello';
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(el.value).toEqual('hello');
|
||||
|
||||
component.value = 'world';
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(el.value).toEqual('world');
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('counts characters', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputBaseTestController)
|
||||
.then((fixture) => {
|
||||
let instance = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
let inputInstance = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
|
||||
expect(inputInstance.characterCount).toEqual(0);
|
||||
|
||||
instance.model = 'hello';
|
||||
fixture.detectChanges();
|
||||
expect(inputInstance.characterCount).toEqual(5);
|
||||
});
|
||||
}));
|
||||
|
||||
it('copies aria attributes to the inner input', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputAriaTestController)
|
||||
.then((fixture) => {
|
||||
let instance = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
expect(el.getAttribute('aria-label')).toEqual('label');
|
||||
instance.ariaLabel = 'label 2';
|
||||
fixture.detectChanges();
|
||||
expect(el.getAttribute('aria-label')).toEqual('label 2');
|
||||
|
||||
expect(el.getAttribute('aria-disabled')).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('validates there\'s only one hint label per side', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputInvalidHintTestController)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrow(new MdInputDuplicatedHintException('start'));
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it(`validates there's only one hint label per side (attribute)`, injectAsync([], () => {
|
||||
return builder.createAsync(MdInputInvalidHint2TestController)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrow(new MdInputDuplicatedHintException('start'));
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('validates there\'s only one placeholder', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputInvalidPlaceholderTestController)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrow(new MdInputPlaceholderConflictException());
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('validates the type', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputInvalidTypeTestController)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
// Technically this throws during the OnChanges detection phase,
|
||||
// so the error is really a ChangeDetectionError and it becomes
|
||||
// hard to build a full exception to compare with.
|
||||
// We just check for any exception in this case.
|
||||
expect(() => fixture.detectChanges())
|
||||
.toThrow(/* new MdInputUnsupportedTypeException('file') */);
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('supports hint labels attribute', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputHintLabelTestController)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// If the hint label is empty, expect no label.
|
||||
expect(fixture.debugElement.query(By.css('.md-hint'))).toBeNull();
|
||||
|
||||
fixture.componentInstance.label = 'label';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.query(By.css('.md-hint'))).not.toBeNull();
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('supports hint labels elements', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputHintLabel2TestController)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// In this case, we should have an empty <md-hint>.
|
||||
let el = fixture.debugElement.query(By.css('md-hint')).nativeElement;
|
||||
expect(el.textContent).toBeFalsy();
|
||||
|
||||
fixture.componentInstance.label = 'label';
|
||||
fixture.detectChanges();
|
||||
el = fixture.debugElement.query(By.css('md-hint')).nativeElement;
|
||||
expect(el.textContent).toBe('label');
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('supports placeholder attribute', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputPlaceholderAttrTestComponent)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el = fixture.debugElement.query(By.css('label'));
|
||||
expect(el).toBeNull();
|
||||
|
||||
fixture.componentInstance.placeholder = 'Other placeholder';
|
||||
fixture.detectChanges();
|
||||
el = fixture.debugElement.query(By.css('label'));
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.nativeElement.textContent).toMatch('Other placeholder');
|
||||
expect(el.nativeElement.textContent).not.toMatch(/\*/g);
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('supports placeholder element', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputPlaceholderElementTestComponent)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el = fixture.debugElement.query(By.css('label'));
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.nativeElement.textContent).toMatch('Default Placeholder');
|
||||
|
||||
fixture.componentInstance.placeholder = 'Other placeholder';
|
||||
fixture.detectChanges();
|
||||
el = fixture.debugElement.query(By.css('label'));
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.nativeElement.textContent).toMatch('Other placeholder');
|
||||
expect(el.nativeElement.textContent).not.toMatch(/\*/g);
|
||||
})();
|
||||
});
|
||||
}));
|
||||
|
||||
it('supports placeholder required star', injectAsync([], () => {
|
||||
return builder.createAsync(MdInputPlaceholderRequiredTestComponent)
|
||||
.then((fixture: ComponentFixture) => {
|
||||
fakeAsync(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el = fixture.debugElement.query(By.css('label'));
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.nativeElement.textContent).toMatch(/hello\s+\*/g);
|
||||
})();
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input required placeholder="hello">
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputPlaceholderRequiredTestComponent {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input>
|
||||
<md-placeholder>{{placeholder}}</md-placeholder>
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputPlaceholderElementTestComponent {
|
||||
placeholder: string = 'Default Placeholder';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input [placeholder]="placeholder">
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputPlaceholderAttrTestComponent {
|
||||
placeholder: string = '';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input>
|
||||
<md-hint>{{label}}</md-hint>
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputHintLabel2TestController {
|
||||
label: string = '';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input [hintLabel]="label">
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputHintLabelTestController {
|
||||
label: string = '';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input type="file">
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputInvalidTypeTestController {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input placeholder="Hello">
|
||||
<md-placeholder>World</md-placeholder>
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputInvalidPlaceholderTestController {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input hintLabel="Hello">
|
||||
<md-hint>World</md-hint>
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputInvalidHint2TestController {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input>
|
||||
<md-hint>Hello</md-hint>
|
||||
<md-hint>World</md-hint>
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
class MdInputInvalidHintTestController {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input [(ngModel)]="model">
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MdInput]
|
||||
})
|
||||
class MdInputBaseTestController {
|
||||
model: any = '';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-input-controller',
|
||||
template: `
|
||||
<md-input [aria-label]="ariaLabel" [aria-disabled]="ariaDisabled">
|
||||
</md-input>
|
||||
`,
|
||||
directives: [MdInput]
|
||||
})
|
||||
class MdInputAriaTestController {
|
||||
ariaLabel: string = 'label';
|
||||
ariaDisabled: boolean = true;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
forwardRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
Input,
|
||||
Provider,
|
||||
Directive,
|
||||
AfterContentInit,
|
||||
ContentChild,
|
||||
SimpleChange,
|
||||
ContentChildren,
|
||||
QueryList,
|
||||
OnChanges,
|
||||
} from 'angular2/core';
|
||||
import {CONST_EXPR, noop} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
ControlValueAccessor
|
||||
} from 'angular2/src/common/forms/directives/control_value_accessor';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {BooleanFieldValue} from '../../core/annotations/field-value';
|
||||
import {OneOf} from '../../core/annotations/one-of';
|
||||
|
||||
|
||||
const MD_INPUT_CONTROL_VALUE_ACCESSOR = CONST_EXPR(new Provider(
|
||||
NG_VALUE_ACCESSOR, {
|
||||
useExisting: forwardRef(() => MdInput),
|
||||
multi: true
|
||||
}));
|
||||
|
||||
// Invalid input type. Using one of these will throw an MdInputUnsupportedTypeException.
|
||||
const MD_INPUT_INVALID_INPUT_TYPE = CONST_EXPR([
|
||||
'file',
|
||||
'radio',
|
||||
'checkbox',
|
||||
]);
|
||||
|
||||
|
||||
let nextUniqueId = 0;
|
||||
|
||||
|
||||
export class MdInputPlaceholderConflictException extends BaseException {
|
||||
constructor() {
|
||||
super('Placeholder attribute and child element were both specified.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MdInputUnsupportedTypeException extends BaseException {
|
||||
constructor(type: string) {
|
||||
super(`Input type "${type}" isn't supported by md-input.`);
|
||||
}
|
||||
}
|
||||
|
||||
export class MdInputDuplicatedHintException extends BaseException {
|
||||
constructor(align: string) {
|
||||
super(`A hint was already declared for 'align="${align}"'.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The placeholder directive. The content can declare this to implement more
|
||||
* complex placeholders.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'md-placeholder'
|
||||
})
|
||||
export class MdPlaceholder {}
|
||||
|
||||
|
||||
/**
|
||||
* The hint directive, used to tag content as hint labels (going under the input).
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'md-hint',
|
||||
host: {
|
||||
'[class.md-right]': 'align == "end"',
|
||||
'[class.md-hint]': 'true'
|
||||
}
|
||||
})
|
||||
export class MdHint {
|
||||
// Whether to align the hint label at the start or end of the line.
|
||||
@Input() @OneOf(['start', 'end']) align: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component that represents a text input. It encapsulates the <input> HTMLElement and
|
||||
* improve on its behaviour, along with styling it according to the Material Design.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'md-input',
|
||||
templateUrl: 'components/input/input.html',
|
||||
styleUrls: ['components/input/input.css'],
|
||||
providers: [MD_INPUT_CONTROL_VALUE_ACCESSOR],
|
||||
})
|
||||
export class MdInput implements ControlValueAccessor, AfterContentInit, OnChanges {
|
||||
private _focused: boolean = false;
|
||||
private _value: any = '';
|
||||
|
||||
/** Callback registered via registerOnTouched (ControlValueAccessor) */
|
||||
private _onTouchedCallback: () => void = noop;
|
||||
/** Callback registered via registerOnChange (ControlValueAccessor) */
|
||||
private _onChangeCallback: (_: any) => void = noop;
|
||||
|
||||
/**
|
||||
* Aria related inputs.
|
||||
*/
|
||||
@Input('aria-label') ariaLabel: string;
|
||||
@Input('aria-labelledby') ariaLabelledBy: string;
|
||||
@Input('aria-disabled') @BooleanFieldValue() ariaDisabled: boolean;
|
||||
@Input('aria-required') @BooleanFieldValue() ariaRequired: boolean;
|
||||
@Input('aria-invalid') @BooleanFieldValue() ariaInvalid: boolean;
|
||||
|
||||
/**
|
||||
* Content directives.
|
||||
*/
|
||||
@ContentChild(MdPlaceholder) private _placeholderChild: MdPlaceholder;
|
||||
@ContentChildren(MdHint) private _hintChildren: QueryList<MdHint>;
|
||||
|
||||
/** Readonly properties. */
|
||||
get focused() { return this._focused; }
|
||||
get empty() { return this._value == null || this._value === ''; }
|
||||
get characterCount(): number {
|
||||
return this.empty ? 0 : ('' + this._value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindings.
|
||||
*/
|
||||
@Input() @OneOf(['start', 'end']) align: string = 'start';
|
||||
@Input() @BooleanFieldValue() disabled: boolean = false;
|
||||
@Input() @OneOf(['primary', 'accent']) dividerColor: string = 'primary';
|
||||
@Input() @BooleanFieldValue() floatingPlaceholder: boolean = true;
|
||||
@Input() hintLabel: string = '';
|
||||
@Input() id: string = `md-input-${nextUniqueId++}`;
|
||||
@Input() maxLength: number = -1;
|
||||
@Input() placeholder: string;
|
||||
@Input() @BooleanFieldValue() required: boolean = false;
|
||||
@Input() type: string = 'text';
|
||||
|
||||
get value(): any { return this._value; };
|
||||
@Input() set value(v: any) {
|
||||
if (v !== this._value) {
|
||||
this._value = v;
|
||||
this._onChangeCallback(v);
|
||||
}
|
||||
}
|
||||
|
||||
// This is to remove the `align` property of the `md-input` itself. Otherwise HTML5
|
||||
// might place it as RTL when we don't want to. We still want to use `align` as an
|
||||
// Input though, so we use HostBinding.
|
||||
@HostBinding('attr.align') private get _align(): any { return null; }
|
||||
|
||||
/** @internal */
|
||||
onFocus() {
|
||||
this._focused = true;
|
||||
}
|
||||
/** @internal */
|
||||
onBlur() {
|
||||
this._focused = false;
|
||||
this._onTouchedCallback();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
hasPlaceholder(): boolean {
|
||||
return !!this.placeholder || this._placeholderChild != null;
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
writeValue(value: any) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
registerOnChange(fn: any) {
|
||||
this._onChangeCallback = fn;
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
registerOnTouched(fn: any) {
|
||||
this._onTouchedCallback = fn;
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this._validateConstraints();
|
||||
|
||||
// Trigger validation when the hint children change.
|
||||
this._hintChildren.changes.subscribe(() => {
|
||||
this._validateConstraints();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {[key: string]: SimpleChange}) {
|
||||
this._validateConstraints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all constraints defined by the API are validated, or throw errors otherwise.
|
||||
* Constraints for now:
|
||||
* - placeholder attribute and <md-placeholder> are mutually exclusive.
|
||||
* - type attribute is not one of the forbidden types (see constant at the top).
|
||||
* - Maximum one of each `<md-hint>` alignment specified, with the attribute being
|
||||
* considered as align="start".
|
||||
* @private
|
||||
*/
|
||||
private _validateConstraints() {
|
||||
if (this.placeholder != '' && this.placeholder != null && this._placeholderChild != null) {
|
||||
throw new MdInputPlaceholderConflictException();
|
||||
}
|
||||
if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this.type) != -1) {
|
||||
throw new MdInputUnsupportedTypeException(this.type);
|
||||
}
|
||||
|
||||
if (this._hintChildren) {
|
||||
// Validate the hint labels.
|
||||
let startHint: MdHint = null;
|
||||
let endHint: MdHint = null;
|
||||
this._hintChildren.forEach((hint: MdHint) => {
|
||||
if (hint.align == 'start') {
|
||||
if (startHint || this.hintLabel) {
|
||||
throw new MdInputDuplicatedHintException('start');
|
||||
}
|
||||
startHint = hint;
|
||||
} else if (hint.align == 'end') {
|
||||
if (endHint) {
|
||||
throw new MdInputDuplicatedHintException('end');
|
||||
}
|
||||
endHint = hint;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MD_INPUT_DIRECTIVES: any[] = CONST_EXPR([
|
||||
MdPlaceholder,
|
||||
MdInput,
|
||||
MdHint,
|
||||
]);
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Annotation for a @BooleanFieldValue() property.
|
||||
*/
|
||||
class BooleanFieldValue {
|
||||
const BooleanFieldValue();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {BooleanFieldValue} from './field-value';
|
||||
|
||||
describe('BooleanFieldValue', () => {
|
||||
it('should work for null values', () => {
|
||||
let x = new BooleanFieldValueTest();
|
||||
|
||||
x.field = null;
|
||||
expect(x.field).toBe(false);
|
||||
|
||||
x.field = undefined;
|
||||
expect(x.field).toBe(false);
|
||||
});
|
||||
|
||||
it('should work for string values', () => {
|
||||
let x = new BooleanFieldValueTest();
|
||||
|
||||
(<any>x).field = 'hello';
|
||||
expect(x.field).toBe(true);
|
||||
|
||||
(<any>x).field = 'true';
|
||||
expect(x.field).toBe(true);
|
||||
|
||||
(<any>x).field = '';
|
||||
expect(x.field).toBe(true);
|
||||
|
||||
(<any>x).field = 'false';
|
||||
expect(x.field).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class BooleanFieldValueTest {
|
||||
@BooleanFieldValue() field: boolean;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
|
||||
|
||||
declare var Symbol: any;
|
||||
|
||||
|
||||
/**
|
||||
* Annotation Factory that allows HTML style boolean attributes. For example,
|
||||
* a field declared like this:
|
||||
|
||||
* @Directive({ selector: 'component' }) class MyComponent {
|
||||
* @Input() @BooleanFieldValueFactory() myField: boolean;
|
||||
* }
|
||||
*
|
||||
* You could set it up this way:
|
||||
* <component myField>
|
||||
* or:
|
||||
* <component myField="">
|
||||
*/
|
||||
function booleanFieldValueFactory() {
|
||||
return function booleanFieldValueMetadata(target: any, key: string): void {
|
||||
const defaultValue = target[key];
|
||||
|
||||
// Use a fallback if Symbol isn't available.
|
||||
const localKey = isPresent(Symbol) ? Symbol(key) : `__md_private_symbol_${key}`;
|
||||
target[localKey] = defaultValue;
|
||||
|
||||
Object.defineProperty(target, key, {
|
||||
get() { return this[localKey]; },
|
||||
set(value: boolean) {
|
||||
this[localKey] = isPresent(value) && value !== null && String(value) != 'false';
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export { booleanFieldValueFactory as BooleanFieldValue };
|
||||
@@ -8,6 +8,7 @@
|
||||
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
|
||||
<li><a [routerLink]="['OverlayDemo']">Overlay demo</a></li>
|
||||
<li><a [routerLink]="['CheckboxDemo']">Checkbox demo</a></li>
|
||||
<li><a [routerLink]="['InputDemo']">Input demo</a></li>
|
||||
<li><a [routerLink]="['ToolbarDemo']">Toolbar demo</a></li>
|
||||
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>
|
||||
<li><a [routerLink]="['ListDemo']">List demo</a></li>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {PortalDemo} from './portal/portal-demo';
|
||||
import {ToolbarDemo} from './toolbar/toolbar-demo';
|
||||
import {OverlayDemo} from './overlay/overlay-demo';
|
||||
import {ListDemo} from './list/list-demo';
|
||||
import {InputDemo} from './input/input-demo';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'home',
|
||||
@@ -37,6 +39,7 @@ export class Home {}
|
||||
new Route({path: '/portal', name: 'PortalDemo', component: PortalDemo}),
|
||||
new Route({path: '/overlay', name: 'OverlayDemo', component: OverlayDemo}),
|
||||
new Route({path: '/checkbox', name: 'CheckboxDemo', component: CheckboxDemo}),
|
||||
new Route({path: '/input', name: 'InputDemo', component: InputDemo}),
|
||||
new Route({path: '/toolbar', name: 'ToolbarDemo', component: ToolbarDemo}),
|
||||
new Route({path: '/list', name: 'ListDemo', component: ListDemo})
|
||||
])
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<md-card class="demo-card">
|
||||
<md-card-title>
|
||||
Hello <md-input [(ngModel)]="name" type="string" placeholder="First name"></md-input>,
|
||||
how are you?
|
||||
</md-card-title>
|
||||
<md-card-content>
|
||||
<p>
|
||||
<md-input disabled placeholder="Disabled field" value="Value"></md-input>
|
||||
<md-input required placeholder="Required field"></md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-input placeholder="100% width placeholder" style="width: 100%"></md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-input placeholder="Character count (100 max)" maxLength="100" style="width: 100%"
|
||||
#input>
|
||||
<md-hint align="end">{{input.characterCount}} / 100</md-hint>
|
||||
</md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-input placeholder="Show Hint Label" style="width: 100%"
|
||||
hintLabel="Hint label"></md-input>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<md-input>
|
||||
<md-placeholder>
|
||||
I <i class="material-icons demo-icons">favorite</i> <b>bold</b> placeholder
|
||||
</md-placeholder>
|
||||
<md-hint>
|
||||
I also <i class="material-icons demo-icons">home</i> <i>italic</i> hint labels
|
||||
</md-hint>
|
||||
</md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-input placeholder="Show Hint Label With Character Count" style="width: 100%"
|
||||
hintLabel="Hint label" characterCounter></md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-checkbox [(ngModel)]="dividerColor">Check to change the divider color:</md-checkbox>
|
||||
<md-input [dividerColor]="dividerColor ? 'primary' : 'accent'"
|
||||
[placeholder]="dividerColor ? 'Primary color' : 'Accent color'"></md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-checkbox [(ngModel)]="requiredField"> Check to make required:</md-checkbox>
|
||||
<md-input [required]="requiredField"
|
||||
[placeholder]="requiredField ? 'Required field' : 'Not required field'"></md-input>
|
||||
</p>
|
||||
<p>
|
||||
<md-checkbox [(ngModel)]="floatingLabel"> Check to make floating label:</md-checkbox>
|
||||
<md-input [floatingPlaceholder]="floatingLabel"
|
||||
[placeholder]="floatingLabel ? 'Floating label' : 'Not floating label'"></md-input>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<md-input placeholder="Prefixed" value="example">
|
||||
<div md-prefix>Example: </div>
|
||||
</md-input>
|
||||
<md-input placeholder="Suffixed" value="123" align="end">
|
||||
<span md-suffix>.00 €</span>
|
||||
</md-input>
|
||||
<br/>
|
||||
Both:
|
||||
<md-input #email placeholder="Email Address" value="angular-core" align="end">
|
||||
<span md-prefix><i [class.primary]="email.focused" class="material-icons demo-icons demo-transform">email</i> </span>
|
||||
<span md-suffix class="demo-transform" [class.primary]="email.focused"> @gmail.com</span>
|
||||
</md-input>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Empty: <md-input></md-input>
|
||||
<label>Label: <md-input></md-input></label>
|
||||
</p>
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
|
||||
<md-card class="demo-card">
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<td>Table</td>
|
||||
<td colspan="3">
|
||||
<button (click)="addABunch(5)">Add 5</button>
|
||||
<button (click)="addABunch(10)">Add 10</button>
|
||||
<button (click)="addABunch(100)">Add 100</button>
|
||||
<button (click)="addABunch(1000)">Add 1000</button>
|
||||
</td>
|
||||
</thead>
|
||||
<tr *ngFor="#item of items; #i = index">
|
||||
<td>{{i+1}}</td>
|
||||
<td>
|
||||
<md-input type="number" placeholder="value" aria-label="hello" [(ngModel)]="items[i].value"></md-input>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" [(ngModel)]="items[i].value">
|
||||
</td>
|
||||
<td>{{item.value}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</md-card>
|
||||
@@ -0,0 +1,19 @@
|
||||
@import 'default-theme';
|
||||
@import 'variables';
|
||||
|
||||
|
||||
.demo-icons {
|
||||
font-size: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.demo-transform {
|
||||
transition: color $swift-ease-out-duration $swift-ease-out-timing-function;
|
||||
}
|
||||
.demo-primary {
|
||||
color: md-color($md-primary);
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {Component} from 'angular2/core';
|
||||
import {MD_INPUT_DIRECTIVES} from '../../components/input/input';
|
||||
import {MdButton} from '../../components/button/button';
|
||||
import {MdCard} from '../../components/card/card';
|
||||
import {MdCheckbox} from '../../components/checkbox/checkbox';
|
||||
|
||||
|
||||
let max = 5;
|
||||
|
||||
@Component({
|
||||
selector: 'input-demo',
|
||||
templateUrl: 'demo-app/input/input-demo.html',
|
||||
styleUrls: ['demo-app/input/input-demo.css'],
|
||||
directives: [MdCard, MdCheckbox, MdButton, MD_INPUT_DIRECTIVES]
|
||||
})
|
||||
export class InputDemo {
|
||||
dividerColor: boolean;
|
||||
requiredField: boolean;
|
||||
floatingLabel: boolean;
|
||||
name: string;
|
||||
items: any[] = [
|
||||
{ value: 10 },
|
||||
{ value: 20 },
|
||||
{ value: 30 },
|
||||
{ value: 40 },
|
||||
{ value: 50 },
|
||||
];
|
||||
|
||||
addABunch(n: number) {
|
||||
for (let x = 0; x < n; x++) {
|
||||
this.items.push({ value: ++max });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user