Skip to content

Commit

Permalink
feat: functions as request handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestmarcinko committed Oct 17, 2023
1 parent a6d5633 commit 1bd7ef3
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 38 deletions.
2 changes: 1 addition & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added img/auto-increment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 44 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,51 @@ <h1>Buttons</h1>
</button>
</label>
</p>
<script defer>
<p>
<!-- FORM POST -->
<form hx-post="/submitmail" hx-ext="serverless">
<label> Email
<input type="text" name="email_address">
</label>
<label> Button 2
<button type="submit">Go!</button>
</label>
</form>

<!-- FORM GET -->
<form hx-get="/submitmail" hx-ext="serverless">
<label> Email
<input type="text" name="email_address">
</label>
<label> Button 2
<button type="submit">Go!</button>
</label>
</form>
</p>

<span class="counter"></span>
<button hx-get="/example" hx-target="previous .counter" hx-trigger="load, click" hx-vals='js:{myVal: i++}' hx-ext="serverless">Click to Increment</button>


<script>
htmxServerless.handlers.set('/clicked', '<button hx-post="/clicked2" hx-swap="outerHTML" hx-ext="serverless">Click again!</button>')
htmxServerless.handlers.set('/clicked2', '<button hx-post="/clicked" hx-swap="outerHTML" hx-ext="serverless">Click!</button>')
htmxServerless.handlers.set('/clicked2', '<button hx-post="/clicked" hx-swap="outerHTML" hx-ext="serverless">Click!</button>');

// Form
htmxServerless.handlers.set('/submitmail', function(text, params, xhr){
if ( params?.email_address != '[email protected]' ) {
return this.outerHTML;
} else {
return "Thanks!"
}
});

