@hyperspan/framework 1.0.0-alpha.1 → 1.0.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tsconfig.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "compileOnSave": true,
3
3
  "compilerOptions": {
4
- "rootDir": "src",
5
4
  "outDir": "dist",
6
5
  "target": "es2019",
7
6
  "lib": ["ESNext", "dom", "dom.iterable"],
@@ -25,5 +24,6 @@
25
24
  "@hyperspan/html": ["../html/src/html.ts"]
26
25
  }
27
26
  },
27
+ "references": [{ "path": "../html" }],
28
28
  "exclude": ["node_modules", "__tests__", "*.test.ts"]
29
29
  }
@@ -1,224 +0,0 @@
1
- import { html } from '@hyperspan/html';
2
- import { Idiomorph } from './idiomorph';
3
-
4
- /**
5
- * Used for streaming content from the server to the client.
6
- */
7
- function htmlAsyncContentObserver() {
8
- if (typeof MutationObserver != 'undefined') {
9
- // Hyperspan - Async content loader
10
- // Puts streamed content in its place immediately after it is added to the DOM
11
- const asyncContentObserver = new MutationObserver((list) => {
12
- const asyncContent = list
13
- .map((mutation) =>
14
- Array.from(mutation.addedNodes).find((node: any) => {
15
- if (!node || !node?.id || typeof node.id !== 'string') {
16
- return false;
17
- }
18
- return node.id?.startsWith('async_loading_') && node.id?.endsWith('_content');
19
- })
20
- )
21
- .filter((node: any) => node);
22
-
23
- asyncContent.forEach((templateEl: any) => {
24
- try {
25
- // Also observe for content inside the template content (shadow DOM is separate)
26
- asyncContentObserver.observe(templateEl.content, { childList: true, subtree: true });
27
-
28
- const slotId = templateEl.id.replace('_content', '');
29
- const slotEl = document.getElementById(slotId);
30
-
31
- if (slotEl) {
32
- // Content AND slot are present - let's insert the content into the slot
33
- // Ensure the content is fully done streaming in before inserting it into the slot
34
- waitForContent(templateEl.content, (el2) => {
35
- return Array.from(el2.childNodes).find(
36
- (node) => node.nodeType === Node.COMMENT_NODE && node.nodeValue === 'end'
37
- );
38
- })
39
- .then((endComment) => {
40
- templateEl.content.removeChild(endComment);
41
- const content = templateEl.content.cloneNode(true);
42
- Idiomorph.morph(slotEl, content);
43
- templateEl.parentNode.removeChild(templateEl);
44
- lazyLoadScripts();
45
- })
46
- .catch(console.error);
47
- } else {
48
- // Slot is NOT present - wait for it to be added to the DOM so we can insert the content into it
49
- waitForContent(document.body, () => {
50
- return document.getElementById(slotId);
51
- }).then((slotEl) => {
52
- Idiomorph.morph(slotEl, templateEl.content.cloneNode(true));
53
- lazyLoadScripts();
54
- });
55
- }
56
- } catch (e) {
57
- console.error(e);
58
- }
59
- });
60
- });
61
- asyncContentObserver.observe(document.body, { childList: true, subtree: true });
62
- }
63
- }
64
- htmlAsyncContentObserver();
65
-
66
- /**
67
- * Wait until ALL of the content inside an element is present from streaming in.
68
- * Large chunks of content can sometimes take more than a single tick to write to DOM.
69
- */
70
- async function waitForContent(
71
- el: HTMLElement,
72
- waitFn: (
73
- node: HTMLElement
74
- ) => HTMLElement | HTMLTemplateElement | Node | ChildNode | null | undefined,
75
- options: { timeoutMs?: number; intervalMs?: number } = { timeoutMs: 10000, intervalMs: 20 }
76
- ): Promise<HTMLElement | HTMLTemplateElement | Node | ChildNode | null | undefined> {
77
- return new Promise((resolve, reject) => {
78
- let timeout: NodeJS.Timeout;
79
- const interval = setInterval(() => {
80
- const content = waitFn(el);
81
- if (content) {
82
- if (timeout) {
83
- clearTimeout(timeout);
84
- }
85
- clearInterval(interval);
86
- resolve(content);
87
- }
88
- }, options.intervalMs || 20);
89
- timeout = setTimeout(() => {
90
- clearInterval(interval);
91
- reject(new Error(`[Hyperspan] Timeout waiting for end of streaming content ${el.id}`));
92
- }, options.timeoutMs || 10000);
93
- });
94
- }
95
-
96
- /**
97
- * Server action component to handle the client-side form submission and HTML replacement
98
- */
99
- class HSAction extends HTMLElement {
100
- constructor() {
101
- super();
102
- }
103
-
104
- connectedCallback() {
105
- actionFormObserver.observe(this, { childList: true, subtree: true });
106
- bindHSActionForm(this, this.querySelector('form') as HTMLFormElement);
107
- }
108
- }
109
- window.customElements.define('hs-action', HSAction);
110
- const actionFormObserver = new MutationObserver((list) => {
111
- list.forEach((mutation) => {
112
- mutation.addedNodes.forEach((node) => {
113
- if (node && ('closest' in node || node instanceof HTMLFormElement)) {
114
- bindHSActionForm(
115
- (node as HTMLElement).closest('hs-action') as HSAction,
116
- node instanceof HTMLFormElement
117
- ? node
118
- : ((node as HTMLElement | HTMLFormElement).querySelector('form') as HTMLFormElement)
119
- );
120
- }
121
- });
122
- });
123
- });
124
-
125
- /**
126
- * Bind the form inside an hs-action element to the action URL and submit handler
127
- */
128
- function bindHSActionForm(hsActionElement: HSAction, form: HTMLFormElement) {
129
- if (!hsActionElement || !form) {
130
- return;
131
- }
132
-
133
- form.setAttribute('action', hsActionElement.getAttribute('url') || '');
134
- const submitHandler = (e: Event) => {
135
- e.preventDefault();
136
- formSubmitToRoute(e, form as HTMLFormElement, {
137
- afterResponse: () => bindHSActionForm(hsActionElement, form),
138
- });
139
- form.removeEventListener('submit', submitHandler);
140
- };
141
- form.addEventListener('submit', submitHandler);
142
- }
143
-
144
- /**
145
- * Submit form data to route and replace contents with response
146
- */
147
- type TFormSubmitOptons = { afterResponse: () => any };
148
- function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOptons) {
149
- const formData = new FormData(form);
150
- const formUrl = form.getAttribute('action') || '';
151
- const method = form.getAttribute('method')?.toUpperCase() || 'POST';
152
- const headers = {
153
- Accept: 'text/html',
154
- 'X-Request-Type': 'partial',
155
- };
156
-
157
- const hsActionTag = form.closest('hs-action');
158
- const submitBtn = form.querySelector('button[type=submit],input[type=submit]');
159
- if (submitBtn) {
160
- submitBtn.setAttribute('disabled', 'disabled');
161
- }
162
-
163
- fetch(formUrl, { body: formData, method, headers })
164
- .then((res: Response) => {
165
- // Look for special header that indicates a redirect.
166
- // fetch() automatically follows 3xx redirects, so we need to handle this manually to redirect the user to the full page
167
- if (res.headers.has('X-Redirect-Location')) {
168
- const newUrl = res.headers.get('X-Redirect-Location');
169
- if (newUrl) {
170
- window.location.assign(newUrl);
171
- }
172
- return '';
173
- }
174
-
175
- return res.text();
176
- })
177
- .then((content: string) => {
178
- // No content = DO NOTHING (redirect or something else happened)
179
- if (!content) {
180
- return;
181
- }
182
-
183
- const target = content.includes('<html') ? window.document.body : hsActionTag || form;
184
-
185
- Idiomorph.morph(target, content);
186
- opts.afterResponse && opts.afterResponse();
187
- lazyLoadScripts();
188
- });
189
- }
190
-
191
- /**
192
- * Intersection observer for lazy loading <script> tags
193
- */
194
- const lazyLoadScriptObserver = new IntersectionObserver(
195
- (entries, observer) => {
196
- entries
197
- .filter((entry) => entry.isIntersecting)
198
- .forEach((entry) => {
199
- observer.unobserve(entry.target);
200
- // @ts-ignore
201
- if (entry.target.children[0]?.content) {
202
- // @ts-ignore
203
- entry.target.replaceWith(entry.target.children[0].content);
204
- }
205
- });
206
- },
207
- { rootMargin: '0px 0px -200px 0px' }
208
- );
209
-
210
- /**
211
- * Lazy load <script> tags in the current document
212
- */
213
- function lazyLoadScripts() {
214
- document
215
- .querySelectorAll('div[data-loading=lazy]')
216
- .forEach((el) => lazyLoadScriptObserver.observe(el));
217
- }
218
-
219
- window.addEventListener('load', () => {
220
- lazyLoadScripts();
221
- });
222
-
223
- // @ts-ignore
224
- window.html = html;
File without changes