select.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import { Component, ContentChildren, ElementRef, EventEmitter, HostListener, Input, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
  2. import { NG_VALUE_ACCESSOR } from '@angular/forms';
  3. import { ActionSheet } from '../action-sheet/action-sheet';
  4. import { Alert } from '../alert/alert';
  5. import { Popover } from '../popover/popover';
  6. import { App } from '../app/app';
  7. import { Config } from '../../config/config';
  8. import { DeepLinker } from '../../navigation/deep-linker';
  9. import { Form } from '../../util/form';
  10. import { BaseInput } from '../../util/base-input';
  11. import { deepCopy, deepEqual, isCheckedProperty, isTrueProperty } from '../../util/util';
  12. import { Item } from '../item/item';
  13. import { Option } from '../option/option';
  14. import { SelectPopover } from './select-popover-component';
  15. /**
  16. * @name Select
  17. * @description
  18. * The `ion-select` component is similar to an HTML `<select>` element, however,
  19. * Ionic's select component makes it easier for users to sort through and select
  20. * the preferred option or options. When users tap the select component, a
  21. * dialog will appear with all of the options in a large, easy to select list
  22. * for users.
  23. *
  24. * The select component takes child `ion-option` components. If `ion-option` is not
  25. * given a `value` attribute then it will use its text as the value.
  26. *
  27. * If `ngModel` is bound to `ion-select`, the selected value will be based on the
  28. * bound value of the model. Otherwise, the `selected` attribute can be used on
  29. * `ion-option` components.
  30. *
  31. * ### Interfaces
  32. *
  33. * By default, the `ion-select` uses the {@link ../../alert/AlertController AlertController API}
  34. * to open up the overlay of options in an alert. The interface can be changed to use the
  35. * {@link ../../action-sheet/ActionSheetController ActionSheetController API} or
  36. * {@link ../../popover/PopoverController PopoverController API} by passing `action-sheet` or `popover`,
  37. * respectively, to the `interface` property. Read on to the other sections for the limitations
  38. * of the different interfaces.
  39. *
  40. * ### Single Value: Radio Buttons
  41. *
  42. * The standard `ion-select` component allows the user to select only one
  43. * option. When selecting only one option the alert interface presents users with
  44. * a radio button styled list of options. The action sheet interface can only be
  45. * used with a single value select. If the number of options exceed 6, it will
  46. * use the `alert` interface even if `action-sheet` is passed. The `ion-select`
  47. * component's value receives the value of the selected option's value.
  48. *
  49. * ```html
  50. * <ion-item>
  51. * <ion-label>Gender</ion-label>
  52. * <ion-select [(ngModel)]="gender">
  53. * <ion-option value="f">Female</ion-option>
  54. * <ion-option value="m">Male</ion-option>
  55. * </ion-select>
  56. * </ion-item>
  57. * ```
  58. *
  59. * ### Multiple Value: Checkboxes
  60. *
  61. * By adding the `multiple="true"` attribute to `ion-select`, users are able
  62. * to select multiple options. When multiple options can be selected, the alert
  63. * overlay presents users with a checkbox styled list of options. The
  64. * `ion-select multiple="true"` component's value receives an array of all the
  65. * selected option values. In the example below, because each option is not given
  66. * a `value`, then it'll use its text as the value instead.
  67. *
  68. * Note: the `action-sheet` and `popover` interfaces will not work with a multi-value select.
  69. *
  70. * ```html
  71. * <ion-item>
  72. * <ion-label>Toppings</ion-label>
  73. * <ion-select [(ngModel)]="toppings" multiple="true">
  74. * <ion-option>Bacon</ion-option>
  75. * <ion-option>Black Olives</ion-option>
  76. * <ion-option>Extra Cheese</ion-option>
  77. * <ion-option>Mushrooms</ion-option>
  78. * <ion-option>Pepperoni</ion-option>
  79. * <ion-option>Sausage</ion-option>
  80. * </ion-select>
  81. * </ion-item>
  82. * ```
  83. *
  84. * ### Select Buttons
  85. * By default, the two buttons read `Cancel` and `OK`. Each button's text
  86. * can be customized using the `cancelText` and `okText` attributes:
  87. *
  88. * ```html
  89. * <ion-select okText="Okay" cancelText="Dismiss">
  90. * ...
  91. * </ion-select>
  92. * ```
  93. *
  94. * The `action-sheet` and `popover` interfaces do not have an `OK` button, clicking
  95. * on any of the options will automatically close the overlay and select
  96. * that value.
  97. *
  98. * ### Select Options
  99. *
  100. * Since `ion-select` uses the `Alert`, `Action Sheet` and `Popover` interfaces, options can be
  101. * passed to these components through the `selectOptions` property. This can be used
  102. * to pass a custom title, subtitle, css class, and more. See the
  103. * {@link ../../alert/AlertController/#create AlertController API docs},
  104. * {@link ../../action-sheet/ActionSheetController/#create ActionSheetController API docs}, and
  105. * {@link ../../popover/PopoverController/#create PopoverController API docs}
  106. * for the properties that each interface accepts.
  107. *
  108. * For example, to change the `mode` of the overlay, pass it into `selectOptions`.
  109. *
  110. * ```html
  111. * <ion-select [selectOptions]="selectOptions">
  112. * ...
  113. * </ion-select>
  114. * ```
  115. *
  116. * ```ts
  117. * this.selectOptions = {
  118. * title: 'Pizza Toppings',
  119. * subTitle: 'Select your toppings',
  120. * mode: 'md'
  121. * };
  122. * ```
  123. *
  124. * ### Object Value References
  125. *
  126. * When using objects for select values, it is possible for the identities of these objects to
  127. * change if they are coming from a server or database, while the selected value's identity
  128. * remains the same. For example, this can occur when an existing record with the desired object value
  129. * is loaded into the select, but the newly retrieved select options now have different identities. This will
  130. * result in the select appearing to have no value at all, even though the original selection in still intact.
  131. *
  132. * Using the `compareWith` `Input` is the solution to this problem
  133. *
  134. * ```html
  135. * <ion-item>
  136. * <ion-label>Employee</ion-label>
  137. * <ion-select [(ngModel)]="employee" [compareWith]="compareFn">
  138. * <ion-option *ngFor="let employee of employees" [value]="employee">{{employee.name}}</ion-option>
  139. * </ion-select>
  140. * </ion-item>
  141. * ```
  142. *
  143. * ```ts
  144. * compareFn(e1: Employee, e2: Employee): boolean {
  145. * return e1 && e2 ? e1.id === e2.id : e1 === e2;
  146. * }
  147. * ```
  148. *
  149. * @demo /docs/demos/src/select/
  150. */
  151. export class Select extends BaseInput {
  152. constructor(_app, form, config, elementRef, renderer, item, deepLinker) {
  153. super(config, elementRef, renderer, 'select', [], form, item, null);
  154. this._app = _app;
  155. this.config = config;
  156. this.deepLinker = deepLinker;
  157. this._multi = false;
  158. this._texts = [];
  159. this._text = '';
  160. this._compareWith = isCheckedProperty;
  161. /**
  162. * @input {string} The text to display on the cancel button. Default: `Cancel`.
  163. */
  164. this.cancelText = 'Cancel';
  165. /**
  166. * @input {string} The text to display on the ok button. Default: `OK`.
  167. */
  168. this.okText = 'OK';
  169. /**
  170. * @input {any} Any additional options that the `alert` or `action-sheet` interface can take.
  171. * See the [AlertController API docs](../../alert/AlertController/#create) and the
  172. * [ActionSheetController API docs](../../action-sheet/ActionSheetController/#create) for the
  173. * create options for each interface.
  174. */
  175. this.selectOptions = {};
  176. /**
  177. * @input {string} The interface the select should use: `action-sheet`, `popover` or `alert`. Default: `alert`.
  178. */
  179. this.interface = '';
  180. /**
  181. * @input {string} The text to display instead of the selected option's value.
  182. */
  183. this.selectedText = '';
  184. /**
  185. * @output {any} Emitted when the selection was cancelled.
  186. */
  187. this.ionCancel = new EventEmitter();
  188. }
  189. /**
  190. * @input {Function} The function that will be called to compare object values
  191. */
  192. set compareWith(fn) {
  193. if (typeof fn !== 'function') {
  194. throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
  195. }
  196. this._compareWith = fn;
  197. }
  198. _click(ev) {
  199. ev.preventDefault();
  200. ev.stopPropagation();
  201. this.open(ev);
  202. }
  203. _keyup() {
  204. this.open();
  205. }
  206. /**
  207. * @hidden
  208. */
  209. getValues() {
  210. const values = Array.isArray(this._value) ? this._value : [this._value];
  211. (void 0) /* assert */;
  212. return values;
  213. }
  214. /**
  215. * Open the select interface.
  216. */
  217. open(ev) {
  218. if (this.isFocus() || this._disabled) {
  219. return;
  220. }
  221. (void 0) /* console.debug */;
  222. // the user may have assigned some options specifically for the alert
  223. const selectOptions = deepCopy(this.selectOptions);
  224. // make sure their buttons array is removed from the options
  225. // and we create a new array for the alert's two buttons
  226. selectOptions.buttons = [{
  227. text: this.cancelText,
  228. role: 'cancel',
  229. handler: () => {
  230. this.ionCancel.emit(this);
  231. }
  232. }];
  233. // if the selectOptions didn't provide a title then use the label's text
  234. if (!selectOptions.title && this._item) {
  235. selectOptions.title = this._item.getLabelText();
  236. }
  237. let options = this._options.toArray();
  238. if ((this.interface === 'action-sheet' || this.interface === 'popover') && this._multi) {
  239. console.warn('Interface cannot be "' + this.interface + '" with a multi-value select. Using the "alert" interface.');
  240. this.interface = 'alert';
  241. }
  242. if (this.interface === 'popover' && !ev) {
  243. console.warn('Interface cannot be "popover" without UIEvent.');
  244. this.interface = 'alert';
  245. }
  246. let overlay;
  247. if (this.interface === 'action-sheet') {
  248. selectOptions.buttons = selectOptions.buttons.concat(options.map(input => {
  249. return {
  250. role: (input.selected ? 'selected' : ''),
  251. text: input.text,
  252. handler: () => {
  253. this.value = input.value;
  254. input.ionSelect.emit(input.value);
  255. }
  256. };
  257. }));
  258. var selectCssClass = 'select-action-sheet';
  259. // If the user passed a cssClass for the select, add it
  260. selectCssClass += selectOptions.cssClass ? ' ' + selectOptions.cssClass : '';
  261. selectOptions.cssClass = selectCssClass;
  262. overlay = new ActionSheet(this._app, selectOptions, this.config);
  263. }
  264. else if (this.interface === 'popover') {
  265. let popoverOptions = options.map(input => ({
  266. text: input.text,
  267. checked: input.selected,
  268. disabled: input.disabled,
  269. value: input.value,
  270. handler: () => {
  271. this.value = input.value;
  272. input.ionSelect.emit(input.value);
  273. }
  274. }));
  275. var popoverCssClass = 'select-popover';
  276. // If the user passed a cssClass for the select, add it
  277. popoverCssClass += selectOptions.cssClass ? ' ' + selectOptions.cssClass : '';
  278. overlay = new Popover(this._app, SelectPopover, {
  279. options: popoverOptions
  280. }, {
  281. cssClass: popoverCssClass
  282. }, this.config, this.deepLinker);
  283. // ev.target is readonly.
  284. // place popover regarding to ion-select instead of .button-inner
  285. Object.defineProperty(ev, 'target', { value: ev.currentTarget });
  286. selectOptions.ev = ev;
  287. }
  288. else {
  289. // default to use the alert interface
  290. this.interface = 'alert';
  291. // user cannot provide inputs from selectOptions
  292. // alert inputs must be created by ionic from ion-options
  293. selectOptions.inputs = this._options.map(input => {
  294. return {
  295. type: (this._multi ? 'checkbox' : 'radio'),
  296. label: input.text,
  297. value: input.value,
  298. checked: input.selected,
  299. disabled: input.disabled,
  300. handler: (selectedOption) => {
  301. // Only emit the select event if it is being checked
  302. // For multi selects this won't emit when unchecking
  303. if (selectedOption.checked) {
  304. input.ionSelect.emit(input.value);
  305. }
  306. }
  307. };
  308. });
  309. let selectCssClass = 'select-alert';
  310. // create the alert instance from our built up selectOptions
  311. overlay = new Alert(this._app, selectOptions, this.config);
  312. if (this._multi) {
  313. // use checkboxes
  314. selectCssClass += ' multiple-select-alert';
  315. }
  316. else {
  317. // use radio buttons
  318. selectCssClass += ' single-select-alert';
  319. }
  320. // If the user passed a cssClass for the select, add it
  321. selectCssClass += selectOptions.cssClass ? ' ' + selectOptions.cssClass : '';
  322. overlay.setCssClass(selectCssClass);
  323. overlay.addButton({
  324. text: this.okText,
  325. handler: (selectedValues) => this.value = selectedValues
  326. });
  327. }
  328. overlay.present(selectOptions);
  329. this._fireFocus();
  330. overlay.onDidDismiss(() => {
  331. this._fireBlur();
  332. this._overlay = undefined;
  333. });
  334. this._overlay = overlay;
  335. }
  336. /**
  337. * Close the select interface.
  338. */
  339. close() {
  340. if (!this._overlay || !this.isFocus()) {
  341. return;
  342. }
  343. return this._overlay.dismiss();
  344. }
  345. /**
  346. * @input {boolean} If true, the element can accept multiple values.
  347. */
  348. get multiple() {
  349. return this._multi;
  350. }
  351. set multiple(val) {
  352. this._multi = isTrueProperty(val);
  353. }
  354. /**
  355. * @hidden
  356. */
  357. get text() {
  358. return (this._multi ? this._texts : this._texts.join());
  359. }
  360. /**
  361. * @private
  362. */
  363. set options(val) {
  364. this._options = val;
  365. const values = this.getValues();
  366. if (values.length === 0) {
  367. // there are no values set at this point
  368. // so check to see who should be selected
  369. // we use writeValue() because we don't want to update ngModel
  370. this.writeValue(val.filter(o => o.selected).map(o => o.value));
  371. }
  372. else {
  373. this._updateText();
  374. }
  375. }
  376. _inputShouldChange(val) {
  377. return !deepEqual(this._value, val);
  378. }
  379. /**
  380. * TODO: REMOVE THIS
  381. * @hidden
  382. */
  383. _inputChangeEvent() {
  384. return this.value;
  385. }
  386. /**
  387. * @hidden
  388. */
  389. _updateText() {
  390. this._texts.length = 0;
  391. if (this._options) {
  392. this._options.forEach(option => {
  393. // check this option if the option's value is in the values array
  394. option.selected = this.getValues().some(selectValue => {
  395. return this._compareWith(selectValue, option.value);
  396. });
  397. if (option.selected) {
  398. this._texts.push(option.text);
  399. }
  400. });
  401. }
  402. this._text = this._texts.join(', ');
  403. }
  404. /**
  405. * @hidden
  406. */
  407. _inputUpdated() {
  408. this._updateText();
  409. super._inputUpdated();
  410. }
  411. }
  412. Select.decorators = [
  413. { type: Component, args: [{
  414. selector: 'ion-select',
  415. template: '<div *ngIf="!_text" class="select-placeholder select-text">{{placeholder}}</div>' +
  416. '<div *ngIf="_text" class="select-text">{{selectedText || _text}}</div>' +
  417. '<div class="select-icon">' +
  418. '<div class="select-icon-inner"></div>' +
  419. '</div>' +
  420. '<button aria-haspopup="true" ' +
  421. 'type="button" ' +
  422. '[id]="id" ' +
  423. 'ion-button="item-cover" ' +
  424. '[attr.aria-labelledby]="_labelId" ' +
  425. '[attr.aria-disabled]="_disabled" ' +
  426. 'class="item-cover">' +
  427. '</button>',
  428. host: {
  429. '[class.select-disabled]': '_disabled'
  430. },
  431. providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: Select, multi: true }],
  432. encapsulation: ViewEncapsulation.None,
  433. },] },
  434. ];
  435. /** @nocollapse */
  436. Select.ctorParameters = () => [
  437. { type: App, },
  438. { type: Form, },
  439. { type: Config, },
  440. { type: ElementRef, },
  441. { type: Renderer, },
  442. { type: Item, decorators: [{ type: Optional },] },
  443. { type: DeepLinker, },
  444. ];
  445. Select.propDecorators = {
  446. 'cancelText': [{ type: Input },],
  447. 'okText': [{ type: Input },],
  448. 'placeholder': [{ type: Input },],
  449. 'selectOptions': [{ type: Input },],
  450. 'interface': [{ type: Input },],
  451. 'selectedText': [{ type: Input },],
  452. 'compareWith': [{ type: Input },],
  453. 'ionCancel': [{ type: Output },],
  454. '_click': [{ type: HostListener, args: ['click', ['$event'],] },],
  455. '_keyup': [{ type: HostListener, args: ['keyup.space',] },],
  456. 'multiple': [{ type: Input },],
  457. 'options': [{ type: ContentChildren, args: [Option,] },],
  458. };
  459. //# sourceMappingURL=select.js.map