Skip to content

Commit 745d808

Browse files
Viktor Zhakhalovbrandyscarney
authored andcommitted
feat(select): add popover interface as an option
1 parent 314f7e5 commit 745d808

File tree

8 files changed

+212
-23
lines changed

8 files changed

+212
-23
lines changed

demos/src/select/pages/page-one/page-one.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,62 @@
6565

6666
</ion-list>
6767

68+
<ion-list>
69+
<ion-list-header>Popover Interface Select</ion-list-header>
70+
71+
<ion-item>
72+
<ion-label>Gender</ion-label>
73+
<ion-select [(ngModel)]="gender" interface="popover">
74+
<ion-option value="f">Female</ion-option>
75+
<ion-option value="m">Male</ion-option>
76+
</ion-select>
77+
</ion-item>
78+
79+
<ion-item>
80+
<ion-label>Gaming</ion-label>
81+
<ion-select [(ngModel)]="gaming" okText="Okay" cancelText="Dismiss" interface="popover">
82+
<ion-option value="nes">NES</ion-option>
83+
<ion-option value="n64">Nintendo64</ion-option>
84+
<ion-option value="ps">PlayStation</ion-option>
85+
<ion-option value="genesis">Sega Genesis</ion-option>
86+
<ion-option value="saturn">Sega Saturn</ion-option>
87+
<ion-option value="snes">SNES</ion-option>
88+
</ion-select>
89+
</ion-item>
90+
91+
<ion-item>
92+
<ion-label>Date</ion-label>
93+
<ion-select (ionChange)="monthChange($event)" interface="popover">
94+
<ion-option value="01">January</ion-option>
95+
<ion-option value="02">February</ion-option>
96+
<ion-option value="03" selected="true">March</ion-option>
97+
<ion-option value="04">April</ion-option>
98+
<ion-option value="05">May</ion-option>
99+
<ion-option value="06">June</ion-option>
100+
<ion-option value="07">July</ion-option>
101+
<ion-option value="08">August</ion-option>
102+
<ion-option value="09">September</ion-option>
103+
<ion-option value="10">October</ion-option>
104+
<ion-option value="11">November</ion-option>
105+
<ion-option value="12">December</ion-option>
106+
</ion-select>
107+
<ion-select (ionChange)="yearChange($event)" interface="popover">
108+
<ion-option>1989</ion-option>
109+
<ion-option>1990</ion-option>
110+
<ion-option>1991</ion-option>
111+
<ion-option>1992</ion-option>
112+
<ion-option>1993</ion-option>
113+
<ion-option selected="true">1994</ion-option>
114+
<ion-option>1995</ion-option>
115+
<ion-option>1996</ion-option>
116+
<ion-option>1997</ion-option>
117+
<ion-option>1998</ion-option>
118+
<ion-option>1999</ion-option>
119+
</ion-select>
120+
</ion-item>
121+
122+
</ion-list>
123+
68124
<ion-list>
69125
<ion-list-header>Multiple Value Select</ion-list-header>
70126

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { NavParams } from '../../navigation/nav-params';
3+
import { ViewController } from '../../navigation/view-controller';
4+
5+
/** @private */
6+
export interface SelectPopoverOption {
7+
text: string;
8+
value: string;
9+
disabled: boolean;
10+
checked: boolean;
11+
}
12+
13+
/** @private */
14+
@Component({
15+
template: `
16+
<ion-list radio-group [(ngModel)]="value">
17+
<ion-item *ngFor="let option of options; let i = index">
18+
<ion-label>{{option.text}}</ion-label>
19+
<ion-radio [checked]="option.checked" [value]="option.value" [disabled]="option.disabled"></ion-radio>
20+
</ion-item>
21+
</ion-list>
22+
`
23+
})
24+
export class SelectPopover implements OnInit {
25+
26+
public get value() {
27+
let checkedOption = this.options.find(option => option.checked);
28+
29+
return checkedOption ? checkedOption.value : undefined;
30+
}
31+
32+
public set value(value: any) {
33+
this.viewController.dismiss(value);
34+
}
35+
36+
private options: SelectPopoverOption[];
37+
38+
constructor(
39+
private navParams: NavParams,
40+
private viewController: ViewController
41+
) { }
42+
43+
public ngOnInit() {
44+
this.options = this.navParams.data.options;
45+
}
46+
}

src/components/select/select.module.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule, ModuleWithProviders } from '@angular/core';
3+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
34

45
import { Select } from './select';
6+
import { SelectPopover } from './select-popover-component';
7+
8+
import { ItemModule } from '../item/item.module';
9+
import { LabelModule } from '../label/label.module';
10+
import { ListModule } from '../list/list.module';
11+
import { RadioModule } from '../radio/radio.module';
12+
513

