-
Notifications
You must be signed in to change notification settings - Fork 795
/
Copy pathupdate-component.ts
480 lines (436 loc) · 17 KB
/
update-component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
import { BUILD, NAMESPACE } from '@app-data';
import { Build, consoleError, getHostRef, nextTick, plt, win, writeTask } from '@platform';
import { CMP_FLAGS, HOST_FLAGS } from '@utils';
import type * as d from '../declarations';
import { emitEvent } from './event-emitter';
import { createTime } from './profile';
import { PLATFORM_FLAGS } from './runtime-constants';
import { attachStyles } from './styles';
import { renderVdom } from './vdom/vdom-render';
export const attachToAncestor = (hostRef: d.HostRef, ancestorComponent?: d.HostElement) => {
if (BUILD.asyncLoading && ancestorComponent && !hostRef.$onRenderResolve$ && ancestorComponent['s-p']) {
const index = ancestorComponent['s-p'].push(
new Promise(
(r) =>
(hostRef.$onRenderResolve$ = () => {
ancestorComponent['s-p'].splice(index - 1, 1);
r();
}),
),
);
}
};
export const scheduleUpdate = (hostRef: d.HostRef, isInitialLoad: boolean) => {
if (BUILD.taskQueue && BUILD.updatable) {
hostRef.$flags$ |= HOST_FLAGS.isQueuedForUpdate;
}
if (BUILD.asyncLoading && hostRef.$flags$ & HOST_FLAGS.isWaitingForChildren) {
hostRef.$flags$ |= HOST_FLAGS.needsRerender;
return;
}
attachToAncestor(hostRef, hostRef.$ancestorComponent$);
// there is no ancestor component or the ancestor component
// has already fired off its lifecycle update then
// fire off the initial update
const dispatch = () => dispatchHooks(hostRef, isInitialLoad);
return BUILD.taskQueue ? writeTask(dispatch) : dispatch();
};
/**
* Dispatch initial-render and update lifecycle hooks, enqueuing calls to
* component lifecycle methods like `componentWillLoad` as well as
* {@link updateComponent}, which will kick off the virtual DOM re-render.
*
* @param hostRef a reference to a host DOM node
* @param isInitialLoad whether we're on the initial load or not
* @returns an empty Promise which is used to enqueue a series of operations for
* the component
*/
const dispatchHooks = (hostRef: d.HostRef, isInitialLoad: boolean): Promise<void> => {
const elm = hostRef.$hostElement$;
const endSchedule = createTime('scheduleUpdate', hostRef.$cmpMeta$.$tagName$);
const instance = BUILD.lazyLoad ? hostRef.$lazyInstance$ : elm;
/**
* Given a user imports a component compiled with a `dist-custom-element`
* output target into a Stencil project compiled with a `dist` output target,
* then `instance` will be `undefined` as `hostRef` won't have a `lazyInstance`
* property. In this case, the component will fail to render in one of the
* subsequent functions.
*
* For this scenario to work the user needs to set the `externalRuntime` flag
* for the `dist-custom-element` component that is being imported into the `dist`
* Stencil project.
*/
if (!instance) {
throw new Error(
`Can't render component <${elm.tagName.toLowerCase()} /> with invalid Stencil runtime! ` +
'Make sure this imported component is compiled with a `externalRuntime: true` flag. ' +
'For more information, please refer to https://stenciljs.com/docs/custom-elements#externalruntime',
);
}
// We're going to use this variable together with `enqueue` to implement a
// little promise-based queue. We start out with it `undefined`. When we add
// the first function to the queue we'll set this variable to be that
// function's return value. When we attempt to add subsequent values to the
// queue we'll check that value and, if it was a `Promise`, we'll then chain
// the new function off of that `Promise` using `.then()`. This will give our
// queue two nice properties:
//
// 1. If all functions added to the queue are synchronous they'll be called
// synchronously right away.
// 2. If all functions added to the queue are asynchronous they'll all be
// called in order after `dispatchHooks` exits.
let maybePromise: Promise<void> | undefined;
if (isInitialLoad) {
if (BUILD.lazyLoad && BUILD.hostListener) {
hostRef.$flags$ |= HOST_FLAGS.isListenReady;
if (hostRef.$queuedListeners$) {
hostRef.$queuedListeners$.map(([methodName, event]) => safeCall(instance, methodName, event, elm));
hostRef.$queuedListeners$ = undefined;
}
}
emitLifecycleEvent(elm, 'componentWillLoad');
// If `componentWillLoad` returns a `Promise` then we want to wait on
// whatever's going on in that `Promise` before we launch into
// rendering the component, doing other lifecycle stuff, etc. So
// in that case we assign the returned promise to the variable we
// declared above to hold a possible 'queueing' Promise
maybePromise = safeCall(instance, 'componentWillLoad', undefined, elm);
} else {
emitLifecycleEvent(elm, 'componentWillUpdate');
// Like `componentWillLoad` above, we allow Stencil component
// authors to return a `Promise` from this lifecycle callback, and
// we specify that our runtime will wait for that `Promise` to
// resolve before the component re-renders. So if the method
// returns a `Promise` we need to keep it around!
maybePromise = safeCall(instance, 'componentWillUpdate', undefined, elm);
}
emitLifecycleEvent(elm, 'componentWillRender');
maybePromise = enqueue(maybePromise, () => safeCall(instance, 'componentWillRender', undefined, elm));
endSchedule();
return enqueue(maybePromise, () => updateComponent(hostRef, instance, isInitialLoad));
};
/**
* This function uses a Promise to implement a simple first-in, first-out queue
* of functions to be called.
*
* The queue is ordered on the basis of the first argument. If it's
* `undefined`, then nothing is on the queue yet, so the provided function can
* be called synchronously (although note that this function may return a
* `Promise`). The idea is that then the return value of that enqueueing
* operation is kept around, so that if it was a `Promise` then subsequent
* functions can be enqueued by calling this function again with that `Promise`
* as the first argument.
*
* @param maybePromise either a `Promise` which should resolve before the next function is called or an 'empty' sentinel
* @param fn a function to enqueue
* @returns either a `Promise` or the return value of the provided function
*/
const enqueue = (maybePromise: Promise<void> | undefined, fn: () => Promise<void>): Promise<void> | undefined =>
isPromisey(maybePromise)
? maybePromise.then(fn).catch((err) => {
console.error(err);
fn();
})
: fn();
/**
* Check that a value is a `Promise`. To check, we first see if the value is an
* instance of the `Promise` global. In a few circumstances, in particular if
* the global has been overwritten, this is could be misleading, so we also do
* a little 'duck typing' check to see if the `.then` property of the value is
* defined and a function.
*
* @param maybePromise it might be a promise!
* @returns whether it is or not
*/
const isPromisey = (maybePromise: Promise<void> | unknown): maybePromise is Promise<void> =>
maybePromise instanceof Promise ||
(maybePromise && (maybePromise as any).then && typeof (maybePromise as Promise<void>).then === 'function');
/**
* Update a component given reference to its host elements and so on.
*
* @param hostRef an object containing references to the element's host node,
* VDom nodes, and other metadata
* @param instance a reference to the underlying host element where it will be
* rendered
* @param isInitialLoad whether or not this function is being called as part of
* the first render cycle
*/
const updateComponent = async (
hostRef: d.HostRef,
instance: d.HostElement | d.ComponentInterface,
isInitialLoad: boolean,
) => {
const elm = hostRef.$hostElement$ as d.RenderNode;
const endUpdate = createTime('update', hostRef.$cmpMeta$.$tagName$);
const rc = elm['s-rc'];
if (BUILD.style && isInitialLoad) {
// DOM WRITE!
attachStyles(hostRef);
}
const endRender = createTime('render', hostRef.$cmpMeta$.$tagName$);
if (BUILD.isDev) {
hostRef.$flags$ |= HOST_FLAGS.devOnRender;
}
if (BUILD.hydrateServerSide) {
await callRender(hostRef, instance, elm, isInitialLoad);
} else {
callRender(hostRef, instance, elm, isInitialLoad);
}
if (BUILD.isDev) {
hostRef.$renderCount$ = hostRef.$renderCount$ === undefined ? 1 : hostRef.$renderCount$ + 1;
hostRef.$flags$ &= ~HOST_FLAGS.devOnRender;
}
if (BUILD.hydrateServerSide) {
try {
// manually connected child components during server-side hydrate
serverSideConnected(elm);
if (isInitialLoad) {
// using only during server-side hydrate
if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.shadowDomEncapsulation) {
elm['s-en'] = '';
} else if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.scopedCssEncapsulation) {
elm['s-en'] = 'c';
}
}
} catch (e) {
consoleError(e, elm);
}
}
if (BUILD.asyncLoading && rc) {
// ok, so turns out there are some child host elements
// waiting on this parent element to load
// let's fire off all update callbacks waiting
rc.map((cb) => cb());
elm['s-rc'] = undefined;
}
endRender();
endUpdate();
if (BUILD.asyncLoading) {
const childrenPromises = elm['s-p'] ?? [];
const postUpdate = () => postUpdateComponent(hostRef);
if (childrenPromises.length === 0) {
postUpdate();
} else {
Promise.all(childrenPromises).then(postUpdate);
hostRef.$flags$ |= HOST_FLAGS.isWaitingForChildren;
childrenPromises.length = 0;
}
} else {
postUpdateComponent(hostRef);
}
};
let renderingRef: any = null;
/**
* Handle making the call to the VDom renderer with the proper context given
* various build variables
*
* @param hostRef an object containing references to the element's host node,
* VDom nodes, and other metadata
* @param instance a reference to the underlying host element where it will be
* rendered
* @param elm the Host element for the component
* @param isInitialLoad whether or not this function is being called as part of
* @returns an empty promise
*/
const callRender = (hostRef: d.HostRef, instance: any, elm: HTMLElement, isInitialLoad: boolean) => {
// in order for bundlers to correctly tree-shake the BUILD object
// we need to ensure BUILD is not deoptimized within a try/catch
// https://rollupjs.org/guide/en/#treeshake tryCatchDeoptimization
const allRenderFn = BUILD.allRenderFn ? true : false;
const lazyLoad = BUILD.lazyLoad ? true : false;
const taskQueue = BUILD.taskQueue ? true : false;
const updatable = BUILD.updatable ? true : false;
try {
renderingRef = instance;
/**
* minification optimization: `allRenderFn` is `true` if all components have a `render`
* method, so we can call the method immediately. If not, check before calling it.
*/
instance = allRenderFn ? instance.render() : instance.render && instance.render();
if (updatable && taskQueue) {
hostRef.$flags$ &= ~HOST_FLAGS.isQueuedForUpdate;
}
if (updatable || lazyLoad) {
hostRef.$flags$ |= HOST_FLAGS.hasRendered;
}
if (BUILD.hasRenderFn || BUILD.reflect) {
if (BUILD.vdomRender || BUILD.reflect) {
// looks like we've got child nodes to render into this host element
// or we need to update the css class/attrs on the host element
// DOM WRITE!
if (BUILD.hydrateServerSide) {
return Promise.resolve(instance).then((value) => renderVdom(hostRef, value, isInitialLoad));
} else {
renderVdom(hostRef, instance, isInitialLoad);
}
} else {
const shadowRoot = elm.shadowRoot;
if (hostRef.$cmpMeta$.$flags$ & CMP_FLAGS.shadowDomEncapsulation) {
shadowRoot.textContent = instance;
} else {
elm.textContent = instance;
}
}
}
} catch (e) {
consoleError(e, hostRef.$hostElement$);
}
renderingRef = null;
return null;
};
export const getRenderingRef = () => renderingRef;
export const postUpdateComponent = (hostRef: d.HostRef) => {
const tagName = hostRef.$cmpMeta$.$tagName$;
const elm = hostRef.$hostElement$;
const endPostUpdate = createTime('postUpdate', tagName);
const instance = BUILD.lazyLoad ? hostRef.$lazyInstance$ : (elm as any);
const ancestorComponent = hostRef.$ancestorComponent$;
if (BUILD.isDev) {
hostRef.$flags$ |= HOST_FLAGS.devOnRender;
}
safeCall(instance, 'componentDidRender', undefined, elm);
if (BUILD.isDev) {
hostRef.$flags$ &= ~HOST_FLAGS.devOnRender;
}
emitLifecycleEvent(elm, 'componentDidRender');
if (!(hostRef.$flags$ & HOST_FLAGS.hasLoadedComponent)) {
hostRef.$flags$ |= HOST_FLAGS.hasLoadedComponent;
if (BUILD.asyncLoading && BUILD.cssAnnotations) {
// DOM WRITE!
addHydratedFlag(elm);
}
if (BUILD.isDev) {
hostRef.$flags$ |= HOST_FLAGS.devOnDidLoad;
}
safeCall(instance, 'componentDidLoad', undefined, elm);
if (BUILD.isDev) {
hostRef.$flags$ &= ~HOST_FLAGS.devOnDidLoad;
}
emitLifecycleEvent(elm, 'componentDidLoad');
endPostUpdate();
if (BUILD.asyncLoading) {
hostRef.$onReadyResolve$(elm);
if (!ancestorComponent) {
appDidLoad(tagName);
}
}
} else {
// we've already loaded this component
// fire off the user's componentDidUpdate method (if one was provided)
// componentDidUpdate runs AFTER render() has been called
// and all child components have finished updating
if (BUILD.isDev) {
hostRef.$flags$ |= HOST_FLAGS.devOnRender;
}
safeCall(instance, 'componentDidUpdate', undefined, elm);
if (BUILD.isDev) {
hostRef.$flags$ &= ~HOST_FLAGS.devOnRender;
}
emitLifecycleEvent(elm, 'componentDidUpdate');
endPostUpdate();
}
if (BUILD.method && BUILD.lazyLoad) {
hostRef.$onInstanceResolve$(elm);
}
// load events fire from bottom to top
// the deepest elements load first then bubbles up
if (BUILD.asyncLoading) {
if (hostRef.$onRenderResolve$) {
hostRef.$onRenderResolve$();
hostRef.$onRenderResolve$ = undefined;
}
if (hostRef.$flags$ & HOST_FLAGS.needsRerender) {
nextTick(() => scheduleUpdate(hostRef, false));
}
hostRef.$flags$ &= ~(HOST_FLAGS.isWaitingForChildren | HOST_FLAGS.needsRerender);
}
// ( •_•)
// ( •_•)>⌐■-■
// (⌐■_■)
};
export const forceUpdate = (ref: any) => {
if (BUILD.updatable && (Build.isBrowser || Build.isTesting)) {
const hostRef = getHostRef(ref);
const isConnected = hostRef.$hostElement$.isConnected;
if (
isConnected &&
(hostRef.$flags$ & (HOST_FLAGS.hasRendered | HOST_FLAGS.isQueuedForUpdate)) === HOST_FLAGS.hasRendered
) {
scheduleUpdate(hostRef, false);
}
// Returns "true" when the forced update was successfully scheduled
return isConnected;
}
return false;
};
export const appDidLoad = (who: string) => {
if (BUILD.asyncQueue) {
plt.$flags$ |= PLATFORM_FLAGS.appLoaded;
}
nextTick(() => emitEvent(win, 'appload', { detail: { namespace: NAMESPACE } }));
if (BUILD.profile && performance.measure) {
performance.measure(`[Stencil] ${NAMESPACE} initial load (by ${who})`, 'st:app:start');
}
};
/**
* Allows to safely call a method, e.g. `componentDidLoad`, on an instance,
* e.g. custom element node. If a build figures out that e.g. no component
* has a `componentDidLoad` method, the instance method gets removed from the
* output bundle and this function returns `undefined`.
* @param instance any object that may or may not contain methods
* @param method method name
* @param arg single arbitrary argument
* @param elm the element which made the call
* @returns result of method call if it exists, otherwise `undefined`
*/
export const safeCall = (instance: any, method: string, arg?: any, elm?: HTMLElement) => {
if (instance && instance[method]) {
try {
return instance[method](arg);
} catch (e) {
consoleError(e, elm);
}
}
return undefined;
};
/**
* For debugging purposes as `BUILD.lifecycleDOMEvents` is `false` by default and will
* get removed by the compiler. Used for timing events to see how long they take.
* @param elm the target of the Event
* @param lifecycleName name of the event
*/
const emitLifecycleEvent = (elm: EventTarget, lifecycleName: string) => {
if (BUILD.lifecycleDOMEvents) {
emitEvent(elm, 'stencil_' + lifecycleName, {
bubbles: true,
composed: true,
detail: {
namespace: NAMESPACE,
},
});
}
};
/**
* Set the hydrated flag on a DOM element
*
* @param elm a reference to a DOM element
* @returns undefined
*/
const addHydratedFlag = (elm: Element) =>
BUILD.hydratedClass
? elm.classList.add(BUILD.hydratedSelectorName ?? 'hydrated')
: BUILD.hydratedAttribute
? elm.setAttribute(BUILD.hydratedSelectorName ?? 'hydrated', '')
: undefined;
const serverSideConnected = (elm: any) => {
const children = elm.children;
if (children != null) {
for (let i = 0, ii = children.length; i < ii; i++) {
const childElm = children[i] as any;
if (typeof childElm.connectedCallback === 'function') {
childElm.connectedCallback();
}
serverSideConnected(childElm);
}
}
};