refactor(all): consistent inputs

fixes #8578
This commit is contained in:
Manu Mtz.-Almeida
2017-03-22 00:10:52 +01:00
committed by Manuel Mtz-Almeida
parent 54acc74fdb
commit 9a4d81b329
19 changed files with 961 additions and 1000 deletions

View File

@ -11,7 +11,9 @@ import { PageOneModule } from '../pages/page-one/page-one.module';
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
IonicModule.forRoot(AppComponent, {}), IonicModule.forRoot(AppComponent, {
mode: 'ios'
}),
PageOneModule PageOneModule
], ],
bootstrap: [IonicApp] bootstrap: [IonicApp]

View File

@ -1,10 +1,10 @@
import { AfterContentInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostListener, Input, OnDestroy, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Form, IonicTapInput } from '../../util/form';
import { Ion } from '../ion';
import { isTrueProperty } from '../../util/util'; import { isTrueProperty } from '../../util/util';
import { Form, IonicTapInput } from '../../util/form';
import { BaseInput } from '../../util/base-input';
import { Item } from '../item/item'; import { Item } from '../item/item';
export const CHECKBOX_VALUE_ACCESSOR: any = { export const CHECKBOX_VALUE_ACCESSOR: any = {
@ -54,14 +54,14 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
@Component({ @Component({
selector: 'ion-checkbox', selector: 'ion-checkbox',
template: template:
'<div class="checkbox-icon" [class.checkbox-checked]="_checked">' + '<div class="checkbox-icon" [class.checkbox-checked]="_value">' +
'<div class="checkbox-inner"></div>' + '<div class="checkbox-inner"></div>' +
'</div>' + '</div>' +
'<button role="checkbox" ' + '<button role="checkbox" ' +
'type="button" ' + 'type="button" ' +
'ion-button="item-cover" ' + 'ion-button="item-cover" ' +
'[id]="id" ' + '[id]="id" ' +
'[attr.aria-checked]="_checked" ' + '[attr.aria-checked]="_value" ' +
'[attr.aria-labelledby]="_labelId" ' + '[attr.aria-labelledby]="_labelId" ' +
'[attr.aria-disabled]="_disabled" ' + '[attr.aria-disabled]="_disabled" ' +
'class="item-cover"> ' + 'class="item-cover"> ' +
@ -72,129 +72,30 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
providers: [CHECKBOX_VALUE_ACCESSOR], providers: [CHECKBOX_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Checkbox extends Ion implements IonicTapInput, AfterContentInit, ControlValueAccessor, OnDestroy { export class Checkbox extends BaseInput<boolean> implements IonicTapInput, AfterViewInit, OnDestroy {
/** @hidden */
_checked: boolean = false;
/** @hidden */
_init: boolean;
/** @hidden */
_disabled: boolean = false;
/** @hidden */
_labelId: string;
/** @hidden */
_fn: Function;
/** @hidden */
id: string;
/**
* @output {Checkbox} Emitted when the checkbox value changes.
*/
@Output() ionChange: EventEmitter<Checkbox> = new EventEmitter<Checkbox>();
constructor(
config: Config,
private _form: Form,
@Optional() private _item: Item,
elementRef: ElementRef,
renderer: Renderer,
private _cd: ChangeDetectorRef
) {
super(config, elementRef, renderer, 'checkbox');
_form.register(this);
if (_item) {
this.id = 'chk-' + _item.registerInput('checkbox');
this._labelId = 'lbl-' + _item.id;
this._item.setElementClass('item-checkbox', true);
}
}
/**
* @hidden
*/
@HostListener('click', ['$event'])
_click(ev: UIEvent) {
console.debug('checkbox, checked');
ev.preventDefault();
ev.stopPropagation();
this.onChange(!this._checked);
}
/** /**
* @input {boolean} If true, the element is selected. * @input {boolean} If true, the element is selected.
*/ */
@Input() @Input()
get checked(): boolean { get checked(): boolean {
return this._checked; return this.value;
} }
set checked(val: boolean) { set checked(val: boolean) {
this._setChecked(isTrueProperty(val)); this.value = val;
this.onChange(this._checked);
} }
/** constructor(
* @hidden config: Config,
*/ form: Form,
_setChecked(isChecked: boolean) { @Optional() item: Item,
if (isChecked !== this._checked) { elementRef: ElementRef,
this._checked = isChecked; renderer: Renderer,
if (this._init) { private _cd: ChangeDetectorRef
this.ionChange.emit(this); ) {
} super(config, elementRef, renderer, 'checkbox', form, item, null);
this._item && this._item.setElementClass('item-checkbox-checked', isChecked); this._value = false;
}
}
/**
* @hidden
*/
writeValue(val: any) {
this._setChecked(isTrueProperty(val));
}
/**
* @hidden
*/
registerOnChange(fn: Function): void {
this._fn = fn;
this.onChange = (isChecked: boolean) => {
console.debug('checkbox, onChange', isChecked);
fn(isChecked);
this._setChecked(isChecked);
this.onTouched();
this._cd.detectChanges();
};
}
/**
* @hidden
*/
registerOnTouched(fn: any) { this.onTouched = fn; }
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = isTrueProperty(val);
this._item && this._item.setElementClass('item-checkbox-disabled', this._disabled);
}
/**
* @hidden
*/
onChange(isChecked: boolean) {
// used when this input does not have an ngModel or formControlName
console.debug('checkbox, onChange (no ngModel)', isChecked);
this._setChecked(isChecked);
this.onTouched();
this._cd.detectChanges();
} }
/** /**
@ -207,26 +108,32 @@ export class Checkbox extends Ion implements IonicTapInput, AfterContentInit, Co
/** /**
* @hidden * @hidden
*/ */
onTouched() { } @HostListener('click', ['$event'])
_click(ev: UIEvent) {
/** console.debug('checkbox, checked');
* @hidden ev.preventDefault();
*/ ev.stopPropagation();
ngAfterContentInit() { this.value = !this.value;
this._init = true;
} }
/** /**
* @hidden * @hidden
*/ */
setDisabledState(isDisabled: boolean) { _inputNormalize(val: any): boolean {
this.disabled = isDisabled; return isTrueProperty(val);
}
/**
* @hidden
*/
_inputCheckHasValue(val: boolean) {
this._item && this._item.setElementClass('item-checkbox-checked', val);
} }
/** /**
* @hidden * @hidden
*/ */
ngOnDestroy() { _inputUpdated() {
this._form.deregister(this); this._cd.detectChanges();
} }
} }

View File

@ -0,0 +1,24 @@
import { Checkbox } from '../checkbox';
import { mockConfig, mockElementRef, mockRenderer, mockItem, mockChangeDetectorRef } from '../../../util/mock-providers';
import { commonInputTest, BOOLEAN_CORPUS } from '../../../util/input-tester';
describe('Checkbox', () => {
it('should pass common test', () => {
const config = mockConfig();
const elementRef = mockElementRef();
const renderer = mockRenderer();
const item: any = mockItem();
const cd = mockChangeDetectorRef();
const checkbox = new Checkbox(config, null, item, elementRef, renderer, cd);
commonInputTest(checkbox, {
defaultValue: false,
corpus: BOOLEAN_CORPUS
});
});
});

View File

@ -6,10 +6,10 @@ import { Picker } from '../picker/picker';
import { PickerController } from '../picker/picker-controller'; import { PickerController } from '../picker/picker-controller';
import { PickerColumn } from '../picker/picker-options'; import { PickerColumn } from '../picker/picker-options';
import { Form } from '../../util/form'; import { Form } from '../../util/form';
import { Ion } from '../ion'; import { BaseInput } from '../../util/base-input';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { deepCopy, isBlank, isPresent, isTrueProperty, isArray, isString, assert, clamp } from '../../util/util'; import { deepCopy, isBlank, isPresent, isArray, isString, assert, clamp } from '../../util/util';
import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, convertDataToISO, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util'; import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util';
export const DATETIME_VALUE_ACCESSOR: any = { export const DATETIME_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -273,23 +273,15 @@ export const DATETIME_VALUE_ACCESSOR: any = {
providers: [DATETIME_VALUE_ACCESSOR], providers: [DATETIME_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class DateTime extends Ion implements AfterContentInit, ControlValueAccessor, OnDestroy { export class DateTime extends BaseInput<any> implements AfterContentInit, ControlValueAccessor, OnDestroy {
_disabled: any = false;
_labelId: string;
_text: string = ''; _text: string = '';
_fn: Function;
_isOpen: boolean = false;
_min: DateTimeData; _min: DateTimeData;
_max: DateTimeData; _max: DateTimeData;
_value: DateTimeData = {}; _timeValue: DateTimeData = {};
_locale: LocaleData = {}; _locale: LocaleData = {};
_picker: Picker; _picker: Picker;
/**
* @hidden
*/
id: string;
/** /**
* @input {string} The minimum datetime allowed. Value must be a date string * @input {string} The minimum datetime allowed. Value must be a date string
* following the * following the
@ -421,39 +413,56 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
*/ */
@Input() placeholder: string = ''; @Input() placeholder: string = '';
/**
* @output {any} Emitted when the datetime selection has changed.
*/
@Output() ionChange: EventEmitter<any> = new EventEmitter();
/** /**
* @output {any} Emitted when the datetime selection was cancelled. * @output {any} Emitted when the datetime selection was cancelled.
*/ */
@Output() ionCancel: EventEmitter<any> = new EventEmitter(); @Output() ionCancel: EventEmitter<any> = new EventEmitter();
constructor( constructor(
private _form: Form, form: Form,
config: Config, config: Config,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
@Optional() private _item: Item, @Optional() item: Item,
@Optional() private _pickerCtrl: PickerController @Optional() private _pickerCtrl: PickerController
) { ) {
super(config, elementRef, renderer, 'datetime'); super(config, elementRef, renderer, 'datetime', form, item, null);
_form.register(this);
if (_item) {
this.id = 'dt-' + _item.registerInput('datetime');
this._labelId = 'lbl-' + _item.id;
this._item.setElementClass('item-datetime', true);
} }
/**
* @hidden
*/
ngAfterContentInit() {
// first see if locale names were provided in the inputs
// then check to see if they're in the config
// if neither were provided then it will use default English names
['monthNames', 'monthShortNames', 'dayNames', 'dayShortNames'].forEach(type => {
(<any>this)._locale[type] = convertToArrayOfStrings(isPresent((<any>this)[type]) ? (<any>this)[type] : this._config.get(type), type);
});
// update how the datetime value is displayed as formatted text
this.updateText();
}
/**
* @hidden
*/
_inputUpdated() {
updateDate(this._timeValue, this.value);
this.updateText();
}
/**
* @hidden
*/
_inputShouldChange(): boolean {
return true;
} }
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
_click(ev: UIEvent) { _click(ev: UIEvent) {
if (ev.detail === 0) {
// do not continue if the click event came from a form submit // do not continue if the click event came from a form submit
if (ev.detail === 0) {
return; return;
} }
ev.preventDefault(); ev.preventDefault();
@ -463,17 +472,14 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
@HostListener('keyup.space') @HostListener('keyup.space')
_keyup() { _keyup() {
if (!this._isOpen) {
this.open(); this.open();
} }
}
/** /**
* @hidden * @hidden
*/ */
open() { open() {
assert(!this._isOpen, 'datetime is already open'); if (this._isFocus || this._disabled) {
if (this._disabled) {
return; return;
} }
console.debug('datetime, open picker'); console.debug('datetime, open picker');
@ -481,6 +487,7 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
// the user may have assigned some options specifically for the alert // the user may have assigned some options specifically for the alert
const pickerOptions = deepCopy(this.pickerOptions); const pickerOptions = deepCopy(this.pickerOptions);
// Configure picker under the hood
const picker = this._picker = this._pickerCtrl.create(pickerOptions); const picker = this._picker = this._pickerCtrl.create(pickerOptions);
picker.addButton({ picker.addButton({
text: this.cancelText, text: this.cancelText,
@ -489,27 +496,24 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
}); });
picker.addButton({ picker.addButton({
text: this.doneText, text: this.doneText,
handler: (data: any) => { handler: (data: any) => this.value = data,
console.debug('datetime, done', data);
this.onChange(data);
this.ionChange.emit(data);
}
}); });
this.generate();
this.validate();
picker.ionChange.subscribe(() => { picker.ionChange.subscribe(() => {
this.validate(); this.validate();
picker.refresh(); picker.refresh();
}); });
this._isOpen = true; // Update picker status before presenting
picker.onDidDismiss(() => { this.generate();
this._isOpen = false; this.validate();
});
// Present picker
this._setFocus();
picker.present(pickerOptions); picker.present(pickerOptions);
picker.onDidDismiss(() => {
this._setBlur();
});
} }
/** /**
@ -566,7 +570,7 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
// cool, we've loaded up the columns with options // cool, we've loaded up the columns with options
// preselect the option for this column // preselect the option for this column
const optValue = getValueFromFormat(this._value, format); const optValue = getValueFromFormat(this.getValue(), format);
const selectedIndex = column.options.findIndex(opt => opt.value === optValue); const selectedIndex = column.options.findIndex(opt => opt.value === optValue);
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
// set the select index for this column's options // set the select index for this column's options
@ -729,33 +733,17 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
/** /**
* @hidden * @hidden
*/ */
setValue(newData: any) { updateText() {
updateDate(this._value, newData); // create the text of the formatted data
const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT;
this._text = renderDateTime(template, this.getValue(), this._locale);
} }
/** /**
* @hidden * @hidden
*/ */
getValue(): DateTimeData { getValue(): DateTimeData {
return this._value; return this._timeValue;
}
/**
* @hidden
*/
checkHasValue(inputValue: any) {
if (this._item) {
this._item.setElementClass('input-has-value', !!(inputValue && inputValue !== ''));
}
}
/**
* @hidden
*/
updateText() {
// create the text of the formatted data
const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT;
this._text = renderDateTime(template, this._value, this._locale);
} }
/** /**
@ -812,97 +800,6 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
} }
} }
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = isTrueProperty(val);
this._item && this._item.setElementClass('item-datetime-disabled', this._disabled);
}
/**
* @hidden
*/
writeValue(val: any) {
console.debug('datetime, writeValue', val);
this.setValue(val);
this.updateText();
this.checkHasValue(val);
}
/**
* @hidden
*/
ngAfterContentInit() {
// first see if locale names were provided in the inputs
// then check to see if they're in the config
// if neither were provided then it will use default English names
['monthNames', 'monthShortNames', 'dayNames', 'dayShortNames'].forEach(type => {
(<any>this)._locale[type] = convertToArrayOfStrings(isPresent((<any>this)[type]) ? (<any>this)[type] : this._config.get(type), type);
});
// update how the datetime value is displayed as formatted text
this.updateText();
}
/**
* @hidden
*/
registerOnChange(fn: Function): void {
this._fn = fn;
this.onChange = (val: any) => {
console.debug('datetime, onChange', val);
this.setValue(val);
this.updateText();
this.checkHasValue(val);
// convert DateTimeData value to iso datetime format
fn(convertDataToISO(this._value));
this.onTouched();
};
}
/**
* @hidden
*/
registerOnTouched(fn: any) { this.onTouched = fn; }
/**
* @hidden
*/
onChange(val: any) {
// onChange used when there is not an formControlName
console.debug('datetime, onChange w/out formControlName', val);
this.setValue(val);
this.updateText();
this.checkHasValue(val);
this.onTouched();
}
/**
* @hidden
*/
onTouched() { }
/**
* @hidden
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
/**
* @hidden
*/
ngOnDestroy() {
this._form.deregister(this);
}
} }
/** /**

View File

@ -7,7 +7,7 @@ import { Content, ContentDimensions, ScrollEvent } from '../content/content';
import { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom'; import { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom';
import { DomController } from '../../platform/dom-controller'; import { DomController } from '../../platform/dom-controller';
import { Form, IonicFormInput } from '../../util/form'; import { Form, IonicFormInput } from '../../util/form';
import { Ion } from '../ion'; import { BaseInput } from '../../util/base-input';
import { isString, isTrueProperty } from '../../util/util'; import { isString, isTrueProperty } from '../../util/util';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { NativeInput } from './native-input'; import { NativeInput } from './native-input';
@ -91,7 +91,8 @@ import { Platform } from '../../platform/platform';
'<div (touchstart)="pointerStart($event)" (touchend)="pointerEnd($event)" (mousedown)="pointerStart($event)" (mouseup)="pointerEnd($event)" class="input-cover" tappable *ngIf="_useAssist"></div>', '<div (touchstart)="pointerStart($event)" (touchend)="pointerEnd($event)" (mousedown)="pointerStart($event)" (mouseup)="pointerEnd($event)" class="input-cover" tappable *ngIf="_useAssist"></div>',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class TextInput extends Ion implements IonicFormInput { export class TextInput extends BaseInput<string> implements IonicFormInput {
_autoComplete: string; _autoComplete: string;
_autoCorrect: string; _autoCorrect: string;
_autoFocusAssist: string; _autoFocusAssist: string;
@ -121,17 +122,17 @@ export class TextInput extends Ion implements IonicFormInput {
constructor( constructor(
config: Config, config: Config,
private _plt: Platform, private _plt: Platform,
private _form: Form, form: Form,
private _app: App, private _app: App,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
@Optional() private _content: Content, @Optional() private _content: Content,
@Optional() private _item: Item, @Optional() item: Item,
@Optional() nav: NavController, @Optional() nav: NavController,
@Optional() public ngControl: NgControl, @Optional() public ngControl: NgControl,
private _dom: DomController private _dom: DomController
) { ) {
super(config, elementRef, renderer, 'input'); super(config, elementRef, renderer, 'input', form, item, ngControl);
this._nav = <NavControllerBase>nav; this._nav = <NavControllerBase>nav;
@ -147,12 +148,10 @@ export class TextInput extends Ion implements IonicFormInput {
} }
if (ngControl) { if (ngControl) {
ngControl.valueAccessor = this; // ngControl.valueAccessor = this;
this.inputControl = ngControl; this.inputControl = ngControl;
} }
_form.register(this);
// only listen to content scroll events if there is content // only listen to content scroll events if there is content
if (_content) { if (_content) {
this._scrollStart = _content.ionScrollStart.subscribe((ev: ScrollEvent) => { this._scrollStart = _content.ionScrollStart.subscribe((ev: ScrollEvent) => {
@ -162,8 +161,6 @@ export class TextInput extends Ion implements IonicFormInput {
this.scrollHideFocus(ev, false); this.scrollHideFocus(ev, false);
}); });
} }
this.mode = config.get('mode');
} }
/** /**
@ -182,18 +179,6 @@ export class TextInput extends Ion implements IonicFormInput {
this._clearInput = (this._type !== TEXTAREA && isTrueProperty(val)); this._clearInput = (this._type !== TEXTAREA && isTrueProperty(val));
} }
/**
* @input {string} The text value of the input.
*/
@Input()
get value() {
return this._value;
}
set value(val: any) {
this._value = val;
this.checkHasValue(val);
}
/** /**
* @input {string} The type of control to display. The default type is text. Possible values are: `"text"`, `"password"`, `"email"`, `"number"`, `"search"`, `"tel"`, or `"url"`. * @input {string} The type of control to display. The default type is text. Possible values are: `"text"`, `"password"`, `"email"`, `"number"`, `"search"`, `"tel"`, or `"url"`.
*/ */
@ -215,31 +200,21 @@ export class TextInput extends Ion implements IonicFormInput {
} }
} }
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this.setDisabled(this._disabled = isTrueProperty(val));
}
/** /**
* @hidden * @hidden
*/ */
setDisabled(val: boolean) { setDisabled(val: boolean) {
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'disabled', val ? '' : null); this.setDisabledState(val);
this._item && this._item.setElementClass('item-input-disabled', val);
this._native && this._native.isDisabled(val);
} }
/** /**
* @hidden * @hidden
*/ */
setDisabledState(isDisabled: boolean) { setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled; this._disabled = isDisabled;
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'disabled', isDisabled ? '' : null);
this._item && this._item.setElementClass('item-input-disabled', isDisabled);
this._native && this._native.isDisabled(isDisabled);
} }
/** /**
@ -376,8 +351,7 @@ export class TextInput extends Ion implements IonicFormInput {
} }
nativeInput.valueChange.subscribe((inputValue: any) => { nativeInput.valueChange.subscribe((inputValue: any) => {
this.onChange(inputValue); this.value = inputValue;
this.checkHasValue(inputValue);
}); });
nativeInput.keydown.subscribe((inputValue: any) => { nativeInput.keydown.subscribe((inputValue: any) => {
@ -387,13 +361,12 @@ export class TextInput extends Ion implements IonicFormInput {
this.focusChange(this.hasFocus()); this.focusChange(this.hasFocus());
nativeInput.focusChange.subscribe((textInputHasFocus: any) => { nativeInput.focusChange.subscribe((textInputHasFocus: any) => {
this.focusChange(textInputHasFocus); this.focusChange(textInputHasFocus);
this.checkHasValue(nativeInput.getValue()); // this.checkHasValue(nativeInput.getValue());
if (!textInputHasFocus) { if (!textInputHasFocus) {
this.onTouched(textInputHasFocus); this.onTouched(textInputHasFocus);
} }
}); });
this.value = nativeInput.getValue();
this.checkHasValue(nativeInput.getValue());
var ionInputEle: HTMLElement = this._elementRef.nativeElement; var ionInputEle: HTMLElement = this._elementRef.nativeElement;
var nativeInputEle: HTMLElement = nativeInput.element(); var nativeInputEle: HTMLElement = nativeInput.element();
@ -556,21 +529,6 @@ export class TextInput extends Ion implements IonicFormInput {
this.focus.emit(ev); this.focus.emit(ev);
} }
/**
* @hidden
*/
writeValue(val: any) {
this._value = val;
this.checkHasValue(val);
}
/**
* @hidden
*/
onChange(val: any) {
this.checkHasValue(val);
}
/** /**
* @hidden * @hidden
*/ */
@ -601,16 +559,6 @@ export class TextInput extends Ion implements IonicFormInput {
return (inputValue !== null && inputValue !== undefined && inputValue !== ''); return (inputValue !== null && inputValue !== undefined && inputValue !== '');
} }
/**
* @hidden
*/
checkHasValue(inputValue: any) {
if (this._item) {
var hasValue = (inputValue !== null && inputValue !== undefined && inputValue !== '');
this._item.setElementClass('input-has-value', hasValue);
}
}
/** /**
* @hidden * @hidden
*/ */
@ -722,7 +670,7 @@ export class TextInput extends Ion implements IonicFormInput {
* @hidden * @hidden
*/ */
ngOnDestroy() { ngOnDestroy() {
this._form.deregister(this); super.ngOnDestroy();
// only stop listening to content scroll events if there is content // only stop listening to content scroll events if there is content
if (this._content) { if (this._content) {
@ -736,9 +684,7 @@ export class TextInput extends Ion implements IonicFormInput {
*/ */
clearTextInput() { clearTextInput() {
console.debug('Should clear input'); console.debug('Should clear input');
this._value = ''; this.value = '';
this.onChange(this._value);
this.writeValue(this._value);
} }
/** /**
@ -760,23 +706,6 @@ export class TextInput extends Ion implements IonicFormInput {
this._didBlurAfterEdit = false; this._didBlurAfterEdit = false;
} }
/**
* @hidden
* Angular2 Forms API method called by the view (formControlName) to register the
* onChange event handler that updates the model (Control).
* @param {Function} fn the onChange event handler.
*/
registerOnChange(fn: any) { this.onChange = fn; }
/**
* @hidden
* Angular2 Forms API method called by the view (formControlName) to register
* the onTouched event handler that marks model (Control) as touched.
* @param {Function} fn onTouched event handler.
*/
registerOnTouched(fn: any) { this.onTouched = fn; }
/** /**
* @hidden * @hidden
*/ */

View File

@ -45,6 +45,11 @@
<ion-input (input)="input6($event.target.value)"></ion-input> <ion-input (input)="input6($event.target.value)"></ion-input>
</ion-item> </ion-item>
<ion-item>
<ion-label floating>Date of Birth</ion-label>
<ion-datetime></ion-datetime>
</ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>

View File

@ -13,26 +13,30 @@ import { isPresent, isTrueProperty } from '../../util/util';
selector: 'ion-option' selector: 'ion-option'
}) })
export class Option { export class Option {
_selected: any = false;
_disabled: any = false; _selected: boolean = false;
_disabled: boolean = false;
_value: any; _value: any;
/** /**
* @output {any} Event to evaluate when option is selected. * @input {boolean} If true, the user cannot interact with this element.
*/ */
@Output() ionSelect: EventEmitter<any> = new EventEmitter(); @Input()
get disabled(): boolean {
constructor(private _elementRef: ElementRef) {} return this._disabled;
}
set disabled(val: boolean) {
this._disabled = isTrueProperty(val);
}
/** /**
* @input {boolean} If true, the element is selected. * @input {boolean} If true, the element is selected.
*/ */
@Input() @Input()
get selected() { get selected(): boolean {
return this._selected; return this._selected;
} }
set selected(val: boolean) {
set selected(val) {
this._selected = isTrueProperty(val); this._selected = isTrueProperty(val);
} }
@ -46,22 +50,16 @@ export class Option {
} }
return this.text; return this.text;
} }
set value(val: any) { set value(val: any) {
this._value = val; this._value = val;
} }
/** /**
* @input {boolean} If true, the user cannot interact with this element. * @output {any} Event to evaluate when option is selected.
*/ */
@Input() @Output() ionSelect: EventEmitter<any> = new EventEmitter();
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) { constructor(private _elementRef: ElementRef) {}
this._disabled = isTrueProperty(val);
}
/** /**
* @hidden * @hidden

View File

@ -1,12 +1,12 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, Optional, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { clamp, isPresent, isTrueProperty } from '../../util/util'; import { clamp, isTrueProperty } from '../../util/util';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { DomController } from '../../platform/dom-controller'; import { DomController } from '../../platform/dom-controller';
import { Form } from '../../util/form'; import { Form } from '../../util/form';
import { Haptic } from '../../tap-click/haptic'; import { Haptic } from '../../tap-click/haptic';
import { Ion } from '../ion'; import { BaseInput } from '../../util/base-input';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
import { PointerCoordinates, pointerCoord } from '../../util/dom'; import { PointerCoordinates, pointerCoord } from '../../util/dom';
@ -112,13 +112,11 @@ export const RANGE_VALUE_ACCESSOR: any = {
providers: [RANGE_VALUE_ACCESSOR], providers: [RANGE_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Range extends Ion implements AfterViewInit, ControlValueAccessor, OnDestroy { export class Range extends BaseInput<any> implements AfterViewInit, ControlValueAccessor, OnDestroy {
_dual: boolean; _dual: boolean;
_pin: boolean; _pin: boolean;
_disabled: boolean = false;
_pressed: boolean; _pressed: boolean;
_lblId: string;
_fn: Function;
_activeB: boolean; _activeB: boolean;
_rect: ClientRect; _rect: ClientRect;
@ -146,16 +144,6 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
@ViewChild('slider') public _slider: ElementRef; @ViewChild('slider') public _slider: ElementRef;
/**
* @hidden
*/
value: any;
/**
* @hidden
*/
id: string;
/** /**
* @input {number} Minimum integer value of the range. Defaults to `0`. * @input {number} Minimum integer value of the range. Defaults to `0`.
*/ */
@ -245,19 +233,6 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
this._dual = isTrueProperty(val); this._dual = isTrueProperty(val);
} }
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = val = isTrueProperty(val);
const item = this._item;
item && item.setElementClass('item-range-disabled', val);
}
/** /**
* Returns the ratio of the knob's is current location, which is a number * Returns the ratio of the knob's is current location, which is a number
* between `0` and `1`. If two knobs are used, this property represents * between `0` and `1`. If two knobs are used, this property represents
@ -282,25 +257,10 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
return null; return null;
} }
/**
* @output {Range} Emitted when the range selector drag starts.
*/
@Output() ionFocus: EventEmitter<Range> = new EventEmitter<Range>();
/**
* @output {Range} Emitted when the range value changes.
*/
@Output() ionChange: EventEmitter<Range> = new EventEmitter<Range>();
/**
* @output {Range} Emitted when the range selector drag ends.
*/
@Output() ionBlur: EventEmitter<Range> = new EventEmitter<Range>();
constructor( constructor(
private _form: Form, form: Form,
private _haptic: Haptic, private _haptic: Haptic,
@Optional() private _item: Item, @Optional() item: Item,
config: Config, config: Config,
private _plt: Platform, private _plt: Platform,
elementRef: ElementRef, elementRef: ElementRef,
@ -308,21 +268,17 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
private _dom: DomController, private _dom: DomController,
private _cd: ChangeDetectorRef private _cd: ChangeDetectorRef
) { ) {
super(config, elementRef, renderer, 'range'); super(config, elementRef, renderer, 'range', form, item, null);
this._events = new UIEventManager(_plt); this._events = new UIEventManager(_plt);
_form.register(this); this._value = 0;
if (_item) {
this.id = 'rng-' + _item.registerInput('range');
this._lblId = 'lbl-' + _item.id;
_item.setElementClass('item-range', true);
}
} }
/** /**
* @hidden * @hidden
*/ */
ngAfterViewInit() { ngAfterViewInit() {
this._initialize();
// add touchstart/mousedown listeners // add touchstart/mousedown listeners
this._events.pointerEvents({ this._events.pointerEvents({
element: this._slider.nativeElement, element: this._slider.nativeElement,
@ -346,7 +302,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
} }
// trigger ionFocus event // trigger ionFocus event
this.ionFocus.emit(this); this._setFocus();
// prevent default so scrolling does not happen // prevent default so scrolling does not happen
ev.preventDefault(); ev.preventDefault();
@ -375,7 +331,9 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
/** @internal */ /** @internal */
_pointerMove(ev: UIEvent) { _pointerMove(ev: UIEvent) {
if (!this._disabled) { if (this._disabled) {
return;
}
// prevent default so scrolling does not happen // prevent default so scrolling does not happen
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -389,11 +347,12 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
this._haptic.gestureSelectionChanged(); this._haptic.gestureSelectionChanged();
} }
} }
}
/** @internal */ /** @internal */
_pointerUp(ev: UIEvent) { _pointerUp(ev: UIEvent) {
if (!this._disabled) { if (this._disabled) {
return;
}
// prevent default so scrolling does not happen // prevent default so scrolling does not happen
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -405,8 +364,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
this._haptic.gestureSelectionEnd(); this._haptic.gestureSelectionEnd();
// trigger ionBlur event // trigger ionBlur event
this.ionBlur.emit(this); this._setBlur();
}
} }
/** @internal */ /** @internal */
@ -447,27 +405,24 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
} }
// value has been updated // value has been updated
let value;
if (this._dual) { if (this._dual) {
// dual knobs have an lower and upper value // dual knobs have an lower and upper value
if (!this.value) { value = {
// ensure we're always updating the same object lower: Math.min(this._valA, this._valB),
this.value = {}; upper: Math.max(this._valA, this._valB)
} };
this.value.lower = Math.min(this._valA, this._valB);
this.value.upper = Math.max(this._valA, this._valB);
console.debug(`range, updateKnob: ${ratio}, lower: ${this.value.lower}, upper: ${this.value.upper}`); console.debug(`range, updateKnob: ${ratio}, lower: ${this.value.lower}, upper: ${this.value.upper}`);
} else { } else {
// single knob only has one value // single knob only has one value
this.value = this._valA; value = this._valA;
console.debug(`range, updateKnob: ${ratio}, value: ${this.value}`); console.debug(`range, updateKnob: ${ratio}, value: ${this.value}`);
} }
this._debouncer.debounce(() => { // Update input value
this.onChange(this.value); this.value = value;
this.ionChange.emit(this);
});
return true; return true;
} }
@ -566,13 +521,20 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
return clamp(0, value, 1); return clamp(0, value, 1);
} }
_inputNormalize(val: any): any {
if (this._dual) {
return val;
} else {
val = parseFloat(val);
return isNaN(val) ? undefined : val;
}
}
/** /**
* @hidden * @hidden
*/ */
writeValue(val: any) { _inputUpdated() {
if (isPresent(val)) { const val = this.value;
this.value = val;
if (this._dual) { if (this._dual) {
this._valA = val.lower; this._valA = val.lower;
this._valB = val.upper; this._valB = val.upper;
@ -585,51 +547,14 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
} }
this._updateBar(); this._updateBar();
}
}
/**
* @hidden
*/
registerOnChange(fn: Function): void {
this._fn = fn;
this.onChange = (val: any) => {
fn(val);
this.onTouched();
};
}
/**
* @hidden
*/
registerOnTouched(fn: any) { this.onTouched = fn; }
/**
* @hidden
*/
onChange(val: any) {
// used when this input does not have an ngModel or formControlName
this.onTouched();
this._cd.detectChanges(); this._cd.detectChanges();
} }
/**
* @hidden
*/
onTouched() { }
/**
* @hidden
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
/** /**
* @hidden * @hidden
*/ */
ngOnDestroy() { ngOnDestroy() {
this._form.deregister(this); super.ngOnDestroy();
this._events.destroy(); this._events.destroy();
} }
} }

