Skip to content

Commit ebafe05

Browse files
committed
Enhance ServerSentEventGenerator with new methods and detailed documentation
- Added `executeScript` method to run JavaScript in the client browser with options for auto-removal and custom attributes. - Introduced `removeElements` and `removeSignals` methods for convenient removal of DOM elements and signals, respectively. - Updated documentation for `patchElements` and `patchSignals` methods with usage examples and detailed parameter descriptions. - Refactored event handling in tests to utilize new methods, improving clarity and consistency across implementations. - Updated README.md documentation to reflect all changes above.
1 parent 752fd16 commit ebafe05

File tree

6 files changed

+342
-346
lines changed

6 files changed

+342
-346
lines changed

README.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
A TypeScript SDK for building reactive web applications with [Datastar](https://github.com/starfederation/datastar).
99

10-
Implements the [SDK spec](../README.md) and exposes an abstract ServerSentEventGenerator class that can be used to implement runtime specific classes. NodeJS and web standard runtimes are currently implemented.
10+
Implements the [SDK spec](https://github.com/starfederation/datastar/blob/develop/sdk/README.md) and exposes an abstract ServerSentEventGenerator class that can be used to implement runtime specific classes. NodeJS and web standard runtimes are currently implemented.
1111

1212
Currently it only exposes an http1 server, if you want http2 I recommend you use a reverse proxy until http2 support is added.
1313

@@ -169,6 +169,59 @@ Patches HTML elements into the client DOM.
169169
stream.patchElements('<div id="myDiv">Updated content</div>');
170170
```
171171

172+
##### `removeElements(selector?, elements?, options?)`
173+
Removes elements from the client DOM by selector or by HTML string with IDs.
174+
175+
**Parameters:**
176+
- `selector`: CSS selector for elements to remove (optional; mutually exclusive with elements)
177+
- `elements`: HTML string of elements with IDs to remove (optional; required if selector is not provided)
178+
- `options`: Optional configuration object with `eventId`, `retryDuration`
179+
180+
**Examples:**
181+
```javascript
182+
// Remove by selector
183+
stream.removeElements('#feed, #otherid');
184+
// Remove by HTML elements with IDs
185+
stream.removeElements(undefined, '<div id="first"></div><div id="second"></div>');
186+
```
187+
188+
##### `removeSignals(signalKeys, options?)`
189+
Removes one or more signals from the client signal store.
190+
191+
**Parameters:**
192+
- `signalKeys`: The signal key or array of keys to remove
193+
- `options`: Optional configuration object with `onlyIfMissing`, `eventId`, `retryDuration`
194+
195+
**Examples:**
196+
```javascript
197+
// Remove a single signal
198+
stream.removeSignals('foo');
199+
// Remove multiple signals
200+
stream.removeSignals(['foo', 'bar']);
201+
```
202+
203+
##### `executeScript(script, options?)`
204+
Executes a script on the client by sending a <script> tag via SSE.
205+
206+
**Parameters:**
207+
- `script`: The JavaScript code to execute
208+
- `options`: Optional configuration object:
209+
- `autoRemove`: If true (default), adds data-effect="el.remove()" to the script tag
210+
- `attributes`: Object of script tag attributes (preferred)
211+
- `eventId`, `retryDuration`
212+
213+
**Examples:**
214+
```javascript
215+
// Execute a simple script
216+
stream.executeScript('console.log("Hello from server!")');
217+
218+
// Execute a script and keep it in the DOM
219+
stream.executeScript('alert("Persistent!")', { autoRemove: false });
220+
221+
// Execute with custom attributes (object form preferred)
222+
stream.executeScript('doSomething()', { attributes: { type: "module", async: "true" } });
223+
```
224+
172225
## Development
173226

174227
### Prerequisites
@@ -257,4 +310,7 @@ You'll need to implement:
257310

258311
The abstract class provides these public methods:
259312
- `patchElements(elements, options?)`: Patch HTML elements
260-
- `patchSignals(signals, options?)`: Patch signal data
313+
- `patchSignals(signals, options?)`: Patch signal data
314+
- `removeElements(selector?, elements?, options?)`: Remove elements by selector or HTML string
315+
- `removeSignals(signalKeys, options?)`: Remove one or more signals
316+
- `executeScript(script, options?)`: Execute a script on the client

deno.lock

Lines changed: 16 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/abstractServerSentEventGenerator.ts

Lines changed: 180 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,26 @@ export abstract class ServerSentEventGenerator {
115115
/**
116116
* Patches HTML elements into the DOM.
117117
*
118-
* @param elements - HTML elements that will be patched.
119-
* @param [options] - Additional options for patching.
120-
* @throws {Error} If validation fails
118+
* Use this to insert, update, or remove elements in the client DOM. Supports various patch modes and options.
119+
*
120+
* Examples:
121+
* ```
122+
* // Insert new element inside #container
123+
* patchElements('<div id="new">Hello</div>', { selector: '#container', mode: 'append' });
124+
*
125+
* // Replace element by ID
126+
* patchElements('<div id="replaceMe">Replaced</div>');
127+
*
128+
* // Remove by selector, note that you can also use removeElements
129+
* patchElements('', { selector: '#toRemove', mode: 'remove' });
130+
*
131+
* // Remove by elements with IDs, note that you can also use removeElements
132+
* patchElements('<div id="first"></div><div id="second"></div>', { mode: 'remove' });
133+
* ```
134+
*
135+
* @param elements - HTML string of elements to patch (must have IDs unless using selector).
136+
* @param options - Patch options: selector, mode, useViewTransition, eventId, retryDuration.
137+
* @returns The SSE lines to send.
121138
*/
122139
public patchElements(
123140
elements: string,
@@ -166,9 +183,20 @@ export abstract class ServerSentEventGenerator {
166183
/**
167184
* Patches signals into the signal store.
168185
*
186+
* Use this to update client-side signals using RFC 7386 JSON Merge Patch semantics.
187+
*
188+
* Examples:
189+
* ```
190+
* // Patch a single signal
191+
* patchSignals('{"show": true}');
192+
*
193+
* // Patch multiple signals with onlyIfMissing option
194+
* patchSignals('{"output": "Test", "user": {"name": "Alice"}}', { onlyIfMissing: true });
195+
* ```
196+
*
169197
* @param signals - JSON string containing signal data to patch.
170-
* @param [options] - Additional options for patching.
171-
* @throws {Error} If validation fails
198+
* @param options - Patch options: onlyIfMissing, eventId, retryDuration.
199+
* @returns The SSE lines to send.
172200
*/
173201
public patchSignals(
174202
signals: string,
@@ -189,4 +217,151 @@ export abstract class ServerSentEventGenerator {
189217
retryDuration,
190218
});
191219
}
220+
221+
/**
222+
* Executes a script on the client by sending a <script> tag via SSE.
223+
*
224+
* Use this to run JavaScript in the client browser. By default, the script tag will auto-remove after execution.
225+
*
226+
* Examples:
227+
* ```
228+
* // Execute a simple script
229+
* executeScript('console.log("Hello from server!")');
230+
*
231+
* // Execute a script and keep it in the DOM
232+
* executeScript('alert("Persistent!")', { autoRemove: false });
233+
*
234+
* // Execute with custom attributes (object form preferred)
235+
* executeScript('doSomething()', { attributes: { type: "module", async: "true" } });
236+
*
237+
* // (Advanced) Execute with custom attributes as array of strings
238+
* executeScript('doSomething()', { attributes: ['type="module"', 'async'] });
239+
* ```
240+
*
241+
* @param script - The JavaScript code to execute.
242+
* @param options - Options: autoRemove, attributes (object preferred), eventId, retryDuration.
243+
* @returns The SSE lines to send.
244+
*/
245+
public executeScript(
246+
script: string,
247+
options?: {
248+
autoRemove?: boolean;
249+
attributes?: string[] | Record<string, string>;
250+
eventId?: string;
251+
retryDuration?: number;
252+
}
253+
): ReturnType<typeof this.send> {
254+
const {
255+
autoRemove = true,
256+
attributes = {},
257+
eventId,
258+
retryDuration,
259+
} = options || {};
260+
261+
let attrString = "";
262+
263+
// Handle attributes as object (preferred by test)
264+
if (attributes && typeof attributes === "object" && !Array.isArray(attributes)) {
265+
attrString = Object.entries(attributes)
266+
.map(([k, v]) => ` ${k}="${v}"`)
267+
.join("");
268+
} else if (Array.isArray(attributes)) {
269+
attrString = attributes.length > 0 ? " " + attributes.join(" ") : "";
270+
}
271+
272+
// Only add data-effect if autoRemove is true
273+
if (autoRemove) {
274+
attrString += ' data-effect="el.remove()"';
275+
}
276+
277+
const scriptTag = `<script${attrString}>${script}</script>`;
278+
279+
const dataLines = [
280+
...this.eachNewlineIsADataLine("mode", "append"),
281+
...this.eachNewlineIsADataLine("selector", "body"),
282+
...this.eachNewlineIsADataLine("elements", scriptTag),
283+
];
284+
285+
return this.send("datastar-patch-elements", dataLines, {
286+
eventId,
287+
retryDuration,
288+
});
289+
}
290+
291+
/**
292+
* Convenience method to remove elements from the DOM.
293+
*
294+
* Provide either a CSS selector (to remove all matching elements) OR an HTML string of elements with IDs (to remove specific elements by ID).
295+
*
296+
* - If `selector` is provided, it will be used to target elements for removal (elements param is ignored).
297+
* - If `selector` is not provided, `elements` must be a non-empty HTML string where each top-level element has an ID.
298+
*
299+
* Examples:
300+
* ```
301+
* // Remove by selector
302+
* removeElements('#feed, #otherid');
303+
* // Remove by HTML elements with IDs
304+
* removeElements(undefined, '<div id="first"></div><div id="second"></div>');
305+
* ```
306+
* @param selector - CSS selector for elements to remove (optional; mutually exclusive with elements).
307+
* @param elements - HTML string of elements with IDs to remove (optional; required if selector is not provided).
308+
* @param options - Additional options: eventId, retryDuration.
309+
* @returns The SSE lines to send.
310+
*/
311+
public removeElements(
312+
selector?: string,
313+
elements?: string,
314+
options?: {
315+
eventId?: string;
316+
retryDuration?: number;
317+
}
318+
): ReturnType<typeof this.send> {
319+
// If selector is not provided, elements must be present and non-empty
320+
if (!selector && (!elements || elements.trim() === '')) {
321+
throw new Error('Either selector or elements (with IDs) must be provided to remove elements.');
322+
}
323+
return this.patchElements(elements ?? '', {
324+
selector,
325+
mode: 'remove',
326+
eventId: options?.eventId,
327+
retryDuration: options?.retryDuration,
328+
});
329+
}
330+
331+
/**
332+
* Convenience method to remove one or more signals from the client signal store.
333+
*
334+
* This sends a JSON Merge Patch where each specified key is set to null, per RFC 7386 and the Datastar spec.
335+
*
336+
* Examples:
337+
* ```
338+
* // Remove a single signal
339+
* removeSignals('foo');
340+
*
341+
* // Remove multiple signals
342+
* removeSignals(['foo', 'bar']);
343+
*
344+
* // Remove with options
345+
* removeSignals('foo', { eventId: '123' });
346+
* ```
347+
*
348+
* @param signalKeys - The signal key or array of keys to remove.
349+
* @param options - Patch options: onlyIfMissing, eventId, retryDuration.
350+
* @returns The SSE lines to send.
351+
*/
352+
public removeSignals(
353+
signalKeys: string | string[],
354+
options?: {
355+
onlyIfMissing?: boolean;
356+
eventId?: string;
357+
retryDuration?: number;
358+
}
359+
): ReturnType<typeof this.send> {
360+
const keys = Array.isArray(signalKeys) ? signalKeys : [signalKeys];
361+
const patch: Record<string, null> = {};
362+
for (const key of keys) {
363+
patch[key] = null;
364+
}
365+
return this.patchSignals(JSON.stringify(patch), options);
366+
}
192367
}

0 commit comments

Comments
 (0)