diff --git a/AI/fasthtml.md b/AI/fasthtml.md new file mode 100644 index 0000000..cfedeac --- /dev/null +++ b/AI/fasthtml.md @@ -0,0 +1,2619 @@ + +Things to remember when writing FastHTML apps: + +- Although parts of its API are inspired by FastAPI, it is *not* compatible with FastAPI syntax and is not targeted at creating API services +- FastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. Support for the Surreal and css-scope-inline libraries are also included, but both are optional +- FastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte +- Use `serve()` for running uvicorn (`if __name__ == "__main__"` is not needed since it's automatic) +- When a title is needed with a response, use `Titled`; note that that already wraps children in `Container`, and already includes both the meta title as well as the H1 element. + + # Web Devs Quickstart + + +## Installation + +``` bash +pip install python-fasthtml +``` + +## A Minimal Application + +A minimal FastHTML application looks something like this: + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("FastHTML", P("Let's do this!")) + +serve() +``` + +
+ +Line 1 +We import what we need for rapid development! A carefully-curated set of +FastHTML functions and other Python objects is brought into our global +namespace for convenience. + +Line 3 +We instantiate a FastHTML app with the `fast_app()` utility function. +This provides a number of really useful defaults that we’ll take +advantage of later in the tutorial. + +Line 5 +We use the `rt()` decorator to tell FastHTML what to return when a user +visits `/` in their browser. + +Line 6 +We connect this route to HTTP GET requests by defining a view function +called `get()`. + +Line 7 +A tree of Python function calls that return all the HTML required to +write a properly formed web page. You’ll soon see the power of this +approach. + +Line 9 +The `serve()` utility configures and runs FastHTML using a library +called `uvicorn`. + +Run the code: + +``` bash +python main.py +``` + +The terminal will look like this: + +``` bash +INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit) +INFO: Started reloader process [58058] using WatchFiles +INFO: Started server process [58060] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +Confirm FastHTML is running by opening your web browser to +[127.0.0.1:5001](http://127.0.0.1:5001). You should see something like +the image below: + +![](quickstart-web-dev/quickstart-fasthtml.png) + +
+ +> **Note** +> +> While some linters and developers will complain about the wildcard +> import, it is by design here and perfectly safe. FastHTML is very +> deliberate about the objects it exports in `fasthtml.common`. If it +> bothers you, you can import the objects you need individually, though +> it will make the code more verbose and less readable. +> +> If you want to learn more about how FastHTML handles imports, we cover +> that [here](https://docs.fastht.ml/explains/faq.html#why-use-import). + +
+ +## A Minimal Charting Application + +The +[`Script`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#script) +function allows you to include JavaScript. You can use Python to +generate parts of your JS or JSON like this: + +``` python +import json +from fasthtml.common import * + +app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),)) + +data = json.dumps({ + "data": [{"x": [1, 2, 3, 4],"type": "scatter"}, + {"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}], + "title": "Plotly chart in FastHTML ", + "description": "This is a demo dashboard", + "type": "scatter" +}) + + +@rt("/") +def get(): + return Titled("Chart Demo", Div(id="myDiv"), + Script(f"var data = {data}; Plotly.newPlot('myDiv', data);")) + +serve() +``` + +## Debug Mode + +When we can’t figure out a bug in FastHTML, we can run it in `DEBUG` +mode. When an error is thrown, the error screen is displayed in the +browser. This error setting should never be used in a deployed app. + +``` python +from fasthtml.common import * + +app, rt = fast_app(debug=True) + +@rt("/") +def get(): + 1/0 + return Titled("FastHTML Error!", P("Let's error!")) + +serve() +``` + +Line 3 +`debug=True` sets debug mode on. + +Line 7 +Python throws an error when it tries to divide an integer by zero. + +## Routing + +FastHTML builds upon FastAPI’s friendly decorator pattern for specifying +URLs, with extra features: + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("FastHTML", P("Let's do this!")) + +@rt("/hello") +def get(): + return Titled("Hello, world!") + +serve() +``` + +
+ +Line 5 +The “/” URL on line 5 is the home of a project. This would be accessed +at [127.0.0.1:5001](http://127.0.0.1:5001). + +Line 9 +“/hello” URL on line 9 will be found by the project if the user visits +[127.0.0.1:5001/hello](http://127.0.0.1:5001/hello). + +
+ +> **Tip** +> +> It looks like `get()` is being defined twice, but that’s not the case. +> Each function decorated with `rt` is totally separate, and is injected +> into the router. We’re not calling them in the module’s namespace +> (`locals()`). Rather, we’re loading them into the routing mechanism +> using the `rt` decorator. + +
+ +You can do more! Read on to learn what we can do to make parts of the +URL dynamic. + +## Variables in URLs + +You can add variable sections to a URL by marking them with +`{variable_name}`. Your function then receives the `{variable_name}` as +a keyword argument, but only if it is the correct type. Here’s an +example: + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/{name}/{age}") +def get(name: str, age: int): + return Titled(f"Hello {name.title()}, age {age}") + +serve() +``` + +
+ +Line 5 +We specify two variable names, `name` and `age`. + +Line 6 +We define two function arguments named identically to the variables. You +will note that we specify the Python types to be passed. + +Line 7 +We use these functions in our project. + +Try it out by going to this address: +[127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5). You should get a +page that says, + +> “Hello Uma, age 5”. + +### What happens if we enter incorrect data? + +The [127.0.0.1:5001/uma/5](http://127.0.0.1:5001/uma/5) URL works +because `5` is an integer. If we enter something that is not, such as +[127.0.0.1:5001/uma/five](http://127.0.0.1:5001/uma/five), then FastHTML +will return an error instead of a web page. + +
+ +> **FastHTML URL routing supports more complex types** +> +> The two examples we provide here use Python’s built-in `str` and `int` +> types, but you can use your own types, including more complex ones +> such as those defined by libraries like +> [attrs](https://pypi.org/project/attrs/), +> [pydantic](https://pypi.org/project/pydantic/), and even +> [sqlmodel](https://pypi.org/project/sqlmodel/). + +
+ +## HTTP Methods + +FastHTML matches function names to HTTP methods. So far the URL routes +we’ve defined have been for HTTP GET methods, the most common method for +web pages. + +Form submissions often are sent as HTTP POST. When dealing with more +dynamic web page designs, also known as Single Page Apps (SPA for +short), the need can arise for other methods such as HTTP PUT and HTTP +DELETE. The way FastHTML handles this is by changing the function name. + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app() + +@rt("/") +def get(): + return Titled("HTTP GET", P("Handle GET")) + +@rt("/") +def post(): + return Titled("HTTP POST", P("Handle POST")) + +serve() +``` + +
+ +Line 6 +On line 6 because the `get()` function name is used, this will handle +HTTP GETs going to the `/` URI. + +Line 10 +On line 10 because the `post()` function name is used, this will handle +HTTP POSTs going to the `/` URI. + +## CSS Files and Inline Styles + +Here we modify default headers to demonstrate how to use the [Sakura CSS +microframework](https://github.com/oxalorg/sakura) instead of FastHTML’s +default of Pico CSS. + +
+ +**main.py** + +``` python +from fasthtml.common import * + +app, rt = fast_app( + pico=False, + hdrs=( + Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'), + Link(rel='stylesheet', href='assets/sakura.css', type='text/css'), + Style("p {color: red;}") +)) + +@app.get("/") +def home(): + return Titled("FastHTML", + P("Let's do this!"), + ) + +serve() +``` + +
+ +Line 4 +By setting `pico` to `False`, FastHTML will not include `pico.min.css`. + +Line 7 +This will generate an HTML `` tag for sourcing the css for Sakura. + +Line 8 +If you want an inline styles, the `Style()` function will put the result +into the HTML. + +## Other Static Media File Locations + +As you saw, +[`Script`](https://AnswerDotAI.github.io/fasthtml/api/xtend.html#script) +and `Link` are specific to the most common static media use cases in web +apps: including JavaScript, CSS, and images. But it also works with +videos and other static media files. The default behavior is to look for +these files in the root directory - typically we don’t do anything +special to include them. We can change the default directory that is +looked in for files by adding the `static_path` parameter to the +[`fast_app`](https://AnswerDotAI.github.io/fasthtml/api/fastapp.html#fast_app) +function. + +``` python +app, rt = fast_app(static_path='public') +``` + +FastHTML also allows us to define a route that uses `FileResponse` to +serve the file at a specified path. This is useful for serving images, +videos, and other media files from a different directory without having +to change the paths of many files. So if we move the directory +containing the media files, we only need to change the path in one +place. In the example below, we call images from a directory called +`public`. + +``` python +@rt("/{fname:path}.{ext:static}") +async def get(fname:str, ext:str): + return FileResponse(f'public/{fname}.{ext}') +``` + +## Rendering Markdown + +``` python +from fasthtml.common import * + +hdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), ) + +app, rt = fast_app(hdrs=hdrs) + +content = """ +Here are some _markdown_ elements. + +- This is a list item +- This is another list item +- And this is a third list item + +**Fenced code blocks work here.** +""" + +@rt('/') +def get(req): + return Titled("Markdown rendering example", Div(content,cls="marked")) + +serve() +``` + +## Code highlighting + +Here’s how to highlight code without any markdown configuration. + +``` python +from fasthtml.common import * + +# Add the HighlightJS built-in header +hdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),) + +app, rt = fast_app(hdrs=hdrs) + +code_example = """ +import datetime +import time + +for i in range(10): + print(f"{datetime.datetime.now()}") + time.sleep(1) +""" + +@rt('/') +def get(req): + return Titled("Markdown rendering example", + Div( + # The code example needs to be surrounded by + # Pre & Code elements + Pre(Code(code_example)) + )) + +serve() +``` + +## Defining new `ft` components + +We can build our own `ft` components and combine them with other +components. The simplest method is defining them as a function. + +``` python +from fasthtml.common import * +``` + +``` python +def hero(title, statement): + return Div(H1(title),P(statement), cls="hero") + +# usage example +Main( + hero("Hello World", "This is a hero statement") +) +``` + +``` html +
+
+

Hello World

+

This is a hero statement

+
+
+``` + +### Pass through components + +For when we need to define a new component that allows zero-to-many +components to be nested within them, we lean on Python’s `*args` and +`**kwargs` mechanism. Useful for creating page layout controls. + +``` python +def layout(*args, **kwargs): + """Dashboard layout for all our dashboard views""" + return Main( + H1("Dashboard"), + Div(*args, **kwargs), + cls="dashboard", + ) + +# usage example +layout( + Ul(*[Li(o) for o in range(3)]), + P("Some content", cls="description"), +) +``` + +``` html +
+

Dashboard

+
+
    +
  • 0
  • +
  • 1
  • +
  • 2
  • +
+

Some content

+
+
+``` + +### Dataclasses as ft components + +While functions are easy to read, for more complex components some might +find it easier to use a dataclass. + +``` python +from dataclasses import dataclass + +@dataclass +class Hero: + title: str + statement: str + + def __ft__(self): + """ The __ft__ method renders the dataclass at runtime.""" + return Div(H1(self.title),P(self.statement), cls="hero") + +# usage example +Main( + Hero("Hello World", "This is a hero statement") +) +``` + +``` html +
+
+

Hello World

+

This is a hero statement

+
+
+``` + +## Testing views in notebooks + +Because of the ASGI event loop it is currently impossible to run +FastHTML inside a notebook. However, we can still test the output of our +views. To do this, we leverage Starlette, an ASGI toolkit that FastHTML +uses. + +``` python +# First we instantiate our app, in this case we remove the +# default headers to reduce the size of the output. +app, rt = fast_app(default_hdrs=False) + +# Setting up the Starlette test client +from starlette.testclient import TestClient +client = TestClient(app) + +# Usage example +@rt("/") +def get(): + return Titled("FastHTML is awesome", + P("The fastest way to create web apps in Python")) + +print(client.get("/").text) +``` + + + + + + FastHTML is awesome + + +
+

FastHTML is awesome

+

The fastest way to create web apps in Python

+
+ + + +## Forms + +To validate data coming from users, first define a dataclass +representing the data you want to check. Here’s an example representing +a signup form. + +``` python +from dataclasses import dataclass + +@dataclass +class Profile: email:str; phone:str; age:int +``` + +Create an FT component representing an empty version of that form. Don’t +pass in any value to fill the form, that gets handled later. + +``` python +profile_form = Form(method="post", action="/profile")( + Fieldset( + Label('Email', Input(name="email")), + Label("Phone", Input(name="phone")), + Label("Age", Input(name="age")), + ), + Button("Save", type="submit"), + ) +profile_form +``` + +``` html +
+
+ + + +
+ +
+``` + +Once the dataclass and form function are completed, we can add data to +the form. To do that, instantiate the profile dataclass: + +``` python +profile = Profile(email='john@example.com', phone='123456789', age=5) +profile +``` + + Profile(email='john@example.com', phone='123456789', age=5) + +Then add that data to the `profile_form` using FastHTML’s +[`fill_form`](https://AnswerDotAI.github.io/fasthtml/api/components.html#fill_form) +class: + +``` python +fill_form(profile_form, profile) +``` + +``` html +
+
+ + + +
+ +
+``` + +### Forms with views + +The usefulness of FastHTML forms becomes more apparent when they are +combined with FastHTML views. We’ll show how this works by using the +test client from above. First, let’s create a SQlite database: + +``` python +db = Database("profiles.db") +profiles = db.create(Profile, pk="email") +``` + +Now we insert a record into the database: + +``` python +profiles.insert(profile) +``` + + Profile(email='john@example.com', phone='123456789', age=5) + +And we can then demonstrate in the code that form is filled and +displayed to the user. + +``` python +@rt("/profile/{email}") +def profile(email:str): + profile = profiles[email] + filled_profile_form = fill_form(profile_form, profile) + return Titled(f'Profile for {profile.email}', filled_profile_form) + +print(client.get(f"/profile/john@example.com").text) +``` + +Line 3 +Fetch the profile using the profile table’s `email` primary key + +Line 4 +Fill the form for display. + + + + + + + Profile for john@example.com + + +
+

Profile for john@example.com

+
+
+ + + +
+ +
+
+ + + +And now let’s demonstrate making a change to the data. + +``` python +@rt("/profile") +def post(profile: Profile): + profiles.update(profile) + return RedirectResponse(url=f"/profile/{profile.email}") + +new_data = dict(email='john@example.com', phone='7654321', age=25) +print(client.post("/profile", data=new_data).text) +``` + +Line 2 +We use the `Profile` dataclass definition to set the type for the +incoming `profile` content. This validates the field types for the +incoming data + +Line 3 +Taking our validated data, we updated the profiles table + +Line 4 +We redirect the user back to their profile view + +Line 7 +The display is of the profile form view showing the changes in data. + + + + + + + Profile for john@example.com + + +
+

Profile for john@example.com

+
+
+ + + +
+ +
+
+ + + +## Strings and conversion order + +The general rules for rendering are: - `__ft__` method will be called +(for default components like `P`, `H2`, etc. or if you define your own +components) - If you pass a string, it will be escaped - On other python +objects, `str()` will be called + +As a consequence, if you want to include plain HTML tags directly into +e.g. a `Div()` they will get escaped by default (as a security measure +to avoid code injections). This can be avoided by using `NotStr()`, a +convenient way to reuse python code that returns already HTML. If you +use pandas, you can use `pandas.DataFrame.to_html()` to get a nice +table. To include the output a FastHTML, wrap it in `NotStr()`, like +`Div(NotStr(df.to_html()))`. + +Above we saw how a dataclass behaves with the `__ft__` method defined. +On a plain dataclass, `str()` will be called (but not escaped). + +``` python +from dataclasses import dataclass + +@dataclass +class Hero: + title: str + statement: str + +# rendering the dataclass with the default method +Main( + Hero("

Hello World

", "This is a hero statement") +) +``` + +``` html +
Hero(title='

Hello World

', statement='This is a hero statement')
+``` + +``` python +# This will display the HTML as text on your page +Div("Let's include some HTML here:
Some HTML
") +``` + +``` html +
Let's include some HTML here: <div>Some HTML</div>
+``` + +``` python +# Keep the string untouched, will be rendered on the page +Div(NotStr("

Some HTML

")) +``` + +``` html +

Some HTML

+``` + +## Custom exception handlers + +FastHTML allows customization of exception handlers, but does so +gracefully. What this means is by default it includes all the `` +tags needed to display attractive content. Try it out! + +``` python +from fasthtml.common import * + +def not_found(req, exc): return Titled("404: I don't exist!") + +exception_handlers = {404: not_found} + +app, rt = fast_app(exception_handlers=exception_handlers) + +@rt('/') +def get(): + return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error")))) + +serve() +``` + +We can also use lambda to make things more terse: + +``` python +from fasthtml.common import * + +exception_handlers={ + 404: lambda req, exc: Titled("404: I don't exist!"), + 418: lambda req, exc: Titled("418: I'm a teapot!") +} + +app, rt = fast_app(exception_handlers=exception_handlers) + +@rt('/') +def get(): + return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error")))) + +serve() +``` + +## Cookies + +We can set cookies using the `cookie()` function. In our example, we’ll +create a `timestamp` cookie. + +``` python +from datetime import datetime +from IPython.display import HTML +``` + +``` python +@rt("/settimestamp") +def get(req): + now = datetime.now() + return P(f'Set to {now}'), cookie('now', datetime.now()) + +HTML(client.get('/settimestamp').text) +``` + + + + + FastHTML page + + +

Set to 2024-09-04 18:30:34.896373

+ + + +Now let’s get it back using the same name for our parameter as the +cookie name. + +``` python +@rt('/gettimestamp') +def get(now:date): return f'Cookie was set at time {now.time()}' + +client.get('/gettimestamp').text +``` + + 'Cookie was set at time 18:30:34.896405' + +## Sessions + +For convenience and security, FastHTML has a mechanism for storing small +amounts of data in the user’s browser. We can do this by adding a +`session` argument to routes. FastHTML sessions are Python dictionaries, +and we can leverage to our benefit. The example below shows how to +concisely set and get sessions. + +``` python +@rt('/adder/{num}') +def get(session, num: int): + session.setdefault('sum', 0) + session['sum'] = session.get('sum') + num + return Response(f'The sum is {session["sum"]}.') +``` + +## Toasts (also known as Messages) + +Toasts, sometimes called “Messages” are small notifications usually in +colored boxes used to notify users that something has happened. Toasts +can be of four types: + +- info +- success +- warning +- error + +Examples toasts might include: + +- “Payment accepted” +- “Data submitted” +- “Request approved” + +Toasts require the use of the `setup_toasts()` function plus every view +needs these two features: + +- The session argument +- Must return FT components + +``` python +setup_toasts(app) + +@rt('/toasting') +def get(session): + # Normally one toast is enough, this allows us to see + # different toast types in action. + add_toast(session, f"Toast is being cooked", "info") + add_toast(session, f"Toast is ready", "success") + add_toast(session, f"Toast is getting a bit crispy", "warning") + add_toast(session, f"Toast is burning!", "error") + return Titled("I like toast") +``` + +Line 1 +`setup_toasts` is a helper function that adds toast dependencies. +Usually this would be declared right after `fast_app()` + +Line 4 +Toasts require sessions + +Line 11 +Views with Toasts must return FT components. + +## Authentication and authorization + +In FastHTML the tasks of authentication and authorization are handled +with Beforeware. Beforeware are functions that run before the route +handler is called. They are useful for global tasks like ensuring users +are authenticated or have permissions to access a view. + +First, we write a function that accepts a request and session arguments: + +``` python +# Status code 303 is a redirect that can change POST to GET, +# so it's appropriate for a login page. +login_redir = RedirectResponse('/login', status_code=303) + +def user_auth_before(req, sess): + # The `auth` key in the request scope is automatically provided + # to any handler which requests it, and can not be injected + # by the user using query params, cookies, etc, so it should + # be secure to use. + auth = req.scope['auth'] = sess.get('auth', None) + # If the session key is not there, it redirects to the login page. + if not auth: return login_redir +``` + +Now we pass our `user_auth_before` function as the first argument into a +[`Beforeware`](https://AnswerDotAI.github.io/fasthtml/api/core.html#beforeware) +class. We also pass a list of regular expressions to the `skip` +argument, designed to allow users to still get to the home and login +pages. + +``` python +beforeware = Beforeware( + user_auth_before, + skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/'] +) + +app, rt = fast_app(before=beforeware) +``` + +## Server-sent events (SSE) + +With [server-sent +events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), +it’s possible for a server to send new data to a web page at any time, +by pushing messages to the web page. Unlike WebSockets, SSE can only go +in one direction: server to client. SSE is also part of the HTTP +specification unlike WebSockets which uses its own specification. + +FastHTML introduces several tools for working with SSE which are covered +in the example below. While concise, there’s a lot going on in this +function so we’ve annotated it quite a bit. + +``` python +import random +from asyncio import sleep +from fasthtml.common import * + +hdrs=(Script(src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"),) +app,rt = fast_app(hdrs=hdrs) + +@rt +def index(): + return Titled("SSE Random Number Generator", + P("Generate pairs of random numbers, as the list grows scroll downwards."), + Div(hx_ext="sse", + sse_connect="/number-stream", + hx_swap="beforeend show:bottom", + sse_swap="message")) + +shutdown_event = signal_shutdown() + +async def number_generator(): + while not shutdown_event.is_set(): + data = Article(random.randint(1, 100)) + yield sse_message(data) + await sleep(1) + +@rt("/number-stream") +async def get(): return EventStream(number_generator()) +``` + +Line 5 +Import the HTMX SSE extension + +Line 12 +Tell HTMX to load the SSE extension + +Line 13 +Look at the `/number-stream` endpoint for SSE content + +Line 14 +When new items come in from the SSE endpoint, add them at the end of the +current content within the div. If they go beyond the screen, scroll +downwards + +Line 15 +Specify the name of the event. FastHTML’s default event name is +“message”. Only change if you have more than one call to SSE endpoints +within a view + +Line 17 +Set up the asyncio event loop + +Line 19 +Don’t forget to make this an `async` function! + +Line 20 +Iterate through the asyncio event loop + +Line 22 +We yield the data. Data ideally should be comprised of FT components as +that plugs nicely into HTMX in the browser + +Line 26 +The endpoint view needs to be an async function that returns a +[`EventStream`](https://AnswerDotAI.github.io/fasthtml/api/core.html#eventstream) + +
+ +> **New content as of September 1, 2024** +> +> - [Forms](../tutorials/quickstart_for_web_devs.html#forms) +> - [Server-side Events +> (SSE)](../tutorials/quickstart_for_web_devs.html#server-sent-events-sse) +> +> We’re going to be adding more to this document, so check back +> frequently for updates. + +
+ +## Unwritten quickstart sections + +- Websockets +- Tables
+ +++ +title = "Reference" ++++ + +## Contents + +* [htmx Core Attributes](#attributes) +* [htmx Additional Attributes](#attributes-additional) +* [htmx CSS Classes](#classes) +* [htmx Request Headers](#request_headers) +* [htmx Response Headers](#response_headers) +* [htmx Events](#events) +* [htmx Extensions](https://extensions.htmx.org) +* [JavaScript API](#api) +* [Configuration Options](#config) + +## Core Attribute Reference {#attributes} + +The most common attributes when using htmx. + +
+ +| Attribute | Description | +|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| [`hx-get`](@/attributes/hx-get.md) | issues a `GET` to the specified URL | +| [`hx-post`](@/attributes/hx-post.md) | issues a `POST` to the specified URL | +| [`hx-on*`](@/attributes/hx-on.md) | handle events with inline scripts on elements | +| [`hx-push-url`](@/attributes/hx-push-url.md) | push a URL into the browser location bar to create history | +| [`hx-select`](@/attributes/hx-select.md) | select content to swap in from a response | +| [`hx-select-oob`](@/attributes/hx-select-oob.md) | select content to swap in from a response, somewhere other than the target (out of band) | +| [`hx-swap`](@/attributes/hx-swap.md) | controls how content will swap in (`outerHTML`, `beforeend`, `afterend`, ...) | +| [`hx-swap-oob`](@/attributes/hx-swap-oob.md) | mark element to swap in from a response (out of band) | +| [`hx-target`](@/attributes/hx-target.md) | specifies the target element to be swapped | +| [`hx-trigger`](@/attributes/hx-trigger.md) | specifies the event that triggers the request | +| [`hx-vals`](@/attributes/hx-vals.md) | add values to submit with the request (JSON format) | + +
+ +## Additional Attribute Reference {#attributes-additional} + +All other attributes available in htmx. + +
+ +| Attribute | Description | +|------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| [`hx-boost`](@/attributes/hx-boost.md) | add [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) for links and forms | +| [`hx-confirm`](@/attributes/hx-confirm.md) | shows a `confirm()` dialog before issuing a request | +| [`hx-delete`](@/attributes/hx-delete.md) | issues a `DELETE` to the specified URL | +| [`hx-disable`](@/attributes/hx-disable.md) | disables htmx processing for the given node and any children nodes | +| [`hx-disabled-elt`](@/attributes/hx-disabled-elt.md) | adds the `disabled` attribute to the specified elements while a request is in flight | +| [`hx-disinherit`](@/attributes/hx-disinherit.md) | control and disable automatic attribute inheritance for child nodes | +| [`hx-encoding`](@/attributes/hx-encoding.md) | changes the request encoding type | +| [`hx-ext`](@/attributes/hx-ext.md) | extensions to use for this element | +| [`hx-headers`](@/attributes/hx-headers.md) | adds to the headers that will be submitted with the request | +| [`hx-history`](@/attributes/hx-history.md) | prevent sensitive data being saved to the history cache | +| [`hx-history-elt`](@/attributes/hx-history-elt.md) | the element to snapshot and restore during history navigation | +| [`hx-include`](@/attributes/hx-include.md) | include additional data in requests | +| [`hx-indicator`](@/attributes/hx-indicator.md) | the element to put the `htmx-request` class on during the request | +| [`hx-inherit`](@/attributes/hx-inherit.md) | control and enable automatic attribute inheritance for child nodes if it has been disabled by default | +| [`hx-params`](@/attributes/hx-params.md) | filters the parameters that will be submitted with a request | +| [`hx-patch`](@/attributes/hx-patch.md) | issues a `PATCH` to the specified URL | +| [`hx-preserve`](@/attributes/hx-preserve.md) | specifies elements to keep unchanged between requests | +| [`hx-prompt`](@/attributes/hx-prompt.md) | shows a `prompt()` before submitting a request | +| [`hx-put`](@/attributes/hx-put.md) | issues a `PUT` to the specified URL | +| [`hx-replace-url`](@/attributes/hx-replace-url.md) | replace the URL in the browser location bar | +| [`hx-request`](@/attributes/hx-request.md) | configures various aspects of the request | +| [`hx-sync`](@/attributes/hx-sync.md) | control how requests made by different elements are synchronized | +| [`hx-validate`](@/attributes/hx-validate.md) | force elements to validate themselves before a request | +| [`hx-vars`](@/attributes/hx-vars.md) | adds values dynamically to the parameters to submit with the request (deprecated, please use [`hx-vals`](@/attributes/hx-vals.md)) | + +
+ +## CSS Class Reference {#classes} + +
+ +| Class | Description | +|-----------|-------------| +| `htmx-added` | Applied to a new piece of content before it is swapped, removed after it is settled. +| `htmx-indicator` | A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present +| `htmx-request` | Applied to either the element or the element specified with [`hx-indicator`](@/attributes/hx-indicator.md) while a request is ongoing +| `htmx-settling` | Applied to a target after content is swapped, removed after it is settled. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md). +| `htmx-swapping` | Applied to a target before any content is swapped, removed after it is swapped. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md). + +
+ +## HTTP Header Reference {#headers} + +### Request Headers Reference {#request_headers} + +
+ +| Header | Description | +|--------|-------------| +| `HX-Boosted` | indicates that the request is via an element using [hx-boost](@/attributes/hx-boost.md) +| `HX-Current-URL` | the current URL of the browser +| `HX-History-Restore-Request` | "true" if the request is for history restoration after a miss in the local history cache +| `HX-Prompt` | the user response to an [hx-prompt](@/attributes/hx-prompt.md) +| `HX-Request` | always "true" +| `HX-Target` | the `id` of the target element if it exists +| `HX-Trigger-Name` | the `name` of the triggered element if it exists +| `HX-Trigger` | the `id` of the triggered element if it exists + +
+ +### Response Headers Reference {#response_headers} + +
+ +| Header | Description | +|------------------------------------------------------|-------------| +| [`HX-Location`](@/headers/hx-location.md) | allows you to do a client-side redirect that does not do a full page reload +| [`HX-Push-Url`](@/headers/hx-push-url.md) | pushes a new url into the history stack +| `HX-Redirect` | can be used to do a client-side redirect to a new location +| `HX-Refresh` | if set to "true" the client-side will do a full refresh of the page +| [`HX-Replace-Url`](@/headers/hx-replace-url.md) | replaces the current URL in the location bar +| `HX-Reswap` | allows you to specify how the response will be swapped. See [hx-swap](@/attributes/hx-swap.md) for possible values +| `HX-Retarget` | a CSS selector that updates the target of the content update to a different element on the page +| `HX-Reselect` | a CSS selector that allows you to choose which part of the response is used to be swapped in. Overrides an existing [`hx-select`](@/attributes/hx-select.md) on the triggering element +| [`HX-Trigger`](@/headers/hx-trigger.md) | allows you to trigger client-side events +| [`HX-Trigger-After-Settle`](@/headers/hx-trigger.md) | allows you to trigger client-side events after the settle step +| [`HX-Trigger-After-Swap`](@/headers/hx-trigger.md) | allows you to trigger client-side events after the swap step + +
+ +## Event Reference {#events} + +
+ +| Event | Description | +|-------|-------------| +| [`htmx:abort`](@/events.md#htmx:abort) | send this event to an element to abort a request +| [`htmx:afterOnLoad`](@/events.md#htmx:afterOnLoad) | triggered after an AJAX request has completed processing a successful response +| [`htmx:afterProcessNode`](@/events.md#htmx:afterProcessNode) | triggered after htmx has initialized a node +| [`htmx:afterRequest`](@/events.md#htmx:afterRequest) | triggered after an AJAX request has completed +| [`htmx:afterSettle`](@/events.md#htmx:afterSettle) | triggered after the DOM has settled +| [`htmx:afterSwap`](@/events.md#htmx:afterSwap) | triggered after new content has been swapped in +| [`htmx:beforeCleanupElement`](@/events.md#htmx:beforeCleanupElement) | triggered before htmx [disables](@/attributes/hx-disable.md) an element or removes it from the DOM +| [`htmx:beforeOnLoad`](@/events.md#htmx:beforeOnLoad) | triggered before any response processing occurs +| [`htmx:beforeProcessNode`](@/events.md#htmx:beforeProcessNode) | triggered before htmx initializes a node +| [`htmx:beforeRequest`](@/events.md#htmx:beforeRequest) | triggered before an AJAX request is made +| [`htmx:beforeSwap`](@/events.md#htmx:beforeSwap) | triggered before a swap is done, allows you to configure the swap +| [`htmx:beforeSend`](@/events.md#htmx:beforeSend) | triggered just before an ajax request is sent +| [`htmx:configRequest`](@/events.md#htmx:configRequest) | triggered before the request, allows you to customize parameters, headers +| [`htmx:confirm`](@/events.md#htmx:confirm) | triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request +| [`htmx:historyCacheError`](@/events.md#htmx:historyCacheError) | triggered on an error during cache writing +| [`htmx:historyCacheMiss`](@/events.md#htmx:historyCacheMiss) | triggered on a cache miss in the history subsystem +| [`htmx:historyCacheMissError`](@/events.md#htmx:historyCacheMissError) | triggered on a unsuccessful remote retrieval +| [`htmx:historyCacheMissLoad`](@/events.md#htmx:historyCacheMissLoad) | triggered on a successful remote retrieval +| [`htmx:historyRestore`](@/events.md#htmx:historyRestore) | triggered when htmx handles a history restoration action +| [`htmx:beforeHistorySave`](@/events.md#htmx:beforeHistorySave) | triggered before content is saved to the history cache +| [`htmx:load`](@/events.md#htmx:load) | triggered when new content is added to the DOM +| [`htmx:noSSESourceError`](@/events.md#htmx:noSSESourceError) | triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined +| [`htmx:onLoadError`](@/events.md#htmx:onLoadError) | triggered when an exception occurs during the onLoad handling in htmx +| [`htmx:oobAfterSwap`](@/events.md#htmx:oobAfterSwap) | triggered after an out of band element as been swapped in +| [`htmx:oobBeforeSwap`](@/events.md#htmx:oobBeforeSwap) | triggered before an out of band element swap is done, allows you to configure the swap +| [`htmx:oobErrorNoTarget`](@/events.md#htmx:oobErrorNoTarget) | triggered when an out of band element does not have a matching ID in the current DOM +| [`htmx:prompt`](@/events.md#htmx:prompt) | triggered after a prompt is shown +| [`htmx:pushedIntoHistory`](@/events.md#htmx:pushedIntoHistory) | triggered after an url is pushed into history +| [`htmx:responseError`](@/events.md#htmx:responseError) | triggered when an HTTP response error (non-`200` or `300` response code) occurs +| [`htmx:sendError`](@/events.md#htmx:sendError) | triggered when a network error prevents an HTTP request from happening +| [`htmx:sseError`](@/events.md#htmx:sseError) | triggered when an error occurs with a SSE source +| [`htmx:sseOpen`](/events#htmx:sseOpen) | triggered when a SSE source is opened +| [`htmx:swapError`](@/events.md#htmx:swapError) | triggered when an error occurs during the swap phase +| [`htmx:targetError`](@/events.md#htmx:targetError) | triggered when an invalid target is specified +| [`htmx:timeout`](@/events.md#htmx:timeout) | triggered when a request timeout occurs +| [`htmx:validation:validate`](@/events.md#htmx:validation:validate) | triggered before an element is validated +| [`htmx:validation:failed`](@/events.md#htmx:validation:failed) | triggered when an element fails validation +| [`htmx:validation:halted`](@/events.md#htmx:validation:halted) | triggered when a request is halted due to validation errors +| [`htmx:xhr:abort`](@/events.md#htmx:xhr:abort) | triggered when an ajax request aborts +| [`htmx:xhr:loadend`](@/events.md#htmx:xhr:loadend) | triggered when an ajax request ends +| [`htmx:xhr:loadstart`](@/events.md#htmx:xhr:loadstart) | triggered when an ajax request starts +| [`htmx:xhr:progress`](@/events.md#htmx:xhr:progress) | triggered periodically during an ajax request that supports progress events + +
+ +## JavaScript API Reference {#api} + +
+ +| Method | Description | +|-------|-------------| +| [`htmx.addClass()`](@/api.md#addClass) | Adds a class to the given element +| [`htmx.ajax()`](@/api.md#ajax) | Issues an htmx-style ajax request +| [`htmx.closest()`](@/api.md#closest) | Finds the closest parent to the given element matching the selector +| [`htmx.config`](@/api.md#config) | A property that holds the current htmx config object +| [`htmx.createEventSource`](@/api.md#createEventSource) | A property holding the function to create SSE EventSource objects for htmx +| [`htmx.createWebSocket`](@/api.md#createWebSocket) | A property holding the function to create WebSocket objects for htmx +| [`htmx.defineExtension()`](@/api.md#defineExtension) | Defines an htmx [extension](https://extensions.htmx.org) +| [`htmx.find()`](@/api.md#find) | Finds a single element matching the selector +| [`htmx.findAll()` `htmx.findAll(elt, selector)`](@/api.md#find) | Finds all elements matching a given selector +| [`htmx.logAll()`](@/api.md#logAll) | Installs a logger that will log all htmx events +| [`htmx.logger`](@/api.md#logger) | A property set to the current logger (default is `null`) +| [`htmx.off()`](@/api.md#off) | Removes an event listener from the given element +| [`htmx.on()`](@/api.md#on) | Creates an event listener on the given element, returning it +| [`htmx.onLoad()`](@/api.md#onLoad) | Adds a callback handler for the `htmx:load` event +| [`htmx.parseInterval()`](@/api.md#parseInterval) | Parses an interval declaration into a millisecond value +| [`htmx.process()`](@/api.md#process) | Processes the given element and its children, hooking up any htmx behavior +| [`htmx.remove()`](@/api.md#remove) | Removes the given element +| [`htmx.removeClass()`](@/api.md#removeClass) | Removes a class from the given element +| [`htmx.removeExtension()`](@/api.md#removeExtension) | Removes an htmx [extension](https://extensions.htmx.org) +| [`htmx.swap()`](@/api.md#swap) | Performs swapping (and settling) of HTML content +| [`htmx.takeClass()`](@/api.md#takeClass) | Takes a class from other elements for the given element +| [`htmx.toggleClass()`](@/api.md#toggleClass) | Toggles a class from the given element +| [`htmx.trigger()`](@/api.md#trigger) | Triggers an event on an element +| [`htmx.values()`](@/api.md#values) | Returns the input values associated with the given element + +
+ + +## Configuration Reference {#config} + +Htmx has some configuration options that can be accessed either programmatically or declaratively. They are +listed below: + +
+ +| Config Variable | Info | +|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `htmx.config.historyEnabled` | defaults to `true`, really only useful for testing | +| `htmx.config.historyCacheSize` | defaults to 10 | +| `htmx.config.refreshOnHistoryMiss` | defaults to `false`, if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request | +| `htmx.config.defaultSwapStyle` | defaults to `innerHTML` | +| `htmx.config.defaultSwapDelay` | defaults to 0 | +| `htmx.config.defaultSettleDelay` | defaults to 20 | +| `htmx.config.includeIndicatorStyles` | defaults to `true` (determines if the indicator styles are loaded) | +| `htmx.config.indicatorClass` | defaults to `htmx-indicator` | +| `htmx.config.requestClass` | defaults to `htmx-request` | +| `htmx.config.addedClass` | defaults to `htmx-added` | +| `htmx.config.settlingClass` | defaults to `htmx-settling` | +| `htmx.config.swappingClass` | defaults to `htmx-swapping` | +| `htmx.config.allowEval` | defaults to `true`, can be used to disable htmx's use of eval for certain features (e.g. trigger filters) | +| `htmx.config.allowScriptTags` | defaults to `true`, determines if htmx will process script tags found in new content | +| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts | +| `htmx.config.inlineStyleNonce` | defaults to `''`, meaning that no nonce will be added to inline styles | +| `htmx.config.attributesToSettle` | defaults to `["class", "style", "width", "height"]`, the attributes to settle during the settling phase | +| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` | +| `htmx.config.wsBinaryType` | defaults to `blob`, the [the type of binary data](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) being received over the WebSocket connection | +| `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent | +| `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates | +| `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated | +| `htmx.config.scrollBehavior` | defaults to 'instant', the behavior for a boosted link on page transitions. The allowed values are `auto`, `instant` and `smooth`. Instant will scroll instantly in a single jump, smooth will scroll smoothly, while auto will behave like a vanilla link. | +| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. | +| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will append the target element to the `GET` request in the format `org.htmx.cache-buster=targetElementId` | +| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. | +| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body | +| `htmx.config.selfRequestsOnly` | defaults to `true`, whether to only allow AJAX requests to the same domain as the current document | +| `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content | +| `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. | +| `htmx.config.triggerSpecsCache` | defaults to `null`, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) | +| `htmx.config.allowNestedOobSwaps` | defaults to `true`, whether to process OOB swaps on elements that are nested within the main response element. See [Nested OOB Swaps](@/attributes/hx-swap-oob.md#nested-oob-swaps). | + +
+ +You can set them directly in javascript, or you can use a `meta` tag: + +```html + +```
+ # 🗿 Surreal +### Tiny jQuery alternative for plain Javascript with inline [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/)! + +![cover](https://user-images.githubusercontent.com/24665/171092805-b41286b2-be4a-4aab-9ee6-d604699cc507.png) +(Art by [shahabalizadeh](https://www.deviantart.com/shahabalizadeh)) + + +## Why does this exist? + +For devs who love ergonomics! You may appreciate Surreal if: + +* You want to stay as close as possible to Vanilla JS. +* Hate typing `document.querySelector` over.. and over.. +* Hate typing `addEventListener` over.. and over.. +* Really wish `document.querySelectorAll` had Array functions.. +* Really wish `this` would work in any inline ` + +``` + +See the [Live Example](https://gnat.github.io/surreal/example.html)! Then [view source](https://github.com/gnat/surreal/blob/main/example.html). + +## 🎁 Install + +Surreal is only 320 lines. No build step. No dependencies. + +[📥 Download](https://raw.githubusercontent.com/gnat/surreal/main/surreal.js) into your project, and add `` in your `` + +Or, 🌐 via CDN: `` + +## ⚡ Usage + +### 🔍️ DOM Selection + +* Select **one** element: `me(...)` + * Can be any of: + * CSS selector: `".button"`, `"#header"`, `"h1"`, `"body > .block"` + * Variables: `body`, `e`, `some_element` + * Events: `event.currentTarget` will be used. + * Surreal selectors: `me()`,`any()` + * Choose the start location in the DOM with the 2nd arg. (Default: `document`) + * 🔥 `any('button', me('#header')).classAdd('red')` + * Add `.red` to any ` + +``` +See the [Live Example](https://gnat.github.io/css-scope-inline/example.html)! Then [view source](https://github.com/gnat/css-scope-inline/blob/main/example.html). + +## 🌘 How does it work? + +This uses `MutationObserver` to monitor the DOM, and the moment a ` + red +
green
+
green
+
green
+
yellow
+
blue
+
green
+
green
+ + +
+ red +
green
+
green
+
green
+
yellow
+
blue
+
green
+
green
+
+``` + +### CSS variables and child elements +At first glance, **Tailwind Example 2** looks very promising! Exciting ...but: +* 🔴 **Every child style requires an explicit selector.** + * Tailwinds' shorthand advantages sadly disappear. + * Any more child styles added in Tailwind will become longer than vanilla CSS. + * This limited example is the best case scenario for Tailwind. +* 🔴 Not visible on github: **no highlighting for properties and units** begins to be painful. +```html + + + + + + + + + +
+ +
Home
+
Team
+
Profile
+
Settings
+
Log Out
+
+ + + + + + + + +``` +## 🔎 Technical FAQ +* Why do you use `querySelectorAll()` and not just process the `MutationObserver` results directly? + * This was indeed the original design; it will work well up until you begin recieving subtrees (ex: DOM swaps with [htmx](https://htmx.org), ajax, jquery, etc.) which requires walking all subtree elements to ensure we do not miss a `