View File

@ -1,10 +1,21 @@
import { Range } from '../range'; import { Range } from '../range';
import { mockChangeDetectorRef, mockConfig, mockDomController, mockElementRef, mockHaptic, mockPlatform, mockRenderer } from '../../../util/mock-providers'; import { mockChangeDetectorRef, mockConfig, mockDomController, mockItem, mockElementRef, mockHaptic, mockPlatform, mockRenderer } from '../../../util/mock-providers';
import { Form } from '../../../util/form'; import { Form } from '../../../util/form';
import { commonInputTest, NUMBER_CORPUS } from '../../../util/input-tester';
describe('Range', () => { describe('Range', () => {
it('should pass common test', () => {
const range = createRange();
range._slider = mockElementRef();
commonInputTest(range, {
defaultValue: 0,
corpus: NUMBER_CORPUS
});
});
describe('valueToRatio', () => { describe('valueToRatio', () => {
it('step=1', () => { it('step=1', () => {
let range = createRange(); let range = createRange();
@ -68,5 +79,5 @@ describe('Range', () => {
function createRange(): Range { function createRange(): Range {
let form = new Form(); let form = new Form();
return new Range(form, mockHaptic(), null, mockConfig(), mockPlatform(), mockElementRef(), mockRenderer(), mockDomController(), mockChangeDetectorRef()); return new Range(form, mockHaptic(), mockItem(), mockConfig(), mockPlatform(), mockElementRef(), mockRenderer(), mockDomController(), mockChangeDetectorRef());
} }

View File

@ -2,7 +2,7 @@ import { Component, ElementRef, EventEmitter, HostBinding, Input, Optional, Outp
import { NgControl } from '@angular/forms'; import { NgControl } from '@angular/forms';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Ion } from '../ion'; import { BaseInput } from '../../util/base-input';
import { isPresent, isTrueProperty } from '../../util/util'; import { isPresent, isTrueProperty } from '../../util/util';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
import { TimeoutDebouncer } from '../../util/debouncer'; import { TimeoutDebouncer } from '../../util/debouncer';
@ -53,9 +53,8 @@ import { TimeoutDebouncer } from '../../util/debouncer';
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class Searchbar extends Ion { export class Searchbar extends BaseInput<string> {
_value: string|number = '';
_shouldBlur: boolean = true; _shouldBlur: boolean = true;
_shouldAlignLeft: boolean = true; _shouldAlignLeft: boolean = true;
_isCancelVisible: boolean = false; _isCancelVisible: boolean = false;
@ -144,16 +143,6 @@ export class Searchbar extends Ion {
*/ */
@Output() ionInput: EventEmitter<UIEvent> = new EventEmitter<UIEvent>(); @Output() ionInput: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
/**
* @output {event} Emitted when the Searchbar input has blurred.
*/
@Output() ionBlur: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
/**
* @output {event} Emitted when the Searchbar input has focused.
*/
@Output() ionFocus: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
/** /**
* @output {event} Emitted when the cancel button is clicked. * @output {event} Emitted when the cancel button is clicked.
*/ */
@ -176,12 +165,7 @@ export class Searchbar extends Ion {
renderer: Renderer, renderer: Renderer,
@Optional() ngControl: NgControl @Optional() ngControl: NgControl
) { ) {
super(config, elementRef, renderer, 'searchbar'); super(config, elementRef, renderer, 'searchbar', null, null, ngControl);
// If the user passed a ngControl we need to set the valueAccessor
if (ngControl) {
ngControl.valueAccessor = this;
}
} }
@ViewChild('searchbarInput') _searchbarInput: ElementRef; @ViewChild('searchbarInput') _searchbarInput: ElementRef;
@ -191,21 +175,12 @@ export class Searchbar extends Ion {
@ViewChild('cancelButton', {read: ElementRef}) _cancelButton: ElementRef; @ViewChild('cancelButton', {read: ElementRef}) _cancelButton: ElementRef;
/** /**
* @input {string} Set the input value. * @hidden
* After View Checked position the elements
*/ */
@Input() ngAfterViewInit() {
get value() { this._initialize();
return this._value; this.positionElements();
}
set value(val) {
this._value = val;
if (this._searchbarInput) {
let ele = this._searchbarInput.nativeElement;
if (ele) {
ele.value = val;
}
}
} }
/** /**
@ -213,7 +188,7 @@ export class Searchbar extends Ion {
* On Initialization check for attributes * On Initialization check for attributes
*/ */
ngOnInit() { ngOnInit() {
let showCancelButton = this.showCancelButton; const showCancelButton = this.showCancelButton;
if (typeof showCancelButton === 'string') { if (typeof showCancelButton === 'string') {
this.showCancelButton = (showCancelButton === '' || showCancelButton === 'true'); this.showCancelButton = (showCancelButton === '' || showCancelButton === 'true');
} }
@ -221,9 +196,14 @@ export class Searchbar extends Ion {
/** /**
* @hidden * @hidden
* After View Checked position the elements
*/ */
ngAfterContentInit() { _inputUpdated() {
if (this._searchbarInput) {
var ele = this._searchbarInput.nativeElement;
if (ele) {
ele.value = this.value;
}
}
this.positionElements(); this.positionElements();
} }
@ -233,9 +213,9 @@ export class Searchbar extends Ion {
* based on the input value and if it is focused. (ios only) * based on the input value and if it is focused. (ios only)
*/ */
positionElements() { positionElements() {
let isAnimated = this._animated; const isAnimated = this._animated;
let prevAlignLeft = this._shouldAlignLeft; const prevAlignLeft = this._shouldAlignLeft;
let shouldAlignLeft = (!isAnimated || (this._value && this._value.toString().trim() !== '') || this._sbHasFocus === true); const shouldAlignLeft = (!isAnimated || (this._value && this._value.toString().trim() !== '') || this._sbHasFocus === true);
this._shouldAlignLeft = shouldAlignLeft; this._shouldAlignLeft = shouldAlignLeft;
if (this._mode !== 'ios') { if (this._mode !== 'ios') {
@ -254,8 +234,8 @@ export class Searchbar extends Ion {
if (!this._searchbarInput || !this._searchbarIcon) { if (!this._searchbarInput || !this._searchbarIcon) {
return; return;
} }
let inputEle = this._searchbarInput.nativeElement; const inputEle = this._searchbarInput.nativeElement;
let iconEle = this._searchbarIcon.nativeElement; const iconEle = this._searchbarIcon.nativeElement;
if (this._shouldAlignLeft) { if (this._shouldAlignLeft) {
inputEle.removeAttribute('style'); inputEle.removeAttribute('style');
@ -290,15 +270,15 @@ export class Searchbar extends Ion {
if (!this._cancelButton || !this._cancelButton.nativeElement) { if (!this._cancelButton || !this._cancelButton.nativeElement) {
return; return;
} }
let showShowCancel = this._sbHasFocus; const showShowCancel = this._sbHasFocus;
if (showShowCancel !== this._isCancelVisible) { if (showShowCancel !== this._isCancelVisible) {
let cancelStyleEle = this._cancelButton.nativeElement; var cancelStyleEle = this._cancelButton.nativeElement;
let cancelStyle = cancelStyleEle.style; var cancelStyle = cancelStyleEle.style;
this._isCancelVisible = showShowCancel; this._isCancelVisible = showShowCancel;
if (showShowCancel) { if (showShowCancel) {
cancelStyle.marginRight = '0'; cancelStyle.marginRight = '0';
} else { } else {
let offset = cancelStyleEle.offsetWidth; var offset = cancelStyleEle.offsetWidth;
if (offset > 0) { if (offset > 0) {
cancelStyle.marginRight = -offset + 'px'; cancelStyle.marginRight = -offset + 'px';
} }
@ -312,11 +292,7 @@ export class Searchbar extends Ion {
* Update the Searchbar input value when the input changes * Update the Searchbar input value when the input changes
*/ */
inputChanged(ev: any) { inputChanged(ev: any) {
this._value = ev.target.value; this.value = ev.target.value;
this._debouncer.debounce(() => {
this.onChange(this._value);
this.ionInput.emit(ev);
});
} }
/** /**
@ -324,11 +300,8 @@ export class Searchbar extends Ion {
* Sets the Searchbar to focused and active on input focus. * Sets the Searchbar to focused and active on input focus.
*/ */
inputFocused(ev: UIEvent) { inputFocused(ev: UIEvent) {
this.ionFocus.emit(ev); this._setFocus();
this._sbHasFocus = true;
this._isActive = true; this._isActive = true;
this.positionElements();
} }
/** /**
@ -344,10 +317,7 @@ export class Searchbar extends Ion {
this._shouldBlur = true; this._shouldBlur = true;
return; return;
} }
this.ionBlur.emit(ev); this._setBlur();
this._sbHasFocus = false;
this.positionElements();
} }
/** /**
@ -363,8 +333,6 @@ export class Searchbar extends Ion {
let value = this._value; let value = this._value;
if (isPresent(value) && value !== '') { if (isPresent(value) && value !== '') {
this.value = ''; // DOM WRITE this.value = ''; // DOM WRITE
this.onChange(this._value);
this.ionInput.emit(ev);
} }
}, 16 * 4); }, 16 * 4);
this._shouldBlur = false; this._shouldBlur = false;
@ -384,42 +352,8 @@ export class Searchbar extends Ion {
this._isActive = false; this._isActive = false;
} }
/** _setFocus() {
* @hidden
* Write a new value to the element.
*/
writeValue(val: any) {
this.value = val;
this.positionElements();
}
/**
* @hidden
*/
onChange = (_: any) => {};
/**
* @hidden
*/
onTouched = () => {};
/**
* @hidden
* Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn;
}
/**
* @hidden
* Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
setFocus() {
this._renderer.invokeElementMethod(this._searchbarInput.nativeElement, 'focus'); this._renderer.invokeElementMethod(this._searchbarInput.nativeElement, 'focus');
super._setFocus();
} }
} }

View File

@ -1,10 +1,8 @@
import { ContentChildren, Directive, ElementRef, EventEmitter, Input, Output, Optional, QueryList, Renderer } from '@angular/core'; import { ContentChildren, Directive, ElementRef, Optional, QueryList, Renderer } from '@angular/core';
import { NgControl } from '@angular/forms'; import { NgControl } from '@angular/forms';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Ion } from '../ion'; import { BaseInput } from '../../util/base-input';
import { isPresent, isTrueProperty } from '../../util/util';
import { SegmentButton } from './segment-button'; import { SegmentButton } from './segment-button';
/** /**
@ -66,19 +64,7 @@ import { SegmentButton } from './segment-button';
@Directive({ @Directive({
selector: 'ion-segment' selector: 'ion-segment'
}) })
export class Segment extends Ion { export class Segment extends BaseInput<string> {
_disabled: boolean = false;
/**
* @hidden
*/
value: string;
/**
* @output {Any} Emitted when a segment button has been changed.
*/
@Output() ionChange: EventEmitter<SegmentButton> = new EventEmitter<SegmentButton>();
/** /**
* @hidden * @hidden
@ -91,85 +77,32 @@ export class Segment extends Ion {
renderer: Renderer, renderer: Renderer,
@Optional() ngControl: NgControl @Optional() ngControl: NgControl
) { ) {
super(config, elementRef, renderer, 'segment'); super(config, elementRef, renderer, 'segment', null, null, ngControl);
if (ngControl) {
ngControl.valueAccessor = this;
}
}
/**
* @input {boolean} If true, the user cannot interact with any of the buttons in the segment.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = isTrueProperty(val);
if (this._buttons) {
this._buttons.forEach(button => {
button._setElementClass('segment-button-disabled', this._disabled);
});
}
}
/**
* @hidden
* Write a new value to the element.
*/
writeValue(value: any) {
this.value = isPresent(value) ? value : '';
if (this._buttons) {
let buttons = this._buttons.toArray();
for (let button of buttons) {
button.isActive = (button.value === this.value);
}
}
} }
/** /**
* @hidden * @hidden
*/ */
ngAfterViewInit() { ngAfterViewInit() {
this._initialize();
this._buttons.forEach(button => { this._buttons.forEach(button => {
button.ionSelect.subscribe((selectedButton: any) => { button.ionSelect.subscribe((selectedButton: any) => this.value = selectedButton.value);
this.writeValue(selectedButton.value);
this.onChange(selectedButton.value);
this.ionChange.emit(selectedButton);
});
if (isPresent(this.value)) {
button.isActive = (button.value === this.value);
}
if (isTrueProperty(this._disabled)) {
button._setElementClass('segment-button-disabled', this._disabled);
}
}); });
} }
/** /**
* @hidden * @hidden
* Write a new value to the element.
*/ */
onChange = (_: any) => {}; _inputUpdated() {
/** if (this._buttons) {
* @hidden var buttons = this._buttons.toArray();
*/ var value = this.value;
onTouched = (_: any) => {}; for (var button of buttons) {
button.isActive = (button.value === value);
}
}
}
/**
* @hidden
* Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: any) { this.onChange = fn; }
/**
* @hidden
* Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: any) { this.onTouched = fn; }
} }

View File

@ -1,13 +1,13 @@
import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, HostListener, OnDestroy, Optional, Output, Renderer, QueryList, ViewEncapsulation } from '@angular/core'; import { AfterViewInit, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, HostListener, OnDestroy, Optional, Output, Renderer, QueryList, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ActionSheet } from '../action-sheet/action-sheet'; import { ActionSheet } from '../action-sheet/action-sheet';
import { Alert } from '../alert/alert'; import { Alert } from '../alert/alert';
import { App } from '../app/app'; import { App } from '../app/app';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Form } from '../../util/form'; import { Form } from '../../util/form';
import { Ion } from '../ion'; import { BaseInput } from '../../util/base-input';
import { isBlank, isCheckedProperty, isTrueProperty, deepCopy } from '../../util/util'; import { isCheckedProperty, isTrueProperty, isBlank, deepCopy } from '../../util/util';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { NavController } from '../../navigation/nav-controller'; import { NavController } from '../../navigation/nav-controller';
import { Option } from '../option/option'; import { Option } from '../option/option';
@ -141,21 +141,13 @@ export const SELECT_VALUE_ACCESSOR: any = {
providers: [SELECT_VALUE_ACCESSOR], providers: [SELECT_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Select extends Ion implements AfterContentInit, ControlValueAccessor, OnDestroy { export class Select extends BaseInput<string[]> implements AfterViewInit, OnDestroy {
_disabled: any = false;
_labelId: string;
_multi: boolean = false; _multi: boolean = false;
_options: QueryList<Option>; _options: QueryList<Option>;
_values: string[] = []; _values: string[] = [];
_texts: string[] = []; _texts: string[] = [];
_text: string = ''; _text: string = '';
_fn: Function;
_isOpen: boolean = false;
/**
* @hidden
*/
id: string;
/** /**
* @input {string} The text to display on the cancel button. Default: `Cancel`. * @input {string} The text to display on the cancel button. Default: `Cancel`.
@ -190,11 +182,6 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
*/ */
@Input() selectedText: string = ''; @Input() selectedText: string = '';
/**
* @output {any} Emitted when the selection has changed.
*/
@Output() ionChange: EventEmitter<any> = new EventEmitter();
/** /**
* @output {any} Emitted when the selection was cancelled. * @output {any} Emitted when the selection was cancelled.
*/ */
@ -202,22 +189,26 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
constructor( constructor(
private _app: App, private _app: App,
private _form: Form, form: Form,
public config: Config, public config: Config,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
@Optional() public _item: Item, @Optional() item: Item,
@Optional() private _nav: NavController @Optional() private _nav: NavController
) { ) {
super(config, elementRef, renderer, 'select'); super(config, elementRef, renderer, 'select', form, item, null);
_form.register(this);
if (_item) {
this.id = 'sel-' + _item.registerInput('select');
this._labelId = 'lbl-' + _item.id;
this._item.setElementClass('item-select', true);
} }
/**
* @hidden
*/
ngAfterViewInit() {
this._initialize();
}
ngAfterContentInit() {
this._inputUpdated();
} }
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
@ -233,16 +224,14 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
@HostListener('keyup.space') @HostListener('keyup.space')
_keyup() { _keyup() {
if (!this._isOpen) {
this.open(); this.open();
} }
}
/** /**
* Open the select interface. * Open the select interface.
*/ */
open() { open() {
if (this._disabled) { if (this._isFocus || this._disabled) {
return; return;
} }
@ -284,8 +273,7 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
role: (input.selected ? 'selected' : ''), role: (input.selected ? 'selected' : ''),
text: input.text, text: input.text,
handler: () => { handler: () => {
this.onChange(input.value); this.value = input.value;
this.ionChange.emit(input.value);
input.ionSelect.emit(input.value); input.ionSelect.emit(input.value);
} }
}; };
@ -340,19 +328,15 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
overlay.addButton({ overlay.addButton({
text: this.okText, text: this.okText,
handler: (selectedValues: any) => { handler: (selectedValues: any) => this.value = selectedValues
this.onChange(selectedValues);
this.ionChange.emit(selectedValues);
}
}); });
} }
overlay.present(selectOptions); overlay.present(selectOptions);
this._setFocus();
this._isOpen = true;
overlay.onDidDismiss(() => { overlay.onDidDismiss(() => {
this._isOpen = false; this._setBlur();
}); });
} }
@ -377,20 +361,6 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
return (this._multi ? this._texts : this._texts.join()); return (this._multi ? this._texts : this._texts.join());
} }
/**
* @hidden
*/
checkHasValue(inputValue: any) {
if (this._item) {
let hasValue: boolean;
if (Array.isArray(inputValue)) {
hasValue = inputValue.length > 0;
} else {
hasValue = !isBlank(inputValue);
}
this._item.setElementClass('input-has-value', hasValue);
}
}
/** /**
* @private * @private
@ -399,19 +369,21 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
set options(val: QueryList<Option>) { set options(val: QueryList<Option>) {
this._options = val; this._options = val;
if (!this._values.length) { if (this._values.length === 0) {
// there are no values set at this point // there are no values set at this point
// so check to see who should be selected // so check to see who should be selected
this._values = val.filter(o => o.selected).map(o => o.value); this._values = val.filter(o => o.selected).map(o => o.value);
} }
this._updOpts(); this._inputUpdated();
} }
/** /**
* @hidden * @hidden
*/ */
_updOpts() { _inputUpdated() {
const val = this.value;
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._texts = []; this._texts = [];
if (this._options) { if (this._options) {
@ -430,84 +402,4 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
this._text = this._texts.join(', '); this._text = this._texts.join(', ');
} }
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = isTrueProperty(val);
this._item && this._item.setElementClass('item-select-disabled', this._disabled);
}
/**
* @hidden
*/
writeValue(val: any) {
console.debug('select, writeValue', val);
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._updOpts();
this.checkHasValue(val);
}
/**
* @hidden
*/
ngAfterContentInit() {
this._updOpts();
}
/**
* @hidden
*/
registerOnChange(fn: Function): void {
this._fn = fn;
this.onChange = (val: any) => {
console.debug('select, onChange', val);
fn(val);
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._updOpts();
this.checkHasValue(val);
this.onTouched();
};
}
/**
* @hidden
*/
registerOnTouched(fn: any) { this.onTouched = fn; }
/**
* @hidden
*/
onChange(val: any) {
// onChange used when there is not an formControlName
console.debug('select, onChange w/out formControlName', val);
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._updOpts();
this.checkHasValue(val);
this.onTouched();
}
/**
* @hidden
*/
onTouched() { }
/**
* @hidden
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
/**
* @hidden
*/
ngOnDestroy() {
this._form.deregister(this);
}
} }

View File

@ -0,0 +1,28 @@
import { Toggle } from '../toggle';
import { mockConfig, mockPlatform, mockHaptic, mockElementRef, mockGestureController, mockRenderer, mockItem, mockForm, mockChangeDetectorRef } from '../../../util/mock-providers';
import { commonInputTest, BOOLEAN_CORPUS } from '../../../util/input-tester';
describe('Toggle', () => {
it('should pass common test', () => {
const platform = mockPlatform();
const config = mockConfig();
const elementRef = mockElementRef();
const renderer = mockRenderer();
const item: any = mockItem();
const form = mockForm();
const haptic = mockHaptic();
const cd = mockChangeDetectorRef();
const gesture = mockGestureController();
const toggle = new Toggle(form, config, platform, elementRef, renderer, haptic, item, gesture, null, cd);
commonInputTest(toggle, {
defaultValue: false,
corpus: BOOLEAN_CORPUS
});
});
});

View File

@ -1,13 +1,13 @@
import { AfterContentInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostListener, Input, OnDestroy, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { DomController } from '../../platform/dom-controller'; import { DomController } from '../../platform/dom-controller';
import { Form, IonicTapInput } from '../../util/form'; import { Form, IonicTapInput } from '../../util/form';
import { GestureController } from '../../gestures/gesture-controller'; import { GestureController } from '../../gestures/gesture-controller';
import { Haptic } from '../../tap-click/haptic'; import { Haptic } from '../../tap-click/haptic';
import { Ion } from '../ion'; import { assert, isTrueProperty } from '../../util/util';
import { isTrueProperty, assert } from '../../util/util'; import { BaseInput } from '../../util/base-input';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { KEY_ENTER, KEY_SPACE } from '../../platform/key'; import { KEY_ENTER, KEY_SPACE } from '../../platform/key';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
@ -60,14 +60,14 @@ export const TOGGLE_VALUE_ACCESSOR: any = {
@Component({ @Component({
selector: 'ion-toggle', selector: 'ion-toggle',
template: template:
'<div class="toggle-icon" [class.toggle-checked]="_checked" [class.toggle-activated]="_activated">' + '<div class="toggle-icon" [class.toggle-checked]="_value" [class.toggle-activated]="_activated">' +
'<div class="toggle-inner"></div>' + '<div class="toggle-inner"></div>' +
'</div>' + '</div>' +
'<button role="checkbox" ' + '<button role="checkbox" ' +
'type="button" ' + 'type="button" ' +
'ion-button="item-cover" ' + 'ion-button="item-cover" ' +
'[id]="id" ' + '[id]="id" ' +
'[attr.aria-checked]="_checked" ' + '[attr.aria-checked]="_value" ' +
'[attr.aria-labelledby]="_labelId" ' + '[attr.aria-labelledby]="_labelId" ' +
'[attr.aria-disabled]="_disabled" ' + '[attr.aria-disabled]="_disabled" ' +
'class="item-cover">' + 'class="item-cover">' +
@ -78,57 +78,65 @@ export const TOGGLE_VALUE_ACCESSOR: any = {
providers: [TOGGLE_VALUE_ACCESSOR], providers: [TOGGLE_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Toggle extends Ion implements IonicTapInput, AfterContentInit, ControlValueAccessor, OnDestroy { export class Toggle extends BaseInput<boolean> implements IonicTapInput, AfterViewInit, OnDestroy {
_checked: boolean = false;
_init: boolean = false;
_disabled: boolean = false;
_labelId: string;
_activated: boolean = false; _activated: boolean = false;
_startX: number; _startX: number;
_msPrv: number = 0; _msPrv: number = 0;
_fn: Function = null;
_gesture: ToggleGesture; _gesture: ToggleGesture;
/** @hidden */
id: string;
/** /**
* @output {Toggle} Emitted when the toggle value changes. * @input {boolean} If true, the element is selected.
*/ */
@Output() ionChange: EventEmitter<Toggle> = new EventEmitter<Toggle>(); @Input()
get checked(): boolean {
return this.value;
}
set checked(val: boolean) {
this.value = val;
}
constructor( constructor(
public _form: Form, form: Form,
config: Config, config: Config,
private _plt: Platform, private _plt: Platform,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
private _haptic: Haptic, private _haptic: Haptic,
@Optional() public _item: Item, @Optional() item: Item,
private _gestureCtrl: GestureController, private _gestureCtrl: GestureController,
private _domCtrl: DomController, private _domCtrl: DomController,
private _cd: ChangeDetectorRef private _cd: ChangeDetectorRef
) { ) {
super(config, elementRef, renderer, 'toggle'); super(config, elementRef, renderer, 'toggle', form, item, null);
_form.register(this); this._value = false;
if (_item) {
this.id = 'tgl-' + _item.registerInput('toggle');
this._labelId = 'lbl-' + _item.id;
this._item.setElementClass('item-toggle', true);
}
} }
/** /**
* @hidden * @hidden
*/ */
ngAfterContentInit() { ngAfterViewInit() {
this._init = true; this._initialize();
this._gesture = new ToggleGesture(this._plt, this, this._gestureCtrl, this._domCtrl); this._gesture = new ToggleGesture(this._plt, this, this._gestureCtrl, this._domCtrl);
this._gesture.listen(); this._gesture.listen();
} }
/**
* @hidden
*/
_inputNormalize(val: any): boolean {
return isTrueProperty(val);
}
/**
* @hidden
*/
_inputUpdated() {
this._item && this._item.setElementClass('item-toggle-checked', this.value);
this._cd.detectChanges();
}
/** /**
* @hidden * @hidden
*/ */
@ -137,6 +145,7 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
console.debug('toggle, _onDragStart', startX); console.debug('toggle, _onDragStart', startX);
this._startX = startX; this._startX = startX;
this._setFocus();
this._activated = true; this._activated = true;
} }
@ -151,16 +160,16 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
console.debug('toggle, _onDragMove', currentX); console.debug('toggle, _onDragMove', currentX);
if (this._checked) { if (this._value) {
if (currentX + 15 < this._startX) { if (currentX + 15 < this._startX) {
this.onChange(false); this.value = false;
this._haptic.selection(); this._haptic.selection();
this._startX = currentX; this._startX = currentX;
this._activated = true; this._activated = true;
} }
} else if (currentX - 15 > this._startX) { } else if (currentX - 15 > this._startX) {
this.onChange(true); this.value = true;
this._haptic.selection(); this._haptic.selection();
this._startX = currentX; this._startX = currentX;
this._activated = (currentX < this._startX + 5); this._activated = (currentX < this._startX + 5);
@ -177,98 +186,22 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
} }
console.debug('toggle, _onDragEnd', endX); console.debug('toggle, _onDragEnd', endX);
if (this.checked) { if (this._value) {
if (this._startX + 4 > endX) { if (this._startX + 4 > endX) {
this.onChange(false); this.value = false;
this._haptic.selection(); this._haptic.selection();
} }
} else if (this._startX - 4 < endX) { } else if (this._startX - 4 < endX) {
this.onChange(true); this.value = true;
this._haptic.selection(); this._haptic.selection();
} }
this._activated = false; this._activated = false;
this._setBlur();
this._startX = null; this._startX = null;
} }
/**
* @input {boolean} If true, the element is selected.
*/
@Input()
get checked(): boolean {
return this._checked;
}
set checked(val: boolean) {
this._setChecked(isTrueProperty(val));
this.onChange(this._checked);
}
/**
* @hidden
*/
_setChecked(isChecked: boolean) {
if (isChecked !== this._checked) {
this._checked = isChecked;
if (this._init) {
this.ionChange.emit(this);
}
this._item && this._item.setElementClass('item-toggle-checked', isChecked);
}
}
/**
* @hidden
*/
writeValue(val: any) {
this._setChecked( isTrueProperty(val) );
}
/**
* @hidden
*/
registerOnChange(fn: Function): void {
this._fn = fn;
}
/**
* @hidden
*/
registerOnTouched(fn: any) {
this.onTouched = fn;
}
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this._disabled = isTrueProperty(val);
this._item && this._item.setElementClass('item-toggle-disabled', this._disabled);
}
/**
* @hidden
*/
onChange(isChecked: boolean) {
// used when this input does not have an ngModel or formControlName
console.debug('toggle, onChange', isChecked);
this._fn && this._fn(isChecked);
this._setChecked(isChecked);
this.onTouched();
this._cd.detectChanges();
}
/**
* @hidden
*/
onTouched() {}
/** /**
* @hidden * @hidden
*/ */
@ -277,7 +210,7 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
console.debug(`toggle, keyup: ${ev.keyCode}`); console.debug(`toggle, keyup: ${ev.keyCode}`);
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.onChange(!this._checked); this.value = !this.value;
} }
} }
@ -288,20 +221,12 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
this._elementRef.nativeElement.querySelector('button').focus(); this._elementRef.nativeElement.querySelector('button').focus();
} }
/**
* @hidden
*/
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
/** /**
* @hidden * @hidden
*/ */
ngOnDestroy() { ngOnDestroy() {
this._form && this._form.deregister(this); super.ngOnDestroy();
this._gesture && this._gesture.destroy(); this._gesture && this._gesture.destroy();
this._fn = null;
} }
} }

259
src/util/base-input.ts Normal file
View File

@ -0,0 +1,259 @@
import { ElementRef, EventEmitter, Input, Output, Renderer } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { NgControl } from '@angular/forms';
import { isPresent, isArray, isTrueProperty, assert } from './util';
import { Ion } from '../components/ion';
import { Config } from '../config/config';
import { Item } from '../components/item/item';
import { Form } from './form';
import { TimeoutDebouncer } from './debouncer';
export interface CommonInput<T> extends ControlValueAccessor {
id: string;
disabled: boolean;
value: T;
ionFocus: EventEmitter<CommonInput<T>>;
ionChange: EventEmitter<BaseInput<T>>;
ionBlur: EventEmitter<BaseInput<T>>;
initFocus(): void;
isFocus(): boolean;
_inputNormalize(val: any): T;
_inputShouldChange(val: T): boolean;
_inputUpdated(): void;
}
export class BaseInput<T> extends Ion implements CommonInput<T> {
_value: T = null;
_onChanged: Function;
_onTouched: Function;
_isFocus: boolean = false;
_labelId: string;
_disabled: boolean = false;
_debouncer: TimeoutDebouncer;
_init: boolean = false;
id: string;
/**
* @output {Range} Emitted when the range selector drag starts.
*/
@Output() ionFocus: EventEmitter<BaseInput<T>> = new EventEmitter<BaseInput<T>>();
/**
* @output {Range} Emitted when the range value changes.
*/
@Output() ionChange: EventEmitter<BaseInput<T>> = new EventEmitter<BaseInput<T>>();
/**
* @output {Range} Emitted when the range selector drag ends.
*/
@Output() ionBlur: EventEmitter<BaseInput<T>> = new EventEmitter<BaseInput<T>>();
/**
* @input {boolean} If true, the user cannot interact with this element.
*/
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(val: boolean) {
this.setDisabledState(val);
}
constructor(
config: Config,
elementRef: ElementRef,
renderer: Renderer,
name: string,
public _form: Form,
public _item: Item,
ngControl: NgControl
) {
super(config, elementRef, renderer, name);
_form && _form.register(this);
if (_item) {
this.id = name + '-' + _item.registerInput(name);
this._labelId = 'lbl-' + _item.id;
this._item.setElementClass('item-' + name, true);
}
// If the user passed a ngControl we need to set the valueAccessor
if (ngControl) {
ngControl.valueAccessor = this;
}
}
get value(): T {
return this._value;
}
set value(val: T) {
if (this._writeValue(val)) {
this.onChange();
}
}
// 1. Updates the value
// 2. Calls _inputUpdated()
// 3. Dispatch onChange events
setValue(val: T) {
this.value = val;
}
/**
* @hidden
*/
setDisabledState(isDisabled: boolean) {
this._disabled = isTrueProperty(isDisabled);
this._item && this._item.setElementClass('item-select-disabled', isDisabled);
}
/**
* @hidden
*/
writeValue(val: any) {
this._writeValue(val);
}
_writeValue(val: any): boolean {
const normalized = this._inputNormalize(val);
const shouldUpdate = this._inputShouldChange(normalized);
if (shouldUpdate) {
console.debug('BaseInput: value has changed:', val);
this._value = normalized;
this._inputCheckHasValue(normalized);
this._inputUpdated();
if (this._init) {
this.ionChange.emit(this);
}
return true;
}
return false;
}
/**
* @hidden
*/
registerOnChange(fn: Function) {
this._onChanged = fn;
}
/**
* @hidden
*/
registerOnTouched(fn: any) {
this._onTouched = fn;
}
/**
* @hidden
*/
_initialize() {
if (this._init) {
assert(false, 'input was already initilized');
return;
}
this._init = true;
}
/**
* @hidden
*/
_setFocus() {
if (this._isFocus) {
return;
}
this._isFocus = true;
this.ionFocus.emit(this);
this._inputUpdated();
}
/**
* @hidden
*/
_setBlur() {
if (!this._isFocus) {
return;
}
this._isFocus = false;
this.ionBlur.emit(this);
this._inputUpdated();
}
/**
* @hidden
*/
private onChange() {
this._onChanged && this._onChanged(this._value);
this._onTouched && this._onTouched();
}
/**
* @hidden
*/
isFocus(): boolean {
return this._isFocus;
}
/**
* @hidden
*/
ngOnDestroy() {
this._form && this._form.deregister(this);
this._init = false;
}
/**
* @hidden
*/
ngAfterViewInit() {
this._initialize();
}
/**
* @hidden
*/
_inputCheckHasValue(val: T) {
if (!this._item) {
return;
}
let hasValue: boolean;
if (isArray(val)) {
hasValue = val.length > 0;
} else {
hasValue = isPresent(val);
}
this._item.setElementClass('input-has-value', hasValue);
}
/**
* @hidden
*/
initFocus() {}
/**
* @hidden
*/
_inputNormalize(val: any): T {
return val;
}
/**
* @hidden
*/
_inputShouldChange(val: T): boolean {
return (typeof val !== 'undefined') && this._value !== val;
}
/**
* @hidden
*/
_inputUpdated() {}
}

200
src/util/input-tester.ts Normal file
View File

@ -0,0 +1,200 @@
import { BaseInput } from './base-input';
import { assert } from './util';
const lorem_ipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi maximus nisl lobortis interdum condimentum. Cras volutpat, massa quis vehicula eleifend, turpis mauris sodales erat, ut varius ligula ipsum et turpis. Aliquam erat volutpat. Maecenas sodales pellentesque auctor. Suspendisse faucibus a erat sit amet pretium. Vestibulum nec tempus tellus. Mauris fringilla faucibus dui sed vestibulum. Curabitur porttitor consectetur nisl. Nulla porta, neque sed congue tempus, erat nunc rutrum diam, eu elementum sapien leo quis eros. Donec non convallis felis. Nam eu pharetra sapien.';
export const TEXT_CORPUS: any = [
['', ''],
[' ', ' '],
['hola', 'hola'],
['adiós', 'adiós'],
['hola y adiós', 'hola y adiós'],
[lorem_ipsum, lorem_ipsum]
];
export const NUMBER_CORPUS: any[] = [
[-1, -1],
[0, 0],
[-123456789, -123456789],
[1.1234, 1.1234],
[123456789, 123456789],
['1.1234', 1.1234],
['123456789', 123456789],
['-123456789', -123456789],
];
export const BOOLEAN_CORPUS: any[] = [
[true, true],
[false, false],
['', true],
['false', false],
['true', true],
['hola', false]
];
export const ANY_CORPUS: any[] = [
[true, true],
[false, false],
[null, null],
[0, 0],
['', ''],
[' ', ' '],
['hola', 'hola']
];
export interface TestConfig {
defaultValue: any;
corpus: any;
}
export function commonInputTest<T>(input: BaseInput<T>, config: TestConfig) {
// TODO test form register/deregister
// TODO test item classes
testInput(input, config, false);
input.ngAfterViewInit();
testInput(input, config, true);
input.ngOnDestroy();
assert(!input._init, 'input was not destroyed correctly');
}
function testInput<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
testState(input, config, isInit);
testWriteValue(input, config, isInit);
testNgModelChange(input, config, isInit);
}
function testState<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
assert(input._init === isInit, 'input must be init');
assert(!input._isFocus && !input.isFocus(), 'should not be focus');
assert(input.value === config.defaultValue, 'default value is wrong');
input._setFocus();
assert(input._isFocus && input.isFocus(), 'should be focus');
input._setFocus(); // it should not crash
input._setBlur();
assert(!input._isFocus && !input.isFocus(), 'should not be focus');
input._setBlur(); // it should not crash
}
function testWriteValue<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
let test: any;
let i: number;
let ionChangeCalled = 0;
let OnChangeCalled = 0;
let OnTouchedCalled = 0;
input.value = config.defaultValue;
// Test ionChange
let sub = input.ionChange.subscribe((ev: any) => {
assert(ionChangeCalled === 0, 'internal error');
assert(ev === input, 'ev is not the input');
assert(test[1] === ev.value, 'value does not match');
ionChangeCalled++;
});
// Test registerOnChange
input.registerOnChange((ev: any) => {
assert(OnChangeCalled === 0, 'internal error');
assert(ev === input.value, 'ev output does not match');
assert(test[1] === input.value, 'value does not match');
OnChangeCalled++;
});
// Test registerOnChange
input.registerOnTouched(() => {
assert(OnTouchedCalled === 0, 'internal error');
OnTouchedCalled++;
});
// Run corpus
for (i = 0; i < config.corpus.length; i++) {
test = config.corpus[i];
input.value = test[0];
assert(input.value === test[1], 'input/output does not match');
if (isInit) {
assert(ionChangeCalled === 1, 'ionChange error');
} else {
assert(ionChangeCalled === 0, 'ionChange error');
}
assert(OnChangeCalled === 1, 'OnChangeCalled was not called');
assert(OnTouchedCalled === 1, 'OnTouchedCalled was not called');
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
// Set same value (it should not redispatch)
input.value = test[0];
assert(ionChangeCalled === 0, 'ionChange should not be called');
assert(OnChangeCalled === 0, 'OnChangeCalled should not be called');
// TODO OnTouchedCalled?
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
}
input.registerOnChange(null);
input.registerOnTouched(null);
sub.unsubscribe();
input.value = config.defaultValue;
}
function testNgModelChange<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
let test: any;
let i: number;
let ionChangeCalled = 0;
let OnChangeCalled = 0;
let OnTouchedCalled = 0;
// Test ionChange
let sub = input.ionChange.subscribe((ev: any) => {
assert(ionChangeCalled === 0, 'internal error');
assert(ev === input, 'ev output does not match');
assert(test[1] === ev.value, 'value does not match');
ionChangeCalled++;
});
// Test registerOnChange
input.registerOnChange((ev: any) => {
OnChangeCalled++;
});
// Test registerOnChange
input.registerOnTouched(() => {
OnTouchedCalled++;
});
// Run corpus
for (i = 0; i < config.corpus.length; i++) {
test = config.corpus[i];
input.writeValue(test[0]);
assert(input.value === test[1], 'input/output does not match');
if (isInit) {
assert(ionChangeCalled === 1, 'ionChange error');
} else {
assert(ionChangeCalled === 0, 'ionChange error');
}
assert(OnChangeCalled === 0, 'OnChangeCalled should not be called');
assert(OnTouchedCalled === 0, 'OnTouchedCalled should not be called');
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
// Set same value (it should not redispatch)
input.writeValue(test[0]);
input.value = test[0];
assert(ionChangeCalled === 0, 'ionChange should not be called');
assert(OnChangeCalled === 0, 'OnChangeCalled should not be called');
// TODO OnTouchedCalled?
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
}
input.registerOnChange(null);
input.registerOnTouched(null);
sub.unsubscribe();
input.value = config.defaultValue;
}

View File

@ -26,6 +26,9 @@ import { ViewController } from '../navigation/view-controller';
import { ModuleLoader } from './module-loader'; import { ModuleLoader } from './module-loader';
import { NgModuleLoader } from './ng-module-loader'; import { NgModuleLoader } from './ng-module-loader';
import { DeepLinkConfig, STATE_INITIALIZED } from '../navigation/nav-util'; import { DeepLinkConfig, STATE_INITIALIZED } from '../navigation/nav-util';
import { Ion } from '../components/ion';
import { Item } from '../components/item/item';
import { Form } from './form';
export function mockConfig(config?: any, url: string = '/', platform?: Platform) { export function mockConfig(config?: any, url: string = '/', platform?: Platform) {
@ -231,6 +234,14 @@ export function mockChangeDetectorRef(): ChangeDetectorRef {
return cd; return cd;
} }
export function mockGestureController(app?: App): GestureController {
if (!app) {
app = mockApp();
}
return new GestureController(app);
}
export class MockElementRef implements ElementRef { export class MockElementRef implements ElementRef {
nativeElement: any; nativeElement: any;
constructor(ele: any) { constructor(ele: any) {
@ -242,7 +253,8 @@ export class MockElement {
children: any[] = []; children: any[] = [];
classList = new ClassList(); classList = new ClassList();
attributes: { [name: string]: any } = {}; attributes: { [name: string]: any } = {};
style: {[property: string]: any} = {}; style: { [property: string]: any } = {};
nodeName: string = 'ION-MOCK';
clientWidth = 0; clientWidth = 0;
clientHeight = 0; clientHeight = 0;
@ -258,6 +270,7 @@ export class MockElement {
get className() { get className() {
return this.classList.classes.join(' '); return this.classList.classes.join(' ');
} }
set className(val: string) { set className(val: string) {
this.classList.classes = val.split(' '); this.classList.classes = val.split(' ');
} }
@ -274,6 +287,10 @@ export class MockElement {
this.attributes[name] = val; this.attributes[name] = val;
} }
addEventListener(type: string, listener: Function, options?: any) { }
removeEventListener(type: string, listener: Function, options?: any) { }
removeAttribute(name: string) { removeAttribute(name: string) {
delete this.attributes[name]; delete this.attributes[name];
} }
@ -493,6 +510,25 @@ export function mockTab(parentTabs: Tabs): Tab {
return tab; return tab;
} }
export function mockForm(): Form {
return new Form();
}
export function mockIon(): Ion {
const config = mockConfig();
const elementRef = mockElementRef();
const renderer = mockRenderer();
return new Ion(config, elementRef, renderer, 'ion');
}
export function mockItem(): Item {
const form = mockForm();
const config = mockConfig();
const elementRef = mockElementRef();
const renderer = mockRenderer();
return new Item(form, config, elementRef, renderer, null);
}
export function mockTabs(app?: App): Tabs { export function mockTabs(app?: App): Tabs {
let platform = mockPlatform(); let platform = mockPlatform();
let config = mockConfig(null, '/', platform); let config = mockConfig(null, '/', platform);

View File

@ -0,0 +1,55 @@
import { BaseInput } from '../base-input';
import { Form } from '../form';
import { Item } from '../../components/item/item';
import { commonInputTest, ANY_CORPUS } from '../input-tester';
import { mockConfig, mockPlatform, mockElementRef, mockRenderer, mockForm } from '../mock-providers';
let platform: any;
let config: any;
let elementRef: any;
let renderer: any;
describe('BaseInput', () => {
it('should initialize', () => {
const input = mockInput(null, null, null);
expect(input._init).toBeFalsy();
expect(input._isFocus).toBeFalsy();
expect(input._config).toEqual(config);
expect(input._elementRef).toEqual(elementRef);
expect(input._renderer).toEqual(renderer);
expect(input._componentName).toEqual('input');
expect(input.id).toBeUndefined();
expect(input._labelId).toBeUndefined();
});
it('should configure with item', () => {
const form = new Form();
const item = new Item(form, config, elementRef, renderer, null);
const input = mockInput(form, item, null);
expect(input.id).toEqual('input-0-0');
expect(input._labelId).toEqual('lbl-0');
});
it('should pass base test', () => {
const input = mockInput(mockForm(), null, null);
commonInputTest(input, {
defaultValue: null,
corpus: ANY_CORPUS
});
});
});
function mockInput(form: any, item: any, ngControl: any): BaseInput<any> {
platform = mockPlatform();
config = mockConfig(null, '/', platform);
elementRef = mockElementRef();
renderer = mockRenderer();
return new BaseInput(config, elementRef, renderer, 'input', form, item, ngControl);
}

View File

@ -63,25 +63,26 @@ export function defaults(dest: any, ...args: any[]) {
/** @hidden */ /** @hidden */
export function isBoolean(val: any) { return typeof val === 'boolean'; } export function isBoolean(val: any): val is boolean { return typeof val === 'boolean'; }
/** @hidden */ /** @hidden */
export function isString(val: any) { return typeof val === 'string'; } export function isString(val: any): val is string { return typeof val === 'string'; }
/** @hidden */ /** @hidden */
export function isNumber(val: any) { return typeof val === 'number'; } export function isNumber(val: any): val is number { return typeof val === 'number'; }
/** @hidden */ /** @hidden */
export function isFunction(val: any) { return typeof val === 'function'; } export function isFunction(val: any): val is Function { return typeof val === 'function'; }
/** @hidden */ /** @hidden */
export function isDefined(val: any) { return typeof val !== 'undefined'; } export function isDefined(val: any): boolean { return typeof val !== 'undefined'; }
/** @hidden */ /** @hidden */
export function isUndefined(val: any) { return typeof val === 'undefined'; } export function isUndefined(val: any): val is undefined { return typeof val === 'undefined'; }
/** @hidden */ /** @hidden */
export function isPresent(val: any) { return val !== undefined && val !== null; } export function isPresent(val: any): val is any { return val !== undefined && val !== null; }
/** @hidden */ /** @hidden */
export function isBlank(val: any) { return val === undefined || val === null; } export function isBlank(val: any): val is null { return val === undefined || val === null; }
/** @hidden */ /** @hidden */
export function isObject(val: any) { return typeof val === 'object'; } export function isObject(val: any): val is Object { return typeof val === 'object'; }
/** @hidden */ /** @hidden */
export function isArray(val: any) { return Array.isArray(val); }; export function isArray(val: any): val is any[] { return Array.isArray(val); };
/** @hidden */ /** @hidden */