Skip to content

Commit

Permalink
simplified htmx form sample and added htmx docs
Browse files Browse the repository at this point in the history
  • Loading branch information
fzumstein committed Oct 29, 2024
1 parent f6046d2 commit dfa1886
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 10 deletions.
7 changes: 7 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@ module.exports = {
bracketSameLine: true,
},
},
{
files: ["*.md"],
options: {
// Didn't find a way to make printWidth apply to code blocks inside md files
embeddedLanguageFormatting: "off",
},
},
],
};
4 changes: 4 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@ to copy over the files to the static folder.
## CSP header

To use the most restrictive CSP header, set `XLWINGS_ENABLE_EXCEL_ONLINE=false` for local development.

## Prettier

The VS Code extension prettier requires to set the configuration to `.prettierrc.js` explicitly.
2 changes: 1 addition & 1 deletion app/templates/examples/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- The task pane landing page has to be publicly accessible.
- Everything else can be locked down using the `dep.User` dependency.
- Since it is htmx that provides the `Authorization` header with every request, you'll end up with an authentication error if you use `hx-push-url="true"` or `hx-boost="true"` when right-clicking on the task pane and selecting `Reload`. This happens also with hot-reloading during development. The reason is that a full page reload isn't handled by htmx, which means that the Authorization header is missing. Therefore, the example doesn't push the url: you won't see any authentication errors when reloading, but in return a reload always brings you back to the landing page.
- Since it is htmx that provides the `Authorization` header with every request, you'll end up with an authentication error if you use `hx-push-url="true"` or `hx-boost="true"` when right-clicking on the task pane and selecting `Reload`. This happens also with hot-reloading during development. The reason is that a full page reload isn't handled by htmx, which means that the Authorization header is missing. Therefore, the example doesn't push the url: you won't see any authentication errors when reloading, but in return, a reload always brings you back to the landing page.

To try it out, replace `app/routers/taskpane.py` with the following code:

Expand Down
11 changes: 4 additions & 7 deletions app/templates/examples/htmx_form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,10 @@ async def taskpane(request: Request):
)


