From dd7602662de6612fa94dd40f22fe35e3c2b038b3 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 28 Nov 2016 16:19:51 -0800 Subject: [PATCH 01/35] created md-input-wrapper and removed useless inputs --- src/lib/input/input-wrapper.html | 54 ++++++ src/lib/input/input-wrapper.scss | 0 src/lib/input/input-wrapper.ts | 313 +++++++++++++++++++++++++++++++ 3 files changed, 367 insertions(+) create mode 100644 src/lib/input/input-wrapper.html create mode 100644 src/lib/input/input-wrapper.scss create mode 100644 src/lib/input/input-wrapper.ts diff --git a/src/lib/input/input-wrapper.html b/src/lib/input/input-wrapper.html new file mode 100644 index 000000000000..7a673947df06 --- /dev/null +++ b/src/lib/input/input-wrapper.html @@ -0,0 +1,54 @@ +
+
+ +
+ + + + + + +
+ +
+
+ +
+ +
+ +
{{hintLabel}}
+ diff --git a/src/lib/input/input-wrapper.scss b/src/lib/input/input-wrapper.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/lib/input/input-wrapper.ts b/src/lib/input/input-wrapper.ts new file mode 100644 index 000000000000..e0619af5b54c --- /dev/null +++ b/src/lib/input/input-wrapper.ts @@ -0,0 +1,313 @@ +import { + forwardRef, + Component, + HostBinding, + Input, + Directive, + AfterContentInit, + ContentChild, + SimpleChange, + ContentChildren, + ViewChild, + ElementRef, + QueryList, + OnChanges, + EventEmitter, + Output, + NgModule, + ModuleWithProviders, + ViewEncapsulation, +} from '@angular/core'; +import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms'; +import {CommonModule} from '@angular/common'; +import {MdError, coerceBooleanProperty} from '../core'; +import {Observable} from 'rxjs/Observable'; +import {MdTextareaAutosize} from './autosize'; + + +const noop = () => {}; + + +export const MD_INPUT_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MdInput), + multi: true +}; + +// Invalid input type. Using one of these will throw an MdInputUnsupportedTypeError. +const MD_INPUT_INVALID_INPUT_TYPE = [ + 'file', + 'radio', + 'checkbox', +]; + + +let nextUniqueId = 0; + + +export class MdInputPlaceholderConflictError extends MdError { + constructor() { + super('Placeholder attribute and child element were both specified.'); + } +} + +export class MdInputUnsupportedTypeError extends MdError { + constructor(type: string) { + super(`Input type "${type}" isn't supported by md-input.`); + } +} + +export class MdInputDuplicatedHintError extends MdError { + 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() align: 'start' | 'end' = 'start'; +} + +/** + * 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({ + moduleId: module.id, + selector: 'md-input-wrapper', + templateUrl: 'input-wrapper.html', + styleUrls: ['input-wrapper.css'], + providers: [MD_INPUT_CONTROL_VALUE_ACCESSOR], + host: { + '(click)' : 'focus()' + }, + encapsulation: ViewEncapsulation.None, +}) +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; + + /** + * Content directives. + */ + @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; + @ContentChildren(MdHint) _hintChildren: QueryList; + + /** Readonly properties. */ + get focused() { return this._focused; } + get empty() { return (this._value == null || this._value === '') && this.type !== 'date'; } + get characterCount(): number { + return this.empty ? 0 : ('' + this._value).length; + } + get inputId(): string { return `${this.id}-input`; } + + /** + * Bindings. + */ + @Input() align: 'start' | 'end' = 'start'; + @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary'; + @Input() hintLabel: string = ''; + + @Input() id: string = `md-input-${nextUniqueId++}`; + @Input() placeholder: string = null; + @Input() type: string = 'text'; + + private _floatingPlaceholder: boolean = true; + + @Input() + get floatingPlaceholder(): boolean { return this._floatingPlaceholder; } + set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); } + + private _blurEmitter: EventEmitter = new EventEmitter(); + private _focusEmitter: EventEmitter = new EventEmitter(); + + @Output('blur') + get onBlur(): Observable { + return this._blurEmitter.asObservable(); + } + + @Output('focus') + get onFocus(): Observable { + return this._focusEmitter.asObservable(); + } + + get value(): any { return this._value; }; + @Input() set value(v: any) { + v = this._convertValueForInputType(v); + 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') get _align(): any { return null; } + + + @ViewChild('input') _inputElement: ElementRef; + + _elementType: 'input' | 'textarea'; + + constructor(elementRef: ElementRef) { + // Set the element type depending on normalized selector used(md-input / md-textarea) + this._elementType = elementRef.nativeElement.nodeName.toLowerCase() === 'md-input' ? + 'input' : + 'textarea'; + } + + /** Set focus on input */ + focus() { + this._inputElement.nativeElement.focus(); + } + + _handleFocus(event: FocusEvent) { + this._focused = true; + this._focusEmitter.emit(event); + } + + _handleBlur(event: FocusEvent) { + this._focused = false; + this._onTouchedCallback(); + this._blurEmitter.emit(event); + } + + _handleChange(event: Event) { + this.value = (event.target).value; + this._onTouchedCallback(); + } + + _hasPlaceholder(): boolean { + return !!this.placeholder || this._placeholderChild != null; + } + + /** + * Implemented as part of ControlValueAccessor. + * TODO: internal + */ + writeValue(value: any) { + this._value = value; + } + + /** + * Implemented as part of ControlValueAccessor. + * TODO: internal + */ + registerOnChange(fn: any) { + this._onChangeCallback = fn; + } + + /** + * Implemented as part of ControlValueAccessor. + * TODO: internal + */ + registerOnTouched(fn: any) { + this._onTouchedCallback = fn; + } + + /** TODO: internal */ + ngAfterContentInit() { + this._validateConstraints(); + + // Trigger validation when the hint children change. + this._hintChildren.changes.subscribe(() => { + this._validateConstraints(); + }); + } + + /** TODO: internal */ + ngOnChanges(changes: {[key: string]: SimpleChange}) { + this._validateConstraints(); + } + + /** + * Convert the value passed in to a value that is expected from the type of the md-input. + * This is normally performed by the *_VALUE_ACCESSOR in forms, but since the type is bound + * on our internal input it won't work locally. + * @private + */ + private _convertValueForInputType(v: any): any { + switch (this.type) { + case 'number': return parseFloat(v); + default: return v; + } + } + + /** + * 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 MdInputPlaceholderConflictError(); + } + if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this.type) != -1) { + throw new MdInputUnsupportedTypeError(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 MdInputDuplicatedHintError('start'); + } + startHint = hint; + } else if (hint.align == 'end') { + if (endHint) { + throw new MdInputDuplicatedHintError('end'); + } + endHint = hint; + } + }); + } + } +} + + +@NgModule({ + declarations: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize], + imports: [CommonModule, FormsModule], + exports: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize], +}) +export class MdInputModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdInputModule, + providers: [] + }; + } +} From ea45082a0e9e189265d1314fe4a110c650488838 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 29 Nov 2016 10:36:20 -0800 Subject: [PATCH 02/35] added demo --- src/demo-app/input/input-demo.html | 8 +++++ src/lib/input/index.ts | 1 + src/lib/input/input-wrapper.html | 4 +++ src/lib/input/input-wrapper.ts | 48 +++++++++++++++--------------- src/lib/input/input.spec.ts | 10 +++---- src/lib/input/input.ts | 9 +++--- src/lib/module.ts | 3 ++ 7 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 1bef8bfd2df4..69a1a1f182e0 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -180,3 +180,11 @@

Textarea

textarea autosize

+ + + + + + + + diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts index c630aefeaf7f..8e84e9515d50 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -1,2 +1,3 @@ export * from './input'; +export * from './input-wrapper'; export {MdTextareaAutosize} from './autosize'; diff --git a/src/lib/input/input-wrapper.html b/src/lib/input/input-wrapper.html index 7a673947df06..ba43723fd7d4 100644 --- a/src/lib/input/input-wrapper.html +++ b/src/lib/input/input-wrapper.html @@ -2,6 +2,9 @@
+ + +
diff --git a/src/lib/input/input-wrapper.ts b/src/lib/input/input-wrapper.ts index 364bd1065828..fa51ecdc3278 100644 --- a/src/lib/input/input-wrapper.ts +++ b/src/lib/input/input-wrapper.ts @@ -10,22 +10,17 @@ import { ElementRef, QueryList, OnChanges, - EventEmitter, - Output, NgModule, ModuleWithProviders, - ViewEncapsulation + ViewEncapsulation, + NgZone } from '@angular/core'; import {FormsModule} from '@angular/forms'; import {CommonModule} from '@angular/common'; import {MdError, coerceBooleanProperty} from '../core'; -import {Observable} from 'rxjs/Observable'; import {MdTextareaAutosize} from './autosize'; -const noop = () => {}; - - // Invalid input type. Using one of these will throw an MdInputUnsupportedTypeErrorNew. const MD_INPUT_INVALID_INPUT_TYPE = [ 'file', @@ -96,12 +91,6 @@ export class MdHintNew { }) export class MdInputWrapper implements 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; /** * Content directives. @@ -111,9 +100,8 @@ export class MdInputWrapper implements AfterContentInit, OnChanges { /** Readonly properties. */ get focused() { return this._focused; } - get empty() { return (this._value == null || this._value === '') && this._inputType !== 'date'; } - get characterCount(): number { - return this.empty ? 0 : ('' + this._value).length; + get empty() { + return (this._inputValue == null || this._inputValue === '') && this._inputType !== 'date'; } /** @@ -122,7 +110,6 @@ export class MdInputWrapper implements AfterContentInit, OnChanges { @Input() align: 'start' | 'end' = 'start'; @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary'; @Input() hintLabel: string = ''; - @Input() placeholder: string = null; private _floatingPlaceholder: boolean = true; @@ -130,28 +117,6 @@ export class MdInputWrapper implements AfterContentInit, OnChanges { get floatingPlaceholder(): boolean { return this._floatingPlaceholder; } set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); } - private _blurEmitter: EventEmitter = new EventEmitter(); - private _focusEmitter: EventEmitter = new EventEmitter(); - - @Output('blur') - get onBlur(): Observable { - return this._blurEmitter.asObservable(); - } - - @Output('focus') - get onFocus(): Observable { - return this._focusEmitter.asObservable(); - } - - get value(): any { return this._value; }; - @Input() set value(v: any) { - v = this._convertValueForInputType(v); - 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. @@ -159,34 +124,40 @@ export class MdInputWrapper implements AfterContentInit, OnChanges { private _inputElement: HTMLInputElement | HTMLTextAreaElement; - get _inputId(): string { return this._inputElement && this._inputElement.id } - get _inputType(): string { return this._inputElement && this._inputElement.type || 'text' } + // Do these via DOMMutationObserver + get _inputId(): string { + return this._inputElement && this._inputElement.id || ''; + } + get _inputType(): string { + return this._inputElement && this._inputElement.type || 'text'; + } + get _inputPlaceholder(): string { + return this._inputElement && this._inputElement.placeholder || ''; + } + get _inputRequired(): boolean { + return this._inputElement && this._inputElement.required || false; + } + get _inputValue(): string { + return this._inputElement && this._inputElement.value || ''; + } - constructor(private _elementRef: ElementRef) {} + constructor(private _elementRef: ElementRef, private _ngZone: NgZone) {} /** Set focus on input */ focus() { this._inputElement && this._inputElement.focus(); } - _handleFocus(event: FocusEvent) { + _handleFocus() { this._focused = true; - this._focusEmitter.emit(event); } - _handleBlur(event: FocusEvent) { + _handleBlur() { this._focused = false; - this._onTouchedCallback(); - this._blurEmitter.emit(event); - } - - _handleChange(event: Event) { - this.value = (event.target).value; - this._onTouchedCallback(); } _hasPlaceholder(): boolean { - return !!this.placeholder || this._placeholderChild != null; + return !!this._inputPlaceholder || this._placeholderChild != null; } /** TODO: internal */ @@ -211,26 +182,19 @@ export class MdInputWrapper implements AfterContentInit, OnChanges { // TODO throw } this._inputElement = inputEls[0]; + // TODO(mmalerba): Revalidate when type changes. if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this._inputElement.type || 'text') != -1) { throw new MdInputUnsupportedTypeErrorNew(this._inputType); } + // TODO(mmalerba): Revalidate when placeholder changes. + if (this._inputElement.placeholder && this._placeholderChild != null) { + throw new MdInputPlaceholderConflictErrorNew(); + } + this._inputElement.classList.add('md-input-element'); this._inputElement.id = this._inputElement.id || `md-input-${nextUniqueId++}`; } - /** - * Convert the value passed in to a value that is expected from the type of the md-input. - * This is normally performed by the *_VALUE_ACCESSOR in forms, but since the type is bound - * on our internal input it won't work locally. - * @private - */ - private _convertValueForInputType(v: any): any { - switch (this._inputType) { - case 'number': return parseFloat(v); - default: return v; - } - } - /** * Ensure that all constraints defined by the API are validated, or throw errors otherwise. * Constraints for now: @@ -241,9 +205,7 @@ export class MdInputWrapper implements AfterContentInit, OnChanges { * @private */ private _validateConstraints() { - if (this.placeholder != '' && this.placeholder != null && this._placeholderChild != null) { - throw new MdInputPlaceholderConflictErrorNew(); - } + if (this._hintChildren) { // Validate the hint labels. From f1adbdfe4aef67ba12785b9aafba467e34e47c84 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 29 Nov 2016 15:35:27 -0800 Subject: [PATCH 05/35] typescript fixed, css needs work --- src/lib/button-toggle/button-toggle.html | 4 +- src/lib/input/input-wrapper.html | 14 +- src/lib/input/input-wrapper.ts | 212 ++++++++++++++--------- src/lib/radio/radio.html | 4 +- 4 files changed, 136 insertions(+), 98 deletions(-) diff --git a/src/lib/button-toggle/button-toggle.html b/src/lib/button-toggle/button-toggle.html index a4945b9ca8c5..7e8a27111f91 100644 --- a/src/lib/button-toggle/button-toggle.html +++ b/src/lib/button-toggle/button-toggle.html @@ -1,7 +1,7 @@ -