fix(all): component reusage (#18963)

Use new stencil APIs to allow ionic elements to be reused once removed from the DOM.

fixes #18843
fixes #17344
fixes #16453
fixes #15879
fixes #15788
fixes #15484
fixes #17890
fixes #16364
This commit is contained in:
Manu MA
2019-08-27 16:29:37 +02:00
committed by GitHub
parent a65d897214
commit 48a27636c7
33 changed files with 411 additions and 368 deletions

View File

@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Host, h } from '@stencil/core'; import { Build, Component, ComponentInterface, Element, Host, h } from '@stencil/core';
import { config } from '../../global/config'; import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
@ -14,6 +14,7 @@ export class App implements ComponentInterface {
@Element() el!: HTMLElement; @Element() el!: HTMLElement;
componentDidLoad() { componentDidLoad() {
if (Build.isBrowser) {
rIC(() => { rIC(() => {
const isHybrid = isPlatform(window, 'hybrid'); const isHybrid = isPlatform(window, 'hybrid');
if (!config.getBoolean('_testing')) { if (!config.getBoolean('_testing')) {
@ -29,9 +30,9 @@ export class App implements ComponentInterface {
import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton()); import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton());
} }
import('../../utils/focus-visible').then(module => module.startFocusVisible()); import('../../utils/focus-visible').then(module => module.startFocusVisible());
}); });
} }
}
render() { render() {
const mode = getIonMode(this); const mode = getIonMode(this);

View File

@ -39,14 +39,14 @@ export class Backdrop implements ComponentInterface {
*/ */
@Event() ionBackdropTap!: EventEmitter<void>; @Event() ionBackdropTap!: EventEmitter<void>;
componentDidLoad() { connectedCallback() {
if (this.stopPropagation) { if (this.stopPropagation) {
this.blocker.block(); this.blocker.block();
} }
} }
componentDidUnload() { disconnectedCallback() {
this.blocker.destroy(); this.blocker.unblock();
} }
@Listen('touchstart', { passive: false, capture: true }) @Listen('touchstart', { passive: false, capture: true })

View File

@ -24,6 +24,7 @@ export class Content implements ComponentInterface {
private cTop = -1; private cTop = -1;
private cBottom = -1; private cBottom = -1;
private scrollEl!: HTMLElement; private scrollEl!: HTMLElement;
private mode = getIonMode(this);
// Detail is used in a hot loop in the scroll event, by allocating it here // Detail is used in a hot loop in the scroll event, by allocating it here
// V8 will be able to inline any read/write to it since it's a monomorphic class. // V8 will be able to inline any read/write to it since it's a monomorphic class.
@ -102,21 +103,14 @@ export class Content implements ComponentInterface {
*/ */
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>; @Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
componentWillLoad() { disconnectedCallback() {
if (this.forceOverscroll === undefined) { this.onScrollEnd();
const mode = getIonMode(this);
this.forceOverscroll = mode === 'ios' && isPlatform(window, 'mobile');
}
} }
componentDidLoad() { componentDidLoad() {
this.resize(); this.resize();
} }
componentDidUnload() {
this.onScrollEnd();
}
@Listen('click', { capture: true }) @Listen('click', { capture: true })
onClick(ev: Event) { onClick(ev: Event) {
if (this.isScrolling) { if (this.isScrolling) {
@ -125,6 +119,13 @@ export class Content implements ComponentInterface {
} }
} }
private shouldForceOverscroll() {
const { forceOverscroll, mode } = this;
return forceOverscroll === undefined
? mode === 'ios' && isPlatform(window, 'mobile')
: forceOverscroll;
}
private resize() { private resize() {
if (this.fullscreen) { if (this.fullscreen) {
readTask(this.readDimensions.bind(this)); readTask(this.readDimensions.bind(this));
@ -299,9 +300,9 @@ export class Content implements ComponentInterface {
} }
render() { render() {
const { scrollX, scrollY } = this;
const mode = getIonMode(this); const mode = getIonMode(this);
const { scrollX, scrollY, forceOverscroll } = this; const forceOverscroll = this.shouldForceOverscroll();
const transitionShadow = (mode === 'ios' && config.getBoolean('experimentalTransitionShadow', true)); const transitionShadow = (mode === 'ios' && config.getBoolean('experimentalTransitionShadow', true));
this.resize(); this.resize();
@ -312,7 +313,7 @@ export class Content implements ComponentInterface {
...createColorClasses(this.color), ...createColorClasses(this.color),
[mode]: true, [mode]: true,
'content-sizing': hostContext('ion-popover', this.el), 'content-sizing': hostContext('ion-popover', this.el),
'overscroll': !!this.forceOverscroll, 'overscroll': forceOverscroll,
}} }}
style={{ style={{
'--offset-top': `${this.cTop}px`, '--offset-top': `${this.cTop}px`,
@ -324,7 +325,7 @@ export class Content implements ComponentInterface {
'inner-scroll': true, 'inner-scroll': true,
'scroll-x': scrollX, 'scroll-x': scrollX,
'scroll-y': scrollY, 'scroll-y': scrollY,
'overscroll': (scrollX || scrollY) && !!forceOverscroll 'overscroll': (scrollX || scrollY) && forceOverscroll
}} }}
ref={el => this.scrollEl = el!} ref={el => this.scrollEl = el!}
onScroll={ev => this.onScroll(ev)} onScroll={ev => this.onScroll(ev)}

View File

@ -76,11 +76,13 @@ export class InfiniteScroll implements ComponentInterface {
*/ */
@Event() ionInfinite!: EventEmitter<void>; @Event() ionInfinite!: EventEmitter<void>;
async componentDidLoad() { async connectedCallback() {
const contentEl = this.el.closest('ion-content'); const contentEl = this.el.closest('ion-content');
if (contentEl) { if (!contentEl) {
this.scrollEl = await contentEl.getScrollElement(); console.error('<ion-infinite-scroll> must be used inside an <ion-content>');
return;
} }
this.scrollEl = await contentEl.getScrollElement();
this.thresholdChanged(); this.thresholdChanged();
this.disabledChanged(); this.disabledChanged();
if (this.position === 'top') { if (this.position === 'top') {
@ -92,7 +94,7 @@ export class InfiniteScroll implements ComponentInterface {
} }
} }
componentDidUnload() { disconnectedCallback() {
this.enableScrollEvents(false); this.enableScrollEvents(false);
this.scrollEl = undefined; this.scrollEl = undefined;
} }

View File

@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core'; import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { Color, InputChangeEventDetail, StyleEventDetail, TextFieldTypes } from '../../interface'; import { Color, InputChangeEventDetail, StyleEventDetail, TextFieldTypes } from '../../interface';
@ -66,7 +66,7 @@ export class Input implements ComponentInterface {
/** /**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types. * If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
*/ */
@Prop({ mutable: true }) clearOnEdit?: boolean; @Prop() clearOnEdit?: boolean;
/** /**
* Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke. * Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke.
@ -218,22 +218,22 @@ export class Input implements ComponentInterface {
*/ */
@Event() ionStyle!: EventEmitter<StyleEventDetail>; @Event() ionStyle!: EventEmitter<StyleEventDetail>;
componentWillLoad() { connectedCallback() {
// By default, password inputs clear after focus when they have content
if (this.clearOnEdit === undefined && this.type === 'password') {
this.clearOnEdit = true;
}
this.emitStyle(); this.emitStyle();
}
componentDidLoad() {
this.debounceChanged(); this.debounceChanged();
if (Build.isBrowser) {
this.ionInputDidLoad.emit(); this.el.dispatchEvent(new CustomEvent('ionInputDidLoad', {
detail: this.el
}));
}
} }
componentDidUnload() { disconnectedCallback() {
this.ionInputDidUnload.emit(); if (Build.isBrowser) {
document.dispatchEvent(new CustomEvent('ionInputDidUnload', {
detail: this.el
}));
}
} }
/** /**
@ -255,6 +255,13 @@ export class Input implements ComponentInterface {
return Promise.resolve(this.nativeInput!); return Promise.resolve(this.nativeInput!);
} }
private shouldClearOnEdit() {
const { type, clearOnEdit } = this;
return (clearOnEdit === undefined)
? type === 'password'
: clearOnEdit;
}
private getValue(): string { private getValue(): string {
return this.value || ''; return this.value || '';
} }
@ -295,7 +302,7 @@ export class Input implements ComponentInterface {
} }
private onKeydown = () => { private onKeydown = () => {
if (this.clearOnEdit) { if (this.shouldClearOnEdit()) {
// Did the input value change after it was blurred and edited? // Did the input value change after it was blurred and edited?
if (this.didBlurAfterEdit && this.hasValue()) { if (this.didBlurAfterEdit && this.hasValue()) {
// Clear the input // Clear the input
@ -327,7 +334,7 @@ export class Input implements ComponentInterface {
private focusChanged() { private focusChanged() {
// If clearOnEdit is enabled and the input blurred but has a value, set a flag // If clearOnEdit is enabled and the input blurred but has a value, set a flag
if (this.clearOnEdit && !this.hasFocus && this.hasValue()) { if (!this.hasFocus && this.shouldClearOnEdit() && this.hasValue()) {
this.didBlurAfterEdit = true; this.didBlurAfterEdit = true;
} }
} }

View File

@ -64,7 +64,7 @@ export class ItemSliding implements ComponentInterface {
*/ */
@Event() ionDrag!: EventEmitter; @Event() ionDrag!: EventEmitter;
async componentDidLoad() { async connectedCallback() {
this.item = this.el.querySelector('ion-item'); this.item = this.el.querySelector('ion-item');
await this.updateOptions(); await this.updateOptions();
@ -81,7 +81,7 @@ export class ItemSliding implements ComponentInterface {
this.disabledChanged(); this.disabledChanged();
} }
componentDidUnload() { disconnectedCallback() {
if (this.gesture) { if (this.gesture) {
this.gesture.destroy(); this.gesture.destroy();
this.gesture = undefined; this.gesture = undefined;

View File

@ -136,7 +136,7 @@ export class Menu implements ComponentInterface, MenuI {
*/ */
@Event() protected ionMenuChange!: EventEmitter<MenuChangeEventDetail>; @Event() protected ionMenuChange!: EventEmitter<MenuChangeEventDetail>;
async componentWillLoad() { async connectedCallback() {
if (this.type === undefined) { if (this.type === undefined) {
this.type = config.get('menuType', this.mode === 'ios' ? 'reveal' : 'overlay'); this.type = config.get('menuType', this.mode === 'ios' ? 'reveal' : 'overlay');
} }
@ -182,11 +182,12 @@ export class Menu implements ComponentInterface, MenuI {
this.updateState(); this.updateState();
} }
componentDidLoad() { async componentDidLoad() {
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen }); this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
this.updateState();
} }
componentDidUnload() { disconnectedCallback() {
this.blocker.destroy(); this.blocker.destroy();
menuController._unregister(this); menuController._unregister(this);
if (this.animation) { if (this.animation) {

View File

@ -47,7 +47,7 @@ export class PickerColumnCmp implements ComponentInterface {
this.refresh(); this.refresh();
} }
componentWillLoad() { async connectedCallback() {
let pickerRotateFactor = 0; let pickerRotateFactor = 0;
let pickerScaleFactor = 0.81; let pickerScaleFactor = 0.81;
@ -60,16 +60,6 @@ export class PickerColumnCmp implements ComponentInterface {
this.rotateFactor = pickerRotateFactor; this.rotateFactor = pickerRotateFactor;
this.scaleFactor = pickerScaleFactor; this.scaleFactor = pickerScaleFactor;
}
async componentDidLoad() {
// get the height of one option
const colEl = this.optsEl;
if (colEl) {
this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0);
}
this.refresh();
this.gesture = (await import('../../utils/gesture')).createGesture({ this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.el, el: this.el,
@ -81,14 +71,24 @@ export class PickerColumnCmp implements ComponentInterface {
onEnd: ev => this.onEnd(ev), onEnd: ev => this.onEnd(ev),
}); });
this.gesture.setDisabled(false); this.gesture.setDisabled(false);
this.tmrId = setTimeout(() => { this.tmrId = setTimeout(() => {
this.noAnimate = false; this.noAnimate = false;
this.refresh(true); this.refresh(true);
}, 250); }, 250);
} }
componentDidUnload() { componentDidLoad() {
const colEl = this.optsEl;
if (colEl) {
// DOM READ
// We perfom a DOM read over a rendered item, this needs to happen after the first render
this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0);
}
this.refresh();
}
disconnectedCallback() {
cancelAnimationFrame(this.rafId); cancelAnimationFrame(this.rafId);
clearTimeout(this.tmrId); clearTimeout(this.tmrId);
if (this.gesture) { if (this.gesture) {

View File

@ -1,7 +1,8 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Prop, Watch, h } from '@stencil/core'; import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { RadioGroupChangeEventDetail } from '../../interface'; import { RadioGroupChangeEventDetail } from '../../interface';
import { findCheckedOption, watchForOptions } from '../../utils/watch-options';
@Component({ @Component({
tag: 'ion-radio-group' tag: 'ion-radio-group'
@ -10,7 +11,7 @@ export class RadioGroup implements ComponentInterface {
private inputId = `ion-rg-${radioGroupIds++}`; private inputId = `ion-rg-${radioGroupIds++}`;
private labelId = `${this.inputId}-lbl`; private labelId = `${this.inputId}-lbl`;
private radios: HTMLIonRadioElement[] = []; private mutationO?: MutationObserver;
@Element() el!: HTMLElement; @Element() el!: HTMLElement;
@ -40,58 +41,11 @@ export class RadioGroup implements ComponentInterface {
*/ */
@Event() ionChange!: EventEmitter<RadioGroupChangeEventDetail>; @Event() ionChange!: EventEmitter<RadioGroupChangeEventDetail>;
@Listen('ionRadioDidLoad') async connectedCallback() {
onRadioDidLoad(ev: Event) {
const radio = ev.target as HTMLIonRadioElement;
radio.name = this.name;
// add radio to internal list
this.radios.push(radio);
// this radio-group does not have a value
// but this radio is checked, so let's set the
// radio-group's value from the checked radio
if (this.value == null && radio.checked) {
this.value = radio.value;
} else {
this.updateRadios();
}
}
@Listen('ionRadioDidUnload')
onRadioDidUnload(ev: Event) {
const index = this.radios.indexOf(ev.target as HTMLIonRadioElement);
if (index > -1) {
this.radios.splice(index, 1);
}
}
@Listen('ionSelect')
onRadioSelect(ev: Event) {
const selectedRadio = ev.target as HTMLIonRadioElement | null;
if (selectedRadio) {
this.value = selectedRadio.value;
}
}
@Listen('ionDeselect')
onRadioDeselect(ev: Event) {
if (this.allowEmptySelection) {
const selectedRadio = ev.target as HTMLIonRadioElement | null;
if (selectedRadio) {
selectedRadio.checked = false;
this.value = undefined;
}
}
}
componentDidLoad() {
// Get the list header if it exists and set the id // Get the list header if it exists and set the id
// this is used to set aria-labelledby // this is used to set aria-labelledby
let header = this.el.querySelector('ion-list-header'); const el = this.el;
if (!header) { const header = el.querySelector('ion-list-header') || el.querySelector('ion-item-divider');
header = this.el.querySelector('ion-item-divider');
}
if (header) { if (header) {
const label = header.querySelector('ion-label'); const label = header.querySelector('ion-label');
if (label) { if (label) {
@ -99,13 +53,42 @@ export class RadioGroup implements ComponentInterface {
} }
} }
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<HTMLIonRadioElement>(el, 'ion-radio', newOption => {
if (newOption !== undefined) {
newOption.componentOnReady().then(() => {
this.value = newOption.value;
});
} else {
this.updateRadios();
}
});
this.updateRadios(); this.updateRadios();
} }
private updateRadios() { disconnectedCallback() {
const value = this.value; if (this.mutationO) {
this.mutationO.disconnect();
this.mutationO = undefined;
}
}
private async updateRadios() {
const { value } = this;
const radios = await this.getRadios();
let hasChecked = false; let hasChecked = false;
for (const radio of this.radios) {
// Walk the DOM in reverse order, since the last selected one wins!
for (const radio of radios) {
if (!hasChecked && radio.value === value) { if (!hasChecked && radio.value === value) {
// correct value for this radio // correct value for this radio
// but this radio isn't checked yet // but this radio isn't checked yet
@ -118,6 +101,34 @@ export class RadioGroup implements ComponentInterface {
radio.checked = false; 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;
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;
}
} }
render() { render() {
@ -125,6 +136,8 @@ export class RadioGroup implements ComponentInterface {
<Host <Host
role="radiogroup" role="radiogroup"
aria-labelledby={this.labelId} aria-labelledby={this.labelId}
onIonSelect={this.onSelect}
onIonDeselect={this.allowEmptySelection ? this.onDeselect : undefined}
class={getIonMode(this)} class={getIonMode(this)}
> >
</Host> </Host>

View File

@ -22,7 +22,7 @@
<ion-content class="outer-content"> <ion-content class="outer-content">
<ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff"> <ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff">
<ion-list-header> <ion-list-header>
<ion-label>Luckiest Man On Earth</ion-label> <ion-label>Luckiest Man On Earth <span id="group-value"></span></ion-label>
</ion-list-header> </ion-list-header>
<ion-item> <ion-item>
@ -53,7 +53,10 @@
</ion-radio-group> </ion-radio-group>
<ion-button onClick="toggleDisabled()">Toggle Disabled</ion-button> <ion-button onClick="addSelect()">Add Select</ion-button>
<ion-button onClick="addCheckedSelect()">Add Checked Select</ion-button>
<ion-button onClick="removeSelect()">Remove Select</ion-button>
</ion-content> </ion-content>
<style> <style>
@ -61,12 +64,39 @@
--background: #f2f2f2; --background: #f2f2f2;
} }
</style> </style>
<script> <script>
var dynamicDisabled = document.getElementById('dynamicDisabled'); let count = 0;
const valueEl = document.querySelector('#group-value');
const group = document.querySelector('ion-radio-group');
group.addEventListener('ionChange', (ev) => {
valueEl.textContent = group.value;
});
customElements.whenDefined('ion-radio-group')
.then(() => group.componentOnReady())
.then(() => {
valueEl.textContent = group.value;
});
function toggleDisabled() { function addSelect() {
dynamicDisabled.disabled = !dynamicDisabled.disabled; const item = document.createElement('ion-item');
item.innerHTML = `
<ion-label>Item ${count}</ion-label>
<ion-radio value="item-${count}" slot="start"></ion-radio>
`;
group.appendChild(item);
count++;
}
function addCheckedSelect() {
const item = document.createElement('ion-item');
item.innerHTML = `
<ion-label>Item ${count}</ion-label>
<ion-radio value="item-${count}" slot="start" checked></ion-radio>
`;
group.appendChild(item);
count++;
}
function removeSelect() {
group.children[group.children.length - 1].remove();
} }
</script> </script>
</ion-app> </ion-app>

View File

@ -49,18 +49,6 @@ export class Radio implements ComponentInterface {
*/ */
@Prop({ mutable: true }) value?: any | null; @Prop({ mutable: true }) value?: any | null;
/**
* Emitted when the radio loads.
* @internal
*/
@Event() ionRadioDidLoad!: EventEmitter<void>;
/**
* Emitted when the radio unloads.
* @internal
*/
@Event() ionRadioDidUnload!: EventEmitter<void>;
/** /**
* Emitted when the styles change. * Emitted when the styles change.
* @internal * @internal
@ -116,14 +104,6 @@ export class Radio implements ComponentInterface {
this.emitStyle(); this.emitStyle();
} }
componentDidLoad() {
this.ionRadioDidLoad.emit();
}
componentDidUnload() {
this.ionRadioDidUnload.emit();
}
private emitStyle() { private emitStyle() {
this.ionStyle.emit({ this.ionStyle.emit({
'radio-checked': this.checked, 'radio-checked': this.checked,

View File

@ -169,13 +169,11 @@ export class Range implements ComponentInterface {
*/ */
@Event() ionBlur!: EventEmitter<void>; @Event() ionBlur!: EventEmitter<void>;
componentWillLoad() { async connectedCallback() {
this.updateRatio(); this.updateRatio();
this.debounceChanged(); this.debounceChanged();
this.emitStyle(); this.emitStyle();
}
async componentDidLoad() {
this.gesture = (await import('../../utils/gesture')).createGesture({ this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.rangeSlider!, el: this.rangeSlider!,
gestureName: 'range', gestureName: 'range',
@ -188,7 +186,7 @@ export class Range implements ComponentInterface {
this.gesture.setDisabled(this.disabled); this.gesture.setDisabled(this.disabled);
} }
componentDidUnload() { disconnectedCallback() {
if (this.gesture) { if (this.gesture) {
this.gesture.destroy(); this.gesture.destroy();
this.gesture = undefined; this.gesture = undefined;

View File

@ -97,20 +97,19 @@ export class Refresher implements ComponentInterface {
*/ */
@Event() ionStart!: EventEmitter<void>; @Event() ionStart!: EventEmitter<void>;
async componentDidLoad() { async connectedCallback() {
if (this.el.getAttribute('slot') !== 'fixed') { if (this.el.getAttribute('slot') !== 'fixed') {
console.error('Make sure you use: <ion-refresher slot="fixed">'); console.error('Make sure you use: <ion-refresher slot="fixed">');
return; return;
} }
const contentEl = this.el.closest('ion-content'); const contentEl = this.el.closest('ion-content');
if (contentEl) { if (!contentEl) {
this.scrollEl = await contentEl.getScrollElement(); console.error('<ion-refresher> must be used inside an <ion-content>');
} else { return;
console.error('ion-refresher did not attach, make sure the parent is an ion-content.');
} }
this.scrollEl = await contentEl.getScrollElement();
this.gesture = (await import('../../utils/gesture')).createGesture({ this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.el.closest('ion-content') as any, el: contentEl,
gestureName: 'refresher', gestureName: 'refresher',
gesturePriority: 10, gesturePriority: 10,
direction: 'y', direction: 'y',
@ -125,7 +124,7 @@ export class Refresher implements ComponentInterface {
this.disabledChanged(); this.disabledChanged();
} }
componentDidUnload() { disconnectedCallback() {
this.scrollEl = undefined; this.scrollEl = undefined;
if (this.gesture) { if (this.gesture) {
this.gesture.destroy(); this.gesture.destroy();

View File

@ -52,12 +52,13 @@ export class ReorderGroup implements ComponentInterface {
*/ */
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>; @Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
async componentDidLoad() { async connectedCallback() {
const contentEl = this.el.closest('ion-content'); const contentEl = this.el.closest('ion-content');
if (contentEl) { if (!contentEl) {
this.scrollEl = await contentEl.getScrollElement(); console.error('<ion-reorder-group> must be used inside an <ion-content>');
return;
} }
this.scrollEl = await contentEl.getScrollElement();
this.gesture = (await import('../../utils/gesture')).createGesture({ this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.el, el: this.el,
gestureName: 'reorder', gestureName: 'reorder',
@ -74,7 +75,7 @@ export class ReorderGroup implements ComponentInterface {
this.disabledChanged(); this.disabledChanged();
} }
componentDidUnload() { disconnectedCallback() {
this.onEnd(); this.onEnd();
if (this.gesture) { if (this.gesture) {
this.gesture.destroy(); this.gesture.destroy();

View File

@ -45,10 +45,7 @@ export class RouteRedirect implements ComponentInterface {
this.ionRouteRedirectChanged.emit(); this.ionRouteRedirectChanged.emit();
} }
componentDidLoad() { connectedCallback() {
this.ionRouteRedirectChanged.emit();
}
componentDidUnload() {
this.ionRouteRedirectChanged.emit(); this.ionRouteRedirectChanged.emit();
} }
} }

View File

@ -58,10 +58,7 @@ export class Route implements ComponentInterface {
} }
} }
componentDidLoad() { connectedCallback() {
this.ionRouteDataChanged.emit();
}
componentDidUnload() {
this.ionRouteDataChanged.emit(); this.ionRouteDataChanged.emit();
} }
} }

View File

@ -60,11 +60,7 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
/** @internal */ /** @internal */
@Event({ bubbles: false }) ionNavDidChange!: EventEmitter<void>; @Event({ bubbles: false }) ionNavDidChange!: EventEmitter<void>;
componentWillLoad() { async connectedCallback() {
this.ionNavWillLoad.emit();
}
async componentDidLoad() {
this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture( this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture(
this.el, this.el,
() => !!this.swipeHandler && this.swipeHandler.canStart() && this.animationEnabled, () => !!this.swipeHandler && this.swipeHandler.canStart() && this.animationEnabled,
@ -106,8 +102,11 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
this.swipeHandlerChanged(); this.swipeHandlerChanged();
} }
componentDidUnload() { componentWillLoad() {
this.activeEl = this.activeComponent = undefined; this.ionNavWillLoad.emit();
}
disconnectedCallback() {
if (this.gesture) { if (this.gesture) {
this.gesture.destroy(); this.gesture.destroy();
this.gesture = undefined; this.gesture = undefined;

View File

@ -0,0 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('router: basic', async () => {
const page = await newE2EPage({
url: '/src/components/router/test/basic?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, h } from '@stencil/core'; import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
@ -26,33 +26,7 @@ export class SelectOption implements ComponentInterface {
/** /**
* The text value of the option. * The text value of the option.
*/ */
@Prop({ mutable: true }) value?: any | null; @Prop() value?: any | null;
/**
* Emitted when the select option loads.
* @internal
*/
@Event() ionSelectOptionDidLoad!: EventEmitter<void>;
/**
* Emitted when the select option unloads.
* @internal
*/
@Event() ionSelectOptionDidUnload!: EventEmitter<void>;
componentWillLoad() {
if (this.value === undefined) {
this.value = this.el.textContent || '';
}
}
componentDidLoad() {
this.ionSelectOptionDidLoad.emit();
}
componentDidUnload() {
this.ionSelectOptionDidUnload.emit();
}
render() { render() {
return ( return (

View File

@ -1,10 +1,11 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core'; import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { ActionSheetButton, ActionSheetOptions, AlertInput, AlertOptions, CssClassMap, OverlaySelect, PopoverOptions, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, StyleEventDetail } from '../../interface'; import { ActionSheetButton, ActionSheetOptions, AlertInput, AlertOptions, CssClassMap, OverlaySelect, PopoverOptions, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, StyleEventDetail } from '../../interface';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers'; import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { actionSheetController, alertController, popoverController } from '../../utils/overlays'; import { actionSheetController, alertController, popoverController } from '../../utils/overlays';
import { hostContext } from '../../utils/theme'; import { hostContext } from '../../utils/theme';
import { watchForOptions } from '../../utils/watch-options';
import { SelectCompareFn } from './select-interface'; import { SelectCompareFn } from './select-interface';
@ -21,11 +22,11 @@ import { SelectCompareFn } from './select-interface';
}) })
export class Select implements ComponentInterface { export class Select implements ComponentInterface {
private childOpts: HTMLIonSelectOptionElement[] = [];
private inputId = `ion-sel-${selectIds++}`; private inputId = `ion-sel-${selectIds++}`;
private overlay?: OverlaySelect; private overlay?: OverlaySelect;
private didInit = false; private didInit = false;
private buttonEl?: HTMLButtonElement; private buttonEl?: HTMLButtonElement;
private mutationO?: MutationObserver;
@Element() el!: HTMLIonSelectElement; @Element() el!: HTMLIonSelectElement;
@ -117,64 +118,54 @@ export class Select implements ComponentInterface {
@Event() ionStyle!: EventEmitter<StyleEventDetail>; @Event() ionStyle!: EventEmitter<StyleEventDetail>;
@Watch('disabled') @Watch('disabled')
@Watch('placeholder')
disabledChanged() { disabledChanged() {
this.emitStyle(); this.emitStyle();
} }
@Watch('value') @Watch('value')
valueChanged() { valueChanged() {
if (this.didInit) {
this.updateOptions(); this.updateOptions();
this.emitStyle();
if (this.didInit) {
this.ionChange.emit({ this.ionChange.emit({
value: this.value, value: this.value,
}); });
this.emitStyle();
} }
} }
@Listen('ionSelectOptionDidLoad') async connectedCallback() {
@Listen('ionSelectOptionDidUnload')
async selectOptionChanged() {
await this.loadOptions();
if (this.didInit) {
this.updateOptions();
this.updateOverlayOptions();
this.emitStyle();
/**
* In the event that options
* are not loaded at component load
* this ensures that any value that is
* set is properly rendered once
* options have been loaded
*/
if (this.value !== undefined) {
this.el.forceUpdate();
}
}
}
async componentDidLoad() {
await this.loadOptions();
if (this.value === undefined) { if (this.value === undefined) {
if (this.multiple) { if (this.multiple) {
// there are no values set at this point // there are no values set at this point
// so check to see who should be selected // so check to see who should be selected
const checked = this.childOpts.filter(o => o.selected); const checked = this.childOpts.filter(o => o.selected);
this.value = checked.map(o => o.value); this.value = checked.map(o => getOptionValue(o));
} else { } else {
const checked = this.childOpts.find(o => o.selected); const checked = this.childOpts.find(o => o.selected);
if (checked) { if (checked) {
this.value = checked.value; this.value = getOptionValue(checked);
} }
} }
} }
this.updateOptions(); this.updateOptions();
this.updateOverlayOptions();
this.emitStyle(); this.emitStyle();
this.el.forceUpdate();
this.mutationO = watchForOptions<HTMLIonSelectOptionElement>(this.el, 'ion-select-option', async () => {
this.updateOptions();
this.updateOverlayOptions();
});
}
disconnectedCallback() {
if (this.mutationO) {
this.mutationO.disconnect();
this.mutationO = undefined;
}
}
componentDidLoad() {
this.didInit = true; this.didInit = true;
} }
@ -222,22 +213,24 @@ export class Select implements ComponentInterface {
} }
private updateOverlayOptions(): void { private updateOverlayOptions(): void {
if (!this.overlay) { return; }
const overlay = (this.overlay as any); const overlay = (this.overlay as any);
if (!overlay) {
return;
}
const childOpts = this.childOpts;
switch (this.interface) { switch (this.interface) {
case 'action-sheet': case 'action-sheet':
overlay.buttons = this.createActionSheetButtons(this.childOpts); overlay.buttons = this.createActionSheetButtons(childOpts);
break; break;
case 'popover': case 'popover':
const popover = overlay.querySelector('ion-select-popover'); const popover = overlay.querySelector('ion-select-popover');
if (popover) { if (popover) {
popover.options = this.createPopoverOptions(this.childOpts); popover.options = this.createPopoverOptions(childOpts);
} }
break; break;
default: case 'alert':
const inputType = (this.multiple ? 'checkbox' : 'radio'); const inputType = (this.multiple ? 'checkbox' : 'radio');
overlay.inputs = this.createAlertInputs(this.childOpts, inputType); overlay.inputs = this.createAlertInputs(childOpts, inputType);
break; break;
} }
} }
@ -248,7 +241,7 @@ export class Select implements ComponentInterface {
role: (option.selected ? 'selected' : ''), role: (option.selected ? 'selected' : ''),
text: option.textContent, text: option.textContent,
handler: () => { handler: () => {
this.value = option.value; this.value = getOptionValue(option);
} }
} as ActionSheetButton; } as ActionSheetButton;
}); });
@ -270,7 +263,7 @@ export class Select implements ComponentInterface {
return { return {
type: inputType, type: inputType,
label: o.textContent, label: o.textContent,
value: o.value, value: getOptionValue(o),
checked: o.selected, checked: o.selected,
disabled: o.disabled disabled: o.disabled
} as AlertInput; } as AlertInput;
@ -279,13 +272,14 @@ export class Select implements ComponentInterface {
private createPopoverOptions(data: any[]): SelectPopoverOption[] { private createPopoverOptions(data: any[]): SelectPopoverOption[] {
return data.map(o => { return data.map(o => {
const value = getOptionValue(o);
return { return {
text: o.textContent, text: o.textContent,
value: o.value, value,
checked: o.selected, checked: o.selected,
disabled: o.disabled, disabled: o.disabled,
handler: () => { handler: () => {
this.value = o.value; this.value = value;
this.close(); this.close();
} }
} as SelectPopoverOption; } as SelectPopoverOption;
@ -374,22 +368,18 @@ export class Select implements ComponentInterface {
return this.overlay.dismiss(); return this.overlay.dismiss();
} }
private async loadOptions() {
this.childOpts = await Promise.all(
Array.from(this.el.querySelectorAll('ion-select-option')).map(o => o.componentOnReady())
);
}
private updateOptions() { private updateOptions() {
// iterate all options, updating the selected prop // iterate all options, updating the selected prop
let canSelect = true; let canSelect = true;
for (const selectOption of this.childOpts) { const { value, childOpts, compareWith, multiple } = this;
const selected = canSelect && isOptionSelected(this.value, selectOption.value, this.compareWith); for (const selectOption of childOpts) {
const optValue = getOptionValue(selectOption);
const selected = canSelect && isOptionSelected(value, optValue, compareWith);
selectOption.selected = selected; selectOption.selected = selected;
// if current option is selected and select is single-option, we can't select // if current option is selected and select is single-option, we can't select
// any option more // any option more
if (selected && !this.multiple) { if (selected && !multiple) {
canSelect = false; canSelect = false;
} }
} }
@ -403,6 +393,10 @@ export class Select implements ComponentInterface {
return this.getText() !== ''; return this.getText() !== '';
} }
private get childOpts() {
return Array.from(this.el.querySelectorAll('ion-select-option'));
}
private getText(): string { private getText(): string {
const selectedText = this.selectedText; const selectedText = this.selectedText;
if (selectedText != null && selectedText !== '') { if (selectedText != null && selectedText !== '') {
@ -496,6 +490,13 @@ export class Select implements ComponentInterface {
} }
} }
const getOptionValue = (el: HTMLIonSelectOptionElement) => {
const value = el.value;
return (value === undefined)
? el.textContent || ''
: value;
};
const parseValue = (value: any) => { const parseValue = (value: any) => {
if (value == null) { if (value == null) {
return undefined; return undefined;
@ -543,7 +544,7 @@ const generateText = (opts: HTMLIonSelectOptionElement[], value: any | any[], co
const textForValue = (opts: HTMLIonSelectOptionElement[], value: any, compareWith?: string | SelectCompareFn | null): string | null => { const textForValue = (opts: HTMLIonSelectOptionElement[], value: any, compareWith?: string | SelectCompareFn | null): string | null => {
const selectOpt = opts.find(opt => { const selectOpt = opts.find(opt => {
return compareOptions(opt.value, value, compareWith); return compareOptions(getOptionValue(opt), value, compareWith);
}); });
return selectOpt return selectOpt
? selectOpt.textContent ? selectOpt.textContent

View File

@ -25,6 +25,7 @@
<ion-label position="floating">Label with Placeholder</ion-label> <ion-label position="floating">Label with Placeholder</ion-label>
<ion-select id="actionSheet" interface="action-sheet" placeholder="A Placeholder"></ion-select> <ion-select id="actionSheet" interface="action-sheet" placeholder="A Placeholder"></ion-select>
</ion-item> </ion-item>
<script> <script>
let selects = document.querySelectorAll('ion-select'); let selects = document.querySelectorAll('ion-select');
const options = ['bird', 'dog', 'shark', 'lizard']; const options = ['bird', 'dog', 'shark', 'lizard'];

View File

@ -1,5 +1,4 @@
import { Component, ComponentInterface, Event, Host, h } from '@stencil/core'; import { Component, ComponentInterface, Host, h } from '@stencil/core';
import { EventEmitter } from 'ionicons/dist/types/stencil.core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
@ -9,17 +8,6 @@ import { getIonMode } from '../../global/ionic-global';
}) })
export class Slide implements ComponentInterface { export class Slide implements ComponentInterface {
/** @internal */
@Event() ionSlideChanged!: EventEmitter<void>;
componentDidLoad() {
this.ionSlideChanged.emit();
}
componentDidUnload() {
this.ionSlideChanged.emit();
}
render() { render() {
const mode = getIonMode(this); const mode = getIonMode(this);
return ( return (

View File

@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Method, Prop, Watch, h } from '@stencil/core'; import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { rIC } from '../../utils/helpers.js'; import { rIC } from '../../utils/helpers.js';
@ -20,8 +20,8 @@ export class Slides implements ComponentInterface {
private scrollbarEl?: HTMLElement; private scrollbarEl?: HTMLElement;
private paginationEl?: HTMLElement; private paginationEl?: HTMLElement;
private didInit = false; private swiperReady = false;
private mutationO?: MutationObserver;
private readySwiper!: (swiper: SwiperInterface) => void; private readySwiper!: (swiper: SwiperInterface) => void;
private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; }); private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; });
@ -35,7 +35,7 @@ export class Slides implements ComponentInterface {
@Watch('options') @Watch('options')
async optionsChanged() { async optionsChanged() {
if (this.didInit) { if (this.swiperReady) {
const swiper = await this.getSwiper(); const swiper = await this.getSwiper();
Object.assign(swiper.params, this.options); Object.assign(swiper.params, this.options);
await this.update(); await this.update();
@ -132,20 +132,28 @@ export class Slides implements ComponentInterface {
*/ */
@Event() ionSlideTouchEnd!: EventEmitter<void>; @Event() ionSlideTouchEnd!: EventEmitter<void>;
componentDidLoad() { connectedCallback() {
const mut = this.mutationO = new MutationObserver(() => {
if (this.swiperReady) {
this.update();
}
});
mut.observe(this.el, {
childList: true,
subtree: true
});
rIC(() => this.initSwiper()); rIC(() => this.initSwiper());
} }
async componentDidUnload() { async disconnectedCallback() {
if (this.mutationO) {
this.mutationO.disconnect();
this.mutationO = undefined;
}
const swiper = await this.getSwiper(); const swiper = await this.getSwiper();
swiper.destroy(true, true); swiper.destroy(true, true);
} this.swiper = new Promise(resolve => { this.readySwiper = resolve; });
this.swiperReady = false;
@Listen('ionSlideChanged')
onSlideChanged() {
if (this.didInit) {
this.update();
}
} }
/** /**
@ -154,7 +162,10 @@ export class Slides implements ComponentInterface {
*/ */
@Method() @Method()
async update() { async update() {
const swiper = await this.getSwiper(); const [swiper] = await Promise.all([
this.getSwiper(),
waitForSlides(this.el)
]);
swiper.update(); swiper.update();
} }
@ -315,8 +326,9 @@ export class Slides implements ComponentInterface {
// init swiper core // init swiper core
// @ts-ignore // @ts-ignore
const { Swiper } = await import('./swiper/swiper.bundle.js'); const { Swiper } = await import('./swiper/swiper.bundle.js');
await waitForSlides(this.el);
const swiper = new Swiper(this.el, finalOptions); const swiper = new Swiper(this.el, finalOptions);
this.didInit = true; this.swiperReady = true;
this.readySwiper(swiper); this.readySwiper(swiper);
} }
@ -485,3 +497,9 @@ export class Slides implements ComponentInterface {
); );
} }
} }
const waitForSlides = (el: HTMLElement) => {
return Promise.all(
Array.from(el.querySelectorAll('ion-slide')).map(s => s.componentOnReady())
);
};

View File

@ -33,6 +33,9 @@
<h1>Slide 3</h1> <h1>Slide 3</h1>
</ion-slide> </ion-slide>
</ion-slides> </ion-slides>
<ion-button expand="block" onclick="addSlide()">Add slide</ion-button>
<ion-button expand="block" onclick="removeSlide()">Remove slide</ion-button>
<ion-button expand="block" onclick="slidePrev()">Slide Prev</ion-button> <ion-button expand="block" onclick="slidePrev()">Slide Prev</ion-button>
<ion-button expand="block" onclick="slideNext()">Slide Next</ion-button> <ion-button expand="block" onclick="slideNext()">Slide Next</ion-button>
<ion-button expand="block" onclick="getActiveIndex()">Get Active Index</ion-button> <ion-button expand="block" onclick="getActiveIndex()">Get Active Index</ion-button>
@ -50,10 +53,24 @@
</ion-app> </ion-app>
<script> <script>
let slideCount = 4;
const slides = document.getElementById('slides') const slides = document.getElementById('slides')
slides.pager = false; slides.pager = false;
slides.options = {} slides.options = {}
async function addSlide() {
const slide = document.createElement('ion-slide');
slide.style.background = 'white';
slide.innerHTML = `<h1>Slide ${slideCount}</h1>`;
slideCount++;
slides.querySelector('.swiper-wrapper').appendChild(slide);
};
async function removeSlide() {
const wraper = slides.querySelector('.swiper-wrapper');
wraper.children[wraper.children.length-1].remove();
};
async function slideNext() { async function slideNext() {
await slides.slideNext(500) await slides.slideNext(500)
}; };

View File

@ -57,12 +57,12 @@ export class SplitPane implements ComponentInterface {
this.ionSplitPaneVisible.emit(detail); this.ionSplitPaneVisible.emit(detail);
} }
componentDidLoad() { connectedCallback() {
this.styleChildren(); this.styleChildren();
this.updateState(); this.updateState();
} }
componentDidUnload() { disconnectedCallback() {
if (this.rmL) { if (this.rmL) {
this.rmL(); this.rmL();
this.rmL = undefined; this.rmL = undefined;

View File

@ -64,7 +64,6 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<h1>Page 1</h1> <h1>Page 1</h1>
<ion-button onclick="push()">Push</ion-button>
<ion-button onclick="menu()">Disable/enable menu</ion-button> <ion-button onclick="menu()">Disable/enable menu</ion-button>
<f></f> <f></f>
<f></f> <f></f>
@ -84,9 +83,6 @@
menuCtrl.open('start'); menuCtrl.open('start');
} }
function push() {
}
async function menu() { async function menu() {
menuCtrl.enable(!await menuCtrl.isEnabled()); menuCtrl.enable(!await menuCtrl.isEnabled());
} }

View File

@ -31,7 +31,6 @@ export class Tab implements ComponentInterface {
@Prop() component?: ComponentRef; @Prop() component?: ComponentRef;
componentWillLoad() { componentWillLoad() {
if (Build.isDev) { if (Build.isDev) {
if (this.component !== undefined && this.el.childElementCount > 0) { if (this.component !== undefined && this.el.childElementCount > 0) {
console.error('You can not use a lazy-loaded component in a tab and inlined content at the same time.' + console.error('You can not use a lazy-loaded component in a tab and inlined content at the same time.' +

View File

@ -19,7 +19,6 @@ export class Tabs implements NavOutlet {
@Element() el!: HTMLIonTabsElement; @Element() el!: HTMLIonTabsElement;
@State() tabs: HTMLIonTabElement[] = [];
@State() selectedTab?: HTMLIonTabElement; @State() selectedTab?: HTMLIonTabElement;
/** @internal */ /** @internal */
@ -41,23 +40,18 @@ export class Tabs implements NavOutlet {
*/ */
@Event({ bubbles: false }) ionTabsDidChange!: EventEmitter<{tab: string}>; @Event({ bubbles: false }) ionTabsDidChange!: EventEmitter<{tab: string}>;
componentWillLoad() { async componentWillLoad() {
if (!this.useRouter) { if (!this.useRouter) {
this.useRouter = !!document.querySelector('ion-router') && !this.el.closest('[no-router]'); this.useRouter = !!document.querySelector('ion-router') && !this.el.closest('[no-router]');
} }
this.tabs = Array.from(this.el.querySelectorAll('ion-tab')); if (!this.useRouter) {
this.initSelect().then(() => { const tabs = this.tabs;
await this.select(tabs[0]);
}
this.ionNavWillLoad.emit(); this.ionNavWillLoad.emit();
this.componentWillUpdate();
});
} }
componentDidUnload() { componentWillRender() {
this.tabs.length = 0;
this.selectedTab = this.leavingTab = undefined;
}
componentWillUpdate() {
const tabBar = this.el.querySelector('ion-tab-bar'); const tabBar = this.el.querySelector('ion-tab-bar');
if (tabBar) { if (tabBar) {
const tab = this.selectedTab ? this.selectedTab.tab : undefined; const tab = this.selectedTab ? this.selectedTab.tab : undefined;
@ -131,16 +125,6 @@ export class Tabs implements NavOutlet {
return tabId !== undefined ? { id: tabId, element: this.selectedTab } : undefined; return tabId !== undefined ? { id: tabId, element: this.selectedTab } : undefined;
} }
private async initSelect(): Promise<void> {
if (this.useRouter) {
return;
}
// wait for all tabs to be ready
await Promise.all(this.tabs.map(tab => tab.componentOnReady()));
await this.select(this.tabs[0]);
}
private setActive(selectedTab: HTMLIonTabElement): Promise<void> { private setActive(selectedTab: HTMLIonTabElement): Promise<void> {
if (this.transitioning) { if (this.transitioning) {
return Promise.reject('transitioning already happening'); return Promise.reject('transitioning already happening');
@ -186,16 +170,19 @@ export class Tabs implements NavOutlet {
return selectedTab !== undefined && selectedTab !== leavingTab && !this.transitioning; return selectedTab !== undefined && selectedTab !== leavingTab && !this.transitioning;
} }
private get tabs() {
return Array.from(this.el.querySelectorAll('ion-tab'));
}
private onTabClicked = (ev: CustomEvent<TabButtonClickEventDetail>) => { private onTabClicked = (ev: CustomEvent<TabButtonClickEventDetail>) => {
const { href, tab } = ev.detail; const { href, tab } = ev.detail;
const selectedTab = this.tabs.find(t => t.tab === tab);
if (this.useRouter && href !== undefined) { if (this.useRouter && href !== undefined) {
const router = document.querySelector('ion-router'); const router = document.querySelector('ion-router');
if (router) { if (router) {
router.push(href); router.push(href);
} }
} else if (selectedTab) { } else {
this.select(selectedTab); this.select(tab);
} }
} }

View File

@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, readTask } from '@stencil/core'; import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, readTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
import { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface'; import { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface';
@ -169,28 +169,26 @@ export class Textarea implements ComponentInterface {
*/ */
@Event() ionFocus!: EventEmitter<void>; @Event() ionFocus!: EventEmitter<void>;
/** connectedCallback() {
* Emitted when the input has been created.
* @internal
*/
@Event() ionInputDidLoad!: EventEmitter<void>;
/**
* Emitted when the input has been removed.
* @internal
*/
@Event() ionInputDidUnload!: EventEmitter<void>;
componentWillLoad() {
this.emitStyle(); this.emitStyle();
this.debounceChanged();
if (Build.isBrowser) {
this.el.dispatchEvent(new CustomEvent('ionInputDidLoad', {
detail: this.el
}));
}
}
disconnectedCallback() {
if (Build.isBrowser) {
document.dispatchEvent(new CustomEvent('ionInputDidUnload', {
detail: this.el
}));
}
} }
componentDidLoad() { componentDidLoad() {
this.debounceChanged();
this.runAutoGrow(); this.runAutoGrow();
this.ionInputDidLoad.emit();
} }
// TODO: performance hit, this cause layout thrashing // TODO: performance hit, this cause layout thrashing
@ -204,10 +202,6 @@ export class Textarea implements ComponentInterface {
} }
} }
componentDidUnload() {
this.ionInputDidUnload.emit();
}
/** /**
* Sets focus on the specified `ion-textarea`. Use this method instead of the global * Sets focus on the specified `ion-textarea`. Use this method instead of the global
* `input.focus()`. * `input.focus()`.

View File

@ -96,11 +96,7 @@ export class Toggle implements ComponentInterface {
} }
} }
componentWillLoad() { async connectedCallback() {
this.emitStyle();
}
async componentDidLoad() {
this.gesture = (await import('../../utils/gesture')).createGesture({ this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.el, el: this.el,
gestureName: 'toggle', gestureName: 'toggle',
@ -114,13 +110,17 @@ export class Toggle implements ComponentInterface {
this.disabledChanged(); this.disabledChanged();
} }
componentDidUnload() { disconnectedCallback() {
if (this.gesture) { if (this.gesture) {
this.gesture.destroy(); this.gesture.destroy();
this.gesture = undefined; this.gesture = undefined;
} }
} }
componentWillLoad() {
this.emitStyle();
}
private emitStyle() { private emitStyle() {
this.ionStyle.emit({ this.ionStyle.emit({
'interactive-disabled': this.disabled, 'interactive-disabled': this.disabled,

View File

@ -152,16 +152,14 @@ export class VirtualScroll implements ComponentInterface {
this.updateVirtualScroll(); this.updateVirtualScroll();
} }
async componentDidLoad() { async connectedCallback() {
const contentEl = this.el.closest('ion-content'); const contentEl = this.el.closest('ion-content');
if (!contentEl) { if (!contentEl) {
console.error('virtual-scroll must be used inside ion-content'); console.error('<ion-virtual-scroll> must be used inside an <ion-content>');
return; return;
} }
await contentEl.componentOnReady();
this.contentEl = contentEl;
this.scrollEl = await contentEl.getScrollElement(); this.scrollEl = await contentEl.getScrollElement();
this.contentEl = contentEl;
this.calcCells(); this.calcCells();
this.updateState(); this.updateState();
} }
@ -170,7 +168,7 @@ export class VirtualScroll implements ComponentInterface {
this.updateState(); this.updateState();
} }
componentDidUnload() { disconnectedCallback() {
this.scrollEl = undefined; this.scrollEl = undefined;
} }

View File

@ -75,11 +75,13 @@ export const startInputShims = (config: Config) => {
registerInput(input); registerInput(input);
} }
doc.body.addEventListener('ionInputDidLoad', event => { doc.addEventListener('ionInputDidLoad', ((ev: InputEvent) => {
registerInput(event.target as any); registerInput(ev.detail);
}); }) as any);
doc.body.addEventListener('ionInputDidUnload', event => { doc.addEventListener('ionInputDidUnload', ((ev: InputEvent) => {
unregisterInput(event.target as any); unregisterInput(ev.detail);
}); }) as any);
}; };
type InputEvent = CustomEvent<HTMLElement>;

View File

@ -0,0 +1,32 @@
export const watchForOptions = <T extends HTMLElement>(containerEl: HTMLElement, tagName: string, onChange: (el: T | undefined) => void) => {
const mutation = new MutationObserver(mutationList => {
onChange(getSelectedOption<T>(mutationList, tagName));
});
mutation.observe(containerEl, {
childList: true,
subtree: true
});
return mutation;
};
const getSelectedOption = <T extends HTMLElement>(mutationList: MutationRecord[], tagName: string) => {
let newOption: T | undefined;
mutationList.forEach(mut => {
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < mut.addedNodes.length; i++) {
newOption = findCheckedOption(mut.addedNodes[i], tagName) || newOption;
}
});
return newOption;
};
export const findCheckedOption = (el: any, tagName: string) => {
if (el.nodeType !== 1) {
return undefined;
}
const options = (el.tagName === tagName.toUpperCase())
? [el]
: Array.from(el.querySelectorAll(tagName));
return options.find((o: any) => o.checked === true);
};