node-turbo 1.0.1 → 1.1.0
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/.c8rc.json +3 -1
- package/README.md +111 -18
- package/docs/API.md +250 -30
- package/lib/express/express-turbo-stream.js +5 -0
- package/lib/koa/koa-turbo-stream.js +3 -1
- package/lib/turbo-element.js +8 -2
- package/lib/turbo-frame.js +5 -0
- package/lib/turbo-readable.js +9 -2
- package/lib/turbo-stream-element.js +27 -21
- package/lib/turbo-stream.js +52 -96
- package/lib/ws/ws-turbo-stream.js +8 -0
- package/package.json +30 -15
- package/test/unit/core/turbo-element.test.js +29 -0
- package/test/unit/core/turbo-stream-element.test.js +7 -0
- package/test/unit/core/turbo-stream.test.js +31 -0
package/lib/turbo-readable.js
CHANGED
|
@@ -9,7 +9,7 @@ const debug = db('node-turbo:readable');
|
|
|
9
9
|
* This class represents a readable stream which reads
|
|
10
10
|
* messages/elements from a Turbo Stream instance.
|
|
11
11
|
*
|
|
12
|
-
* @extends {stream~Readable}
|
|
12
|
+
* @extends {node:stream~Readable}
|
|
13
13
|
* @since 1.0.0
|
|
14
14
|
*/
|
|
15
15
|
export class TurboReadable extends Readable {
|
|
@@ -18,6 +18,7 @@ export class TurboReadable extends Readable {
|
|
|
18
18
|
* The Turbo Stream instance to create the readable stream for.
|
|
19
19
|
*
|
|
20
20
|
* @type {TurboStream}
|
|
21
|
+
* @since 1.0.0
|
|
21
22
|
*/
|
|
22
23
|
_turboStream;
|
|
23
24
|
|
|
@@ -34,6 +35,7 @@ export class TurboReadable extends Readable {
|
|
|
34
35
|
* @param {TurboStream} turboStream - The Turbo Stream instance to create the readable stream for.
|
|
35
36
|
* @param {Object} [opts] - The options for the readable stream.
|
|
36
37
|
* @throws {Error} if `turboStream` is not a TurboStream instance
|
|
38
|
+
* @since 1.0.0
|
|
37
39
|
*/
|
|
38
40
|
constructor(turboStream, opts) {
|
|
39
41
|
debug('new TurboReadable()', opts);
|
|
@@ -61,6 +63,7 @@ export class TurboReadable extends Readable {
|
|
|
61
63
|
* Pushes a Turbo Stream element into the read queue.
|
|
62
64
|
*
|
|
63
65
|
* @param {TurboStreamElement} el - The Turbo Stream element.
|
|
66
|
+
* @since 1.0.0
|
|
64
67
|
*/
|
|
65
68
|
_pushElement(el) {
|
|
66
69
|
debug('_pushElement()');
|
|
@@ -73,6 +76,7 @@ export class TurboReadable extends Readable {
|
|
|
73
76
|
* as handler for the `element` event.
|
|
74
77
|
*
|
|
75
78
|
* @type {Function}
|
|
79
|
+
* @since 1.0.0
|
|
76
80
|
*/
|
|
77
81
|
_boundPush = this._pushElement.bind(this);
|
|
78
82
|
|
|
@@ -82,6 +86,7 @@ export class TurboReadable extends Readable {
|
|
|
82
86
|
* This implementation does nothing.
|
|
83
87
|
* (Normally, push data would be pushed into the read queue here.)
|
|
84
88
|
*
|
|
89
|
+
* @since 1.0.0
|
|
85
90
|
* @todo Do we need backpressure handling?
|
|
86
91
|
*/
|
|
87
92
|
_read() {
|
|
@@ -94,6 +99,7 @@ export class TurboReadable extends Readable {
|
|
|
94
99
|
* for the event `element` is removed and the configuration restored.
|
|
95
100
|
*
|
|
96
101
|
* @param {Error} err - The error object, if thrown.
|
|
102
|
+
* @since 1.0.0
|
|
97
103
|
*/
|
|
98
104
|
_destroy(err) {
|
|
99
105
|
debug('_destroy()', err);
|
|
@@ -115,7 +121,8 @@ export class TurboReadable extends Readable {
|
|
|
115
121
|
* Pushes `null` to the readable buffer to signal the end of the input.
|
|
116
122
|
*
|
|
117
123
|
* @todo Should we call this end() or is this confusing because normally
|
|
118
|
-
*
|
|
124
|
+
* only writable streams have this function.
|
|
125
|
+
* @since 1.0.0
|
|
119
126
|
*/
|
|
120
127
|
done() {
|
|
121
128
|
debug('done()');
|
|
@@ -13,31 +13,20 @@ import { AttributeMissingError, AttributeMalformedError, AttributeInvalidError }
|
|
|
13
13
|
export class TurboStreamElement extends TurboElement {
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Validates the attributes. `attributes.
|
|
17
|
-
*
|
|
16
|
+
* Validates the attributes. `attributes.action` is mandatory.
|
|
17
|
+
* `attributes.target` (or `attributes.targets`) is mandatory for any action but 'refresh'.
|
|
18
|
+
* Gets called by the constructor.
|
|
18
19
|
*
|
|
19
20
|
* @throws {AttributeMissingError} when mandatory attributes are missing.
|
|
20
21
|
* @throws {AttributeMalformedError} when mandatory attributes are malformed.
|
|
21
22
|
* @throws {AttributeInvalidError} when attributes are invalid.
|
|
23
|
+
* @since 1.0.0
|
|
22
24
|
*/
|
|
23
25
|
validate() {
|
|
24
26
|
if (typeof this.attributes === 'undefined' || !isPlainObject(this.attributes)) {
|
|
25
27
|
throw new AttributeMissingError('TurboStream: Attributes are missing.');
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
if (!('target' in this.attributes) && !('targets' in this.attributes)) {
|
|
29
|
-
throw new AttributeMissingError('TurboStream: Attribute "target" or "targets" is missing.');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if ('target' in this.attributes && 'targets' in this.attributes) {
|
|
33
|
-
throw new AttributeInvalidError('TurboStream: Attributes "target" and "targets" exclude each other.');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if ((typeof this.attributes.target !== 'string' || this.attributes.target.length === 0) &&
|
|
37
|
-
(typeof this.attributes.targets !== 'string' || this.attributes.targets.length === 0)) {
|
|
38
|
-
throw new AttributeMalformedError('TurboStream: Attribute "target"/"targets" must be a string with non-zero length.');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
30
|
if (!('action' in this.attributes)) {
|
|
42
31
|
throw new AttributeMissingError('TurboStream: Attribute "action" is missing.');
|
|
43
32
|
}
|
|
@@ -45,22 +34,39 @@ import { AttributeMissingError, AttributeMalformedError, AttributeInvalidError }
|
|
|
45
34
|
if (typeof this.attributes.action !== 'string' || this.attributes.action.length === 0) {
|
|
46
35
|
throw new AttributeMalformedError('TurboStream: Attribute "action" must be a string with non-zero length.');
|
|
47
36
|
}
|
|
37
|
+
|
|
38
|
+
if (this.attributes.action !== 'refresh') {
|
|
39
|
+
if (!('target' in this.attributes) && !('targets' in this.attributes)) {
|
|
40
|
+
throw new AttributeMissingError('TurboStream: Attribute "target" or "targets" is missing.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ('target' in this.attributes && 'targets' in this.attributes) {
|
|
44
|
+
throw new AttributeInvalidError('TurboStream: Attributes "target" and "targets" exclude each other.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ((typeof this.attributes.target !== 'string' || this.attributes.target.length === 0) &&
|
|
48
|
+
(typeof this.attributes.targets !== 'string' || this.attributes.targets.length === 0)) {
|
|
49
|
+
throw new AttributeMalformedError('TurboStream: Attribute "target"/"targets" must be a string with non-zero length.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
56
|
* Renders this Turbo Stream element as HTML string. Omits `<template>[content]<template>` when the attribute
|
|
53
|
-
* `action` is 'remove'.
|
|
57
|
+
* `action` is 'remove' or 'refresh'.
|
|
54
58
|
*
|
|
55
59
|
* @returns {String} The rendered HTML fragment.
|
|
56
60
|
* @see https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions
|
|
61
|
+
* @since 1.0.0
|
|
57
62
|
*/
|
|
58
63
|
render() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
switch (this.attributes.action) {
|
|
65
|
+
case 'remove':
|
|
66
|
+
case 'refresh':
|
|
67
|
+
return `<turbo-stream ${this.renderAttributesAsHtml()}></turbo-stream>`;
|
|
68
|
+
default:
|
|
69
|
+
return `<turbo-stream ${this.renderAttributesAsHtml()}><template>${this.content}</template></turbo-stream>`;
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
|
package/lib/turbo-stream.js
CHANGED
|
@@ -9,7 +9,7 @@ const debug = db('node-turbo:turbostream');
|
|
|
9
9
|
/**
|
|
10
10
|
* A Turbo Stream message.
|
|
11
11
|
*
|
|
12
|
-
* @extends {events~EventEmitter}
|
|
12
|
+
* @extends {node:events~EventEmitter}
|
|
13
13
|
* @since 1.0.0
|
|
14
14
|
*/
|
|
15
15
|
export class TurboStream extends EventEmitter {
|
|
@@ -19,6 +19,7 @@ export class TurboStream extends EventEmitter {
|
|
|
19
19
|
*
|
|
20
20
|
* @type {Object<String, String>}
|
|
21
21
|
* @property {Boolean} buffer - Should elements be added to the buffer (default: true)?
|
|
22
|
+
* @since 1.0.0
|
|
22
23
|
*/
|
|
23
24
|
config = {
|
|
24
25
|
buffer: true
|
|
@@ -29,6 +30,7 @@ export class TurboStream extends EventEmitter {
|
|
|
29
30
|
* Array of buffered elements. Gets filled if `config.buffer` is `true`.
|
|
30
31
|
*
|
|
31
32
|
* @type {Array}
|
|
33
|
+
* @since 1.0.0
|
|
32
34
|
*/
|
|
33
35
|
elements = [];
|
|
34
36
|
|
|
@@ -39,17 +41,20 @@ export class TurboStream extends EventEmitter {
|
|
|
39
41
|
* @see https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses
|
|
40
42
|
* @type {String}
|
|
41
43
|
* @static
|
|
44
|
+
* @since 1.0.0
|
|
42
45
|
*/
|
|
43
46
|
static MIME_TYPE = 'text/vnd.turbo-stream.html';
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
|
-
* List of all supported actions:
|
|
48
|
-
*
|
|
50
|
+
* List of all supported official actions:
|
|
51
|
+
* `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`
|
|
52
|
+
* and `refresh`.
|
|
49
53
|
*
|
|
50
54
|
* @see https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions
|
|
51
55
|
* @type {Array}
|
|
52
56
|
* @static
|
|
57
|
+
* @since 1.0.0
|
|
53
58
|
*/
|
|
54
59
|
static ACTIONS = [
|
|
55
60
|
'append',
|
|
@@ -58,7 +63,9 @@ export class TurboStream extends EventEmitter {
|
|
|
58
63
|
'update',
|
|
59
64
|
'remove',
|
|
60
65
|
'before',
|
|
61
|
-
'after'
|
|
66
|
+
'after',
|
|
67
|
+
'morph',
|
|
68
|
+
'refresh'
|
|
62
69
|
];
|
|
63
70
|
|
|
64
71
|
|
|
@@ -66,6 +73,7 @@ export class TurboStream extends EventEmitter {
|
|
|
66
73
|
* The number of buffered Turbo Stream elements.
|
|
67
74
|
*
|
|
68
75
|
* @type {Number}
|
|
76
|
+
* @since 1.0.0
|
|
69
77
|
*/
|
|
70
78
|
get length() {
|
|
71
79
|
return this.elements.length;
|
|
@@ -78,6 +86,7 @@ export class TurboStream extends EventEmitter {
|
|
|
78
86
|
*
|
|
79
87
|
* @param {Object<String, String>} [attributes] - The attributes of this element.
|
|
80
88
|
* @param {String} [content] - The HTML content of this element.
|
|
89
|
+
* @since 1.0.0
|
|
81
90
|
*/
|
|
82
91
|
constructor(attributes, content) {
|
|
83
92
|
super();
|
|
@@ -94,6 +103,7 @@ export class TurboStream extends EventEmitter {
|
|
|
94
103
|
* @param {Object} config - New configuration.
|
|
95
104
|
* @emits config
|
|
96
105
|
* @returns {TurboStream} The instance for chaining.
|
|
106
|
+
* @since 1.0.0
|
|
97
107
|
*/
|
|
98
108
|
updateConfig(config) {
|
|
99
109
|
if (config) {
|
|
@@ -114,6 +124,7 @@ export class TurboStream extends EventEmitter {
|
|
|
114
124
|
* @param {String} content - The HTML content of the element.
|
|
115
125
|
* @emits element
|
|
116
126
|
* @returns {TurboStream} The instance for chaining.
|
|
127
|
+
* @since 1.0.0
|
|
117
128
|
*/
|
|
118
129
|
addElement(attributesOrElement, content) {
|
|
119
130
|
let el;
|
|
@@ -140,6 +151,7 @@ export class TurboStream extends EventEmitter {
|
|
|
140
151
|
*
|
|
141
152
|
* @emits clear
|
|
142
153
|
* @returns {TurboStream} The instance for chaining.
|
|
154
|
+
* @since 1.0.0
|
|
143
155
|
*/
|
|
144
156
|
clear() {
|
|
145
157
|
this.elements = [];
|
|
@@ -154,6 +166,7 @@ export class TurboStream extends EventEmitter {
|
|
|
154
166
|
*
|
|
155
167
|
* @returns {String|null} The rendered Turbo Stream HTML fragment or null if there were no buffered elements.
|
|
156
168
|
* @emits render
|
|
169
|
+
* @since 1.0.0
|
|
157
170
|
*/
|
|
158
171
|
render() {
|
|
159
172
|
const arr = this.renderElements();
|
|
@@ -172,6 +185,7 @@ export class TurboStream extends EventEmitter {
|
|
|
172
185
|
* If there are buffered elements, renders them and returns an array with the HTML fragments.
|
|
173
186
|
*
|
|
174
187
|
* @returns {Array|null} The rendered Turbo Stream HTML fragments as array or null if there were no buffered elements.
|
|
188
|
+
* @since 1.0.0
|
|
175
189
|
*/
|
|
176
190
|
renderElements() {
|
|
177
191
|
if (this.elements.length > 0) {
|
|
@@ -188,6 +202,7 @@ export class TurboStream extends EventEmitter {
|
|
|
188
202
|
* @returns {String|null} The rendered Turbo Stream HTML fragment or null if there were no buffered elements.
|
|
189
203
|
* @emits {render}
|
|
190
204
|
* @emits {clear}
|
|
205
|
+
* @since 1.0.0
|
|
191
206
|
*/
|
|
192
207
|
flush() {
|
|
193
208
|
const html = this.render();
|
|
@@ -204,6 +219,7 @@ export class TurboStream extends EventEmitter {
|
|
|
204
219
|
* @param {String} target - The target ID.
|
|
205
220
|
* @param {String} content - The HTML content of the element.
|
|
206
221
|
* @returns {TurboStream} The instance for chaining.
|
|
222
|
+
* @since 1.0.0
|
|
207
223
|
*/
|
|
208
224
|
custom(action, target, content) {
|
|
209
225
|
return this.addElement({ action, target }, content);
|
|
@@ -217,6 +233,7 @@ export class TurboStream extends EventEmitter {
|
|
|
217
233
|
* @param {String} targets - The query string targeting multiple DOM elements.
|
|
218
234
|
* @param {String} content - The HTML content of the element.
|
|
219
235
|
* @returns {TurboStream} The instance for chaining.
|
|
236
|
+
* @since 1.0.0
|
|
220
237
|
*/
|
|
221
238
|
customAll(action, targets, content) {
|
|
222
239
|
return this.addElement({ action, targets }, content);
|
|
@@ -224,7 +241,14 @@ export class TurboStream extends EventEmitter {
|
|
|
224
241
|
|
|
225
242
|
|
|
226
243
|
/**
|
|
244
|
+
* Creates a readable stream.
|
|
227
245
|
*
|
|
246
|
+
* @param {Object<String, String>} opts - The options for stream creation.
|
|
247
|
+
* @param {Boolean} opts.continuous - If true, a TurboReadable instance is returned.
|
|
248
|
+
* If false, a readable stream created from the buffered items is returned.
|
|
249
|
+
* @param {Object<String, String>} streamOptions - The options for the readable stream itself.
|
|
250
|
+
* @returns {stream.Readable|TurboReadable} Either a readable stream or a TurboReadable instance.
|
|
251
|
+
* @since 1.0.0
|
|
228
252
|
*/
|
|
229
253
|
createReadableStream(opts, streamOptions = {}) {
|
|
230
254
|
opts = Object.assign({}, { continuous: true }, opts);
|
|
@@ -239,11 +263,34 @@ export class TurboStream extends EventEmitter {
|
|
|
239
263
|
return Readable.from(this.length > 0 ? this.render().split('\n') : [], streamOptions);
|
|
240
264
|
}
|
|
241
265
|
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Adds a Turbo Stream Element with the action 'refresh' to the message.
|
|
270
|
+
*
|
|
271
|
+
* @param {String|Object<String, String>} requestIdOrAttributes - Either the request ID as string or all attributes as an object.
|
|
272
|
+
* @returns {TurboStream} The instance for chaining.
|
|
273
|
+
* @since 1.1.0
|
|
274
|
+
*/
|
|
275
|
+
refresh(requestIdOrAttributes) {
|
|
276
|
+
if (typeof requestIdOrAttributes === 'string') {
|
|
277
|
+
return this.addElement({ action: 'refresh', 'request-id': requestIdOrAttributes });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
return this.addElement(Object.assign({ action: 'refresh' }, requestIdOrAttributes));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
242
284
|
}
|
|
243
285
|
|
|
244
286
|
|
|
245
287
|
// Add convenience functions for all supported actions.
|
|
246
288
|
TurboStream.ACTIONS.forEach(action => {
|
|
289
|
+
// This action needs special treatment.
|
|
290
|
+
if (action === 'refresh') {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
247
294
|
TurboStream.prototype[action] = function(targetOrAttributes, content) {
|
|
248
295
|
if (typeof targetOrAttributes === 'string') {
|
|
249
296
|
return this.addElement({ action: action, target: targetOrAttributes }, content);
|
|
@@ -255,96 +302,5 @@ TurboStream.ACTIONS.forEach(action => {
|
|
|
255
302
|
|
|
256
303
|
TurboStream.prototype[`${action}All`] = function(targets, content) {
|
|
257
304
|
return this.addElement({ action: action, targets }, content);
|
|
258
|
-
};
|
|
305
|
+
};
|
|
259
306
|
});
|
|
260
|
-
|
|
261
|
-
// Dynamic functions:
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* @name #append
|
|
265
|
-
* @function
|
|
266
|
-
* @description Adds a Turbo Stream element with the action 'append' to the message.
|
|
267
|
-
* @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
268
|
-
* @param {String} content - The HTML content of the element.
|
|
269
|
-
*/
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* @name #prepend
|
|
273
|
-
* @function
|
|
274
|
-
* @description Adds a Turbo Stream element with the action 'prepend' to the message.
|
|
275
|
-
* @param {(String|Object<String, String>)} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
276
|
-
* @param {String} content - The HTML content of the element.
|
|
277
|
-
*/
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* @name #replace
|
|
281
|
-
* @function
|
|
282
|
-
* @description Adds a Turbo Stream element with the action 'replace' to the message.
|
|
283
|
-
* @param {(String|Object<String, String>)} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
284
|
-
* @param {String} content - The HTML content of the element.
|
|
285
|
-
*/
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* @name #update
|
|
289
|
-
* @function
|
|
290
|
-
* @description Adds a Turbo Stream element with the action 'update' to the message.
|
|
291
|
-
* @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
292
|
-
* @param {String} content - The HTML content of the element.
|
|
293
|
-
*/
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* @name #remove
|
|
297
|
-
* @function
|
|
298
|
-
* @description Adds a Turbo Stream element with the action 'remove' to the message.
|
|
299
|
-
* @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
300
|
-
* @param {String} content - The HTML content of the element.
|
|
301
|
-
*/
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* @name #before
|
|
305
|
-
* @function
|
|
306
|
-
* @description Adds a Turbo Stream element with the action 'before' to the message.
|
|
307
|
-
* @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
308
|
-
* @param {String} content - The HTML content of the element.
|
|
309
|
-
*/
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* @name after
|
|
313
|
-
* @function
|
|
314
|
-
* @description Adds a Turbo Stream element with the action 'after' to the message.
|
|
315
|
-
* @param {String|Object<String, String>} targetOrAttributes - Either the target ID as string or all attributes as object.
|
|
316
|
-
* @param {String} content - The HTML content of the element.
|
|
317
|
-
*/
|
|
318
|
-
|
|
319
|
-
// Events:
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Event element.
|
|
323
|
-
* Gets fired when a Turbo Stream element has been added to a message.
|
|
324
|
-
*
|
|
325
|
-
* @event element
|
|
326
|
-
* @param {TurboStreamElement} - The added element.
|
|
327
|
-
*/
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Event render.
|
|
331
|
-
* Gets fired when a Turbo Stream message has been rendered.
|
|
332
|
-
*
|
|
333
|
-
* @event render
|
|
334
|
-
* @param {String} - The rendered HTML fragment.
|
|
335
|
-
*/
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Event config.
|
|
339
|
-
* Gets fired when the config has been updated.
|
|
340
|
-
*
|
|
341
|
-
* @event config
|
|
342
|
-
* @param {Object} - The updated config object.
|
|
343
|
-
*/
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Event clear.
|
|
347
|
-
* Gets fired when the buffer is cleared.
|
|
348
|
-
*
|
|
349
|
-
* @event clear
|
|
350
|
-
*/
|
|
@@ -6,6 +6,7 @@ import { TurboStream } from '#core';
|
|
|
6
6
|
* This class represents a Turbo Stream message with added functionality for WebSockets.
|
|
7
7
|
*
|
|
8
8
|
* @extends {TurboStream}
|
|
9
|
+
* @since 1.0.0
|
|
9
10
|
*/
|
|
10
11
|
export class WsTurboStream extends TurboStream {
|
|
11
12
|
|
|
@@ -13,6 +14,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
13
14
|
* The WebSocket instance to send to.
|
|
14
15
|
*
|
|
15
16
|
* @type {WebSocket}
|
|
17
|
+
* @since 1.0.0
|
|
16
18
|
*/
|
|
17
19
|
ws;
|
|
18
20
|
|
|
@@ -23,6 +25,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
23
25
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
|
|
24
26
|
* @type {Number}
|
|
25
27
|
* @static
|
|
28
|
+
* @since 1.0.0
|
|
26
29
|
*/
|
|
27
30
|
static OPEN = 1;
|
|
28
31
|
|
|
@@ -34,6 +37,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
34
37
|
* @static
|
|
35
38
|
* @param {WebSocket} ws - The WebSocket instance to send to.
|
|
36
39
|
* @returns {WsTurboStream} - A new WsTurboStream instance.
|
|
40
|
+
* @since 1.0.0
|
|
37
41
|
*/
|
|
38
42
|
static use(ws) {
|
|
39
43
|
return new this(ws, { buffer: false });
|
|
@@ -46,6 +50,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
46
50
|
* @param {WebSocket} ws - The WebSocket to send to.
|
|
47
51
|
* @param {Object} [config] - The config to override.
|
|
48
52
|
* @listens config
|
|
53
|
+
* @since 1.0.0
|
|
49
54
|
*/
|
|
50
55
|
constructor(ws, config) {
|
|
51
56
|
super();
|
|
@@ -70,6 +75,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
70
75
|
* @param {Object} config - The changed config object.
|
|
71
76
|
* @listens element
|
|
72
77
|
* @listens render
|
|
78
|
+
* @since 1.0.0
|
|
73
79
|
*/
|
|
74
80
|
handleConfig(config) {
|
|
75
81
|
if (config.buffer === true) {
|
|
@@ -94,6 +100,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
94
100
|
* Sends the rendered HTML fragment to the WebSocket.
|
|
95
101
|
*
|
|
96
102
|
* @param {String} html - The rendered HTML fragment.
|
|
103
|
+
* @since 1.0.0
|
|
97
104
|
*/
|
|
98
105
|
handleRender(html) {
|
|
99
106
|
this.ws.send(html);
|
|
@@ -104,6 +111,7 @@ export class WsTurboStream extends TurboStream {
|
|
|
104
111
|
* Sends the Turbo Stream element to the WebSocket.
|
|
105
112
|
*
|
|
106
113
|
* @param {TurboStreamElement} element - The Turbo Stream element to send.
|
|
114
|
+
* @since 1.0.0
|
|
107
115
|
*/
|
|
108
116
|
handleElement(element) {
|
|
109
117
|
this.ws.send(element.render());
|
package/package.json
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-turbo",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A library for Node.js to assist with the server side of 37signals' Hotwire Turbo framework. It provides classes and functions for Web servers and also convenience functions for the frameworks Koa and Express as well as for WebSocket and SSE.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node",
|
|
7
|
+
"turbo",
|
|
8
|
+
"node-turbo",
|
|
9
|
+
"nodejs",
|
|
10
|
+
"hotwire",
|
|
11
|
+
"hotwired",
|
|
12
|
+
"server",
|
|
13
|
+
"http",
|
|
14
|
+
"http2",
|
|
15
|
+
"koa",
|
|
16
|
+
"express",
|
|
17
|
+
"sse",
|
|
18
|
+
"websocket",
|
|
19
|
+
"ws"
|
|
20
|
+
],
|
|
6
21
|
"homepage": "https://github.com/VividVisions/node-turbo",
|
|
7
22
|
"bugs": "https://github.com/VividVisions/node-turbo/issues",
|
|
8
23
|
"repository": {
|
|
@@ -39,37 +54,37 @@
|
|
|
39
54
|
"test:package": "NODE_ENV=testing mocha --ignore \"./test/unit/**\" --ignore \"./test/integration/**\"",
|
|
40
55
|
"test:all": "NODE_ENV=testing mocha",
|
|
41
56
|
"test:coverage": "c8 -c './.c8rc.json' npm test",
|
|
42
|
-
"pretest:coverage": "rm -rd ./coverage",
|
|
43
57
|
"docs": "esdoc -c ./.esdoc.json",
|
|
44
58
|
"postdocs": "cp -f ./docs/internal/API.md ./docs"
|
|
45
59
|
},
|
|
46
60
|
"dependencies": {
|
|
61
|
+
"debug": "^4.3.4",
|
|
47
62
|
"is-plain-object": "^5.0.0",
|
|
48
|
-
"negotiator": "^0.6.3"
|
|
49
|
-
"debug": "^4.3.4"
|
|
63
|
+
"negotiator": "^0.6.3"
|
|
50
64
|
},
|
|
51
65
|
"devDependencies": {
|
|
52
66
|
"@enterthenamehere/esdoc": "^2.6.0-dev.1",
|
|
53
67
|
"@enterthenamehere/esdoc-external-nodejs-plugin": "^2.6.0-dev.1",
|
|
54
68
|
"@enterthenamehere/esdoc-importpath-plugin": "^2.6.0-dev.1",
|
|
55
69
|
"@enterthenamehere/esdoc-standard-plugin": "^2.6.0-dev.2",
|
|
70
|
+
"@hotwired/turbo": "^8.0.4",
|
|
71
|
+
"@playwright/test": "^1.42.1",
|
|
56
72
|
"@vividvisions/esdoc-api-doc-markdown-plugin": "file:../esdoc-api-doc-markdown-plugin",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"chai": "^4.3.10",
|
|
73
|
+
"c8": "^9.1.0",
|
|
74
|
+
"chai": "^4.1.1",
|
|
60
75
|
"chai-eventemitter2": "^0.2.1",
|
|
61
76
|
"chai-spies": "^1.1.0",
|
|
62
77
|
"colorette": "^2.0.20",
|
|
63
78
|
"eventsource": "^2.0.2",
|
|
64
|
-
"express": "^4.18.
|
|
79
|
+
"express": "^4.18.3",
|
|
65
80
|
"git-repo-info": "^2.1.1",
|
|
66
|
-
"koa": "^2.
|
|
67
|
-
"koa-route": "^
|
|
81
|
+
"koa": "^2.15.0",
|
|
82
|
+
"koa-route": "^4.0.1",
|
|
68
83
|
"koa-static": "^5.0.0",
|
|
69
|
-
"mocha": "^10.
|
|
70
|
-
"node-mocks-http": "^1.14.
|
|
84
|
+
"mocha": "^10.3.0",
|
|
85
|
+
"node-mocks-http": "^1.14.1",
|
|
71
86
|
"nunjucks": "^3.2.4",
|
|
72
|
-
"supertest": "^6.3.
|
|
73
|
-
"ws": "^8.
|
|
87
|
+
"supertest": "^6.3.4",
|
|
88
|
+
"ws": "^8.16.0"
|
|
74
89
|
}
|
|
75
90
|
}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { expect } from 'chai';
|
|
3
3
|
import { TurboElement } from '#core';
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
class NonThrowingTurboElement extends TurboElement {
|
|
7
|
+
validate() {}
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
describe('TurboElement', function() {
|
|
6
11
|
|
|
7
12
|
it('Constructor correctly assigns properties (and throws Error)', function() {
|
|
@@ -12,4 +17,28 @@ describe('TurboElement', function() {
|
|
|
12
17
|
}).to.throw();
|
|
13
18
|
});
|
|
14
19
|
|
|
20
|
+
|
|
21
|
+
describe('renderAttributesAsHtml()', function() {
|
|
22
|
+
|
|
23
|
+
it('correctly renders attributes', function() {
|
|
24
|
+
const te = new NonThrowingTurboElement({
|
|
25
|
+
foo: 'bar',
|
|
26
|
+
two: 2
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(te.renderAttributesAsHtml()).to.equal('foo="bar" two="2"');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
it('correctly renders booelan attributes', function() {
|
|
34
|
+
const te = new NonThrowingTurboElement({
|
|
35
|
+
foo: 'bar',
|
|
36
|
+
bool: null
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(te.renderAttributesAsHtml()).to.equal('foo="bar" bool');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
});
|
|
43
|
+
|
|
15
44
|
});
|
|
@@ -73,4 +73,11 @@ describe('TurboStreamElement', function() {
|
|
|
73
73
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
|
|
77
|
+
it(`No error is thrown when action is "refresh" with missing attribute "target" or "targets"`, function() {
|
|
78
|
+
expect(function() {
|
|
79
|
+
const tse = new TurboStreamElement({ action: 'refresh' });
|
|
80
|
+
}).to.not.throw();
|
|
81
|
+
});
|
|
82
|
+
|
|
76
83
|
});
|
|
@@ -252,6 +252,15 @@ describe('TurboStream', function() {
|
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
TurboStream.ACTIONS.forEach(action => {
|
|
255
|
+
if (action === 'refresh') {
|
|
256
|
+
it ('refresh() correctly omits <template> and content', function() {
|
|
257
|
+
const ts = new TurboStream({ action: 'refresh' });
|
|
258
|
+
|
|
259
|
+
expect(ts.render()).to.equal('<turbo-stream action="refresh"></turbo-stream>');
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
255
264
|
it (`${action}() adds TurboElement with action \'${action}\'`, function() {
|
|
256
265
|
const ts = new TurboStream();
|
|
257
266
|
ts[action].call(ts, { target: 't' }, 'c');
|
|
@@ -278,6 +287,28 @@ describe('TurboStream', function() {
|
|
|
278
287
|
});
|
|
279
288
|
});
|
|
280
289
|
|
|
290
|
+
|
|
291
|
+
it ('refresh() adds TurboElement with action \'refresh\'', function() {
|
|
292
|
+
const ts = new TurboStream();
|
|
293
|
+
ts.refresh();
|
|
294
|
+
|
|
295
|
+
expect(ts.elements[0] instanceof TurboElement).to.be.true;
|
|
296
|
+
expect(ts.elements[0].attributes.action).to.equal('refresh');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it ('refresh(<string>) adds attribute \'request-id\'', function() {
|
|
300
|
+
const ts = new TurboStream();
|
|
301
|
+
ts.refresh('id');
|
|
302
|
+
expect(ts.elements[0].attributes).to.have.property('request-id');
|
|
303
|
+
expect(ts.elements[0].attributes['request-id']).to.equal('id');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it ('refresh(<object>) adds attributes', function() {
|
|
307
|
+
const ts = new TurboStream();
|
|
308
|
+
ts.refresh({ foo: 'bar' });
|
|
309
|
+
expect(ts.elements[0].attributes).to.have.property('foo');
|
|
310
|
+
expect(ts.elements[0].attributes.foo).to.equal('bar');
|
|
311
|
+
});
|
|
281
312
|
});
|
|
282
313
|
|
|
283
314
|
|