Merge branch 'master' into v4

# Conflicts:
#	src/components/toggle/toggle.ts
This commit is contained in:
Adam Bradley
2017-04-28 23:30:33 -05:00
27 changed files with 491 additions and 233 deletions

View File

@@ -1,3 +1,24 @@
<a name="3.1.1"></a>
## [3.1.1](https://github.com/driftyco/ionic/compare/v3.1.0...v3.1.1) (2017-04-28)
### Bug Fixes
* **datetime:** return the correct value ([af394b5](https://github.com/driftyco/ionic/commit/af394b5))
* **datetime:** always returns a string ([6677d80](https://github.com/driftyco/ionic/commit/6677d80))
* **input:** don't dispatch first ngModel update ([505d27a](https://github.com/driftyco/ionic/commit/505d27a))
* **input:** initialize in ngAfterContentInit ([239b559](https://github.com/driftyco/ionic/commit/239b559))
* **menu:** missing output events in interface ([7eb2f0b](https://github.com/driftyco/ionic/commit/7eb2f0b)), closes [#11383](https://github.com/driftyco/ionic/issues/11383)
* **range:** fix label reference ([ee6c481](https://github.com/driftyco/ionic/commit/ee6c481))
* **searchbar:** add setFocus() back ([9264a04](https://github.com/driftyco/ionic/commit/9264a04)), closes [#11397](https://github.com/driftyco/ionic/issues/11397)
* **select:** use correct assert condition ([ec2a34d](https://github.com/driftyco/ionic/commit/ec2a34d))
* **select:** return the value in ionChange instead of this ([d24c4f4](https://github.com/driftyco/ionic/commit/d24c4f4))
* **select:** allow object as value ([4c8efc2](https://github.com/driftyco/ionic/commit/4c8efc2))
* **select:** remove private reference ([c0276d5](https://github.com/driftyco/ionic/commit/c0276d5))
* **toggle:** initialize in ngAfterContentInit ([539901d](https://github.com/driftyco/ionic/commit/539901d))
<a name="3.1.0"></a>
# [3.1.0](https://github.com/driftyco/ionic/compare/v3.0.1...v3.1.0) (2017-04-26)
@@ -20,7 +41,7 @@ Update your package.json to match the following dependencies, remove the existin
"@ionic-native/splash-screen": "3.4.2",
"@ionic-native/status-bar": "3.4.2",
"@ionic/storage": "2.0.1",
"ionic-angular": "3.1.0",
"ionic-angular": "3.1.1",
"ionicons": "3.0.0",
"rxjs": "5.1.1",
"sw-toolbox": "3.4.0",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "ionic2",
"version": "3.1.0",
"version": "3.1.1",
"description": "A powerful framework for building mobile and progressive web apps with JavaScript and Angular 2",
"keywords": [
"ionic",

View File

@@ -1,3 +1,4 @@
import { spawn } from 'child_process';
import { accessSync, readFileSync, writeFileSync } from 'fs';
import { dirname, join, relative } from 'path';
@@ -9,7 +10,7 @@ import * as runSequence from 'run-sequence';
import { argv } from 'yargs';
import { ES_2015, PROJECT_ROOT, SRC_ROOT, SRC_COMPONENTS_ROOT, SCRIPTS_ROOT } from '../constants';
import { DIST_E2E_COMPONENTS_ROOT, ES_2015, PROJECT_ROOT, SRC_ROOT, SRC_COMPONENTS_ROOT, SCRIPTS_ROOT } from '../constants';
import { createTempTsConfig, createTimestamp, getFolderInfo, readFileAsync, runAppScriptsBuild, writeFileAsync, writePolyfills } from '../util';
import * as pAll from 'p-all';
@@ -240,3 +241,22 @@ task('e2e.polyfill', (done: Function) => {
done(err);
});
});
task('e2e.openProd', (done: Function) => {
runSequence('e2e.prod', 'e2e.open', (err: any) => done(err));
});
task('e2e.open', (done: Function) => {
const folderInfo = getFolderInfo();
if (folderInfo && folderInfo.componentName && folderInfo.componentTest) {
const filePath = `${folderInfo.componentName}/test/${folderInfo.componentTest}/www/index.html`;
const fullPath = join(DIST_E2E_COMPONENTS_ROOT, filePath);
const spawnedCommand = spawn('open', [fullPath]);
spawnedCommand.on('close', (code: number) => {
done();
});
} else {
console.log(`Can't open without folder argument.`);
}
});

View File

@@ -1,7 +1,9 @@
import { EventEmitter } from '@angular/core';
import { Animation } from '../../animations/animation';
import { Side } from '../../util/util';
export interface Menu {
setOpen(shouldOpen: boolean, animated: boolean): Promise<boolean>;
open(): Promise<boolean>;
close(): Promise<boolean>;
@@ -13,6 +15,11 @@ export interface Menu {
side: Side;
id: string;
isRightSide: boolean;
ionDrag: EventEmitter<number>;
ionOpen: EventEmitter<boolean>;
ionClose: EventEmitter<boolean>;
isAnimating(): boolean;
width(): number;
getContentElement(): HTMLElement;

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Config } from '../../config/config';
@@ -66,7 +66,7 @@ import { Item } from '../item/item';
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: Checkbox, multi: true } ],
encapsulation: ViewEncapsulation.None,
})
export class Checkbox extends BaseInput<boolean> implements IonicTapInput, AfterViewInit, OnDestroy {
export class Checkbox extends BaseInput<boolean> implements IonicTapInput, OnDestroy {
/**
* @input {boolean} If true, the element is selected.

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
import { AfterContentInit, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Config } from '../../config/config';
@@ -9,7 +9,7 @@ import { Form } from '../../util/form';
import { BaseInput } from '../../util/base-input';
import { Item } from '../item/item';
import { deepCopy, isBlank, isPresent, isArray, isString, assert, clamp } from '../../util/util';
import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util';
import { dateValueRange, renderDateTime, renderTextFormat, convertDataToISO, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util';
/**
* @name DateTime
@@ -267,7 +267,7 @@ import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, g
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: DateTime, multi: true } ],
encapsulation: ViewEncapsulation.None,
})
export class DateTime extends BaseInput<DateTimeData> implements AfterViewInit, ControlValueAccessor, OnDestroy {
export class DateTime extends BaseInput<DateTimeData> implements AfterContentInit, ControlValueAccessor, OnDestroy {
_text: string = '';
_min: DateTimeData;
@@ -425,7 +425,7 @@ export class DateTime extends BaseInput<DateTimeData> implements AfterViewInit,
/**
* @hidden
*/
ngAfterViewInit() {
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
@@ -436,13 +436,6 @@ export class DateTime extends BaseInput<DateTimeData> implements AfterViewInit,
this._initialize();
}
/**
* @hidden
*/
_inputUpdated() {
this.updateText();
}
/**
* @hidden
*/
@@ -451,6 +444,13 @@ export class DateTime extends BaseInput<DateTimeData> implements AfterViewInit,
return this._value;
}
/**
* @hidden
*/
_inputUpdated() {
this.updateText();
}
/**
* @hidden
*/
@@ -458,6 +458,21 @@ export class DateTime extends BaseInput<DateTimeData> implements AfterViewInit,
return true;
}
/**
* TODO: REMOVE THIS
* @hidden
*/
_inputChangeEvent(): any {
return this.value;
}
/**
* @hidden
*/
_inputNgModelEvent(): any {
return convertDataToISO(this.value);
}
@HostListener('click', ['$event'])
_click(ev: UIEvent) {
// do not continue if the click event came from a form submit

View File

@@ -109,4 +109,9 @@
<ion-datetime displayFormat="HH:mm:ss" [(ngModel)]="time"></ion-datetime>
</ion-item>
<ion-item>
<ion-label>No displayFormat: {{noFormatDate}}</ion-label>
<ion-datetime [(ngModel)]="noFormatDate"></ion-datetime>
</ion-item>
</ion-content>

View File

@@ -139,12 +139,45 @@ describe('DateTime', () => {
expect(columns[1].options[12].disabled).toEqual(true);
});
it('should always return a string', () => {
datetime.monthValues = '6,7,8';
datetime.dayValues = '01,02,03,04,05,06,08,09,10, 11, 12, 13, 31';
datetime.yearValues = '2014,2015';
datetime.registerOnChange((value: string) => {
expect(value).toEqual(jasmine.any(String));
});
});
it('should return a string when setValue is passed an object', zoned(() => {
const dateTimeData = {
hour: {
text: '12',
value: 12,
},
minute: {
text: '09',
value: 9,
},
ampm: {
text: 'pm',
value: 'pm',
},
};
datetime.setValue(dateTimeData);
datetime.registerOnChange((value: string) => {
expect(value).toEqual(jasmine.any(String));
});
}));
});
describe('writeValue', () => {
it('should updateText with default MMM D, YYYY when no displayFormat or pickerFormat', zoned(() => {
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.writeValue('1994-12-15T13:47:20.789Z');
expect(datetime._text).toEqual('Dec 15, 1994');
@@ -152,7 +185,7 @@ describe('DateTime', () => {
it('should updateText with pickerFormat when no displayFormat', zoned(() => {
datetime.pickerFormat = 'YYYY';
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.writeValue('1994-12-15T13:47:20.789Z');
expect(datetime._text).toEqual('1994');
@@ -160,7 +193,7 @@ describe('DateTime', () => {
it('should updateText with displayFormat when no pickerFormat', zoned(() => {
datetime.displayFormat = 'YYYY';
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.writeValue('1994-12-15T13:47:20.789Z');
expect(datetime._text).toEqual('1994');
@@ -172,7 +205,7 @@ describe('DateTime', () => {
it('should generate with default MMM D, YYYY when no displayFormat or pickerFormat', zoned(() => {
datetime.monthShortNames = customLocale.monthShortNames;
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.setValue('1994-12-15T13:47:20.789Z');
datetime.generate();
@@ -186,7 +219,7 @@ describe('DateTime', () => {
it('should generate with only displayFormat', zoned(() => {
datetime.monthShortNames = customLocale.monthShortNames;
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.displayFormat = 'YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
@@ -199,7 +232,7 @@ describe('DateTime', () => {
it('should generate with only pickerFormat', zoned(() => {
datetime.monthShortNames = customLocale.monthShortNames;
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.pickerFormat = 'YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
@@ -212,7 +245,7 @@ describe('DateTime', () => {
it('should generate with custom locale short month names from input property', zoned(() => {
datetime.monthShortNames = customLocale.monthShortNames;
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.pickerFormat = 'MMM YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
@@ -227,7 +260,7 @@ describe('DateTime', () => {
it('should generate with custom locale full month names from input property', zoned(() => {
datetime.monthNames = customLocale.monthNames;
datetime.ngAfterViewInit();
datetime.ngAfterContentInit();
datetime.pickerFormat = 'MMMM YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RootPage } from '../pages/root-page/root-page';
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class AppComponent {
root = RootPage;
}

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { IonicApp, IonicModule } from '../../../../..';
import { AppComponent } from './app.component';
import { RootPageModule } from '../pages/root-page/root-page.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
IonicModule.forRoot(AppComponent),
RootPageModule
],
bootstrap: [IonicApp]
})
export class AppModule {}

View File

@@ -0,0 +1,5 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@@ -0,0 +1,59 @@
<ion-header>
<ion-navbar>
<ion-title>Basic Form</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<form>
<ion-list>
<ion-item>
<ion-label floating>Username</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="pass"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Username</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="pass"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Username</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="pass"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Username</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="pass"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Username</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="pass"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Username</ion-label>
<ion-input type="text"></ion-input>
</ion-item>
<ion-item>
<ion-label floating>Password</ion-label>
<ion-input type="pass"></ion-input>
</ion-item>
</ion-list>
</form>
</ion-content>

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { IonicPageModule } from '../../../../../..';
import { RootPage } from './root-page';
@NgModule({
declarations: [
RootPage,
],
imports: [
IonicPageModule.forChild(RootPage)
]
})
export class RootPageModule {}

View File

@@ -0,0 +1,22 @@
import { Component } from '@angular/core';
@Component({
templateUrl: 'root-page.html'
})
export class RootPage {
myParam = '';
minValue = 8;
maxValue = 12;
stepValue = 2;
myValues = {
value1: 'Dynamic Input',
value2: 'Dynamic Textarea'
};
toggleValues() {
this.minValue === 8 ? this.minValue = 4 : this.minValue = 8;
this.maxValue === 12 ? this.maxValue = 20 : this.maxValue = 12;
this.stepValue === 2 ? this.stepValue = 4 : this.stepValue = 2;
}
}

View File

@@ -3,8 +3,14 @@
// iOS Label
// --------------------------------------------------
/// @prop - Text color of the label by an input, select, or datetime
$label-ios-text-color: unset !default;
/// @prop - Text color of the stacked/floating label when it is focused
$label-ios-text-color-focused: unset !default;
/// @prop - Margin of the label
$label-ios-margin: $item-ios-padding-top ($item-ios-padding-right / 2) $item-ios-padding-bottom 0 !default;
$label-ios-margin: $item-ios-padding-top ($item-ios-padding-right / 2) $item-ios-padding-bottom 0 !default;
// iOS Default Label
@@ -18,6 +24,16 @@ $label-ios-margin: $item-ios-padding-top ($item-ios-padding-right /
// iOS Default Label Inside An Input/Select Item
// --------------------------------------------------
.item-input .label-ios,
.item-select .label-ios,
.item-datetime .label-ios {
color: $label-ios-text-color;
}
// iOS Input By Label
// --------------------------------------------------
.label-ios + ion-input .text-input,
.label-ios + ion-textarea .text-input,
.label-ios + .input + .cloned-input {
@@ -59,6 +75,11 @@ $label-ios-margin: $item-ios-padding-top ($item-ios-padding-right /
margin-bottom: $item-ios-padding-media-bottom - 2;
}
.input-has-focus .label-ios[stacked],
.input-has-focus .label-ios[floating] {
color: $label-ios-text-color-focused;
}
// Generate iOS Label colors
// --------------------------------------------------

View File

@@ -3,10 +3,10 @@
// Material Design Label
// --------------------------------------------------
/// @prop - Text color of the label
/// @prop - Text color of the label by an input, select, or datetime
$label-md-text-color: #999 !default;
/// @prop - Text color of the label when it has focused
/// @prop - Text color of the stacked/floating label when it is focused
$label-md-text-color-focused: color($colors-md, primary) !default;
/// @prop - Margin of the label

View File

@@ -3,10 +3,10 @@
// Windows Label
// --------------------------------------------------
/// @prop - Text color of the label
/// @prop - Text color of the label by an input, select, or datetime
$label-wp-text-color: #999 !default;
/// @prop - Text color of the label when it has focused
/// @prop - Text color of the stacked/floating label when it is focused
$label-wp-text-color-focused: color($colors-wp, primary) !default;

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, Optional, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
import { AfterContentInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, Optional, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { clamp, isTrueProperty } from '../../util/util';
@@ -104,7 +104,7 @@ import { UIEventManager } from '../../gestures/ui-event-manager';
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: Range, multi: true } ],
encapsulation: ViewEncapsulation.None,
})
export class Range extends BaseInput<any> implements AfterViewInit, ControlValueAccessor, OnDestroy {
export class Range extends BaseInput<any> implements AfterContentInit, ControlValueAccessor, OnDestroy {
_dual: boolean;
_pin: boolean;
@@ -266,7 +266,7 @@ export class Range extends BaseInput<any> implements AfterViewInit, ControlValue
/**
* @hidden
*/
ngAfterViewInit() {
ngAfterContentInit() {
this._initialize();
// add touchstart/mousedown listeners

View File

@@ -172,15 +172,6 @@ export class Searchbar extends BaseInput<string> {
@ViewChild('cancelButton', {read: ElementRef}) _cancelButton: ElementRef;
/**
* @hidden
* After View Checked position the elements
*/
ngAfterViewInit() {
this._initialize();
this.positionElements();
}
/**
* @hidden
* On Initialization check for attributes
@@ -196,11 +187,9 @@ export class Searchbar extends BaseInput<string> {
* @hidden
*/
_inputUpdated() {
if (this._searchbarInput) {
var ele = this._searchbarInput.nativeElement;
if (ele) {
ele.value = this.value;
}
const ele = this._searchbarInput.nativeElement;
if (ele) {
ele.value = this.value;
}
this.positionElements();
}
@@ -229,9 +218,6 @@ export class Searchbar extends BaseInput<string> {
}
positionPlaceholder() {
if (!this._searchbarInput || !this._searchbarIcon) {
return;
}
const inputEle = this._searchbarInput.nativeElement;
const iconEle = this._searchbarIcon.nativeElement;
@@ -265,9 +251,6 @@ export class Searchbar extends BaseInput<string> {
* Show the iOS Cancel button on focus, hide it offscreen otherwise
*/
positionCancelButton() {
if (!this._cancelButton || !this._cancelButton.nativeElement) {
return;
}
const showShowCancel = this._isFocus;
if (showShowCancel !== this._isCancelVisible) {
var cancelStyleEle = this._cancelButton.nativeElement;

View File

@@ -44,4 +44,10 @@
<p padding>
<button ion-button block (click)="changeValue()">Change Value</button>
</p>
<button ion-button (click)="activeTab = 'a'">Error Please</button>
<div *ngIf="activeTab == 'a'">
<ion-searchbar animated="true" [showCancelButton]="true"></ion-searchbar>
</div>
</ion-content>

View File

@@ -13,6 +13,7 @@ export class RootPage {
isAutocorrect: string = 'on';
isAutocomplete: string = 'on';
isSpellcheck: boolean = true;
activeTab = '';
constructor(private changeDetectorRef: ChangeDetectorRef) {

View File

@@ -1,8 +1,9 @@
import { ContentChildren, Directive, ElementRef, Optional, QueryList, Renderer } from '@angular/core';
import { AfterContentInit, ContentChildren, Directive, ElementRef, Optional, QueryList, Renderer } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Config } from '../../config/config';
import { BaseInput } from '../../util/base-input';
import { assert } from '../../util/util';
import { SegmentButton } from './segment-button';
/**
@@ -67,7 +68,7 @@ import { SegmentButton } from './segment-button';
'[class.segment-disabled]': '_disabled'
}
})
export class Segment extends BaseInput<string> {
export class Segment extends BaseInput<string> implements AfterContentInit {
/**
* @hidden
@@ -86,7 +87,7 @@ export class Segment extends BaseInput<string> {
/**
* @hidden
*/
ngAfterViewInit() {
ngAfterContentInit() {
this._initialize();
this._buttons.forEach(button => {
button.ionSelect.subscribe((selectedButton: any) => this.value = selectedButton.value);
@@ -98,12 +99,14 @@ export class Segment extends BaseInput<string> {
* Write a new value to the element.
*/
_inputUpdated() {
if (this._buttons) {
var buttons = this._buttons.toArray();
var value = this.value;
for (var button of buttons) {
button.isActive = (button.value === value);
}
if (!this._buttons) {
assert(false, 'buttons are undefined');
return;
}
const buttons = this._buttons.toArray();
const value = this.value;
for (var button of buttons) {
button.isActive = (button.value === value);
}
}

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ContentChildren, ElementRef, EventEmitter, Input, HostListener, OnDestroy, Optional, Output, Renderer, QueryList, ViewEncapsulation } from '@angular/core';
import { Component, ContentChildren, ElementRef, EventEmitter, Input, HostListener, OnDestroy, Optional, Output, Renderer, QueryList, ViewEncapsulation } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ActionSheet } from '../action-sheet/action-sheet';
@@ -9,7 +9,7 @@ import { Config } from '../../config/config';
import { DeepLinker } from '../../navigation/deep-linker';
import { Form } from '../../util/form';
import { BaseInput } from '../../util/base-input';
import { isCheckedProperty, isTrueProperty, deepCopy, deepEqual } from '../../util/util';
import { isCheckedProperty, isTrueProperty, deepCopy, deepEqual, assert } from '../../util/util';
import { Item } from '../item/item';
import { NavController } from '../../navigation/nav-controller';
import { Option } from '../option/option';
@@ -147,13 +147,12 @@ import { SelectPopover, SelectPopoverOption } from './select-popover-component';
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: Select, multi: true } ],
encapsulation: ViewEncapsulation.None,
})
export class Select extends BaseInput<string[]|string> implements AfterViewInit, OnDestroy {
export class Select extends BaseInput<any> implements OnDestroy {
_multi: boolean = false;
_options: QueryList<Option>;
_texts: string[] = [];
_text: string = '';
_values: string[] = [];
/**
* @input {string} The text to display on the cancel button. Default: `Cancel`.
@@ -222,6 +221,15 @@ export class Select extends BaseInput<string[]|string> implements AfterViewInit,
this.open();
}
/**
* @hidden
*/
getValues(): any[] {
const values = Array.isArray(this._value) ? this._value : [this._value];
assert(this._multi || values.length <= 1, 'single only can have one value');
return values;
}
/**
* Open the select interface.
*/
@@ -394,8 +402,8 @@ export class Select extends BaseInput<string[]|string> implements AfterViewInit,
@ContentChildren(Option)
set options(val: QueryList<Option>) {
this._options = val;
if (this._values.length === 0) {
const values = this.getValues();
if (values.length === 0) {
// there are no values set at this point
// so check to see who should be selected
// we use writeValue() because we don't want to update ngModel
@@ -409,17 +417,24 @@ export class Select extends BaseInput<string[]|string> implements AfterViewInit,
return !deepEqual(this._value, val);
}
/**
* TODO: REMOVE THIS
* @hidden
*/
_inputChangeEvent(): any {
return this.value;
}
/**
* @hidden
*/
_inputUpdated() {
this._texts.length = 0;
this._values = Array.isArray(this._value) ? this._value : [this._value + ''];
if (this._options) {
this._options.forEach(option => {
// check this option if the option's value is in the values array
option.selected = this._values.some(selectValue => {
option.selected = this.getValues().some(selectValue => {
return isCheckedProperty(selectValue, option.value);
});

View File

@@ -1,30 +1,31 @@
import { Select } from '../select';
import { mockApp, mockConfig, mockDeepLinker, mockElementRef, mockRenderer, mockItem, mockForm } from '../../../util/mock-providers';
import { commonInputTest } from '../../../util/input-tester';
// import { Select } from '../select';
// import { mockApp, mockConfig, mockDeepLinker, mockElementRef, mockRenderer, mockItem, mockForm } from '../../../util/mock-providers';
// import { commonInputTest } from '../../../util/input-tester';
describe('Select', () => {
it('should pass common test', () => {
const app = mockApp();
const config = mockConfig();
const deepLinker = mockDeepLinker();
const elementRef = mockElementRef();
const renderer = mockRenderer();
const item: any = mockItem();
const form = mockForm();
const select = new Select(app, form, config, elementRef, renderer, item, null, deepLinker);
// TODO: needs to be enabled in v4
// const app = mockApp();
// const config = mockConfig();
// const deepLinker = mockDeepLinker();
// const elementRef = mockElementRef();
// const renderer = mockRenderer();
// const item: any = mockItem();
// const form = mockForm();
// const select = new Select(app, form, config, elementRef, renderer, item, null, deepLinker);
commonInputTest(select, {
defaultValue: [],
corpus: [
[['hola'], ['hola']],
[null, []],
['hola', 'hola'],
[['hola', 'adios'], ['hola', 'adios']]
]
});
// commonInputTest(select, {
// defaultValue: [],
// corpus: [
// [['hola'], ['hola']],
// [null, []],
// ['hola', 'hola'],
// [['hola', 'adios'], ['hola', 'adios']]
// ]
// });
});

View File

@@ -1,146 +1,117 @@
import { BooleanInputComponent, GestureDetail } from '../../util/interfaces';
import { Component, h, Ionic, Listen, Prop, Watch } from '../index';
import { Component, h, Ionic } from '../index';
/**
* @name Toolbar
* @description
* A Toolbar is a generic bar that is positioned above or below content.
* Unlike a [Navbar](../../navbar/Navbar), a toolbar can be used as a subheader.
* When toolbars are placed within an `<ion-header>` or `<ion-footer>`,
* the toolbars stay fixed in their respective location. When placed within
* `<ion-content>`, toolbars will scroll with the content.
*
*
* ### Buttons in a Toolbar
* Buttons placed in a toolbar should be placed inside of the `<ion-buttons>`
* element. An exception to this is a [menuToggle](../../menu/MenuToggle) button.
* It should not be placed inside of the `<ion-buttons>` element. Both the
* `<ion-buttons>` element and the `menuToggle` can be positioned inside of the
* toolbar using different properties. The below chart has a description of each
* property.
*
* | Property | Description |
* |-------------|-----------------------------------------------------------------------------------------------------------------------|
* | `start` | Positions element to the left of the content in `ios` mode, and directly to the right in `md` and `wp` mode. |
* | `end` | Positions element to the right of the content in `ios` mode, and to the far right in `md` and `wp` mode. |
* | `left` | Positions element to the left of all other elements. |
* | `right` | Positions element to the right of all other elements. |
*
*
* ### Header / Footer Box Shadow and Border
* In `md` mode, the `<ion-header>` will receive a box-shadow on the bottom, and the
* `<ion-footer>` will receive a box-shadow on the top. In `ios` mode, the `<ion-header>`
* will receive a border on the bottom, and the `<ion-footer>` will receive a border on the
* top. Both the `md` box-shadow and the `ios` border can be removed by adding the `no-border`
* attribute to the element.
*
* ```html
* <ion-header no-border>
* <ion-toolbar>
* <ion-title>Header</ion-title>
* </ion-toolbar>
* </ion-header>
*
* <ion-content>
* </ion-content>
*
* <ion-footer no-border>
* <ion-toolbar>
* <ion-title>Footer</ion-title>
* </ion-toolbar>
* </ion-footer>
* ```
*
* @usage
*
* ```html
*
* <ion-header no-border>
*
* <ion-toolbar>
* <ion-title>My Toolbar Title</ion-title>
* </ion-toolbar>
*
* <ion-toolbar>
* <ion-title>I'm a subheader</ion-title>
* </ion-toolbar>
*
* <ion-header>
*
*
* <ion-content>
*
* <ion-toolbar>
* <ion-title>Scrolls with the content</ion-title>
* </ion-toolbar>
*
* </ion-content>
*
*
* <ion-footer no-border>
*
* <ion-toolbar>
* <ion-title>I'm a footer</ion-title>
* </ion-toolbar>
*
* </ion-footer>
* ```
*
* @demo /docs/demos/src/toolbar/
* @see {@link ../../navbar/Navbar/ Navbar API Docs}
*/
@Component({
tag: 'ion-toggle',
tag: 'ion-toolbar',
styleUrls: {
ios: 'toggle.ios.scss',
md: 'toggle.md.scss',
wp: 'toggle.wp.scss'
ios: 'toolbar.ios.scss',
md: 'toolbar.md.scss',
wp: 'toolbar.wp.scss'
}
})
export class Toggle implements BooleanInputComponent {
activated: boolean;
hasFocus: boolean;
id: string;
labelId: string;
startX: number;
export class Toolbar {
private sbPadding: boolean;
@Prop() checked: boolean;
@Prop() disabled: boolean;
@Prop() value: string;
@Watch('checked')
changed(val: boolean) {
Ionic.emit(this, 'ionChange', { detail: { checked: val } });
constructor() {
this.sbPadding = Ionic.config.getBoolean('statusbarPadding');
}
canStart() {
return !this.disabled;
}
onDragStart(detail: GestureDetail) {
this.startX = detail.startX;
this.fireFocus();
}
onDragMove(detail: GestureDetail) {
if (this.checked) {
if (detail.currentX + 15 < this.startX) {
this.checked = false;
this.activated = true;
this.startX = detail.currentX;
}
} else if (detail.currentX - 15 > this.startX) {
this.checked = true;
this.activated = (detail.currentX < this.startX + 5);
this.startX = detail.currentX;
}
}
onDragEnd(detail: GestureDetail) {
if (this.checked) {
if (detail.startX + 4 > detail.currentX) {
this.checked = false;
}
} else if (detail.startX - 4 < detail.currentX) {
this.checked = true;
}
this.activated = false;
this.fireBlur();
this.startX = null;
}
@Listen('keydown.space')
onSpace(ev: KeyboardEvent) {
this.toggle();
ev.stopPropagation();
ev.preventDefault();
}
toggle() {
if (!this.disabled) {
this.checked = !this.checked;
this.fireFocus();
}
}
fireFocus() {
if (!this.hasFocus) {
this.hasFocus = true;
Ionic.emit(this, 'ionFocus');
}
}
fireBlur() {
if (this.hasFocus) {
this.hasFocus = false;
Ionic.emit(this, 'ionBlur');
}
}
render() {
return h(this,
h('ion-gesture', Ionic.theme(this, 'toggle', {
class: {
'toggle-activated': this.activated,
'toggle-checked': this.checked,
'toggle-disabled': this.disabled,
},
props: {
'canStart': this.canStart.bind(this),
'onStart': this.onDragStart.bind(this),
'onMove': this.onDragMove.bind(this),
'onEnd': this.onDragEnd.bind(this),
'onPress': this.toggle.bind(this),
'gestureName': 'toggle',
'gesturePriority': 30,
'type': 'pan,press',
'direction': 'x',
'threshold': 20,
'listenOn': 'parent'
}
}),
[
h('div.toggle-icon',
h('div.toggle-inner')
),
h('div.toggle-cover', {
attrs: {
'id': this.id,
'aria-checked': this.checked ? 'true' : false,
'aria-disabled': this.disabled ? 'true' : false,
'aria-labelledby': this.labelId,
'role': 'checkbox',
'tabindex': 0
}
})
]
)
return h(this, { class: { 'statusbar-padding': this.sbPadding } },
h('div', Ionic.theme(this, 'toolbar'), [
h('div', Ionic.theme(this, 'toolbar-background')),
h('div', Ionic.theme(this, 'toolbar-content'),
h('slot')
),
])
);
}

View File

@@ -1,4 +1,4 @@
import { ElementRef, EventEmitter, Input, NgZone, Output, Renderer } from '@angular/core';
import { AfterContentInit, ElementRef, EventEmitter, Input, NgZone, Output, Renderer } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { NgControl } from '@angular/forms';
@@ -10,7 +10,7 @@ import { Form } from './form';
import { TimeoutDebouncer } from './debouncer';
export interface CommonInput<T> extends ControlValueAccessor {
export interface CommonInput<T> extends ControlValueAccessor, AfterContentInit {
id: string;
disabled: boolean;
@@ -38,6 +38,8 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
_disabled: boolean = false;
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(0);
_init: boolean = false;
_initModel: boolean = false;
id: string;
/**
@@ -122,8 +124,14 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
*/
writeValue(val: any) {
if (this._writeValue(val)) {
this._fireIonChange();
if (this._initModel) {
this._fireIonChange();
} else if (this._init) {
// ngModel fires the first time too late, we need to skip the first ngModel update
this._initModel = true;
}
}
}
/**
@@ -135,9 +143,12 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
if (isUndefined(val)) {
return false;
}
const normalized = (val === null)
? deepCopy(this._defaultValue)
: this._inputNormalize(val);
let normalized;
if (val === null) {
normalized = deepCopy(this._defaultValue);
} else {
normalized = this._inputNormalize(val);
}
const notUpdate = isUndefined(normalized) || !this._inputShouldChange(normalized);
if (notUpdate) {
@@ -160,7 +171,8 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
if (this._init) {
this._debouncer.debounce(() => {
assert(NgZone.isInAngularZone(), 'IonChange: should be zoned');
this.ionChange.emit(this);
this.ionChange.emit(this._inputChangeEvent());
this._initModel = true;
});
}
}
@@ -227,7 +239,7 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
* @hidden
*/
private onChange() {
this._onChanged && this._onChanged(this._value);
this._onChanged && this._onChanged(this._inputNgModelEvent());
this._onTouched && this._onTouched();
}
@@ -259,7 +271,7 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
/**
* @hidden
*/
ngAfterViewInit() {
ngAfterContentInit() {
this._initialize();
}
@@ -276,7 +288,7 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
/**
* @hidden
*/
initFocus() {}
initFocus() { }
/**
* @hidden
@@ -292,6 +304,21 @@ export class BaseInput<T> extends Ion implements CommonInput<T> {
return this._value !== val;
}
/**
* @hidden
*/
_inputChangeEvent(): any {
return this;
}
/**
* @hidden
*/
_inputNgModelEvent(): any {
return this._value;
}
/**
* @hidden
*/

View File

@@ -55,7 +55,7 @@ export function commonInputTest<T>(input: BaseInput<T>, config: TestConfig) {
const zone = new NgZone(true);
zone.run(() => {
testInput(input, config, false);
input.ngAfterViewInit();
input.ngAfterContentInit();
testInput(input, config, true);
input.ngOnDestroy();
assert(!input._init, 'input was not destroyed correctly');