From 74d9bc0bfa5bf703688e3c2e8b258e5d8e1b3667 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Sun, 13 Mar 2016 12:47:26 -0700 Subject: [PATCH] feat(): md-input --- src/components/input/README.md | 42 +++ src/components/input/input.html | 47 +++ src/components/input/input.scss | 184 +++++++++++ src/components/input/input.spec.ts | 355 +++++++++++++++++++++ src/components/input/input.ts | 241 ++++++++++++++ src/core/annotations/field-value.dart | 6 + src/core/annotations/field-value.spec.dart | 1 + src/core/annotations/field-value.spec.ts | 34 ++ src/core/annotations/field-value.ts | 38 +++ src/demo-app/demo-app.html | 1 + src/demo-app/demo-app.ts | 3 + src/demo-app/input/input-demo.html | 99 ++++++ src/demo-app/input/input-demo.scss | 19 ++ src/demo-app/input/input-demo.ts | 34 ++ 14 files changed, 1104 insertions(+) create mode 100644 src/components/input/README.md create mode 100644 src/components/input/input.html create mode 100644 src/components/input/input.scss create mode 100644 src/components/input/input.spec.ts create mode 100644 src/components/input/input.ts create mode 100644 src/core/annotations/field-value.dart create mode 100644 src/core/annotations/field-value.spec.dart create mode 100644 src/core/annotations/field-value.spec.ts create mode 100644 src/core/annotations/field-value.ts create mode 100644 src/demo-app/input/input-demo.html create mode 100644 src/demo-app/input/input-demo.scss create mode 100644 src/demo-app/input/input-demo.ts diff --git a/src/components/input/README.md b/src/components/input/README.md new file mode 100644 index 000000000000..a3faa9067bc4 --- /dev/null +++ b/src/components/input/README.md @@ -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 `` element in the ``. + +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 + + $ + .00 + +``` + +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 `` directive in the ``, 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 + diff --git a/src/components/input/input.html b/src/components/input/input.html new file mode 100644 index 000000000000..82d460776bef --- /dev/null +++ b/src/components/input/input.html @@ -0,0 +1,47 @@ +
+
+
+ + + + + +
+
+ +
+ +
+ +
{{hintLabel}}
+ +
diff --git a/src/components/input/input.scss b/src/components/input/input.scss new file mode 100644 index 000000000000..433e0883de18 --- /dev/null +++ b/src/components/input/input.scss @@ -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 has a system font. + font: inherit; + + // By default, 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; + } + } +} diff --git a/src/components/input/input.spec.ts b/src/components/input/input.spec.ts new file mode 100644 index 000000000000..1cbd7268eeca --- /dev/null +++ b/src/components/input/input.spec.ts @@ -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 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 . + 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: ` + + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputPlaceholderRequiredTestComponent { +} + +@Component({ + selector: 'test-input-controller', + template: ` + + {{placeholder}} + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputPlaceholderElementTestComponent { + placeholder: string = 'Default Placeholder'; +} + +@Component({ + selector: 'test-input-controller', + template: ` + + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputPlaceholderAttrTestComponent { + placeholder: string = ''; +} + +@Component({ + selector: 'test-input-controller', + template: ` + + {{label}} + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputHintLabel2TestController { + label: string = ''; +} + +@Component({ + selector: 'test-input-controller', + template: ` + + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputHintLabelTestController { + label: string = ''; +} + +@Component({ + selector: 'test-input-controller', + template: ` + + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputInvalidTypeTestController { +} + +@Component({ + selector: 'test-input-controller', + template: ` + + World + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputInvalidPlaceholderTestController { +} + +@Component({ + selector: 'test-input-controller', + template: ` + + World + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputInvalidHint2TestController { +} + +@Component({ + selector: 'test-input-controller', + template: ` + + Hello + World + + `, + directives: [MD_INPUT_DIRECTIVES] +}) +class MdInputInvalidHintTestController { +} + +@Component({ + selector: 'test-input-controller', + template: ` + + + `, + directives: [MdInput] +}) +class MdInputBaseTestController { + model: any = ''; +} + +@Component({ + selector: 'test-input-controller', + template: ` + + + `, + directives: [MdInput] +}) +class MdInputAriaTestController { + ariaLabel: string = 'label'; + ariaDisabled: boolean = true; +} diff --git a/src/components/input/input.ts b/src/components/input/input.ts new file mode 100644 index 000000000000..025999e46c57 --- /dev/null +++ b/src/components/input/input.ts @@ -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 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; + + /** 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 are mutually exclusive. + * - type attribute is not one of the forbidden types (see constant at the top). + * - Maximum one of each `` 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, +]); diff --git a/src/core/annotations/field-value.dart b/src/core/annotations/field-value.dart new file mode 100644 index 000000000000..56329b4dfc8e --- /dev/null +++ b/src/core/annotations/field-value.dart @@ -0,0 +1,6 @@ +/** + * Annotation for a @BooleanFieldValue() property. + */ +class BooleanFieldValue { + const BooleanFieldValue(); +} diff --git a/src/core/annotations/field-value.spec.dart b/src/core/annotations/field-value.spec.dart new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/src/core/annotations/field-value.spec.dart @@ -0,0 +1 @@ + diff --git a/src/core/annotations/field-value.spec.ts b/src/core/annotations/field-value.spec.ts new file mode 100644 index 000000000000..55bcc5d25c0a --- /dev/null +++ b/src/core/annotations/field-value.spec.ts @@ -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(); + + (x).field = 'hello'; + expect(x.field).toBe(true); + + (x).field = 'true'; + expect(x.field).toBe(true); + + (x).field = ''; + expect(x.field).toBe(true); + + (x).field = 'false'; + expect(x.field).toBe(false); + }); +}); + + +class BooleanFieldValueTest { + @BooleanFieldValue() field: boolean; +} diff --git a/src/core/annotations/field-value.ts b/src/core/annotations/field-value.ts new file mode 100644 index 000000000000..f587ed3cfa83 --- /dev/null +++ b/src/core/annotations/field-value.ts @@ -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: + * + * or: + * + */ +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 }; diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index 6abc9c03925d..9ce3d2de0ea1 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -8,6 +8,7 @@

Angular Material2 Demos

  • Portal demo
  • Overlay demo
  • Checkbox demo
  • +
  • Input demo
  • Toolbar demo
  • Radio demo
  • List demo
  • diff --git a/src/demo-app/demo-app.ts b/src/demo-app/demo-app.ts index 7bb3eb28573d..6fe0f29cee63 100644 --- a/src/demo-app/demo-app.ts +++ b/src/demo-app/demo-app.ts @@ -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}) ]) diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html new file mode 100644 index 000000000000..6508b0223eaf --- /dev/null +++ b/src/demo-app/input/input-demo.html @@ -0,0 +1,99 @@ + + + Hello , + how are you? + + +

    + + +

    +

    + +

    +

    + + {{input.characterCount}} / 100 + +

    +

    + +

    + +

    + + + I favorite bold placeholder + + + I also home italic hint labels + + +

    +

    + +

    +

    + Check to change the divider color: + +

    +

    + Check to make required: + +

    +

    + Check to make floating label: + +

    + +

    + +

    Example: 
    +
    + + .00 € + +
    + Both: + + email  +  @gmail.com + +

    + +

    + Empty: + +

    + + + + + + + + + + + + + + + +
    Table + + + + +
    {{i+1}} + + + + {{item.value}}
    +
    diff --git a/src/demo-app/input/input-demo.scss b/src/demo-app/input/input-demo.scss new file mode 100644 index 000000000000..fd4d46b40cfc --- /dev/null +++ b/src/demo-app/input/input-demo.scss @@ -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; +} diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts new file mode 100644 index 000000000000..d66095c6a25c --- /dev/null +++ b/src/demo-app/input/input-demo.ts @@ -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 }); + } + } +}