Skip to content

Commit

Permalink
feat(select): add popover interface as an option
Browse files Browse the repository at this point in the history
  • Loading branch information
Viktor Zhakhalov authored and brandyscarney committed Apr 17, 2017
1 parent 314f7e5 commit 745d808
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 23 deletions.
56 changes: 56 additions & 0 deletions demos/src/select/pages/page-one/page-one.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,62 @@

</ion-list>

<ion-list>
<ion-list-header>Popover Interface Select</ion-list-header>

<ion-item>
<ion-label>Gender</ion-label>
<ion-select [(ngModel)]="gender" interface="popover">
<ion-option value="f">Female</ion-option>
<ion-option value="m">Male</ion-option>
</ion-select>
</ion-item>

<ion-item>
<ion-label>Gaming</ion-label>
<ion-select [(ngModel)]="gaming" okText="Okay" cancelText="Dismiss" interface="popover">
<ion-option value="nes">NES</ion-option>
<ion-option value="n64">Nintendo64</ion-option>
<ion-option value="ps">PlayStation</ion-option>
<ion-option value="genesis">Sega Genesis</ion-option>
<ion-option value="saturn">Sega Saturn</ion-option>
<ion-option value="snes">SNES</ion-option>
</ion-select>
</ion-item>

<ion-item>
<ion-label>Date</ion-label>
<ion-select (ionChange)="monthChange($event)" interface="popover">
<ion-option value="01">January</ion-option>
<ion-option value="02">February</ion-option>
<ion-option value="03" selected="true">March</ion-option>
<ion-option value="04">April</ion-option>
<ion-option value="05">May</ion-option>
<ion-option value="06">June</ion-option>
<ion-option value="07">July</ion-option>
<ion-option value="08">August</ion-option>
<ion-option value="09">September</ion-option>
<ion-option value="10">October</ion-option>
<ion-option value="11">November</ion-option>
<ion-option value="12">December</ion-option>
</ion-select>
<ion-select (ionChange)="yearChange($event)" interface="popover">
<ion-option>1989</ion-option>
<ion-option>1990</ion-option>
<ion-option>1991</ion-option>
<ion-option>1992</ion-option>
<ion-option>1993</ion-option>
<ion-option selected="true">1994</ion-option>
<ion-option>1995</ion-option>
<ion-option>1996</ion-option>
<ion-option>1997</ion-option>
<ion-option>1998</ion-option>
<ion-option>1999</ion-option>
</ion-select>
</ion-item>

</ion-list>

<ion-list>
<ion-list-header>Multiple Value Select</ion-list-header>

Expand Down
46 changes: 46 additions & 0 deletions src/components/select/select-popover-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component, OnInit } from '@angular/core';
import { NavParams } from '../../navigation/nav-params';
import { ViewController } from '../../navigation/view-controller';

/** @private */
export interface SelectPopoverOption {
text: string;
value: string;
disabled: boolean;
checked: boolean;
}

/** @private */
@Component({
template: `
<ion-list radio-group [(ngModel)]="value">
<ion-item *ngFor="let option of options; let i = index">
<ion-label>{{option.text}}</ion-label>
<ion-radio [checked]="option.checked" [value]="option.value" [disabled]="option.disabled"></ion-radio>
</ion-item>
</ion-list>
`
})
export class SelectPopover implements OnInit {

public get value() {
let checkedOption = this.options.find(option => option.checked);

return checkedOption ? checkedOption.value : undefined;
}

public set value(value: any) {
this.viewController.dismiss(value);
}

private options: SelectPopoverOption[];

constructor(
private navParams: NavParams,
private viewController: ViewController
) { }

public ngOnInit() {
this.options = this.navParams.data.options;
}
}
25 changes: 22 additions & 3 deletions src/components/select/select.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import { CommonModule } from '@angular/common';
import { NgModule, ModuleWithProviders } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { Select } from './select';
import { SelectPopover } from './select-popover-component';

import { ItemModule } from '../item/item.module';
import { LabelModule } from '../label/label.module';
import { ListModule } from '../list/list.module';
import { RadioModule } from '../radio/radio.module';