614
/** @hidden */
715
@NgModule({
816
imports: [
9-
CommonModule
17+
CommonModule,
18+
FormsModule,
19+
ReactiveFormsModule,
20+
ItemModule,
21+
LabelModule,
22+
ListModule,
23+
RadioModule
1024
],
1125
declarations: [
12-
Select
26+
Select,
27+
SelectPopover
1328
],
1429
exports: [
15-
Select
30+
Select,
31+
SelectPopover
32+
],
33+
entryComponents: [
34+
SelectPopover
1635
]
1736
})
1837
export class SelectModule {

src/components/select/select.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
// Select
44
// --------------------------------------------------
55

6+
/// @prop - Margin top of the select popover list
7+
$select-popover-list-margin-top: -1px !default;
8+
9+
/// @prop - Margin right of the select popover list
10+
$select-popover-list-margin-right: 0 !default;
11+
12+
/// @prop - Margin bottom of the select popover list
13+
$select-popover-list-margin-bottom: -1px !default;
14+
15+
/// @prop - Margin left of the select popover list
16+
$select-popover-list-margin-left: 0 !default;
17+
18+
619
ion-select {
720
display: flex;
821
overflow: hidden;
@@ -32,3 +45,7 @@ ion-select {
3245

3346
pointer-events: none;
3447
}
48+
49+
.select-popover ion-list {
50+
margin: $select-popover-list-margin-top $select-popover-list-margin-right $select-popover-list-margin-bottom $select-popover-list-margin-left;
51+
}

src/components/select/select.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
33

44
import { ActionSheet } from '../action-sheet/action-sheet';
55
import { Alert } from '../alert/alert';
6+
import { Popover } from '../popover/popover';
67
import { App } from '../app/app';
78
import { Config } from '../../config/config';
9+
import { DeepLinker } from '../../navigation/deep-linker';
810
import { Form } from '../../util/form';
911
import { BaseInput } from '../../util/base-input';
1012
import { isCheckedProperty, isTrueProperty, deepCopy, deepEqual } from '../../util/util';
1113
import { Item } from '../item/item';
1214
import { NavController } from '../../navigation/nav-controller';
1315
import { Option } from '../option/option';
16+
import { SelectPopover, SelectPopoverOption } from './select-popover-component';
1417

1518
export const SELECT_VALUE_ACCESSOR: any = {
1619
provide: NG_VALUE_ACCESSOR,
@@ -30,17 +33,18 @@ export const SELECT_VALUE_ACCESSOR: any = {
3033
* The select component takes child `ion-option` components. If `ion-option` is not
3134
* given a `value` attribute then it will use its text as the value.
3235
*
33-
* If `ngModel` is bound to `ion-select`, the selected value will be based on the
36+
* If `ngModel` is bound to `ion-select`, the selected value will be based on the
3437
* bound value of the model. Otherwise, the `selected` attribute can be used on
3538
* `ion-option` components.
3639
*
3740
* ### Interfaces
3841
*
3942
* By default, the `ion-select` uses the {@link ../../alert/AlertController AlertController API}
4043
* to open up the overlay of options in an alert. The interface can be changed to use the
41-
* {@link ../../action-sheet/ActionSheetController ActionSheetController API} by passing
42-
* `action-sheet` to the `interface` property. Read the other sections for the limitations of the
43-
* action sheet interface.
44+
* {@link ../../action-sheet/ActionSheetController ActionSheetController API} or
45+
* {@link ../../popover/PopoverController PopoverController API} by passing `action-sheet` or `popover`,
46+
* respectively, to the `interface` property. Read on to the other sections for the limitations
47+
* of the different interfaces.
4448
*
4549
* ### Single Value: Radio Buttons
4650
*
@@ -70,7 +74,7 @@ export const SELECT_VALUE_ACCESSOR: any = {
7074
* selected option values. In the example below, because each option is not given
7175
* a `value`, then it'll use its text as the value instead.
7276
*
73-
* Note: the action sheet interface will not work with a multi-value select.
77+
* Note: the `action-sheet` and `popover` interfaces will not work with a multi-value select.
7478
*
7579
* ```html
7680
* <ion-item>
@@ -96,19 +100,22 @@ export const SELECT_VALUE_ACCESSOR: any = {
96100
* </ion-select>
97101
* ```
98102
*
99-
* The action sheet interface does not have an `OK` button, clicking
103+
* The `action-sheet` and `popover` interfaces do not have an `OK` button, clicking
100104
* on any of the options will automatically close the overlay and select
101105
* that value.
102106
*
103107
* ### Select Options
104108
*
105-
* Since `ion-select` uses the `Alert` and `Action Sheet` interfaces, options can be
109+
* Since `ion-select` uses the `Alert`, `Action Sheet` and `Popover` interfaces, options can be
106110
* passed to these components through the `selectOptions` property. This can be used
107111
* to pass a custom title, subtitle, css class, and more. See the
108-
* {@link ../../alert/AlertController/#create AlertController API docs} and
109-
* {@link ../../action-sheet/ActionSheetController/#create ActionSheetController API docs}
112+
* {@link ../../alert/AlertController/#create AlertController API docs},
113+
* {@link ../../action-sheet/ActionSheetController/#create ActionSheetController API docs}, and
114+
* {@link ../../popover/PopoverController/#create PopoverController API docs}
110115
* for the properties that each interface accepts.
111116
*
117+
* For example, to change the `mode` of the overlay, pass it into `selectOptions`.
118+
*
112119
* ```html
113120
* <ion-select [selectOptions]="selectOptions">
114121
* ...
@@ -118,7 +125,8 @@ export const SELECT_VALUE_ACCESSOR: any = {
118125
* ```ts
119126
* this.selectOptions = {
120127
* title: 'Pizza Toppings',
121-
* subTitle: 'Select your toppings'
128+
* subTitle: 'Select your toppings',
129+
* mode: 'md'
122130
* };
123131
* ```
124132
*
@@ -176,7 +184,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
176184
@Input() selectOptions: any = {};
177185

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

@@ -197,7 +205,8 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
197205
elementRef: ElementRef,
198206
renderer: Renderer,
199207
@Optional() item: Item,
200-
@Optional() private _nav: NavController
208+
@Optional() private _nav: NavController,
209+
public deepLinker: DeepLinker
201210
) {
202211
super(config, elementRef, renderer, 'select', [], form, item, null);
203212
}
@@ -215,7 +224,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
215224
}
216225
ev.preventDefault();
217226
ev.stopPropagation();
218-
this.open();
227+
this.open(ev);
219228
}
220229

221230
@HostListener('keyup.space')
@@ -226,7 +235,7 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
226235
/**
227236
* Open the select interface.
228237
*/
229-
open() {
238+
open(ev?: UIEvent) {
230239
if (this.isFocus() || this._disabled) {
231240
return;
232241
}
@@ -257,12 +266,18 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
257266
this.interface = 'alert';
258267
}
259268

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

265-
let overlay: ActionSheet | Alert;
274+
if (this.interface === 'popover' && !ev) {
275+
console.warn('Interface cannot be "popover" without UIEvent.');
276+
this.interface = 'alert';
277+
}
278+
279+
let overlay: ActionSheet | Alert | Popover;
280+
266281
if (this.interface === 'action-sheet') {
267282
selectOptions.buttons = selectOptions.buttons.concat(options.map(input => {
268283
return {
@@ -282,6 +297,25 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
282297
selectOptions.cssClass = selectCssClass;
283298
overlay = new ActionSheet(this._app, selectOptions, this.config);
284299

300+
} else if (this.interface === 'popover') {
301+
let popoverOptions: SelectPopoverOption[] = options.map(input => ({
302+
text: input.text,
303+
checked: input.selected,
304+
disabled: input.disabled,
305+
value: input.value
306+
}));
307+
308+
overlay = new Popover(this._app, SelectPopover, {
309+
options: popoverOptions
310+
}, {
311+
cssClass: 'select-popover'
312+
}, this.config, this.deepLinker);
313+
314+
// ev.target is readonly.
315+
// place popover regarding to ion-select instead of .button-inner
316+
Object.defineProperty(ev, 'target', { value: ev.currentTarget });
317+
selectOptions.ev = ev;
318+
285319
} else {
286320
// default to use the alert interface
287321
this.interface = 'alert';
@@ -330,9 +364,15 @@ export class Select extends BaseInput<string[]> implements AfterViewInit, OnDest
330364
}
331365

332366
overlay.present(selectOptions);
367+
333368
this._fireFocus();
334-
overlay.onDidDismiss(() => {
369+
overlay.onDidDismiss((value: any) => {
335370
this._fireBlur();
371+
372+
if (this.interface === 'popover' && value) {
373+
this.value = value;
374+
this.ionChange.emit(value);
375+
}
336376
});
337377
}
338378

src/components/select/test/select.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

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

66
describe('Select', () => {
@@ -9,11 +9,12 @@ describe('Select', () => {
99

1010
const app = mockApp();
1111
const config = mockConfig();
12+
const deepLinker = mockDeepLinker();
1213
const elementRef = mockElementRef();
1314
const renderer = mockRenderer();
1415
const item: any = mockItem();
1516
const form = mockForm();
16-
const select = new Select(app, form, config, elementRef, renderer, item, null);
17+
const select = new Select(app, form, config, elementRef, renderer, item, null, deepLinker);
1718

1819
commonInputTest(select, {
1920
defaultValue: [],

0 commit comments

Comments
 (0)