/
piercing-fragment-outlet.ts
158 lines (137 loc) · 4.87 KB
/
piercing-fragment-outlet.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
import { DOMAttributes } from "react";
import WritableDOMStream from "writable-dom";
import { getBus } from "./message-bus/get-bus";
import { PiercingFragmentHost } from "./piercing-fragment-host/piercing-fragment-host";
/**
* Registers the "piercing-fragment-outlet" web component so that it can be used throughout
* the application.
*/
export function registerPiercingFragmentOutlet() {
window.customElements.define(
"piercing-fragment-outlet",
PiercingFragmentOutlet
);
}
/**
* Set of ids of fragments that have been previously unmounted.
*
* We keep track of this to know whether to manually run side-effects in modules
* that are referenced as part of the fragment.
*
* When we fetch a fragment for the first time any side-effects in module-type scripts
* that are referenced will be executed.
* Later, if we fetch the fragment again, its side-effects will not run automatically.
* To workaround this we ensure that all such modules also expose their side-effects
* via a default export function.
* We then manually call that default export to re-execute the side-effects.
*/
const unmountedFragmentIds: Set<string> = new Set();
export class PiercingFragmentOutlet extends HTMLElement {
// this field can be read from the outside to check if this element
// is a PiercingFragmentOutlet (without relying on `instanceof`)
// @ts-ignore - the following field is accessed by `fragmentIsPierced.isFragmentPierced`.
private readonly piercingFragmentOutlet = true;
private fragmentHost: PiercingFragmentHost | null = null;
constructor() {
super();
}
async connectedCallback() {
const fragmentId = this.getAttribute("fragment-id");
if (!fragmentId) {
throw new Error(
"The fragment outlet component has been applied without" +
" providing a fragment-id"
);
}
this.fragmentHost = this.getFragmentHost(fragmentId);
if (this.fragmentHost) {
// There is already a fragment host in the DOM that we can pierce into this outlet
this.innerHTML == "";
this.fragmentHost.pierceInto(this);
} else {
// We need to fetch and create the fragment host
const fragmentStream = await this.fetchFragmentStream(fragmentId);
await this.streamFragmentIntoOutlet(fragmentId, fragmentStream);
this.fragmentHost = this.getFragmentHost(fragmentId, true);
}
if (!this.fragmentHost) {
throw new Error(
`The fragment with id "${fragmentId}" is not present and` +
" it could not be fetched"
);
}
// We need to dispatch a qinit so that Qwik can run different necessary
// checks/logic on Qwik fragments (which it would otherwise not with this
// fragments implementation).
// (for more info see: https://github.com/BuilderIO/qwik/issues/1947)
document.dispatchEvent(new Event("qinit"));
}
disconnectedCallback() {
if (this.fragmentHost) {
unmountedFragmentIds.add(this.fragmentHost.fragmentId);
this.fragmentHost = null;
}
}
private async fetchFragmentStream(fragmentId: string) {
const url = this.getFragmentUrl(fragmentId);
const state = getBus().state;
const req = new Request(url, {
headers: {
"message-bus-state": JSON.stringify(state),
},
});
const response = await fetch(req);
if (!response.body) {
throw new Error(
"An empty response has been provided when fetching" +
` the fragment with id ${fragmentId}`
);
}
return response.body;
}
private getFragmentUrl(fragmentId: string): string {
return `/piercing-fragment/${fragmentId}`;
}
private async streamFragmentIntoOutlet(
fragmentId: string,
fragmentStream: ReadableStream
) {
await fragmentStream
.pipeThrough(new TextDecoderStream())
.pipeTo(new WritableDOMStream(this as ParentNode));
this.reapplyFragmentModuleScripts(fragmentId);
}
private reapplyFragmentModuleScripts(fragmentId: string) {
if (unmountedFragmentIds.has(fragmentId)) {
this.querySelectorAll("script").forEach((script) => {
if (script.src && script.type === "module") {
import(/* @vite-ignore */ script.src).then((scriptModule) =>
scriptModule.default?.()
);
}
});
}
}
private getFragmentHost(
fragmentId: string,
insideOutlet = false
): PiercingFragmentHost | null {
return (insideOutlet ? this : document).querySelector(
`piercing-fragment-host[fragment-id="${fragmentId}"]`
);
}
}
declare global {
namespace JSX {
interface IntrinsicElements {
"piercing-fragment-outlet": PiercingFragmentOutletAttributes;
}
type PiercingFragmentOutletAttributes = { "fragment-id": string } & Partial<
PiercingFragmentOutlet &
DOMAttributes<PiercingFragmentOutlet> & {
"fragment-fetch-params"?: string;
[onProp: `on${string}`]: Function | undefined;
}
>;
}
}