@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/package.json +8 -8
- package/src/actions.test.ts +147 -0
- package/src/actions.ts +118 -0
- package/src/client/_hs/hyperspan-actions.client.ts +98 -0
- package/src/client/_hs/hyperspan-scripts.client.ts +31 -0
- package/src/client/_hs/hyperspan-streaming.client.ts +94 -0
- package/src/client/js.ts +0 -21
- package/src/cookies.ts +234 -0
- package/src/index.ts +2 -0
- package/src/plugins.ts +2 -4
- package/src/server.test.ts +141 -17
- package/src/server.ts +58 -86
- package/src/types.ts +73 -19
- package/src/utils.test.ts +196 -0
- package/src/utils.ts +135 -1
- package/tsconfig.json +1 -1
- package/src/clientjs/hyperspan-client.ts +0 -224
- /package/src/{clientjs → client/_hs}/idiomorph.ts +0 -0
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
|