/** @hidden */
@NgModule({
imports: [
CommonModule
CommonModule,
FormsModule,
ReactiveFormsModule,
ItemModule,
LabelModule,
ListModule,
RadioModule
],
declarations: [
Select
Select,
SelectPopover
],
exports: [
Select
Select,
SelectPopover
],
entryComponents: [
SelectPopover
]
})
export class SelectModule {
Expand Down
17 changes: 17 additions & 0 deletions src/components/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
// Select
// --------------------------------------------------

/// @prop - Margin top of the select popover list
$select-popover-list-margin-top: -1px !default;

/// @prop - Margin right of the select popover list
$select-popover-list-margin-right: 0 !default;

/// @prop - Margin bottom of the select popover list
$select-popover-list-margin-bottom: -1px !default;

/// @prop - Margin left of the select popover list
$select-popover-list-margin-left: 0 !default;


ion-select {
display: flex;
overflow: hidden;
Expand Down Expand Up @@ -32,3 +45,7 @@ ion-select {

pointer-events: none;
}

.select-popover ion-list {
margin: $select-popover-list-margin-top $select-popover-list-margin-right $select-popover-list-margin-bottom $select-popover-list-margin-left;
}
76 changes: 58 additions & 18 deletions src/components/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';

import { ActionSheet } from '../action-sheet/action-sheet';
import { Alert } from '../alert/alert';
import { Popover } from '../popover/popover';
import { App } from '../app/app';
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 { Item } from '../item/item';
import { NavController } from '../../navigation/nav-controller';
import { Option } from '../option/option';
import { SelectPopover, SelectPopoverOption } from './select-popover-component';

export const SELECT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
Expand All @@ -30,17 +33,18 @@ export const SELECT_VALUE_ACCESSOR: any = {
* The select component takes child `ion-option` components. If `ion-option` is not
* given a `value` attribute then it will use its text as the value.
*
* If `ngModel` is bound to `ion-select`, the selected value will be based on the
* If `ngModel` is bound to `ion-select`, the selected value will be based on the
* bound value of the model. Otherwise, the `selected` attribute can be used on
* `ion-option` components.
*
* ### Interfaces
*
* By default, the `ion-select` uses the {@link ../../alert/AlertController AlertController API}
* to open up the overlay of options in an alert. The interface can be changed to use the
* {@link ../../action-sheet/ActionSheetController ActionSheetController API} by passing
* `action-sheet` to the `interface` property. Read the other sections for the limitations of the
* action sheet interface.
* {@link ../../action-sheet/ActionSheetController ActionSheetController API} or
* {@link ../../popover/PopoverController PopoverController API} by passing `action-sheet` or `popover`,
* respectively, to the `interface` property. Read on to the other sections for the limitations
* of the different interfaces.
*
* ### Single Value: Radio Buttons
*
Expand Down Expand Up @@ -70,7 +74,7 @@ export const SELECT_VALUE_ACCESSOR: any = {
* selected option values. In the example below, because each option is not given
* a `value`, then it'll use its text as the value instead.
*
* Note: the action sheet interface will not work with a multi-value select.
* Note: the `action-sheet` and `popover` interfaces will not work with a multi-value select.
*
* ```html
* <ion-item>
Expand All @@ -96,19 +100,22 @@ export const SELECT_VALUE_ACCESSOR: any = {
* </ion-select>
* ```
*
* The action sheet interface does not have an `OK` button, clicking
* The `action-sheet` and `popover` interfaces do not have an `OK` button, clicking
* on any of the options will automatically close the overlay and select
* that value.
*
* ### Select Options
*
* Since `ion-select` uses the `Alert` and `Action Sheet` interfaces, options can be
* Since `ion-select` uses the `Alert`, `Action Sheet` and `Popover` interfaces, options can be
* passed to these components through the `selectOptions` property. This can be used
* to pass a custom title, subtitle, css class, and more. See the
* {@link ../../alert/AlertController/#create AlertController API docs} and
* {@link ../../action-sheet/ActionSheetController/#create ActionSheetController API docs}
* {@link ../../alert/AlertController/#create AlertController API docs},
* {@link ../../action-sheet/ActionSheetController/#create ActionSheetController API docs}, and
* {@link ../../popover/PopoverController/#create PopoverController API docs}
* for the properties that each interface accepts.
*
* For example, to change the `mode` of the overlay, pass it into `selectOptions`.
*
* ```html
* <ion-select [selectOptions]="selectOptions">
* ...
Expand All @@ -118,7 +125,8 @@ export const SELECT_VALUE_ACCESSOR: any = {
* ```ts
* this.selectOptions = {
* title: 'Pizza Toppings',
* subTitle: 'Select your toppings'
* subTitle: 'Select your toppings',
* mode: 'md'
* };
* ```
*
Expand Down Expand Up @@ -176,7 +184,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
@Input() selectOptions: any = {};

/**
* @input {string} The interface the select should use: `action-sheet` or `alert`. Default: `alert`.
* @input {string} The interface the select should use: `action-sheet`, `popover` or `alert`. Default: `alert`.
*/
@Input() interface: string = '';

Expand All @@ -197,7 +205,8 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
elementRef: ElementRef,
renderer: Renderer,
@Optional() item: Item,
@Optional() private _nav: NavController
@Optional() private _nav: NavController,
public deepLinker: DeepLinker
) {
super(config, elementRef, renderer, 'select', [], form, item, null);
}
Expand All @@ -215,7 +224,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
}
ev.preventDefault();
ev.stopPropagation();
this.open();
this.open(ev);
}

@HostListener('keyup.space')
Expand All @@ -226,7 +235,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
/**
* Open the select interface.
*/
open() {
open(ev?: UIEvent) {
if (this.isFocus() || this._disabled) {
return;
}
Expand Down Expand Up @@ -257,12 +266,18 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
this.interface = 'alert';
}

if (this.interface === 'action-sheet' && this._multi) {
console.warn('Interface cannot be "action-sheet" with a multi-value select. Using the "alert" interface.');
if ((this.interface === 'action-sheet' || this.interface === 'popover') && this._multi) {
console.warn('Interface cannot be "' + this.interface + '" with a multi-value select. Using the "alert" interface.');
this.interface = 'alert';
}

let overlay: ActionSheet | Alert;
if (this.interface === 'popover' && !ev) {
console.warn('Interface cannot be "popover" without UIEvent.');
this.interface = 'alert';
}

let overlay: ActionSheet | Alert | Popover;

if (this.interface === 'action-sheet') {
selectOptions.buttons = selectOptions.buttons.concat(options.map(input => {
return {
Expand All @@ -282,6 +297,25 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
selectOptions.cssClass = selectCssClass;
overlay = new ActionSheet(this._app, selectOptions, this.config);

} else if (this.interface === 'popover') {
let popoverOptions: SelectPopoverOption[] = options.map(input => ({
text: input.text,
checked: input.selected,
disabled: input.disabled,
value: input.value
}));

overlay = new Popover(this._app, SelectPopover, {
options: popoverOptions
}, {
cssClass: 'select-popover'
}, this.config, this.deepLinker);

// ev.target is readonly.
// place popover regarding to ion-select instead of .button-inner
Object.defineProperty(ev, 'target', { value: ev.currentTarget });
selectOptions.ev = ev;

} else {
// default to use the alert interface
this.interface = 'alert';
Expand Down Expand Up @@ -330,9 +364,15 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
}

overlay.present(selectOptions);

this._fireFocus();
overlay.onDidDismiss(() => {
overlay.onDidDismiss((value: any) => {
this._fireBlur();

if (this.interface === 'popover' && value) {
this.value = value;
this.ionChange.emit(value);
}
});
}

Expand Down
5 changes: 3 additions & 2 deletions src/components/select/test/select.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { Select } from '../select';
import { mockApp, mockConfig, mockElementRef, mockRenderer, mockItem, mockForm } from '../../../util/mock-providers';
import { mockApp, mockConfig, mockDeepLinker, mockElementRef, mockRenderer, mockItem, mockForm } from '../../../util/mock-providers';
import { commonInputTest } from '../../../util/input-tester';

describe('Select', () => {
Expand All @@ -9,11 +9,12 @@ describe('Select', () => {

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);
const select = new Select(app, form, config, elementRef, renderer, item, null, deepLinker);

commonInputTest(select, {
defaultValue: [],
Expand Down
Loading

0 comments on commit 745d808

Please sign in to comment.