mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 10:41:13 +08:00
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:
@ -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 { getIonMode } from '../../global/ionic-global';
|
||||
@ -14,6 +14,7 @@ export class App implements ComponentInterface {
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
componentDidLoad() {
|
||||
if (Build.isBrowser) {
|
||||
rIC(() => {
|
||||
const isHybrid = isPlatform(window, 'hybrid');
|
||||
if (!config.getBoolean('_testing')) {
|
||||
@ -29,9 +30,9 @@ export class App implements ComponentInterface {
|
||||
import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton());
|
||||
}
|
||||
import('../../utils/focus-visible').then(module => module.startFocusVisible());
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
|
@ -39,14 +39,14 @@ export class Backdrop implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionBackdropTap!: EventEmitter<void>;
|
||||
|
||||
componentDidLoad() {
|
||||
connectedCallback() {
|
||||
if (this.stopPropagation) {
|
||||
this.blocker.block();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.blocker.destroy();
|
||||
disconnectedCallback() {
|
||||
this.blocker.unblock();
|
||||
}
|
||||
|
||||
@Listen('touchstart', { passive: false, capture: true })
|
||||
|
@ -24,6 +24,7 @@ export class Content implements ComponentInterface {
|
||||
private cTop = -1;
|
||||
private cBottom = -1;
|
||||
private scrollEl!: HTMLElement;
|
||||
private mode = getIonMode(this);
|
||||
|
||||
// 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.
|
||||
@ -102,21 +103,14 @@ export class Content implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
||||
|
||||
componentWillLoad() {
|
||||
if (this.forceOverscroll === undefined) {
|
||||
const mode = getIonMode(this);
|
||||
this.forceOverscroll = mode === 'ios' && isPlatform(window, 'mobile');
|
||||
}
|
||||
disconnectedCallback() {
|
||||
this.onScrollEnd();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.resize();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.onScrollEnd();
|
||||
}
|
||||
|
||||
@Listen('click', { capture: true })
|
||||
onClick(ev: Event) {
|
||||
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() {
|
||||
if (this.fullscreen) {
|
||||
readTask(this.readDimensions.bind(this));
|
||||
@ -299,9 +300,9 @@ export class Content implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { scrollX, scrollY } = this;
|
||||
const mode = getIonMode(this);
|
||||
const { scrollX, scrollY, forceOverscroll } = this;
|
||||
|
||||
const forceOverscroll = this.shouldForceOverscroll();
|
||||
const transitionShadow = (mode === 'ios' && config.getBoolean('experimentalTransitionShadow', true));
|
||||
|
||||
this.resize();
|
||||
@ -312,7 +313,7 @@ export class Content implements ComponentInterface {
|
||||
...createColorClasses(this.color),
|
||||
[mode]: true,
|
||||
'content-sizing': hostContext('ion-popover', this.el),
|
||||
'overscroll': !!this.forceOverscroll,
|
||||
'overscroll': forceOverscroll,
|
||||
}}
|
||||
style={{
|
||||
'--offset-top': `${this.cTop}px`,
|
||||
@ -324,7 +325,7 @@ export class Content implements ComponentInterface {
|
||||
'inner-scroll': true,
|
||||
'scroll-x': scrollX,
|
||||
'scroll-y': scrollY,
|
||||
'overscroll': (scrollX || scrollY) && !!forceOverscroll
|
||||
'overscroll': (scrollX || scrollY) && forceOverscroll
|
||||
}}
|
||||
ref={el => this.scrollEl = el!}
|
||||
onScroll={ev => this.onScroll(ev)}
|
||||
|
@ -76,11 +76,13 @@ export class InfiniteScroll implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionInfinite!: EventEmitter<void>;
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
if (contentEl) {
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
if (!contentEl) {
|
||||
console.error('<ion-infinite-scroll> must be used inside an <ion-content>');
|
||||
return;
|
||||
}
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
this.thresholdChanged();
|
||||
this.disabledChanged();
|
||||
if (this.position === 'top') {
|
||||
@ -92,7 +94,7 @@ export class InfiniteScroll implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
this.enableScrollEvents(false);
|
||||
this.scrollEl = undefined;
|
||||
}
|
||||
|
@ -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 { 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.
|
||||
*/
|
||||
@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.
|
||||
@ -218,22 +218,22 @@ export class Input implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionStyle!: EventEmitter<StyleEventDetail>;
|
||||
|
||||
componentWillLoad() {
|
||||
// By default, password inputs clear after focus when they have content
|
||||
if (this.clearOnEdit === undefined && this.type === 'password') {
|
||||
this.clearOnEdit = true;
|
||||
}
|
||||
connectedCallback() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.debounceChanged();
|
||||
|
||||
this.ionInputDidLoad.emit();
|
||||
if (Build.isBrowser) {
|
||||
this.el.dispatchEvent(new CustomEvent('ionInputDidLoad', {
|
||||
detail: this.el
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.ionInputDidUnload.emit();
|
||||
disconnectedCallback() {
|
||||
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!);
|
||||
}
|
||||
|
||||
private shouldClearOnEdit() {
|
||||
const { type, clearOnEdit } = this;
|
||||
return (clearOnEdit === undefined)
|
||||
? type === 'password'
|
||||
: clearOnEdit;
|
||||
}
|
||||
|
||||
private getValue(): string {
|
||||
return this.value || '';
|
||||
}
|
||||
@ -295,7 +302,7 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
private onKeydown = () => {
|
||||
if (this.clearOnEdit) {
|
||||
if (this.shouldClearOnEdit()) {
|
||||
// Did the input value change after it was blurred and edited?
|
||||
if (this.didBlurAfterEdit && this.hasValue()) {
|
||||
// Clear the input
|
||||
@ -327,7 +334,7 @@ export class Input implements ComponentInterface {
|
||||
|
||||
private focusChanged() {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export class ItemSliding implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionDrag!: EventEmitter;
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
this.item = this.el.querySelector('ion-item');
|
||||
await this.updateOptions();
|
||||
|
||||
@ -81,7 +81,7 @@ export class ItemSliding implements ComponentInterface {
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
|
@ -136,7 +136,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
*/
|
||||
@Event() protected ionMenuChange!: EventEmitter<MenuChangeEventDetail>;
|
||||
|
||||
async componentWillLoad() {
|
||||
async connectedCallback() {
|
||||
if (this.type === undefined) {
|
||||
this.type = config.get('menuType', this.mode === 'ios' ? 'reveal' : 'overlay');
|
||||
}
|
||||
@ -182,11 +182,12 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
async componentDidLoad() {
|
||||
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
this.blocker.destroy();
|
||||
menuController._unregister(this);
|
||||
if (this.animation) {
|
||||
|
@ -47,7 +47,7 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
async connectedCallback() {
|
||||
let pickerRotateFactor = 0;
|
||||
let pickerScaleFactor = 0.81;
|
||||
|
||||
@ -60,16 +60,6 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
|
||||
this.rotateFactor = pickerRotateFactor;
|
||||
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({
|
||||
el: this.el,
|
||||
@ -81,14 +71,24 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
onEnd: ev => this.onEnd(ev),
|
||||
});
|
||||
this.gesture.setDisabled(false);
|
||||
|
||||
this.tmrId = setTimeout(() => {
|
||||
this.noAnimate = false;
|
||||
this.refresh(true);
|
||||
}, 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);
|
||||
clearTimeout(this.tmrId);
|
||||
if (this.gesture) {
|
||||
|
@ -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 { RadioGroupChangeEventDetail } from '../../interface';
|
||||
import { findCheckedOption, watchForOptions } from '../../utils/watch-options';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-radio-group'
|
||||
@ -10,7 +11,7 @@ export class RadioGroup implements ComponentInterface {
|
||||
|
||||
private inputId = `ion-rg-${radioGroupIds++}`;
|
||||
private labelId = `${this.inputId}-lbl`;
|
||||
private radios: HTMLIonRadioElement[] = [];
|
||||
private mutationO?: MutationObserver;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
@ -40,58 +41,11 @@ export class RadioGroup implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionChange!: EventEmitter<RadioGroupChangeEventDetail>;
|
||||
|
||||
@Listen('ionRadioDidLoad')
|
||||
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() {
|
||||
async connectedCallback() {
|
||||
// Get the list header if it exists and set the id
|
||||
// this is used to set aria-labelledby
|
||||
let header = this.el.querySelector('ion-list-header');
|
||||
if (!header) {
|
||||
header = this.el.querySelector('ion-item-divider');
|
||||
}
|
||||
const el = this.el;
|
||||
const header = el.querySelector('ion-list-header') || el.querySelector('ion-item-divider');
|
||||
if (header) {
|
||||
const label = header.querySelector('ion-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();
|
||||
}
|
||||
|
||||
private updateRadios() {
|
||||
const value = this.value;
|
||||
disconnectedCallback() {
|
||||
if (this.mutationO) {
|
||||
this.mutationO.disconnect();
|
||||
this.mutationO = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateRadios() {
|
||||
const { value } = this;
|
||||
const radios = await this.getRadios();
|
||||
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) {
|
||||
// correct value for this radio
|
||||
// but this radio isn't checked yet
|
||||
@ -118,6 +101,34 @@ export class RadioGroup implements ComponentInterface {
|
||||
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() {
|
||||
@ -125,6 +136,8 @@ export class RadioGroup implements ComponentInterface {
|
||||
<Host
|
||||
role="radiogroup"
|
||||
aria-labelledby={this.labelId}
|
||||
onIonSelect={this.onSelect}
|
||||
onIonDeselect={this.allowEmptySelection ? this.onDeselect : undefined}
|
||||
class={getIonMode(this)}
|
||||
>
|
||||
</Host>
|
||||
|
@ -22,7 +22,7 @@
|
||||
<ion-content class="outer-content">
|
||||
<ion-radio-group id="dynamicDisabled" disabled name="tannen" id="group" value="biff">
|
||||
<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-item>
|
||||
@ -53,7 +53,10 @@
|
||||
|
||||
</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>
|
||||
|
||||
<style>
|
||||
@ -61,12 +64,39 @@
|
||||
--background: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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() {
|
||||
dynamicDisabled.disabled = !dynamicDisabled.disabled;
|
||||
function addSelect() {
|
||||
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>
|
||||
</ion-app>
|
||||
|
@ -49,18 +49,6 @@ export class Radio implements ComponentInterface {
|
||||
*/
|
||||
@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.
|
||||
* @internal
|
||||
@ -116,14 +104,6 @@ export class Radio implements ComponentInterface {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.ionRadioDidLoad.emit();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.ionRadioDidUnload.emit();
|
||||
}
|
||||
|
||||
private emitStyle() {
|
||||
this.ionStyle.emit({
|
||||
'radio-checked': this.checked,
|
||||
|
@ -169,13 +169,11 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionBlur!: EventEmitter<void>;
|
||||
|
||||
componentWillLoad() {
|
||||
async connectedCallback() {
|
||||
this.updateRatio();
|
||||
this.debounceChanged();
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
async componentDidLoad() {
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.rangeSlider!,
|
||||
gestureName: 'range',
|
||||
@ -188,7 +186,7 @@ export class Range implements ComponentInterface {
|
||||
this.gesture.setDisabled(this.disabled);
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
|
@ -97,20 +97,19 @@ export class Refresher implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionStart!: EventEmitter<void>;
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
if (this.el.getAttribute('slot') !== 'fixed') {
|
||||
console.error('Make sure you use: <ion-refresher slot="fixed">');
|
||||
return;
|
||||
}
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
if (contentEl) {
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
} else {
|
||||
console.error('ion-refresher did not attach, make sure the parent is an ion-content.');
|
||||
if (!contentEl) {
|
||||
console.error('<ion-refresher> must be used inside an <ion-content>');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.el.closest('ion-content') as any,
|
||||
el: contentEl,
|
||||
gestureName: 'refresher',
|
||||
gesturePriority: 10,
|
||||
direction: 'y',
|
||||
@ -125,7 +124,7 @@ export class Refresher implements ComponentInterface {
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
this.scrollEl = undefined;
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
|
@ -52,12 +52,13 @@ export class ReorderGroup implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
if (contentEl) {
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
if (!contentEl) {
|
||||
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({
|
||||
el: this.el,
|
||||
gestureName: 'reorder',
|
||||
@ -74,7 +75,7 @@ export class ReorderGroup implements ComponentInterface {
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
this.onEnd();
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
|
@ -45,10 +45,7 @@ export class RouteRedirect implements ComponentInterface {
|
||||
this.ionRouteRedirectChanged.emit();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.ionRouteRedirectChanged.emit();
|
||||
}
|
||||
componentDidUnload() {
|
||||
connectedCallback() {
|
||||
this.ionRouteRedirectChanged.emit();
|
||||
}
|
||||
}
|
||||
|
@ -58,10 +58,7 @@ export class Route implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.ionRouteDataChanged.emit();
|
||||
}
|
||||
componentDidUnload() {
|
||||
connectedCallback() {
|
||||
this.ionRouteDataChanged.emit();
|
||||
}
|
||||
}
|
||||
|
@ -60,11 +60,7 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
|
||||
/** @internal */
|
||||
@Event({ bubbles: false }) ionNavDidChange!: EventEmitter<void>;
|
||||
|
||||
componentWillLoad() {
|
||||
this.ionNavWillLoad.emit();
|
||||
}
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture(
|
||||
this.el,
|
||||
() => !!this.swipeHandler && this.swipeHandler.canStart() && this.animationEnabled,
|
||||
@ -106,8 +102,11 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
|
||||
this.swipeHandlerChanged();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.activeEl = this.activeComponent = undefined;
|
||||
componentWillLoad() {
|
||||
this.ionNavWillLoad.emit();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
|
10
core/src/components/router/test/basic/e2e.ts
Normal file
10
core/src/components/router/test/basic/e2e.ts
Normal 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();
|
||||
});
|
@ -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';
|
||||
|
||||
@ -26,33 +26,7 @@ export class SelectOption implements ComponentInterface {
|
||||
/**
|
||||
* The text value of the option.
|
||||
*/
|
||||
@Prop({ mutable: true }) 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();
|
||||
}
|
||||
@Prop() value?: any | null;
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -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 { ActionSheetButton, ActionSheetOptions, AlertInput, AlertOptions, CssClassMap, OverlaySelect, PopoverOptions, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, StyleEventDetail } from '../../interface';
|
||||
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
|
||||
import { actionSheetController, alertController, popoverController } from '../../utils/overlays';
|
||||
import { hostContext } from '../../utils/theme';
|
||||
import { watchForOptions } from '../../utils/watch-options';
|
||||
|
||||
import { SelectCompareFn } from './select-interface';
|
||||
|
||||
@ -21,11 +22,11 @@ import { SelectCompareFn } from './select-interface';
|
||||
})
|
||||
export class Select implements ComponentInterface {
|
||||
|
||||
private childOpts: HTMLIonSelectOptionElement[] = [];
|
||||
private inputId = `ion-sel-${selectIds++}`;
|
||||
private overlay?: OverlaySelect;
|
||||
private didInit = false;
|
||||
private buttonEl?: HTMLButtonElement;
|
||||
private mutationO?: MutationObserver;
|
||||
|
||||
@Element() el!: HTMLIonSelectElement;
|
||||
|
||||
@ -117,64 +118,54 @@ export class Select implements ComponentInterface {
|
||||
@Event() ionStyle!: EventEmitter<StyleEventDetail>;
|
||||
|
||||
@Watch('disabled')
|
||||
@Watch('placeholder')
|
||||
disabledChanged() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
@Watch('value')
|
||||
valueChanged() {
|
||||
if (this.didInit) {
|
||||
this.updateOptions();
|
||||
this.emitStyle();
|
||||
if (this.didInit) {
|
||||
this.ionChange.emit({
|
||||
value: this.value,
|
||||
});
|
||||
this.emitStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('ionSelectOptionDidLoad')
|
||||
@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();
|
||||
|
||||
async connectedCallback() {
|
||||
if (this.value === undefined) {
|
||||
if (this.multiple) {
|
||||
// there are no values set at this point
|
||||
// so check to see who should be selected
|
||||
const checked = this.childOpts.filter(o => o.selected);
|
||||
this.value = checked.map(o => o.value);
|
||||
this.value = checked.map(o => getOptionValue(o));
|
||||
} else {
|
||||
const checked = this.childOpts.find(o => o.selected);
|
||||
if (checked) {
|
||||
this.value = checked.value;
|
||||
this.value = getOptionValue(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.updateOptions();
|
||||
this.updateOverlayOptions();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -222,22 +213,24 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
|
||||
private updateOverlayOptions(): void {
|
||||
if (!this.overlay) { return; }
|
||||
const overlay = (this.overlay as any);
|
||||
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
const childOpts = this.childOpts;
|
||||
switch (this.interface) {
|
||||
case 'action-sheet':
|
||||
overlay.buttons = this.createActionSheetButtons(this.childOpts);
|
||||
overlay.buttons = this.createActionSheetButtons(childOpts);
|
||||
break;
|
||||
case 'popover':
|
||||
const popover = overlay.querySelector('ion-select-popover');
|
||||
if (popover) {
|
||||
popover.options = this.createPopoverOptions(this.childOpts);
|
||||
popover.options = this.createPopoverOptions(childOpts);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
case 'alert':
|
||||
const inputType = (this.multiple ? 'checkbox' : 'radio');
|
||||
overlay.inputs = this.createAlertInputs(this.childOpts, inputType);
|
||||
overlay.inputs = this.createAlertInputs(childOpts, inputType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -248,7 +241,7 @@ export class Select implements ComponentInterface {
|
||||
role: (option.selected ? 'selected' : ''),
|
||||
text: option.textContent,
|
||||
handler: () => {
|
||||
this.value = option.value;
|
||||
this.value = getOptionValue(option);
|
||||
}
|
||||
} as ActionSheetButton;
|
||||
});
|
||||
@ -270,7 +263,7 @@ export class Select implements ComponentInterface {
|
||||
return {
|
||||
type: inputType,
|
||||
label: o.textContent,
|
||||
value: o.value,
|
||||
value: getOptionValue(o),
|
||||
checked: o.selected,
|
||||
disabled: o.disabled
|
||||
} as AlertInput;
|
||||
@ -279,13 +272,14 @@ export class Select implements ComponentInterface {
|
||||
|
||||
private createPopoverOptions(data: any[]): SelectPopoverOption[] {
|
||||
return data.map(o => {
|
||||
const value = getOptionValue(o);
|
||||
return {
|
||||
text: o.textContent,
|
||||
value: o.value,
|
||||
value,
|
||||
checked: o.selected,
|
||||
disabled: o.disabled,
|
||||
handler: () => {
|
||||
this.value = o.value;
|
||||
this.value = value;
|
||||
this.close();
|
||||
}
|
||||
} as SelectPopoverOption;
|
||||
@ -374,22 +368,18 @@ export class Select implements ComponentInterface {
|
||||
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() {
|
||||
// iterate all options, updating the selected prop
|
||||
let canSelect = true;
|
||||
for (const selectOption of this.childOpts) {
|
||||
const selected = canSelect && isOptionSelected(this.value, selectOption.value, this.compareWith);
|
||||
const { value, childOpts, compareWith, multiple } = this;
|
||||
for (const selectOption of childOpts) {
|
||||
const optValue = getOptionValue(selectOption);
|
||||
const selected = canSelect && isOptionSelected(value, optValue, compareWith);
|
||||
selectOption.selected = selected;
|
||||
|
||||
// if current option is selected and select is single-option, we can't select
|
||||
// any option more
|
||||
if (selected && !this.multiple) {
|
||||
if (selected && !multiple) {
|
||||
canSelect = false;
|
||||
}
|
||||
}
|
||||
@ -403,6 +393,10 @@ export class Select implements ComponentInterface {
|
||||
return this.getText() !== '';
|
||||
}
|
||||
|
||||
private get childOpts() {
|
||||
return Array.from(this.el.querySelectorAll('ion-select-option'));
|
||||
}
|
||||
|
||||
private getText(): string {
|
||||
const selectedText = this.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) => {
|
||||
if (value == null) {
|
||||
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 selectOpt = opts.find(opt => {
|
||||
return compareOptions(opt.value, value, compareWith);
|
||||
return compareOptions(getOptionValue(opt), value, compareWith);
|
||||
});
|
||||
return selectOpt
|
||||
? selectOpt.textContent
|
||||
|
@ -25,6 +25,7 @@
|
||||
<ion-label position="floating">Label with Placeholder</ion-label>
|
||||
<ion-select id="actionSheet" interface="action-sheet" placeholder="A Placeholder"></ion-select>
|
||||
</ion-item>
|
||||
|
||||
<script>
|
||||
let selects = document.querySelectorAll('ion-select');
|
||||
const options = ['bird', 'dog', 'shark', 'lizard'];
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Component, ComponentInterface, Event, Host, h } from '@stencil/core';
|
||||
import { EventEmitter } from 'ionicons/dist/types/stencil.core';
|
||||
import { Component, ComponentInterface, Host, h } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
|
||||
@ -9,17 +8,6 @@ import { getIonMode } from '../../global/ionic-global';
|
||||
})
|
||||
export class Slide implements ComponentInterface {
|
||||
|
||||
/** @internal */
|
||||
@Event() ionSlideChanged!: EventEmitter<void>;
|
||||
|
||||
componentDidLoad() {
|
||||
this.ionSlideChanged.emit();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.ionSlideChanged.emit();
|
||||
}
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
return (
|
||||
|
@ -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 { rIC } from '../../utils/helpers.js';
|
||||
@ -20,8 +20,8 @@ export class Slides implements ComponentInterface {
|
||||
|
||||
private scrollbarEl?: HTMLElement;
|
||||
private paginationEl?: HTMLElement;
|
||||
private didInit = false;
|
||||
|
||||
private swiperReady = false;
|
||||
private mutationO?: MutationObserver;
|
||||
private readySwiper!: (swiper: SwiperInterface) => void;
|
||||
private swiper: Promise<SwiperInterface> = new Promise(resolve => { this.readySwiper = resolve; });
|
||||
|
||||
@ -35,7 +35,7 @@ export class Slides implements ComponentInterface {
|
||||
|
||||
@Watch('options')
|
||||
async optionsChanged() {
|
||||
if (this.didInit) {
|
||||
if (this.swiperReady) {
|
||||
const swiper = await this.getSwiper();
|
||||
Object.assign(swiper.params, this.options);
|
||||
await this.update();
|
||||
@ -132,20 +132,28 @@ export class Slides implements ComponentInterface {
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
|
||||
async componentDidUnload() {
|
||||
async disconnectedCallback() {
|
||||
if (this.mutationO) {
|
||||
this.mutationO.disconnect();
|
||||
this.mutationO = undefined;
|
||||
}
|
||||
const swiper = await this.getSwiper();
|
||||
swiper.destroy(true, true);
|
||||
}
|
||||
|
||||
@Listen('ionSlideChanged')
|
||||
onSlideChanged() {
|
||||
if (this.didInit) {
|
||||
this.update();
|
||||
}
|
||||
this.swiper = new Promise(resolve => { this.readySwiper = resolve; });
|
||||
this.swiperReady = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,7 +162,10 @@ export class Slides implements ComponentInterface {
|
||||
*/
|
||||
@Method()
|
||||
async update() {
|
||||
const swiper = await this.getSwiper();
|
||||
const [swiper] = await Promise.all([
|
||||
this.getSwiper(),
|
||||
waitForSlides(this.el)
|
||||
]);
|
||||
swiper.update();
|
||||
}
|
||||
|
||||
@ -315,8 +326,9 @@ export class Slides implements ComponentInterface {
|
||||
// init swiper core
|
||||
// @ts-ignore
|
||||
const { Swiper } = await import('./swiper/swiper.bundle.js');
|
||||
await waitForSlides(this.el);
|
||||
const swiper = new Swiper(this.el, finalOptions);
|
||||
this.didInit = true;
|
||||
this.swiperReady = true;
|
||||
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())
|
||||
);
|
||||
};
|
||||
|
@ -33,6 +33,9 @@
|
||||
<h1>Slide 3</h1>
|
||||
</ion-slide>
|
||||
</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="slideNext()">Slide Next</ion-button>
|
||||
<ion-button expand="block" onclick="getActiveIndex()">Get Active Index</ion-button>
|
||||
@ -50,10 +53,24 @@
|
||||
|
||||
</ion-app>
|
||||
<script>
|
||||
let slideCount = 4;
|
||||
const slides = document.getElementById('slides')
|
||||
slides.pager = false;
|
||||
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() {
|
||||
await slides.slideNext(500)
|
||||
};
|
||||
|
@ -57,12 +57,12 @@ export class SplitPane implements ComponentInterface {
|
||||
this.ionSplitPaneVisible.emit(detail);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
connectedCallback() {
|
||||
this.styleChildren();
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
if (this.rmL) {
|
||||
this.rmL();
|
||||
this.rmL = undefined;
|
||||
|
@ -64,7 +64,6 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<h1>Page 1</h1>
|
||||
<ion-button onclick="push()">Push</ion-button>
|
||||
<ion-button onclick="menu()">Disable/enable menu</ion-button>
|
||||
<f></f>
|
||||
<f></f>
|
||||
@ -84,9 +83,6 @@
|
||||
menuCtrl.open('start');
|
||||
}
|
||||
|
||||
function push() {
|
||||
}
|
||||
|
||||
async function menu() {
|
||||
menuCtrl.enable(!await menuCtrl.isEnabled());
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ export class Tab implements ComponentInterface {
|
||||
@Prop() component?: ComponentRef;
|
||||
|
||||
componentWillLoad() {
|
||||
|
||||
if (Build.isDev) {
|
||||
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.' +
|
||||
|
@ -19,7 +19,6 @@ export class Tabs implements NavOutlet {
|
||||
|
||||
@Element() el!: HTMLIonTabsElement;
|
||||
|
||||
@State() tabs: HTMLIonTabElement[] = [];
|
||||
@State() selectedTab?: HTMLIonTabElement;
|
||||
|
||||
/** @internal */
|
||||
@ -41,23 +40,18 @@ export class Tabs implements NavOutlet {
|
||||
*/
|
||||
@Event({ bubbles: false }) ionTabsDidChange!: EventEmitter<{tab: string}>;
|
||||
|
||||
componentWillLoad() {
|
||||
async componentWillLoad() {
|
||||
if (!this.useRouter) {
|
||||
this.useRouter = !!document.querySelector('ion-router') && !this.el.closest('[no-router]');
|
||||
}
|
||||
this.tabs = Array.from(this.el.querySelectorAll('ion-tab'));
|
||||
this.initSelect().then(() => {
|
||||
if (!this.useRouter) {
|
||||
const tabs = this.tabs;
|
||||
await this.select(tabs[0]);
|
||||
}
|
||||
this.ionNavWillLoad.emit();
|
||||
this.componentWillUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.tabs.length = 0;
|
||||
this.selectedTab = this.leavingTab = undefined;
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
componentWillRender() {
|
||||
const tabBar = this.el.querySelector('ion-tab-bar');
|
||||
if (tabBar) {
|
||||
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;
|
||||
}
|
||||
|
||||
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> {
|
||||
if (this.transitioning) {
|
||||
return Promise.reject('transitioning already happening');
|
||||
@ -186,16 +170,19 @@ export class Tabs implements NavOutlet {
|
||||
return selectedTab !== undefined && selectedTab !== leavingTab && !this.transitioning;
|
||||
}
|
||||
|
||||
private get tabs() {
|
||||
return Array.from(this.el.querySelectorAll('ion-tab'));
|
||||
}
|
||||
|
||||
private onTabClicked = (ev: CustomEvent<TabButtonClickEventDetail>) => {
|
||||
const { href, tab } = ev.detail;
|
||||
const selectedTab = this.tabs.find(t => t.tab === tab);
|
||||
if (this.useRouter && href !== undefined) {
|
||||
const router = document.querySelector('ion-router');
|
||||
if (router) {
|
||||
router.push(href);
|
||||
}
|
||||
} else if (selectedTab) {
|
||||
this.select(selectedTab);
|
||||
} else {
|
||||
this.select(tab);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface';
|
||||
@ -169,28 +169,26 @@ export class Textarea implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionFocus!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
connectedCallback() {
|
||||
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() {
|
||||
this.debounceChanged();
|
||||
|
||||
this.runAutoGrow();
|
||||
|
||||
this.ionInputDidLoad.emit();
|
||||
}
|
||||
|
||||
// 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
|
||||
* `input.focus()`.
|
||||
|
@ -96,11 +96,7 @@ export class Toggle implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.el,
|
||||
gestureName: 'toggle',
|
||||
@ -114,13 +110,17 @@ export class Toggle implements ComponentInterface {
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
private emitStyle() {
|
||||
this.ionStyle.emit({
|
||||
'interactive-disabled': this.disabled,
|
||||
|
@ -152,16 +152,14 @@ export class VirtualScroll implements ComponentInterface {
|
||||
this.updateVirtualScroll();
|
||||
}
|
||||
|
||||
async componentDidLoad() {
|
||||
async connectedCallback() {
|
||||
const contentEl = this.el.closest('ion-content');
|
||||
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;
|
||||
}
|
||||
await contentEl.componentOnReady();
|
||||
|
||||
this.contentEl = contentEl;
|
||||
this.scrollEl = await contentEl.getScrollElement();
|
||||
this.contentEl = contentEl;
|
||||
this.calcCells();
|
||||
this.updateState();
|
||||
}
|
||||
@ -170,7 +168,7 @@ export class VirtualScroll implements ComponentInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
disconnectedCallback() {
|
||||
this.scrollEl = undefined;
|
||||
}
|
||||
|
||||
|
@ -75,11 +75,13 @@ export const startInputShims = (config: Config) => {
|
||||
registerInput(input);
|
||||
}
|
||||
|
||||
doc.body.addEventListener('ionInputDidLoad', event => {
|
||||
registerInput(event.target as any);
|
||||
});
|
||||
doc.addEventListener('ionInputDidLoad', ((ev: InputEvent) => {
|
||||
registerInput(ev.detail);
|
||||
}) as any);
|
||||
|
||||
doc.body.addEventListener('ionInputDidUnload', event => {
|
||||
unregisterInput(event.target as any);
|
||||
});
|
||||
doc.addEventListener('ionInputDidUnload', ((ev: InputEvent) => {
|
||||
unregisterInput(ev.detail);
|
||||
}) as any);
|
||||
};
|
||||
|
||||
type InputEvent = CustomEvent<HTMLElement>;
|
||||
|
32
core/src/utils/watch-options.ts
Normal file
32
core/src/utils/watch-options.ts
Normal 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);
|
||||
};
|
Reference in New Issue
Block a user