From a5229d90ca2a608e8bf4db0c8f71c86d481dd649 Mon Sep 17 00:00:00 2001 From: Manu MA Date: Fri, 17 Jan 2020 23:22:26 +0100 Subject: [PATCH] refactor(): remove checked property in favor of parent value (#19449) BREAKING CHANGE: The following components have been updated to remove the checked or selected properties: - Radio - Segment Button - Select Developers should set the value property on the respective parent components in order to managed checked/selected status. Co-authored-by: Liam DeBeasi --- BREAKING.md | 79 ++++++ angular/src/directives/proxies.ts | 17 +- core/api.txt | 5 - core/src/components.d.ts | 33 --- .../alert/test/translucent/index.html | 1 + .../components/item/test/disabled/index.html | 4 +- .../components/item/test/inputs/index.html | 4 +- .../components/radio-group/radio-group.tsx | 94 +------ .../radio-group/test/basic/index.html | 4 +- .../radio-group/test/standalone/index.html | 10 +- core/src/components/radio/radio.tsx | 81 +++--- core/src/components/radio/readme.md | 26 +- .../components/radio/test/basic/index.html | 40 ++- .../radio/test/standalone/index.html | 55 ++-- core/src/components/radio/usage/angular.md | 4 +- core/src/components/radio/usage/javascript.md | 4 +- core/src/components/radio/usage/react.md | 4 +- core/src/components/radio/usage/vue.md | 4 +- core/src/components/segment-button/readme.md | 244 +++++++++--------- .../segment-button/segment-button.tsx | 35 +-- .../segment-button/usage/angular.md | 56 ++-- .../segment-button/usage/javascript.md | 56 ++-- .../components/segment-button/usage/react.md | 56 ++-- .../components/segment-button/usage/vue.md | 68 ++--- core/src/components/segment/readme.md | 80 +++--- core/src/components/segment/segment.tsx | 76 +++--- .../components/segment/test/basic/index.html | 12 +- .../components/segment/test/colors/index.html | 44 ++-- .../components/segment/test/spec/index.html | 91 +++---- .../segment/test/toolbar/index.html | 72 +++--- core/src/components/segment/usage/angular.md | 20 +- .../components/segment/usage/javascript.md | 20 +- core/src/components/segment/usage/react.md | 20 +- core/src/components/segment/usage/vue.md | 20 +- core/src/components/select-option/readme.md | 1 - .../select-option/select-option.tsx | 5 - .../select-popover/select-popover.tsx | 7 +- core/src/components/select/readme.md | 37 +-- core/src/components/select/select.tsx | 115 +++------ .../components/select/test/basic/index.html | 61 +++-- .../components/select/test/label/index.html | 4 +- .../select/test/multiple-value/index.html | 19 +- .../select/test/single-value/index.html | 4 +- .../select/test/standalone/index.html | 32 +-- core/src/components/select/usage/angular.md | 6 +- .../src/components/select/usage/javascript.md | 14 +- core/src/components/select/usage/react.md | 6 +- core/src/components/select/usage/vue.md | 8 +- .../components/toolbar/test/modes/index.html | 12 +- .../components/toolbar/test/spec/index.html | 24 +- core/src/themes/test/css-variables/index.html | 22 +- core/src/utils/watch-options.ts | 2 +- 52 files changed, 879 insertions(+), 939 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index b1838e5c75..6592ab43ab 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -25,8 +25,11 @@ This is a comprehensive list of the breaking changes introduced in the major ver * [List Header](#list-header) * [Menu](#menu) * [Nav Link](#nav-link) + * [Radio](#radio) * [Searchbar](#searchbar) * [Segment](#segment) + * [Segment Button](#segment-button) + * [Select Option](#select-option) * [Skeleton Text](#skeleton-text) * [Split Pane](#split-pane) * [Toast](#toast) @@ -194,6 +197,34 @@ The list header has been redesigned to match the latest iOS spec. This may break The `ion-nav-push`, `ion-nav-back`, and `ion-nav-set-root` components have been removed in favor of using `ion-nav-link` with a `router-direction` property which accepts `”root”`, `“forward”`, and `“back”`. This reduces the need for maintaining multiple components when they all do the same thing with different transition directions. See the [documentation for nav-link](https://ionicframework.com/docs/api/nav-link) for more information. +#### Radio + +The `ion-radio` must be used inside of an `ion-radio-group` even if there is only one `ion-radio`. Additionally, the `checked` property has been removed. Developers should set the `value` property on the parent `ion-radio-group` to match the value of the desired checked radio button. + +Before + +```html +One + + + One + Two + +``` + +After + +```html + + One + + + + One + Two + +``` + #### Searchbar ##### Show Cancel Button @@ -226,6 +257,54 @@ The `inputmode` property for `ion-searchbar` now defaults to `undefined`. To get +#### Segment Button + +The `checked` property has been removed. Developers should set the `value` property on the parent `ion-segment` to match the value of the desired checked segment button. + +Before + +```html + + One + Two + Three + +``` + +After + +```html + + One + Two + Three + +``` + + +#### Select Option + +The `selected` property has been removed. Developers should set the `value` property on the parent `ion-select` to match the desired selected option. + +Before + +```html + + One + Two + +``` + +After + +```html + + One + Two + +``` + + #### Skeleton Text The `width` property has been removed in favor of using CSS styling. diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index dbb7cd6572..46b57543cd 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -534,17 +534,16 @@ export class IonProgressBar { } export declare interface IonRadio extends Components.IonRadio {} -@ProxyCmp({inputs: ['checked', 'color', 'disabled', 'mode', 'name', 'value']}) -@Component({ selector: 'ion-radio', changeDetection: ChangeDetectionStrategy.OnPush, template: '', inputs: ['checked', 'color', 'disabled', 'mode', 'name', 'value'] }) +@ProxyCmp({inputs: ['color', 'disabled', 'mode', 'name', 'value']}) +@Component({ selector: 'ion-radio', changeDetection: ChangeDetectionStrategy.OnPush, template: '', inputs: ['color', 'disabled', 'mode', 'name', 'value'] }) export class IonRadio { - ionSelect!: EventEmitter; ionFocus!: EventEmitter; ionBlur!: EventEmitter; protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionSelect', 'ionFocus', 'ionBlur']); + proxyOutputs(this, this.el, ['ionFocus', 'ionBlur']); } } @@ -680,15 +679,13 @@ export class IonSegment { } export declare interface IonSegmentButton extends Components.IonSegmentButton {} -@ProxyCmp({inputs: ['checked', 'disabled', 'layout', 'mode', 'type', 'value']}) -@Component({ selector: 'ion-segment-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', inputs: ['checked', 'disabled', 'layout', 'mode', 'type', 'value'] }) +@ProxyCmp({inputs: ['disabled', 'layout', 'mode', 'type', 'value']}) +@Component({ selector: 'ion-segment-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', inputs: ['disabled', 'layout', 'mode', 'type', 'value'] }) export class IonSegmentButton { - ionSelect!: EventEmitter; protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionSelect']); } } @@ -709,8 +706,8 @@ export class IonSelect { } export declare interface IonSelectOption extends Components.IonSelectOption {} -@ProxyCmp({inputs: ['disabled', 'selected', 'value']}) -@Component({ selector: 'ion-select-option', changeDetection: ChangeDetectionStrategy.OnPush, template: '', inputs: ['disabled', 'selected', 'value'] }) +@ProxyCmp({inputs: ['disabled', 'value']}) +@Component({ selector: 'ion-select-option', changeDetection: ChangeDetectionStrategy.OnPush, template: '', inputs: ['disabled', 'value'] }) export class IonSelectOption { protected el: HTMLElement; constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { diff --git a/core/api.txt b/core/api.txt index 02ed8bc395..ccbccd1bf4 100644 --- a/core/api.txt +++ b/core/api.txt @@ -790,7 +790,6 @@ ion-progress-bar,css-prop,--buffer-background ion-progress-bar,css-prop,--progress-background ion-radio,shadow -ion-radio,prop,checked,boolean,false,false,false ion-radio,prop,color,string | undefined,undefined,false,false ion-radio,prop,disabled,boolean,false,false,false ion-radio,prop,mode,"ios" | "md",undefined,false,false @@ -798,7 +797,6 @@ ion-radio,prop,name,string,this.inputId,false,false ion-radio,prop,value,any,undefined,false,false ion-radio,event,ionBlur,void,true ion-radio,event,ionFocus,void,true -ion-radio,event,ionSelect,RadioChangeEventDetail,true ion-radio,css-prop,--border-radius ion-radio,css-prop,--color ion-radio,css-prop,--color-checked @@ -952,13 +950,11 @@ ion-segment,event,ionChange,SegmentChangeEventDetail,true ion-segment,css-prop,--background ion-segment-button,shadow -ion-segment-button,prop,checked,boolean,false,false,false ion-segment-button,prop,disabled,boolean,false,false,false ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false ion-segment-button,prop,mode,"ios" | "md",undefined,false,false ion-segment-button,prop,type,"button" | "reset" | "submit",'button',false,false ion-segment-button,prop,value,string,'ion-sb-' + (ids++),false,false -ion-segment-button,event,ionSelect,void,true ion-segment-button,css-prop,--background ion-segment-button,css-prop,--background-checked ion-segment-button,css-prop,--background-disabled @@ -1012,7 +1008,6 @@ ion-select,css-prop,--placeholder-opacity ion-select-option,shadow ion-select-option,prop,disabled,boolean,false,false,false -ion-select-option,prop,selected,boolean,false,false,false ion-select-option,prop,value,any,undefined,false,false ion-skeleton-text,shadow diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 14d557b7ea..b8a7c5f964 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -33,7 +33,6 @@ import { OverlayEventDetail, PickerButton, PickerColumn, - RadioChangeEventDetail, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, @@ -1701,10 +1700,6 @@ export namespace Components { 'value': number; } interface IonRadio { - /** - * If `true`, the radio is selected. - */ - 'checked': boolean; /** * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ @@ -2063,10 +2058,6 @@ export namespace Components { 'value'?: string | null; } interface IonSegmentButton { - /** - * If `true`, the segment button is selected. - */ - 'checked': boolean; /** * If `true`, the user cannot interact with the segment button. */ @@ -2149,10 +2140,6 @@ export namespace Components { */ 'disabled': boolean; /** - * If `true`, the element is selected. - */ - 'selected': boolean; - /** * The text value of the option. */ 'value'?: any | null; @@ -4841,10 +4828,6 @@ declare namespace LocalJSX { 'value'?: number; } interface IonRadio { - /** - * If `true`, the radio is selected. - */ - 'checked'?: boolean; /** * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ @@ -4870,10 +4853,6 @@ declare namespace LocalJSX { */ 'onIonFocus'?: (event: CustomEvent) => void; /** - * Emitted when the radio button is selected. - */ - 'onIonSelect'?: (event: CustomEvent) => void; - /** * the value of the radio. */ 'value'?: any | null; @@ -5243,10 +5222,6 @@ declare namespace LocalJSX { 'value'?: string | null; } interface IonSegmentButton { - /** - * If `true`, the segment button is selected. - */ - 'checked'?: boolean; /** * If `true`, the user cannot interact with the segment button. */ @@ -5260,10 +5235,6 @@ declare namespace LocalJSX { */ 'mode'?: "ios" | "md"; /** - * Emitted when the segment button is clicked. - */ - 'onIonSelect'?: (event: CustomEvent) => void; - /** * The type of the button. */ 'type'?: 'submit' | 'reset' | 'button'; @@ -5344,10 +5315,6 @@ declare namespace LocalJSX { */ 'disabled'?: boolean; /** - * If `true`, the element is selected. - */ - 'selected'?: boolean; - /** * The text value of the option. */ 'value'?: any | null; diff --git a/core/src/components/alert/test/translucent/index.html b/core/src/components/alert/test/translucent/index.html index bc48286175..0d1b3d5601 100644 --- a/core/src/components/alert/test/translucent/index.html +++ b/core/src/components/alert/test/translucent/index.html @@ -188,6 +188,7 @@ label: 'Radio 1', value: 'value1', checked: true + }, { type: 'radio', diff --git a/core/src/components/item/test/disabled/index.html b/core/src/components/item/test/disabled/index.html index c7d78c9392..100923ba72 100644 --- a/core/src/components/item/test/disabled/index.html +++ b/core/src/components/item/test/disabled/index.html @@ -46,10 +46,10 @@ Disabled Select - + No Game Console NES - Nintendo64 + Nintendo64 PlayStation Sega Genesis Sega Saturn diff --git a/core/src/components/item/test/inputs/index.html b/core/src/components/item/test/inputs/index.html index c0a50d0e71..c5e39da0d4 100644 --- a/core/src/components/item/test/inputs/index.html +++ b/core/src/components/item/test/inputs/index.html @@ -38,10 +38,10 @@ Select - + No Game Console NES - Nintendo64 + Nintendo64 PlayStation Sega Genesis Sega Saturn diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index 3564bc3eda..c4e58a79f1 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -2,7 +2,6 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop import { getIonMode } from '../../global/ionic-global'; import { RadioGroupChangeEventDetail } from '../../interface'; -import { findCheckedOption, watchForOptions } from '../../utils/watch-options'; @Component({ tag: 'ion-radio-group' @@ -11,7 +10,6 @@ export class RadioGroup implements ComponentInterface { private inputId = `ion-rg-${radioGroupIds++}`; private labelId = `${this.inputId}-lbl`; - private mutationO?: MutationObserver; @Element() el!: HTMLElement; @@ -32,7 +30,6 @@ export class RadioGroup implements ComponentInterface { @Watch('value') valueChanged(value: any | undefined) { - this.updateRadios(); this.ionChange.emit({ value }); } @@ -52,88 +49,18 @@ export class RadioGroup implements ComponentInterface { this.labelId = label.id = this.name + '-lbl'; } } - - if (this.value === undefined) { - const radio = findCheckedOption(el, 'ion-radio') as HTMLIonRadioElement | undefined; - if (radio !== undefined) { - await radio.componentOnReady(); - if (this.value === undefined) { - this.value = radio.value; - } - } - } - - this.mutationO = watchForOptions(el, 'ion-radio', newOption => { - if (newOption !== undefined) { - newOption.componentOnReady().then(() => { - this.value = newOption.value; - }); - } else { - this.updateRadios(); - } - }); - this.updateRadios(); } - disconnectedCallback() { - if (this.mutationO) { - this.mutationO.disconnect(); - this.mutationO = undefined; - } - } - - private async updateRadios() { - /** - * Make sure we get all radios first - * so values are up to date prior - * to caching the radio group value - */ - const radios = await this.getRadios(); - const { value } = this; - - let hasChecked = false; - - // Walk the DOM in reverse order, since the last selected one wins! - for (const radio of radios) { - if (!hasChecked && radio.value === value) { - // correct value for this radio - // but this radio isn't checked yet - // and we haven't found a checked yet - hasChecked = true; - radio.checked = true; - } else { - // this radio doesn't have the correct value - // or the radio group has been already checked - radio.checked = false; - } - } - - // Reset value if - if (!hasChecked) { - this.value = undefined; - } - } - - private getRadios() { - return Promise.all( - Array - .from(this.el.querySelectorAll('ion-radio')) - .map(r => r.componentOnReady()) - ); - } - - private onSelect = (ev: Event) => { - const selectedRadio = ev.target as HTMLIonRadioElement | null; + private onClick = (ev: Event) => { + const selectedRadio = ev.target && (ev.target as HTMLElement).closest('ion-radio'); if (selectedRadio) { - this.value = selectedRadio.value; - } - } - - private onDeselect = (ev: Event) => { - const selectedRadio = ev.target as HTMLIonRadioElement | null; - if (selectedRadio) { - selectedRadio.checked = false; - this.value = undefined; + const currentValue = this.value; + const newValue = selectedRadio.value; + if (newValue !== currentValue) { + this.value = newValue; + } else if (this.allowEmptySelection) { + this.value = undefined; + } } } @@ -142,8 +69,7 @@ export class RadioGroup implements ComponentInterface { diff --git a/core/src/components/radio-group/test/basic/index.html b/core/src/components/radio-group/test/basic/index.html index 3c844b24b9..b706cf65cc 100644 --- a/core/src/components/radio-group/test/basic/index.html +++ b/core/src/components/radio-group/test/basic/index.html @@ -90,9 +90,11 @@ const item = document.createElement('ion-item'); item.innerHTML = ` Item ${count} - + `; group.appendChild(item); + + group.value = `item-${count}`; count++; } function removeSelect() { diff --git a/core/src/components/radio-group/test/standalone/index.html b/core/src/components/radio-group/test/standalone/index.html index fac45adaab..f9eef4639c 100644 --- a/core/src/components/radio-group/test/standalone/index.html +++ b/core/src/components/radio-group/test/standalone/index.html @@ -12,7 +12,7 @@ - + @@ -20,7 +20,7 @@ - + @@ -32,9 +32,9 @@

allow-empty-selection="true":  - - - + + +

diff --git a/core/src/components/radio/radio.tsx b/core/src/components/radio/radio.tsx index c949412b9d..af50f63fc6 100644 --- a/core/src/components/radio/radio.tsx +++ b/core/src/components/radio/radio.tsx @@ -1,7 +1,7 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, Watch, h } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import { Color, RadioChangeEventDetail, StyleEventDetail } from '../../interface'; +import { Color, StyleEventDetail } from '../../interface'; import { findItemLabel } from '../../utils/helpers'; import { createColorClasses, hostContext } from '../../utils/theme'; @@ -19,9 +19,15 @@ import { createColorClasses, hostContext } from '../../utils/theme'; export class Radio implements ComponentInterface { private inputId = `ion-rb-${radioButtonIds++}`; + private radioGroup: HTMLIonRadioGroupElement | null = null; @Element() el!: HTMLElement; + /** + * If `true`, the radio is selected. + */ + @State() checked = false; + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -39,15 +45,10 @@ export class Radio implements ComponentInterface { */ @Prop() disabled = false; - /** - * If `true`, the radio is selected. - */ - @Prop({ mutable: true }) checked = false; - /** * the value of the radio. */ - @Prop({ mutable: true }) value?: any | null; + @Prop() value?: any | null; /** * Emitted when the styles change. @@ -55,17 +56,6 @@ export class Radio implements ComponentInterface { */ @Event() ionStyle!: EventEmitter; - /** - * Emitted when the radio button is selected. - */ - @Event() ionSelect!: EventEmitter; - - /** - * Emitted when checked radio button is selected. - * @internal - */ - @Event() ionDeselect!: EventEmitter; - /** * Emitted when the radio button has focus. */ @@ -76,41 +66,45 @@ export class Radio implements ComponentInterface { */ @Event() ionBlur!: EventEmitter; - @Watch('color') - colorChanged() { - this.emitStyle(); - } - - @Watch('checked') - checkedChanged(isChecked: boolean) { - if (isChecked) { - this.ionSelect.emit({ - checked: true, - value: this.value - }); + connectedCallback() { + const radioGroup = this.radioGroup = this.el.closest('ion-radio-group'); + if (radioGroup) { + this.updateState(); + radioGroup.addEventListener('ionChange', this.updateState); } - this.emitStyle(); } - @Watch('disabled') - disabledChanged() { - this.emitStyle(); + disconnectedCallback() { + const radioGroup = this.radioGroup; + if (radioGroup) { + radioGroup.removeEventListener('ionChange', this.updateState); + if (this.checked) { + radioGroup.value = undefined; + } + this.radioGroup = null; + } } componentWillLoad() { - if (this.value === undefined) { - this.value = this.inputId; - } this.emitStyle(); } - private emitStyle() { + @Watch('color') + @Watch('checked') + @Watch('disabled') + emitStyle() { this.ionStyle.emit({ 'radio-checked': this.checked, 'interactive-disabled': this.disabled, }); } + private updateState = () => { + if (this.radioGroup) { + this.checked = this.radioGroup.value === this.value; + } + } + private onFocus = () => { this.ionFocus.emit(); } @@ -119,14 +113,6 @@ export class Radio implements ComponentInterface { this.ionBlur.emit(); } - private onClick = () => { - if (this.checked) { - this.ionDeselect.emit(); - } else { - this.checked = true; - } - } - render() { const { inputId, disabled, checked, color, el } = this; const mode = getIonMode(this); @@ -137,7 +123,6 @@ export class Radio implements ComponentInterface { } return ( - + Name Biff - + @@ -49,14 +49,14 @@ import { IonList, IonRadioGroup, IonListHeader, IonLabel, IonItem, IonRadio, Ion export const RadioExample: React.FC = () => ( - + Name Biff - + @@ -80,14 +80,14 @@ export const RadioExample: React.FC = () => ( ```html