Skip to content

Commit 48a2763

Browse files
authored
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
1 parent a65d897 commit 48a2763

File tree

33 files changed

+411
-368
lines changed

33 files changed

+411
-368
lines changed

core/src/components/app/app.tsx

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ComponentInterface, Element, Host, h } from '@stencil/core';
1+
import { Build, Component, ComponentInterface, Element, Host, h } from '@stencil/core';
22

33
import { config } from '../../global/config';
44
import { getIonMode } from '../../global/ionic-global';
@@ -14,23 +14,24 @@ export class App implements ComponentInterface {
1414
@Element() el!: HTMLElement;
1515

1616
componentDidLoad() {
17-
rIC(() => {
18-
const isHybrid = isPlatform(window, 'hybrid');
19-
if (!config.getBoolean('_testing')) {
20-
import('../../utils/tap-click').then(module => module.startTapClick(config));
21-
}
22-
if (config.getBoolean('statusTap', isHybrid)) {
23-
import('../../utils/status-tap').then(module => module.startStatusTap());
24-
}
25-
if (config.getBoolean('inputShims', needInputShims())) {
26-
import('../../utils/input-shims/input-shims').then(module => module.startInputShims(config));
27-
}
28-
if (config.getBoolean('hardwareBackButton', isHybrid)) {
29-
import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton());
30-
}
31-
import('../../utils/focus-visible').then(module => module.startFocusVisible());
32-
33-
});
17+
if (Build.isBrowser) {
18+
rIC(() => {
19+
const isHybrid = isPlatform(window, 'hybrid');
20+
if (!config.getBoolean('_testing')) {
21+
import('../../utils/tap-click').then(module => module.startTapClick(config));
22+
}
23+
if (config.getBoolean('statusTap', isHybrid)) {
24+
import('../../utils/status-tap').then(module => module.startStatusTap());
25+
}
26+
if (config.getBoolean('inputShims', needInputShims())) {
27+
import('../../utils/input-shims/input-shims').then(module => module.startInputShims(config));
28+
}
29+
if (config.getBoolean('hardwareBackButton', isHybrid)) {
30+
import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton());
31+
}
32+
import('../../utils/focus-visible').then(module => module.startFocusVisible());
33+
});
34+
}
3435
}
3536