@router.post("/form-example")
async def form_example(request: Request, name: str = Form(None)):
if name == "":
error = "Please provide a name!"
else:
error = None
greeting = custom_functions.hello(name)
@router.post("/hello")
async def hello(request: Request, fullname: str = Form()):
error = "Please provide a name!" if not fullname else None
greeting = custom_functions.hello(fullname)
return TemplateResponse(
request=request,
name="/examples/htmx_form/_greeting.html",
Expand Down
4 changes: 2 additions & 2 deletions app/templates/examples/htmx_form/taskpane_htmx_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

{% block content %}
<div class="container-fluid pt-3 ps-3">
<form hx-post="/form-example" hx-target="#result">
<form hx-post="/hello" hx-target="#result">
<div class="mb-3">
<label for="inputName" class="form-label">Name</label>
<input class="form-control" name="name" id="inputName" autocorrect="off" />
<input class="form-control" name="fullname" id="inputName" autocorrect="off" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Expand Down
86 changes: 86 additions & 0 deletions docs/htmx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# htmx

htmx is a lightweight JavaScript library that allows you to replace parts of your HTML page with snippets from server-rendered HTML templates. This allows you to create dynamic web apps without full page reloads and without the complexity that comes with heavy frameworks such as React.

Most importantly, you keep your application logic and state on the backend where we can use Python instead of JavaScript.

xlwings Server integrates with htmx to facilitate authentication and provide access to the `xlwings.Book` object, enabling interaction with Excel directly from the task pane.

## First steps

htmx works by adding attributes (`hx-...`) to your HTML tags. Let's have a look at a simple [form example](https://github.com/xlwings/xlwings-server/tree/main/app/templates/examples/htmx_form). The file `taskpane_htmx_form.html` contains this form:

```html
<form hx-post="/hello" hx-target="#result">
<div class="mb-3">
<label for="inputName" class="form-label">Name</label>
<input id="inputName" class="form-control" name="fullname" autocorrect="off" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div id="result" class="mt-4"></div>
```

- `hx-post`: This will trigger a POST request to the indicated endpoint when the button is clicked. It will send the inputs of the form to the backend. Note that `input` elements require a `name` attribute under which the value will be accessible on the backend.
- `hx-target`: this specifies where the response from the server (an HTML snippet), will be swapped. `hx-target="#result"` means that it will be swapped inside the `div` with the `id="result"`. By default, htmx swaps the inner part of the target element. If you wanted to change this to also replace the outer `<div id="result" class="mt-4"></div>`, you would have to add the attribute `hx-swap="outerHTML"`.

On the backend, FastAPI allows you to handle the POST request like this in `taskpane.py`:

```python
@router.post("/hello")
async def hello(request: Request, fullname: str = Form()):
error = "Please provide a name!" if not fullname else None
greeting = custom_functions.hello(fullname)
return TemplateResponse(
request=request,
name="/examples/htmx_form/_greeting.html",
context={"greeting": greeting, "error": error},
)
```

`fullname: str = Form())` assigns the value of the form input element with the attribute `name=fullname` to the Python variable `fullname`. Finally, here is how the template `_greeting.html` looks like (by convention, the leading underscore indicates a partial HTML template):

```html
<h1>Result</h1>
{% if error %}
<div class="alert alert-danger" role="alert">{{ error }}</div>
{% else %}
<p>{{ greeting }}</p>
{% endif %}
```

The `TemplateResponse` renders this template and returns it to the frontend, where htmx takes care of putting it inside the `<div id="result" class="mt-4"></div>`, which, if you write `World` into the name field, will end up looking like this:

```html
<div id="result" class="mt-4">
<h1>Result</h1>
<p>Hello World!</p>
</div>
```

## Interacting with Excel

To see how you can interact with the Excel object model from an htmx task pane, have a look at the example [`app/templates/examples/excel_object_model`](https://github.com/xlwings/xlwings-server/tree/main/app/templates/examples/excel_object_model). In summary, here's what you need to do:

- On the same element where you put the `hx-post` attribute, add `xw-book="true"`. This will provide the backend with the content of the workbook. If you need to include or exclude certain sheets, additionally provide `xw-config='{"exclude": "MySheet"}'` as an attribute with the desired [config](officejs_run_scripts.md#config).
- Include the `Book` dependency in your endpoint: `book: dep.Book` (note the import: `from .. import dependencies as dep`).
- Include the `"book"` key in the `context` of your `TemplateResponse`, e.g., `context={"book": book}`. If you call your book object differently, let's say `wb`, it would look like this: `context={"book": wb}`.
- In your template, in the part that is being swapped into the HTML, add the following line: `{% include "_book.html" %}`.
- Note that the `block_names` parameter of the `TemplateResponse` conveniently allows you to select a specific Jinja block that you want to return.

## Security

- Always return your HTML response via `TemplateResponse` to make sure that user inputs are properly escaped.
- Other security-related htmx configs are set under [`app/static/js/core/htmx-handlers.js`](https://github.com/xlwings/xlwings-server/blob/main/app/static/js/core/htmx-handlers.js).
- Read about htmx security in the official docs: https://htmx.org/docs/#security

## Examples

- Simple form: [`app/templates/examples/htmx_form`](https://github.com/xlwings/xlwings-server/tree/main/app/templates/examples/htmx_form)
- Form with Excel interaction: [`app/templates/examples/excel_object_model`](https://github.com/xlwings/xlwings-server/tree/main/app/templates/examples/excel_object_model)
- Authentication: [`app/templates/examples/auth`](https://github.com/xlwings/xlwings-server/tree/main/app/templates/examples/auth)

## Further Reading

- Docs: [htmx.org](https://htmx.org/)
- Book: [Hypermedia Systems](https://hypermedia.systems/)
1 change: 1 addition & 0 deletions docs/index_taskpane.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
:maxdepth: 1
taskpane_intro
htmx
```

0 comments on commit dfa1886

Please sign in to comment.