// DIV
let i = 0;
htmxServerless.handlers.set('/example', function(text, params, xhr){
let status = params?.myVal < 10 ? "smaller" : "bigger";
return `Value of "i" is: ${i}, it is ${status} than 10. Click to increment!`;
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "htmx-serverless",
"version": "0.1.5",
"version": "0.1.6",
"description": "HTMX serverless XHR requests",
"main": "dist/index.js",
"scripts": {
Expand Down
91 changes: 78 additions & 13 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
# HTMX Serverless requests ![npm](https://img.shields.io/npm/v/htmx-serverless) ![npm](https://img.shields.io/npm/dy/htmx-serverless) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
# HTMX Serverless Client States ![npm](https://img.shields.io/npm/v/htmx-serverless) ![npm](https://img.shields.io/npm/dy/htmx-serverless) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)

To use HTMX you require a back-end server to handle the XHR requests and responses. In some cases it is nice to have only a client side interaction, **without a back-end server**.
To use HTMX you require a back-end server to handle the XHR requests and responses. In some cases it is nice to have only a client side interaction to handle client states, **without network requests**.

This extension uses the HTMX built-in Events to intercept some XHR requests before they fire and define response texts on the client side. No need for mock or "fake" server scripts. It is **HTMX without a server** (sort of).

It is really simple:
- The XHR request will not be sent, the ```.send()``` method is overridden for the intercepted request
- The XHR ```loadstart```, ```load``` and ```loadend``` events are dispatched instead, as if the request was finished "successfully"
- Only requests added to the ```htmxServerless.handlers``` Map are intercepted
- Requests are intercepted based on the request path, request arguments does not matter

## Usage

### In HTML head
Expand All @@ -24,8 +18,14 @@ It is really simple:
Then use the `window.htmxServerless` global to set custom handlers and responses.

```javascript
// Requests to "/handler" are replaced with "<div>Custom HTML</div>"
htmxServerless.handlers.set('/handler', '<div>Custom HTML</div>');
// Requests to "/handler1" are replaced with "<div>Custom HTML</div>"
htmxServerless.handlers.set('/handler1', '<div>Custom HTML</div>');

// Requests to "/handler2" are managed via a function
htmxServerless.handlers.set('/handler2', function(text, params, xhr){
console.log(this, text, params, xhr);
return "<p>Okay!</p>";
});
```

### In custom bundles
Expand All @@ -36,13 +36,12 @@ import htmxServerless from "htmx-serverless";

// Initialize on your local htmx
htmxServerless.init(htmx);

// Requests to "/handler" are replaced with "<div>Custom HTML</div>"
htmxServerless.handlers.set('/handler', '<div>Custom HTML</div>');
```

## Examples

### Handler as a string

Assume we have a button with the `serverless` **hx-ext** sattribute, which triggers a request to the path "/clicked":

```html
Expand All @@ -63,6 +62,72 @@ htmxServerless.handlers.set('/clicked',

The button is then replaced with the HTML defined without triggering a request to the server. It's that simple.

[Try this example here.](https://jsfiddle.net/ernestmarcinko/h0rj5pez/1/)

### Handler as a Function

Tha handler function is a great tool for more complex conditional logic, like it would happen on the server side.
Let's make a simple click based number increment handler:

```html
<span class="counter"></span>
<button hx-get="/example"
hx-target="previous .counter"
hx-trigger="load, click"
hx-vals='js:{myVal: i++}'
hx-ext="serverless">Click to Increment</button>

```

The handler only needs to print the text as "i" is incremented by hx-vals automatically:

```javascript
let i = 0;
htmxServerless.handlers.set('/example', function(text, params, xhr){
let status = params?.myVal < 10 ? "smaller" : "bigger";
return `Value of "i" is: ${i}, it is ${status} than 10`;
});
```
[Try this example here.](https://jsfiddle.net/ernestmarcinko/vzpawq0y/1/)
Output:
![Alt text](img/auto-increment.png)
## Handler function
The handler function accepts 3 parameters (4 including "this") and returns a string:
* **this** => The target element
* **ext** => The replacement text (empty)
* **params** => The GET/POST or xhr-vals arguments
* **xhr** => The current request
```javascript
/**
* The handler function
*
* @param this:Element The target element
* @param text:string The replacement text (empty)
* @param params:Object The GET/POST or xhr-vals arguments
* @param xhr:XMLHttpRequest The current request
*
* @returns string
*/
function handler(text, params, xhr){
console.log(this, text, params, xhr);
return 'Hi!';
}
htmxServerless.handlers.set('/example', handler);
```
## How does it work?
It is really simple:
- The XHR request will not be sent, the ```.send()``` method is overridden for the intercepted request
- The XHR ```loadstart```, ```load``` and ```loadend``` events are dispatched instead, as if the request was finished "successfully"
- Only requests added to the ```htmxServerless.handlers``` Map are intercepted
- Requests are intercepted based on the request path, request arguments does not matter
## What else?
Nothing actually. This is only a baseline solution, but it works. There are no fancy features, as htmx is oath to be a small but effective library. With some creativity, you could make this more convenient, I leave it up to you :)
10 changes: 3 additions & 7 deletions src/Serverless.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import htmx from "htmx.org";
type path = string;
type HTML = string;
import { path, ServerlessHandler, XHRServerless, HtmxElement } from "./types";
export default class Serverless {
handlers: Map<path, HTML>;
handlers: Map<path, ServerlessHandler>;
constructor();
init(h?: typeof htmx): void;
onEvent(name: string, evt: any): void;
transformResponse(text: string, xhr: XMLHttpRequest, elt: Element & {
'htmx-internal-data'?: any;
}): string | undefined;
transformResponse(text: string, xhr: XHRServerless, elt: HtmxElement): string;
shouldIntercept(path: string | undefined): boolean;
}
export {};
38 changes: 24 additions & 14 deletions src/Serverless.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import htmx from "htmx.org";
import { HtmxExtension } from "./types";

type path = string;
type HTML = string;
import htmx from "htmx.org";
import { path, params, HtmxExtension, ServerlessHandler, XHRServerless, HtmxElement } from "./types";

export default class Serverless {
handlers: Map<path, HTML>;
handlers: Map<path, ServerlessHandler>;

constructor() {
this.handlers = new Map();
Expand All @@ -20,33 +17,46 @@ export default class Serverless {
}

onEvent(name: string, evt: any) {
const path:path = evt?.detail?.elt?.['htmx-internal-data']?.path;
const params:params = evt.detail?.requestConfig?.parameters;

if (
typeof evt.detail.xhr !== 'undefined' &&
this.shouldIntercept(evt?.detail?.elt?.['htmx-internal-data']?.path)
this.shouldIntercept(path)
) {
if ( name === "htmx:beforeSend" ) {
const xhr = evt.detail.xhr;
const xhr:XHRServerless = evt.detail.xhr;
xhr.serverless = {
'params': params,
'path': path
};
xhr.send = () => {
xhr.dispatchEvent(new Event('loadstart'));
xhr.dispatchEvent(new Event('load'));
xhr.dispatchEvent(new Event('loadend'));
xhr.readyState == XMLHttpRequest.DONE
};
} else if ( name === "htmx:beforeSwap" ) {
evt.detail.shouldSwap = true;
}
}
}

transformResponse(text: string, xhr: XMLHttpRequest, elt:Element & {'htmx-internal-data'?: any}) {
if ( this.shouldIntercept(elt?.['htmx-internal-data']?.path) ) {
return this.handlers.get(elt?.['htmx-internal-data']?.path)
transformResponse(text: string, xhr: XHRServerless, elt:HtmxElement) {
const path:path = xhr?.serverless?.path ?? '';
const params:params = xhr.serverless?.params;

if ( this.shouldIntercept(path) ) {
const handler = this.handlers.get(path) as ServerlessHandler;
if ( typeof handler === 'function' ) {
return handler.call(elt, text, params, xhr);
} else {
return handler;
}
}
return text;
}

shouldIntercept(path:string|undefined) {
path = path ?? '';
return this.handlers.has(path);
return this.handlers.has( path ?? '');
}
}
20 changes: 20 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ export interface HtmxExtension {
encodeParameters?: (xhr: XMLHttpRequest, parameters: any, elt: any) => any;
}

export type path = string;
export type params = {
[key: string]: any
};

export type HtmxElement = Element & {'htmx-internal-data'?: any}

export type XHRParams = {
params: params,
path: path
}
export type XHRServerless = XMLHttpRequest & {
serverless: XHRParams;
}

type ServerlessHandlerFunc =
(this: HtmxElement, text?: string, params?: any , xhr?: XMLHttpRequest) => string

export type ServerlessHandler = ServerlessHandlerFunc|string;

declare module "htmx.org" {
function defineExtension(name: string, ext: HtmxExtension): void;
type HtmxExtensions = {
Expand Down

0 comments on commit 1bd7ef3

Please sign in to comment.