3637
render() {

core/src/components/backdrop/backdrop.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ export class Backdrop implements ComponentInterface {
3939
*/
4040
@Event() ionBackdropTap!: EventEmitter<void>;
4141

42-
componentDidLoad() {
42+
connectedCallback() {
4343
if (this.stopPropagation) {
4444
this.blocker.block();
4545
}
4646
}
4747

48-
componentDidUnload() {
49-
this.blocker.destroy();
48+
disconnectedCallback() {
49+
this.blocker.unblock();
5050
}
5151

5252
@Listen('touchstart', { passive: false, capture: true })

core/src/components/content/content.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class Content implements ComponentInterface {
2424
private cTop = -1;
2525
private cBottom = -1;
2626
private scrollEl!: HTMLElement;
27+
private mode = getIonMode(this);
2728

2829
// Detail is used in a hot loop in the scroll event, by allocating it here
2930
// 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 {
102103
*/
103104
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
104105

105-
componentWillLoad() {
106-
if (this.forceOverscroll === undefined) {
107-
const mode = getIonMode(this);
108-
this.forceOverscroll = mode === 'ios' && isPlatform(window, 'mobile');
109-
}
106+
disconnectedCallback() {
107+
this.onScrollEnd();
110108
}
111109

112110
componentDidLoad() {
113111
this.resize();
114112
}
115113

116-
componentDidUnload() {
117-
this.onScrollEnd();
118-
}
119-
120114
@Listen('click', { capture: true })
121115
onClick(ev: Event) {
122116
if (this.isScrolling) {
@@ -125,6 +119,13 @@ export class Content implements ComponentInterface {
125119
}
126120
}
127121

122+
private shouldForceOverscroll() {
123+
const { forceOverscroll, mode } = this;
124+
return forceOverscroll === undefined
125+
? mode === 'ios' && isPlatform(window, 'mobile')
126+
: forceOverscroll;
127+
}
128+
128129
private resize() {
129130
if (this.fullscreen) {
130131
readTask(this.readDimensions.bind(this));
@@ -299,9 +300,9 @@ export class Content implements ComponentInterface {
299300
}
300301

301302
render() {
303+
const { scrollX, scrollY } = this;
302304
const mode = getIonMode(this);
303-
const { scrollX, scrollY, forceOverscroll } = this;
304-
305+
const forceOverscroll = this.shouldForceOverscroll();
305306
const transitionShadow = (mode === 'ios' && config.getBoolean('experimentalTransitionShadow', true));
306307

307308
this.resize();
@@ -312,7 +313,7 @@ export class Content implements ComponentInterface {
312313
...createColorClasses(this.color),
313314
[mode]: true,
314315
'content-sizing': hostContext('ion-popover', this.el),
315-
'overscroll': !!this.forceOverscroll,
316+
'overscroll': forceOverscroll,
316317
}}
317318
style={{
318319
'--offset-top': `${this.cTop}px`,
@@ -324,7 +325,7 @@ export class Content implements ComponentInterface {
324325
'inner-scroll': true,
325326
'scroll-x': scrollX,
326327
'scroll-y': scrollY,
327-
'overscroll': (scrollX || scrollY) && !!forceOverscroll
328+
'overscroll': (scrollX || scrollY) && forceOverscroll
328329
}}
329330
ref={el => this.scrollEl = el!}
330331
onScroll={ev => this.onScroll(ev)}

core/src/components/infinite-scroll/infinite-scroll.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ export class InfiniteScroll implements ComponentInterface {
7676
*/
7777
@Event() ionInfinite!: EventEmitter<void>;
7878

79-
async componentDidLoad() {
79+
async connectedCallback() {
8080
const contentEl = this.el.closest('ion-content');
81-
if (contentEl) {
82-
this.scrollEl = await contentEl.getScrollElement();
81+
if (!contentEl) {
82+
console.error('<ion-infinite-scroll> must be used inside an <ion-content>');
83+
return;
8384
}
85+
this.scrollEl = await contentEl.getScrollElement();
8486
this.thresholdChanged();
8587
this.disabledChanged();
8688
if (this.position === 'top') {
@@ -92,7 +94,7 @@ export class InfiniteScroll implements ComponentInterface {
9294
}
9395
}
9496

95-
componentDidUnload() {
97+
disconnectedCallback() {
9698
this.enableScrollEvents(false);
9799
this.scrollEl = undefined;
98100
}

core/src/components/input/input.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
1+
import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
22

33
import { getIonMode } from '../../global/ionic-global';
44
import { Color, InputChangeEventDetail, StyleEventDetail, TextFieldTypes } from '../../interface';
@@ -66,7 +66,7 @@ export class Input implements ComponentInterface {
6666
/**
6767
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
6868
*/
69-
@Prop({ mutable: true }) clearOnEdit?: boolean;
69+
@Prop() clearOnEdit?: boolean;
7070

7171
/**
7272
* 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 {
218218
*/
219219
@Event() ionStyle!: EventEmitter<StyleEventDetail>;
220220

221-
componentWillLoad() {
222-
// By default, password inputs clear after focus when they have content
223-
if (this.clearOnEdit === undefined && this.type === 'password') {
224-
this.clearOnEdit = true;
225-
}
221+
connectedCallback() {
226222
this.emitStyle();
227-
}
228-
229-
componentDidLoad() {
230223
this.debounceChanged();
231-
232-
this.ionInputDidLoad.emit();
224+
if (Build.isBrowser) {
225+
this.el.dispatchEvent(new CustomEvent('ionInputDidLoad', {
226+
detail: this.el
227+
}));
228+
}
233229
}
234230

235-
componentDidUnload() {
236-
this.ionInputDidUnload.emit();
231+
disconnectedCallback() {
232+
if (Build.isBrowser) {
233+
document.dispatchEvent(new CustomEvent('ionInputDidUnload', {
234+
detail: this.el
235+
}));
236+
}
237237
}
238238

239239
/**
@@ -255,6 +255,13 @@ export class Input implements ComponentInterface {
255255
return Promise.resolve(this.nativeInput!);
256256
}
257257

258+
private shouldClearOnEdit() {
259+
const { type, clearOnEdit } = this;
260+
return (clearOnEdit === undefined)
261+
? type === 'password'
262+
: clearOnEdit;
263+
}
264+
258265
private getValue(): string {
259266
return this.value || '';
260267
}
@@ -295,7 +302,7 @@ export class Input implements ComponentInterface {
295302
}
296303

297304
private onKeydown = () => {
298-
if (this.clearOnEdit) {
305+
if (this.shouldClearOnEdit()) {
299306
// Did the input value change after it was blurred and edited?
300307
if (this.didBlurAfterEdit && this.hasValue()) {
301308
// Clear the input
@@ -327,7 +334,7 @@ export class Input implements ComponentInterface {
327334

328335
private focusChanged() {
329336
// If clearOnEdit is enabled and the input blurred but has a value, set a flag
330-
if (this.clearOnEdit && !this.hasFocus && this.hasValue()) {
337+
if (!this.hasFocus && this.shouldClearOnEdit() && this.hasValue()) {
331338
this.didBlurAfterEdit = true;
332339
}
333340
}

core/src/components/item-sliding/item-sliding.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class ItemSliding implements ComponentInterface {
6464
*/
6565
@Event() ionDrag!: EventEmitter;
6666

67-
async componentDidLoad() {
67+
async connectedCallback() {
6868
this.item = this.el.querySelector('ion-item');
6969
await this.updateOptions();
7070

@@ -81,7 +81,7 @@ export class ItemSliding implements ComponentInterface {
8181
this.disabledChanged();
8282
}
8383

84-
componentDidUnload() {
84+
disconnectedCallback() {
8585
if (this.gesture) {
8686
this.gesture.destroy();
8787
this.gesture = undefined;

core/src/components/menu/menu.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class Menu implements ComponentInterface, MenuI {
136136
*/
137137
@Event() protected ionMenuChange!: EventEmitter<MenuChangeEventDetail>;
138138

139-
async componentWillLoad() {
139+
async connectedCallback() {
140140
if (this.type === undefined) {
141141
this.type = config.get('menuType', this.mode === 'ios' ? 'reveal' : 'overlay');
142142
}
@@ -182,11 +182,12 @@ export class Menu implements ComponentInterface, MenuI {
182182
this.updateState();
183183
}
184184

185-
componentDidLoad() {
185+
async componentDidLoad() {
186186
this.ionMenuChange.emit({ disabled: this.disabled, open: this._isOpen });
187+
this.updateState();
187188
}
188189

189-
componentDidUnload() {
190+
disconnectedCallback() {
190191
this.blocker.destroy();
191192
menuController._unregister(this);
192193
if (this.animation) {

core/src/components/picker-column/picker-column.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class PickerColumnCmp implements ComponentInterface {
4747
this.refresh();
4848
}
4949

50-
componentWillLoad() {
50+
async connectedCallback() {
5151
let pickerRotateFactor = 0;
5252
let pickerScaleFactor = 0.81;
5353

@@ -60,16 +60,6 @@ export class PickerColumnCmp implements ComponentInterface {
6060

6161
this.rotateFactor = pickerRotateFactor;
6262
this.scaleFactor = pickerScaleFactor;
63-
}
64-
65-
async componentDidLoad() {
66-
// get the height of one option
67-
const colEl = this.optsEl;
68-
if (colEl) {
69-
this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0);
70-
}
71-
72-
this.refresh();
7363

7464
this.gesture = (await import('../../utils/gesture')).createGesture({
7565
el: this.el,
@@ -81,14 +71,24 @@ export class PickerColumnCmp implements ComponentInterface {
8171
onEnd: ev => this.onEnd(ev),
8272
});
8373
this.gesture.setDisabled(false);
84-
8574
this.tmrId = setTimeout(() => {
8675
this.noAnimate = false;
8776
this.refresh(true);
8877
}, 250);
8978
}
9079

91-
componentDidUnload() {
80+
componentDidLoad() {
81+
const colEl = this.optsEl;
82+
if (colEl) {
83+
// DOM READ
84+
// We perfom a DOM read over a rendered item, this needs to happen after the first render
85+
this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0);
86+
}
87+
88+
this.refresh();
89+
}
90+
91+
disconnectedCallback() {
9292
cancelAnimationFrame(this.rafId);
9393
clearTimeout(this.tmrId);
9494
if (this.gesture) {

0 commit comments

Comments
 (0)