Spaces:
Sleeping
Sleeping
| # FastHTML By Example | |
| <!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! --> | |
| This tutorial provides an alternate introduction to FastHTML by building | |
| out example applications. We also illustrate how to use FastHTML | |
| foundations to create custom web apps. Finally, this document serves as | |
| minimal context for a LLM to turn it into a FastHTML assistant. | |
| Let’s get started. | |
| ## FastHTML Basics | |
| FastHTML is *just Python*. You can install it with | |
| `pip install python-fasthtml`. Extensions/components built for it can | |
| likewise be distributed via PyPI or as simple Python files. | |
| The core usage of FastHTML is to define routes, and then to define what | |
| to do at each route. This is similar to the | |
| [FastAPI](https://fastapi.tiangolo.com/) web framework (in fact we | |
| implemented much of the functionality to match the FastAPI usage | |
| examples), but where FastAPI focuses on returning JSON data to build | |
| APIs, FastHTML focuses on returning HTML data. | |
| Here’s a simple FastHTML app that returns a “Hello, World” message: | |
| ``` python | |
| from fasthtml.common import FastHTML, serve | |
| app = FastHTML() | |
| @app.get("/") | |
| def home(): | |
| return "<h1>Hello, World</h1>" | |
| serve() | |
| ``` | |
| To run this app, place it in a file, say `app.py`, and then run it with | |
| `python app.py`. | |
| INFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example'] | |
| INFO: Uvicorn running on http://127.0.0.1:5001 (Press CTRL+C to quit) | |
| INFO: Started reloader process [871942] using WatchFiles | |
| INFO: Started server process [871945] | |
| INFO: Waiting for application startup. | |
| INFO: Application startup complete. | |
| If you navigate to <http://127.0.0.1:5001> in a browser, you’ll see your | |
| “Hello, World”. If you edit the `app.py` file and save it, the server | |
| will reload and you’ll see the updated message when you refresh the page | |
| in your browser. | |
| ## Constructing HTML | |
| Notice we wrote some HTML in the previous example. We don’t want to do | |
| that! Some web frameworks require that you learn HTML, CSS, JavaScript | |
| AND some templating language AND python. We want to do as much as | |
| possible with just one language. Fortunately, the Python module | |
| [fastcore.xml](https://fastcore.fast.ai/xml.html) has all we need for | |
| constructing HTML from Python, and FastHTML includes all the tags you | |
| need to get started. For example: | |
| ``` python | |
| from fasthtml.common import * | |
| page = Html( | |
| Head(Title('Some page')), | |
| Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass'))) | |
| print(to_xml(page)) | |
| ``` | |
| <!doctype html></!doctype> | |
| <html> | |
| <head> | |
| <title>Some page</title> | |
| </head> | |
| <body> | |
| <div class="myclass"> | |
| Some text, | |
| <a href="https://example.com">A link</a> | |
| <img src="https://placehold.co/200"> | |
| </div> | |
| </body> | |
| </html> | |
| ``` python | |
| show(page) | |
| ``` | |
| <!doctype html></!doctype> | |
| <html> | |
| <head> | |
| <title>Some page</title> | |
| </head> | |
| <body> | |
| <div class="myclass"> | |
| Some text, | |
| <a href="https://example.com">A link</a> | |
| <img src="https://placehold.co/200"> | |
| </div> | |
| </body> | |
| </html> | |
| If that `import *` worries you, you can always import only the tags you | |
| need. | |
| FastHTML is smart enough to know about fastcore.xml, and so you don’t | |
| need to use the `to_xml` function to convert your FT objects to HTML. | |
| You can just return them as you would any other Python object. For | |
| example, if we modify our previous example to use fastcore.xml, we can | |
| return an FT object directly: | |
| ``` python | |
| from fasthtml.common import * | |
| app = FastHTML() | |
| @app.get("/") | |
| def home(): | |
| page = Html( | |
| Head(Title('Some page')), | |
| Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass'))) | |
| return page | |
| serve() | |
| ``` | |
| This will render the HTML in the browser. | |
| For debugging, you can right-click on the rendered HTML in the browser | |
| and select “Inspect” to see the underlying HTML that was generated. | |
| There you’ll also find the ‘network’ tab, which shows you the requests | |
| that were made to render the page. Refresh and look for the request to | |
| `127.0.0.1` - and you’ll see it’s just a `GET` request to `/`, and the | |
| response body is the HTML you just returned. | |
| <div> | |
| > **Live Reloading** | |
| > | |
| > You can also enable [live reloading](../ref/live_reload.ipynb) so you | |
| > don’t have to manually refresh your browser to view updates. | |
| </div> | |
| You can also use Starlette’s `TestClient` to try it out in a notebook: | |
| ``` python | |
| from starlette.testclient import TestClient | |
| client = TestClient(app) | |
| r = client.get("/") | |
| print(r.text) | |
| ``` | |
| <html> | |
| <head><title>Some page</title> | |
| </head> | |
| <body><div class="myclass"> | |
| Some text, | |
| <a href="https://example.com">A link</a> | |
| <img src="https://placehold.co/200"> | |
| </div> | |
| </body> | |
| </html> | |
| FastHTML wraps things in an Html tag if you don’t do it yourself (unless | |
| the request comes from htmx, in which case you get the element | |
| directly). See [FT objects and HTML](#ft-objects-and-html) for more on | |
| creating custom components or adding HTML rendering to existing Python | |
| objects. To give the page a non-default title, return a Title before | |
| your main content: | |
| ``` python | |
| app = FastHTML() | |
| @app.get("/") | |
| def home(): | |
| return Title("Page Demo"), Div(H1('Hello, World'), P('Some text'), P('Some more text')) | |
| client = TestClient(app) | |
| print(client.get("/").text) | |
| ``` | |
| <!doctype html></!doctype> | |
| <html> | |
| <head> | |
| <title>Page Demo</title> | |
| <meta charset="utf-8"></meta> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"></meta> | |
| <script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js"></script> | |
| <script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> | |
| </head> | |
| <body> | |
| <div> | |
| <h1>Hello, World</h1> | |
| <p>Some text</p> | |
| <p>Some more text</p> | |
| </div> | |
| </body> | |
| </html> | |
| We’ll use this pattern often in the examples to follow. | |
| ## Defining Routes | |
| The HTTP protocol defines a number of methods (‘verbs’) to send requests | |
| to a server. The most common are GET, POST, PUT, DELETE, and HEAD. We | |
| saw ‘GET’ in action before - when you navigate to a URL, you’re making a | |
| GET request to that URL. We can do different things on a route for | |
| different HTTP methods. For example: | |
| ``` python | |
| @app.route("/", methods='get') | |
| def home(): | |
| return H1('Hello, World') | |
| @app.route("/", methods=['post', 'put']) | |
| def post_or_put(): | |
| return "got a POST or PUT request" | |
| ``` | |
| This says that when someone navigates to the root URL “/” (i.e. sends a | |
| GET request), they will see the big “Hello, World” heading. When someone | |
| submits a POST or PUT request to the same URL, the server should return | |
| the string “got a post or put request”. | |
| <div> | |
| > **Test the POST request** | |
| > | |
| > You can test the POST request with | |
| > `curl -X POST http://127.0.0.1:8000 -d "some data"`. This sends some | |
| > data to the server, you should see the response “got a post or put | |
| > request” printed in the terminal. | |
| </div> | |
| There are a few other ways you can specify the route+method - FastHTML | |
| has `.get`, `.post`, etc. as shorthand for | |
| `route(..., methods=['get'])`, etc. | |
| ``` python | |
| @app.get("/") | |
| def my_function(): | |
| return "Hello World from a GET request" | |
| ``` | |
| Or you can use the `@rt` decorator without a method but specify the | |
| method with the name of the function. For example: | |
| ``` python | |
| rt = app.route | |
| @rt("/") | |
| def post(): | |
| return "Hello World from a POST request" | |
| ``` | |
| ``` python | |
| client.post("/").text | |
| ``` | |
| 'Hello World from a POST request' | |
| You’re welcome to pick whichever style you prefer. Using routes lets you | |
| show different content on different pages - ‘/home’, ‘/about’ and so on. | |
| You can also respond differently to different kinds of requests to the | |
| same route, as shown above. You can also pass data via the route: | |
| <div class="panel-tabset"> | |
| ## `@app.get` | |
| ``` python | |
| @app.get("/greet/{nm}") | |
| def greet(nm:str): | |
| return f"Good day to you, {nm}!" | |
| client.get("/greet/Dave").text | |
| ``` | |
| 'Good day to you, Dave!' | |
| ## `@rt` | |
| ``` python | |
| @rt("/greet/{nm}") | |
| def get(nm:str): | |
| return f"Good day to you, {nm}!" | |
| client.get("/greet/Dave").text | |
| ``` | |
| 'Good day to you, Dave!' | |
| </div> | |
| More on this in the [More on Routing and Request | |
| Parameters](#more-on-routing-and-request-parameters) section, which goes | |
| deeper into the different ways to get information from a request. | |
| ## Styling Basics | |
| Plain HTML probably isn’t quite what you imagine when you visualize your | |
| beautiful web app. CSS is the go-to language for styling HTML. But | |
| again, we don’t want to learn extra languages unless we absolutely have | |
| to! Fortunately, there are ways to get much more visually appealing | |
| sites by relying on the hard work of others, using existing CSS | |
| libraries. One of our favourites is [PicoCSS](https://picocss.com/). A | |
| common way to add CSS files to web pages is to use a | |
| [`<link>`](https://www.w3schools.com/tags/tag_link.asp) tag inside your | |
| [HTML header](https://www.w3schools.com/tags/tag_header.asp), like this: | |
| ``` html | |
| <header> | |
| ... | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css"> | |
| </header> | |
| ``` | |
| For convenience, FastHTML already defines a Pico component for you with | |
| `picolink`: | |
| ``` python | |
| print(to_xml(picolink)) | |
| ``` | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css"> | |
| <style>:root { --pico-font-size: 100%; }</style> | |
| <div> | |
| > **Note** | |
| > | |
| > `picolink` also includes a `<style>` tag, as we found that setting the | |
| > font-size to 100% to be a good default. We show you how to override | |
| > this below. | |
| </div> | |
| Since we typically want CSS styling on all pages of our app, FastHTML | |
| lets you define a shared HTML header with the `hdrs` argument as shown | |
| below: | |
| ``` python | |
| from fasthtml.common import * | |
| css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}') | |
| app = FastHTML(hdrs=(picolink, css)) | |
| @app.route("/") | |
| def get(): | |
| return (Title("Hello World"), | |
| Main(H1('Hello, World'), cls="container")) | |
| ``` | |
| Line 2 | |
| Custom styling to override the pico defaults | |
| Line 3 | |
| Define shared headers for all pages | |
| Line 8 | |
| As per the [pico docs](https://picocss.com/docs), we put all of our | |
| content inside a `<main>` tag with a class of `container`: | |
| <div> | |
| > **Returning Tuples** | |
| > | |
| > We’re returning a tuple here (a title and the main page). Returning a | |
| > tuple, list, `FT` object, or an object with a `__ft__` method tells | |
| > FastHTML to turn the main body into a full HTML page that includes the | |
| > headers (including the pico link and our custom css) which we passed | |
| > in. This only occurs if the request isn’t from HTMX (for HTMX requests | |
| > we need only return the rendered components). | |
| </div> | |
| You can check out the Pico [examples](https://picocss.com/examples) page | |
| to see how different elements will look. If everything is working, the | |
| page should now render nice text with our custom font, and it should | |
| respect the user’s light/dark mode preferences too. | |
| If you want to [override the default | |
| styles](https://picocss.com/docs/css-variables) or add more custom CSS, | |
| you can do so by adding a `<style>` tag to the headers as shown above. | |
| So you are allowed to write CSS to your heart’s content - we just want | |
| to make sure you don’t necessarily have to! Later on we’ll see examples | |
| using other component libraries and tailwind css to do more fancy | |
| styling things, along with tips to get an LLM to write all those fiddly | |
| bits so you don’t have to. | |
| ## Web Page -\> Web App | |
| Showing content is all well and good, but we typically expect a bit more | |
| *interactivity* from something calling itself a web app! So, let’s add a | |
| few different pages, and use a form to let users add messages to a list: | |
| ``` python | |
| app = FastHTML() | |
| messages = ["This is a message, which will get rendered as a paragraph"] | |
| @app.get("/") | |
| def home(): | |
| return Main(H1('Messages'), | |
| *[P(msg) for msg in messages], | |
| A("Link to Page 2 (to add messages)", href="/page2")) | |
| @app.get("/page2") | |
| def page2(): | |
| return Main(P("Add a message with the form below:"), | |
| Form(Input(type="text", name="data"), | |
| Button("Submit"), | |
| action="/", method="post")) | |
| @app.post("/") | |
| def add_message(data:str): | |
| messages.append(data) | |
| return home() | |
| ``` | |
| We re-render the entire homepage to show the newly added message. This | |
| is fine, but modern web apps often don’t re-render the entire page, they | |
| just update a part of the page. In fact even very complicated | |
| applications are often implemented as ‘Single Page Apps’ (SPAs). This is | |
| where HTMX comes in. | |
| ## HTMX | |
| [HTMX](https://htmx.org/) addresses some key limitations of HTML. In | |
| vanilla HTML, links can trigger a GET request to show a new page, and | |
| forms can send requests containing data to the server. A lot of ‘Web | |
| 1.0’ design revolved around ways to use these to do everything we | |
| wanted. But why should only *some* elements be allowed to trigger | |
| requests? And why should we refresh the *entire page* with the result | |
| each time one does? HTMX extends HTML to allow us to trigger requests | |
| from *any* element on all kinds of events, and to update a part of the | |
| page without refreshing the entire page. It’s a powerful tool for | |
| building modern web apps. | |
| It does this by adding attributes to HTML tags to make them do things. | |
| For example, here’s a page with a counter and a button that increments | |
| it: | |
| ``` python | |
| app = FastHTML() | |
| count = 0 | |
| @app.get("/") | |
| def home(): | |
| return Title("Count Demo"), Main( | |
| H1("Count Demo"), | |
| P(f"Count is set to {count}", id="count"), | |
| Button("Increment", hx_post="/increment", hx_target="#count", hx_swap="innerHTML") | |
| ) | |
| @app.post("/increment") | |
| def increment(): | |
| print("incrementing") | |
| global count | |
| count += 1 | |
| return f"Count is set to {count}" | |
| ``` | |
| The button triggers a POST request to `/increment` (since we set | |
| `hx_post="/increment"`), which increments the count and returns the new | |
| count. The `hx_target` attribute tells HTMX where to put the result. If | |
| no target is specified it replaces the element that triggered the | |
| request. The `hx_swap` attribute specifies how it adds the result to the | |
| page. Useful options are: | |
| - *`innerHTML`*: Replace the target element’s content with the result. | |
| - *`outerHTML`*: Replace the target element with the result. | |
| - *`beforebegin`*: Insert the result before the target element. | |
| - *`beforeend`*: Insert the result inside the target element, after its | |
| last child. | |
| - *`afterbegin`*: Insert the result inside the target element, before | |
| its first child. | |
| - *`afterend`*: Insert the result after the target element. | |
| You can also use an hx_swap of `delete` to delete the target element | |
| regardless of response, or of `none` to do nothing. | |
| By default, requests are triggered by the “natural” event of an | |
| element - click in the case of a button (and most other elements). You | |
| can also specify different triggers, along with various modifiers - see | |
| the [HTMX docs](https://htmx.org/docs/#triggers) for more. | |
| This pattern of having elements trigger requests that modify or replace | |
| other elements is a key part of the HTMX philosophy. It takes a little | |
| getting used to, but once mastered it is extremely powerful. | |
| ### Replacing Elements Besides the Target | |
| Sometimes having a single target is not enough, and we’d like to specify | |
| some additional elements to update or remove. In these cases, returning | |
| elements with an id that matches the element to be replaced and | |
| `hx_swap_oob='true'` will replace those elements too. We’ll use this in | |
| the next example to clear an input field when we submit a form. | |
| ## Full Example \#1 - ToDo App | |
| The canonical demo web app! A TODO list. Rather than create yet another | |
| variant for this tutorial, we recommend starting with this video | |
| tutorial from Jeremy: | |
| <https://www.youtube.com/embed/Auqrm7WFc0I> | |
| <figure> | |
| <img src="by_example_files/figure-commonmark/cell-53-1-image.png" | |
| alt="image.png" /> | |
| <figcaption aria-hidden="true">image.png</figcaption> | |
| </figure> | |
| We’ve made a number of variants of this app - so in addition to the | |
| version shown in the video you can browse | |
| [this](https://github.com/AnswerDotAI/fasthtml-tut) series of examples | |
| with increasing complexity, the heavily-commented [“idiomatic” version | |
| here](https://github.com/AnswerDotAI/fasthtml/blob/main/examples/adv_app.py), | |
| and the | |
| [example](https://github.com/AnswerDotAI/fasthtml-example/tree/main/01_todo_app) | |
| linked from the [FastHTML homepage](https://fastht.ml/). | |
| ## Full Example \#2 - Image Generation App | |
| Let’s create an image generation app. We’d like to wrap a text-to-image | |
| model in a nice UI, where the user can type in a prompt and see a | |
| generated image appear. We’ll use a model hosted by | |
| [Replicate](https://replicate.com) to actually generate the images. | |
| Let’s start with the homepage, with a form to submit prompts and a div | |
| to hold the generated images: | |
| ``` python | |
| # Main page | |
| @app.get("/") | |
| def get(): | |
| inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt") | |
| add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin") | |
| gen_list = Div(id='gen-list') | |
| return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container') | |
| ``` | |
| Submitting the form will trigger a POST request to `/`, so next we need | |
| to generate an image and add it to the list. One problem: generating | |
| images is slow! We’ll start the generation in a separate thread, but | |
| this now surfaces a different problem: we want to update the UI right | |
| away, but our image will only be ready a few seconds later. This is a | |
| common pattern - think about how often you see a loading spinner online. | |
| We need a way to return a temporary bit of UI which will eventually be | |
| replaced by the final image. Here’s how we might do this: | |
| ``` python | |
| def generation_preview(id): | |
| if os.path.exists(f"gens/{id}.png"): | |
| return Div(Img(src=f"/gens/{id}.png"), id=f'gen-{id}') | |
| else: | |
| return Div("Generating...", id=f'gen-{id}', | |
| hx_post=f"/generations/{id}", | |
| hx_trigger='every 1s', hx_swap='outerHTML') | |
| @app.post("/generations/{id}") | |
| def get(id:int): return generation_preview(id) | |
| @app.post("/") | |
| def post(prompt:str): | |
| id = len(generations) | |
| generate_and_save(prompt, id) | |
| generations.append(prompt) | |
| clear_input = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt", hx_swap_oob='true') | |
| return generation_preview(id), clear_input | |
| @threaded | |
| def generate_and_save(prompt, id): ... | |
| ``` | |
| The form sends the prompt to the `/` route, which starts the generation | |
| in a separate thread then returns two things: | |
| - A generation preview element that will be added to the top of the | |
| `gen-list` div (since that is the target_id of the form which | |
| triggered the request) | |
| - An input field that will replace the form’s input field (that has the | |
| same id), using the hx_swap_oob=‘true’ trick. This clears the prompt | |
| field so the user can type another prompt. | |
| The generation preview first returns a temporary “Generating…” message, | |
| which polls the `/generations/{id}` route every second. This is done by | |
| setting hx_post to the route and hx_trigger to ‘every 1s’. The | |
| `/generations/{id}` route returns the preview element every second until | |
| the image is ready, at which point it returns the final image. Since the | |
| final image replaces the temporary one (hx_swap=‘outerHTML’), the | |
| polling stops running and the generation preview is now complete. | |
| This works nicely - the user can submit several prompts without having | |
| to wait for the first one to generate, and as the images become | |
| available they are added to the list. You can see the full code of this | |
| version | |
| [here](https://github.com/AnswerDotAI/fasthtml-example/blob/main/image_app_simple/draft1.py). | |
| ### Again, with Style | |
| The app is functional, but can be improved. The [next | |
| version](https://github.com/AnswerDotAI/fasthtml-example/blob/main/image_app_simple/main.py) | |
| adds more stylish generation previews, lays out the images in a grid | |
| layout that is responsive to different screen sizes, and adds a database | |
| to track generations and make them persistent. The database part is very | |
| similar to the todo list example, so let’s just quickly look at how we | |
| add the nice grid layout. This is what the result looks like: | |
| <figure> | |
| <img src="by_example_files/figure-commonmark/cell-58-1-image.png" | |
| alt="image.png" /> | |
| <figcaption aria-hidden="true">image.png</figcaption> | |
| </figure> | |
| Step one was looking around for existing components. The Pico CSS | |
| library we’ve been using has a rudimentary grid but recommends using an | |
| alternative layout system. One of the options listed was | |
| [Flexbox](http://flexboxgrid.com/). | |
| To use Flexbox you create a “row” with one or more elements. You can | |
| specify how wide things should be with a specific syntax in the class | |
| name. For example, `col-xs-12` means a box that will take up 12 columns | |
| (out of 12 total) of the row on extra small screens, `col-sm-6` means a | |
| column that will take up 6 columns of the row on small screens, and so | |
| on. So if you want four columns on large screens you would use | |
| `col-lg-3` for each item (i.e. each item is using 3 columns out of 12). | |
| ``` html | |
| <div class="row"> | |
| <div class="col-xs-12"> | |
| <div class="box">This takes up the full width</div> | |
| </div> | |
| </div> | |
| ``` | |
| This was non-intuitive to me. Thankfully ChatGPT et al know web stuff | |
| quite well, and we can also experiment in a notebook to test things out: | |
| ``` python | |
| grid = Html( | |
| Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css"), | |
| Div( | |
| Div(Div("This takes up the full width", cls="box", style="background-color: #800000;"), cls="col-xs-12"), | |
| Div(Div("This takes up half", cls="box", style="background-color: #008000;"), cls="col-xs-6"), | |
| Div(Div("This takes up half", cls="box", style="background-color: #0000B0;"), cls="col-xs-6"), | |
| cls="row", style="color: #fff;" | |
| ) | |
| ) | |
| show(grid) | |
| ``` | |
| <!doctype html></!doctype> | |
| <html> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css" type="text/css"> | |
| <div class="row" style="color: #fff;"> | |
| <div class="col-xs-12"> | |
| <div class="box" style="background-color: #800000;">This takes up the full width</div> | |
| </div> | |
| <div class="col-xs-6"> | |
| <div class="box" style="background-color: #008000;">This takes up half</div> | |
| </div> | |
| <div class="col-xs-6"> | |
| <div class="box" style="background-color: #0000B0;">This takes up half</div> | |
| </div> | |
| </div> | |
| </html> | |
| Aside: when in doubt with CSS stuff, add a background color or a border | |
| so you can see what’s happening! | |
| Translating this into our app, we have a new homepage with a | |
| `div (class="row")` to store the generated images / previews, and a | |
| `generation_preview` function that returns boxes with the appropriate | |
| classes and styles to make them appear in the grid. I chose a layout | |
| with different numbers of columns for different screen sizes, but you | |
| could also *just* specify the `col-xs` class if you wanted the same | |
| layout on all devices. | |
| ``` python | |
| gridlink = Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css") | |
| app = FastHTML(hdrs=(picolink, gridlink)) | |
| # Main page | |
| @app.get("/") | |
| def get(): | |
| inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt") | |
| add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin") | |
| gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with last 10 | |
| gen_list = Div(*gen_containers[::-1], id='gen-list', cls="row") # flexbox container: class = row | |
| return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container') | |
| # Show the image (if available) and prompt for a generation | |
| def generation_preview(g): | |
| grid_cls = "box col-xs-12 col-sm-6 col-md-4 col-lg-3" | |
| image_path = f"{g.folder}/{g.id}.png" | |
| if os.path.exists(image_path): | |
| return Div(Card( | |
| Img(src=image_path, alt="Card image", cls="card-img-top"), | |
| Div(P(B("Prompt: "), g.prompt, cls="card-text"),cls="card-body"), | |
| ), id=f'gen-{g.id}', cls=grid_cls) | |
| return Div(f"Generating gen {g.id} with prompt {g.prompt}", | |
| id=f'gen-{g.id}', hx_get=f"/gens/{g.id}", | |
| hx_trigger="every 2s", hx_swap="outerHTML", cls=grid_cls) | |
| ``` | |
| You can see the final result in | |
| [main.py](https://github.com/AnswerDotAI/fasthtml-example/blob/main/image_app_simple/main.py) | |
| in the `image_app_simple` example directory, along with info on | |
| deploying it (tl;dr don’t!). We’ve also deployed a version that only | |
| shows *your* generations (tied to browser session) and has a credit | |
| system to save our bank accounts. You can access that | |
| [here](https://image-gen-public-credit-pool.replit.app/). Now for the | |
| next question: how do we keep track of different users? | |
| ### Again, with Sessions | |
| At the moment everyone sees all images! How do we keep some sort of | |
| unique identifier tied to a user? Before going all the way to setting up | |
| users, login pages etc., let’s look at a way to at least limit | |
| generations to the user’s *session*. You could do this manually with | |
| cookies. For convenience and security, fasthtml (via Starlette) has a | |
| special mechanism for storing small amounts of data in the user’s | |
| browser via the `session` argument to your route. This acts like a | |
| dictionary and you can set and get values from it. For example, here we | |
| look for a `session_id` key, and if it doesn’t exist we generate a new | |
| one: | |
| ``` python | |
| @app.get("/") | |
| def get(session): | |
| if 'session_id' not in session: session['session_id'] = str(uuid.uuid4()) | |
| return H1(f"Session ID: {session['session_id']}") | |
| ``` | |
| Refresh the page a few times - you’ll notice that the session ID remains | |
| the same. If you clear your browsing data, you’ll get a new session ID. | |
| And if you load the page in a different browser (but not a different | |
| tab), you’ll get a new session ID. This will persist within the current | |
| browser, letting us use it as a key for our generations. As a bonus, | |
| someone can’t spoof this session id by passing it in another way (for | |
| example, sending a query parameter). Behind the scenes, the data *is* | |
| stored in a browser cookie but it is signed with a secret key that stops | |
| the user or anyone nefarious from being able to tamper with it. The | |
| cookie is decoded back into a dictionary by something called a | |
| middleware function, which we won’t cover here. All you need to know is | |
| that we can use this to store bits of state in the user’s browser. | |
| In the image app example, we can add a `session_id` column to our | |
| database, and modify our homepage like so: | |
| ``` python | |
| @app.get("/") | |
| def get(session): | |
| if 'session_id' not in session: session['session_id'] = str(uuid.uuid4()) | |
| inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt") | |
| add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list', hx_swap="afterbegin") | |
| gen_containers = [generation_preview(g) for g in gens(limit=10, where=f"session_id == '{session['session_id']}'")] | |
| ... | |
| ``` | |
| So we check if the session id exists in the session, add one if not, and | |
| then limit the generations shown to only those tied to this session id. | |
| We filter the database with a where clause - see \[TODO link Jeremy’s | |
| example for a more reliable way to do this\]. The only other change we | |
| need to make is to store the session id in the database when a | |
| generation is made. You can check out this version | |
| [here](https://github.com/AnswerDotAI/fasthtml-example/blob/main/image_app_session_credits/session.py). | |
| You could instead write this app without relying on a database at all - | |
| simply storing the filenames of the generated images in the session, for | |
| example. But this more general approach of linking some kind of unique | |
| session identifier to users or data in our tables is a useful general | |
| pattern for more complex examples. | |
| ### Again, with Credits! | |
| Generating images with replicate costs money. So next let’s add a pool | |
| of credits that get used up whenever anyone generates an image. To | |
| recover our lost funds, we’ll also set up a payment system so that | |
| generous users can buy more credits for everyone. You could modify this | |
| to let users buy credits tied to their session ID, but at that point you | |
| risk having angry customers losing their money after wiping their | |
| browser history, and should consider setting up proper account | |
| management :) | |
| Taking payments with Stripe is intimidating but very doable. [Here’s a | |
| tutorial](https://testdriven.io/blog/flask-stripe-tutorial/) that shows | |
| the general principle using Flask. As with other popular tasks in the | |
| web-dev world, ChatGPT knows a lot about Stripe - but you should | |
| exercise extra caution when writing code that handles money! | |
| For the [finished | |
| example](https://github.com/AnswerDotAI/fasthtml-example/blob/main/image_app_session_credits/main.py) | |
| we add the bare minimum: | |
| - A way to create a Stripe checkout session and redirect the user to the | |
| session URL | |
| - ‘Success’ and ‘Cancel’ routes to handle the result of the checkout | |
| - A route that listens for a webhook from Stripe to update the number of | |
| credits when a payment is made. | |
| In a typical application you’ll want to keep track of which users make | |
| payments, catch other kinds of stripe events and so on. This example is | |
| more a ‘this is possible, do your own research’ than ‘this is how you do | |
| it’. But hopefully it does illustrate the key idea: there is no magic | |
| here. Stripe (and many other technologies) relies on sending users to | |
| different routes and shuttling data back and forth in requests. And we | |
| know how to do that! | |
| ## More on Routing and Request Parameters | |
| There are a number of ways information can be passed to the server. When | |
| you specify arguments to a route, FastHTML will search the request for | |
| values with the same name, and convert them to the correct type. In | |
| order, it searches | |
| - The path parameters | |
| - The query parameters | |
| - The cookies | |
| - The headers | |
| - The session | |
| - Form data | |
| There are also a few special arguments | |
| - `request` (or any prefix like `req`): gets the raw Starlette `Request` | |
| object | |
| - `session` (or any prefix like `sess`): gets the session object | |
| - `auth` | |
| - `htmx` | |
| - `app` | |
| In this section let’s quickly look at some of these in action. | |
| ``` python | |
| from fasthtml.common import * | |
| from starlette.testclient import TestClient | |
| app = FastHTML() | |
| cli = TestClient(app) | |
| ``` | |
| Part of the route (path parameters): | |
| ``` python | |
| @app.get('/user/{nm}') | |
| def _(nm:str): return f"Good day to you, {nm}!" | |
| cli.get('/user/jph').text | |
| ``` | |
| 'Good day to you, jph!' | |
| Matching with a regex: | |
| ``` python | |
| reg_re_param("imgext", "ico|gif|jpg|jpeg|webm") | |
| @app.get(r'/static/{path:path}/{fn}.{ext:imgext}') | |
| def get_img(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}" | |
| cli.get('/static/foo/jph.ico').text | |
| ``` | |
| 'Getting jph.ico from /foo/' | |
| Using an enum (try using a string that isn’t in the enum): | |
| ``` python | |
| ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet") | |
| @app.get("/models/{nm}") | |
| def model(nm:ModelName): return nm | |
| print(cli.get('/models/alexnet').text) | |
| ``` | |
| alexnet | |
| Casting to a Path: | |
| ``` python | |
| @app.get("/files/{path}") | |
| def txt(path: Path): return path.with_suffix('.txt') | |
| print(cli.get('/files/foo').text) | |
| ``` | |
| foo.txt | |
| An integer with a default value: | |
| ``` python | |
| fake_db = [{"name": "Foo"}, {"name": "Bar"}] | |
| @app.get("/items/") | |
| def read_item(idx: int = 0): return fake_db[idx] | |
| print(cli.get('/items/?idx=1').text) | |
| ``` | |
| {"name":"Bar"} | |
| ``` python | |
| # Equivalent to `/items/?idx=0`. | |
| print(cli.get('/items/').text) | |
| ``` | |
| {"name":"Foo"} | |
| Boolean values (takes anything “truthy” or “falsy”): | |
| ``` python | |
| @app.get("/booly/") | |
| def booly(coming:bool=True): return 'Coming' if coming else 'Not coming' | |
| print(cli.get('/booly/?coming=true').text) | |
| ``` | |
| Coming | |
| ``` python | |
| print(cli.get('/booly/?coming=no').text) | |
| ``` | |
| Not coming | |
| Getting dates: | |
| ``` python | |
| @app.get("/datie/") | |
| def datie(d:parsed_date): return d | |
| date_str = "17th of May, 2024, 2p" | |
| print(cli.get(f'/datie/?d={date_str}').text) | |
| ``` | |
| 2024-05-17 14:00:00 | |
| Matching a dataclass: | |
| ``` python | |
| from dataclasses import dataclass, asdict | |
| @dataclass | |
| class Bodie: | |
| a:int;b:str | |
| @app.route("/bodie/{nm}") | |
| def post(nm:str, data:Bodie): | |
| res = asdict(data) | |
| res['nm'] = nm | |
| return res | |
| cli.post('/bodie/me', data=dict(a=1, b='foo')).text | |
| ``` | |
| '{"a":1,"b":"foo","nm":"me"}' | |
| ### Cookies | |
| Cookies can be set via a Starlette Response object, and can be read back | |
| by specifying the name: | |
| ``` python | |
| from datetime import datetime | |
| @app.get("/setcookie") | |
| def setc(req): | |
| now = datetime.now() | |
| res = Response(f'Set to {now}') | |
| res.set_cookie('now', str(now)) | |
| return res | |
| cli.get('/setcookie').text | |
| ``` | |
| 'Set to 2024-07-20 23:14:54.364793' | |
| ``` python | |
| @app.get("/getcookie") | |
| def getc(now:parsed_date): return f'Cookie was set at time {now.time()}' | |
| cli.get('/getcookie').text | |
| ``` | |
| 'Cookie was set at time 23:14:54.364793' | |
| ### User Agent and HX-Request | |
| An argument of `user_agent` will match the header `User-Agent`. This | |
| holds for special headers like `HX-Request` (used by HTMX to signal when | |
| a request comes from an HTMX request) - the general pattern is that “-” | |
| is replaced with “\_” and strings are turned to lowercase. | |
| ``` python | |
| @app.get("/ua") | |
| async def ua(user_agent:str): return user_agent | |
| cli.get('/ua', headers={'User-Agent':'FastHTML'}).text | |
| ``` | |
| 'FastHTML' | |
| ``` python | |
| @app.get("/hxtest") | |
| def hxtest(htmx): return htmx.request | |
| cli.get('/hxtest', headers={'HX-Request':'1'}).text | |
| ``` | |
| '1' | |
| ### Starlette Requests | |
| If you add an argument called `request`(or any prefix of that, for | |
| example `req`) it will be populated with the Starlette `Request` object. | |
| This is useful if you want to do your own processing manually. For | |
| example, although FastHTML will parse forms for you, you could instead | |
| get form data like so: | |
| ``` python | |
| @app.get("/form") | |
| async def form(request:Request): | |
| form_data = await request.form() | |
| a = form_data.get('a') | |
| ``` | |
| See the [Starlette docs](https://starlette.io/docs/) for more | |
| information on the `Request` object. | |
| ### Starlette Responses | |
| You can return a Starlette Response object from a route to control the | |
| response. For example: | |
| ``` python | |
| @app.get("/redirect") | |
| def redirect(): | |
| return RedirectResponse(url="/") | |
| ``` | |
| We used this to set cookies in the previous example. See the [Starlette | |
| docs](https://starlette.io/docs/) for more information on the `Response` | |
| object. | |
| ### Static Files | |
| We often want to serve static files like images. This is easily done! | |
| For common file types (images, CSS etc) we can create a route that | |
| returns a Starlette `FileResponse` like so: | |
| ``` python | |
| # For images, CSS, etc. | |
| @app.get("/{fname:path}.{ext:static}") | |
| def static(fname: str, ext: str): | |
| return FileResponse(f'{fname}.{ext}') | |
| ``` | |
| You can customize it to suit your needs (for example, only serving files | |
| in a certain directory). You’ll notice some variant of this route in all | |
| our complete examples - even for apps with no static files the browser | |
| will typically request a `/favicon.ico` file, for example, and as the | |
| astute among you will have noticed this has sparked a bit of competition | |
| between Johno and Jeremy regarding which country flag should serve as | |
| the default! | |
| ### WebSockets | |
| For certain applications such as multiplayer games, websockets can be a | |
| powerful feature. Luckily HTMX and FastHTML has you covered! Simply | |
| specify that you wish to include the websocket header extension from | |
| HTMX: | |
| ``` python | |
| app = FastHTML(exts='ws') | |
| rt = app.route | |
| ``` | |
| With that, you are now able to specify the different websocket specific | |
| HTMX goodies. For example, say we have a website we want to setup a | |
| websocket, you can simply: | |
| ``` python | |
| def mk_inp(): return Input(id='msg') | |
| @rt('/') | |
| async def get(request): | |
| cts = Div( | |
| Div(id='notifications'), | |
| Form(mk_inp(), id='form', ws_send=True), | |
| hx_ext='ws', ws_connect='/ws') | |
| return Titled('Websocket Test', cts) | |
| ``` | |
| And this will setup a connection on the route `/ws` along with a form | |
| that will send a message to the websocket whenever the form is | |
| submitted. Let’s go ahead and handle this route: | |
| ``` python | |
| @app.ws('/ws') | |
| async def ws(msg:str, send): | |
| await send(Div('Hello ' + msg, id="notifications")) | |
| await sleep(2) | |
| return Div('Goodbye ' + msg, id="notifications"), mk_inp() | |
| ``` | |
| One thing you might have noticed is a lack of target id for our | |
| websocket trigger for swapping HTML content. This is because HTMX always | |
| swaps content with websockets with Out of Band Swaps. Therefore, HTMX | |
| will look for the id in the returned HTML content from the server for | |
| determining what to swap. To send stuff to the client, you can either | |
| use the `send` parameter or simply return the content or both! | |
| Now, sometimes you might want to perform actions when a client connects | |
| or disconnects such as add or remove a user from a player queue. To hook | |
| into these events, you can pass your connection or disconnection | |
| function to the `app.ws` decorator: | |
| ``` python | |
| async def on_connect(send): | |
| print('Connected!') | |
| await send(Div('Hello, you have connected', id="notifications")) | |
| async def on_disconnect(ws): | |
| print('Disconnected!') | |
| @app.ws('/ws', conn=on_connect, disconn=on_disconnect) | |
| async def ws(msg:str, send): | |
| await send(Div('Hello ' + msg, id="notifications")) | |
| await sleep(2) | |
| return Div('Goodbye ' + msg, id="notifications"), mk_inp() | |
| ``` | |
| ## Full Example \#3 - Chatbot Example with DaisyUI Components | |
| Let’s go back to the topic of adding components or styling beyond the | |
| simple PicoCSS examples so far. How might we adopt a component or | |
| framework? In this example, let’s build a chatbot UI leveraging the | |
| [DaisyUI chat bubble](https://daisyui.com/components/chat/). The final | |
| result will look like this: | |
| <figure> | |
| <img src="by_example_files/figure-commonmark/cell-101-1-image.png" | |
| alt="image.png" /> | |
| <figcaption aria-hidden="true">image.png</figcaption> | |
| </figure> | |
| At first glance, DaisyUI’s chat component looks quite intimidating. The | |
| examples look like this: | |
| ``` html | |
| <div class="chat chat-start"> | |
| <div class="chat-image avatar"> | |
| <div class="w-10 rounded-full"> | |
| <img alt="Tailwind CSS chat bubble component" src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg" /> | |
| </div> | |
| </div> | |
| <div class="chat-header"> | |
| Obi-Wan Kenobi | |
| <time class="text-xs opacity-50">12:45</time> | |
| </div> | |
| <div class="chat-bubble">You were the Chosen One!</div> | |
| <div class="chat-footer opacity-50"> | |
| Delivered | |
| </div> | |
| </div> | |
| <div class="chat chat-end"> | |
| <div class="chat-image avatar"> | |
| <div class="w-10 rounded-full"> | |
| <img alt="Tailwind CSS chat bubble component" src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg" /> | |
| </div> | |
| </div> | |
| <div class="chat-header"> | |
| Anakin | |
| <time class="text-xs opacity-50">12:46</time> | |
| </div> | |
| <div class="chat-bubble">I hate you!</div> | |
| <div class="chat-footer opacity-50"> | |
| Seen at 12:46 | |
| </div> | |
| </div> | |
| ``` | |
| We have several things going for us however. | |
| - ChatGPT knows DaisyUI and Tailwind (DaisyUI is a Tailwind component | |
| library) | |
| - We can build things up piece by piece with AI standing by to help. | |
| <https://h2f.answer.ai/> is a tool that can convert HTML to FT | |
| (fastcore.xml) and back, which is useful for getting a quick starting | |
| point when you have an HTML example to start from. | |
| We can strip out some unnecessary bits and try to get the simplest | |
| possible example working in a notebook first: | |
| ``` python | |
| # Loading tailwind and daisyui | |
| headers = (Script(src="https://cdn.tailwindcss.com"), | |
| Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css")) | |
| # Displaying a single message | |
| d = Div( | |
| Div("Chat header here", cls="chat-header"), | |
| Div("My message goes here", cls="chat-bubble chat-bubble-primary"), | |
| cls="chat chat-start" | |
| ) | |
| # show(Html(*headers, d)) # uncomment to view | |
| ``` | |
| Now we can extend this to render multiple messages, with the message | |
| being on the left (`chat-start`) or right (`chat-end`) depending on the | |
| role. While we’re at it, we can also change the color | |
| (`chat-bubble-primary`) of the message and put them all in a `chat-box` | |
| div: | |
| ``` python | |
| messages = [ | |
| {"role":"user", "content":"Hello"}, | |
| {"role":"assistant", "content":"Hi, how can I assist you?"} | |
| ] | |
| def ChatMessage(msg): | |
| return Div( | |
| Div(msg['role'], cls="chat-header"), | |
| Div(msg['content'], cls=f"chat-bubble chat-bubble-{'primary' if msg['role'] == 'user' else 'secondary'}"), | |
| cls=f"chat chat-{'end' if msg['role'] == 'user' else 'start'}") | |
| chatbox = Div(*[ChatMessage(msg) for msg in messages], cls="chat-box", id="chatlist") | |
| # show(Html(*headers, chatbox)) # Uncomment to view | |
| ``` | |
| Next, it was back to the ChatGPT to tweak the chat box so it wouldn’t | |
| grow as messages were added. I asked: | |
| "I have something like this (it's working now) | |
| [code] | |
| The messages are added to this div so it grows over time. | |
| Is there a way I can set it's height to always be 80% of the total window height with a scroll bar if needed?" | |
| Based on this query GPT4o helpfully shared that “This can be achieved | |
| using Tailwind CSS utility classes. Specifically, you can use h-\[80vh\] | |
| to set the height to 80% of the viewport height, and overflow-y-auto to | |
| add a vertical scroll bar when needed.” | |
| To put it another way: none of the CSS classes in the following example | |
| were written by a human, and what edits I did make were informed by | |
| advice from the AI that made it relatively painless! | |
| The actual chat functionality of the app is based on our | |
| [claudette](https://claudette.answer.ai/) library. As with the image | |
| example, we face a potential hiccup in that getting a response from an | |
| LLM is slow. We need a way to have the user message added to the UI | |
| immediately, and then have the response added once it’s available. We | |
| could do something similar to the image generation example above, or use | |
| websockets. Check out the [full | |
| example](https://github.com/AnswerDotAI/fasthtml-example/tree/main/02_chatbot) | |
| for implementations of both, along with further details. | |
| ## Full Example \#4 - Multiplayer Game of Life Example with Websockets | |
| Let’s see how we can implement a collaborative website using Websockets | |
| in FastHTML. To showcase this, we will use the famous [Conway’s Game of | |
| Life](https://en.wikipedia.org/wiki/Conway's_Game_of_Life), which is a | |
| game that takes place in a grid world. Each cell in the grid can be | |
| either alive or dead. The cell’s state is initially given by a user | |
| before the game is started and then evolves through the iteration of the | |
| grid world once the clock starts. Whether a cell’s state will change | |
| from the previous state depends on simple rules based on its neighboring | |
| cells’ states. Here is the standard Game of Life logic implemented in | |
| Python courtesy of ChatGPT: | |
| ``` python | |
| grid = [[0 for _ in range(20)] for _ in range(20)] | |
| def update_grid(grid: list[list[int]]) -> list[list[int]]: | |
| new_grid = [[0 for _ in range(20)] for _ in range(20)] | |
| def count_neighbors(x, y): | |
| directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] | |
| count = 0 | |
| for dx, dy in directions: | |
| nx, ny = x + dx, y + dy | |
| if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): count += grid[nx][ny] | |
| return count | |
| for i in range(len(grid)): | |
| for j in range(len(grid[0])): | |
| neighbors = count_neighbors(i, j) | |
| if grid[i][j] == 1: | |
| if neighbors < 2 or neighbors > 3: new_grid[i][j] = 0 | |
| else: new_grid[i][j] = 1 | |
| elif neighbors == 3: new_grid[i][j] = 1 | |
| return new_grid | |
| ``` | |
| This would be a very dull game if we were to run it, since the initial | |
| state of everything would remain dead. Therefore, we need a way of | |
| letting the user give an initial state before starting the game. | |
| FastHTML to the rescue! | |
| ``` python | |
| def Grid(): | |
| cells = [] | |
| for y, row in enumerate(game_state['grid']): | |
| for x, cell in enumerate(row): | |
| cell_class = 'alive' if cell else 'dead' | |
| cell = Div(cls=f'cell {cell_class}', hx_put='/update', hx_vals={'x': x, 'y': y}, hx_swap='none', hx_target='#gol', hx_trigger='click') | |
| cells.append(cell) | |
| return Div(*cells, id='grid') | |
| @rt('/update') | |
| async def put(x: int, y: int): | |
| grid[y][x] = 1 if grid[y][x] == 0 else 0 | |
| ``` | |
| Above is a component for representing the game’s state that the user can | |
| interact with and update on the server using cool HTMX features such as | |
| `hx_vals` for determining which cell was clicked to make it dead or | |
| alive. Now, you probably noticed that the HTTP request in this case is a | |
| PUT request, which does not return anything and this means our client’s | |
| view of the grid world and the server’s game state will immediately | |
| become out of sync :(. We could of course just return a new Grid | |
| component with the updated state, but that would only work for a single | |
| client, if we had more, they quickly get out of sync with each other and | |
| the server. Now Websockets to the rescue! | |
| Websockets are a way for the server to keep a persistent connection with | |
| clients and send data to the client without explicitly being requested | |
| for information, which is not possible with HTTP. Luckily FastHTML and | |
| HTMX work well with Websockets. Simply state you wish to use websockets | |
| for your app and define a websocket route: | |
| ``` python | |
| ... | |
| app = FastHTML(hdrs=(picolink, gridlink, css, htmx_ws), exts='ws') | |
| player_queue = [] | |
| async def update_players(): | |
| for i, player in enumerate(player_queue): | |
| try: await player(Grid()) | |
| except: player_queue.pop(i) | |
| async def on_connect(send): player_queue.append(send) | |
| async def on_disconnect(send): await update_players() | |
| @app.ws('/gol', conn=on_connect, disconn=on_disconnect) | |
| async def ws(msg:str, send): pass | |
| def Home(): return Title('Game of Life'), Main(gol, Div(Grid(), id='gol', cls='row center-xs'), hx_ext="ws", ws_connect="/gol") | |
| @rt('/update') | |
| async def put(x: int, y: int): | |
| grid[y][x] = 1 if grid[y][x] == 0 else 0 | |
| await update_players() | |
| ... | |
| ``` | |
| Here we simply keep track of all the players that have connected or | |
| disconnected to our site and when an update occurs, we send updates to | |
| all the players still connected via websockets. Via HTMX, you are still | |
| simply exchanging HTML from the server to the client and will swap in | |
| the content based on how you setup your `hx_swap` attribute. There is | |
| only one difference, that being all swaps are OOB. You can find more | |
| information on the HTMX websocket extension documentation page | |
| [here](https://github.com/bigskysoftware/htmx-extensions/blob/main/src/ws/README.md). | |
| You can find a full fledge hosted example of this app | |
| [here](https://game-of-life-production-ed7f.up.railway.app/). | |
| ## FT objects and HTML | |
| These FT objects create a ‘FastTag’ structure \[tag,children,attrs\] for | |
| `to_xml()`. When we call `Div(...)`, the elements we pass in are the | |
| children. Attributes are passed in as keywords. `class` and `for` are | |
| special words in python, so we use `cls`, `klass` or `_class` instead of | |
| `class` and `fr` or `_for` instead of `for`. Note these objects are just | |
| 3-element lists - you can create custom ones too as long as they’re also | |
| 3-element lists. Alternately, leaf nodes can be strings instead (which | |
| is why you can do `Div('some text')`). If you pass something that isn’t | |
| a 3-element list or a string, it will be converted to a string using | |
| str()… unless (our final trick) you define a `__ft__` method that will | |
| run before str(), so you can render things a custom way. | |
| For example, here’s one way we could make a custom class that can be | |
| rendered into HTML: | |
| ``` python | |
| class Person: | |
| def __init__(self, name, age): | |
| self.name = name | |
| self.age = age | |
| def __ft__(self): | |
| return ['div', [f'{self.name} is {self.age} years old.'], {}] | |
| p = Person('Jonathan', 28) | |
| print(to_xml(Div(p, "more text", cls="container"))) | |
| ``` | |
| <div class="container"> | |
| <div>Jonathan is 28 years old.</div> | |
| more text | |
| </div> | |
| In the examples, you’ll see we often patch in `__ft__` methods to | |
| existing classes to control how they’re rendered. For example, if Person | |
| didn’t have a `__ft__` method or we wanted to override it, we could add | |
| a new one like this: | |
| ``` python | |
| from fastcore.all import patch | |
| @patch | |
| def __ft__(self:Person): | |
| return Div("Person info:", Ul(Li("Name:",self.name), Li("Age:", self.age))) | |
| show(p) | |
| ``` | |
| <div> | |
| Person info: | |
| <ul> | |
| <li> | |
| Name: | |
| Jonathan | |
| </li> | |
| <li> | |
| Age: | |
| 28 | |
| </li> | |
| </ul> | |
| </div> | |
| Some tags from fastcore.xml are overwritten by fasthtml.core and a few | |
| are further extended by fasthtml.xtend using this method. Over time, we | |
| hope to see others developing custom components too, giving us a larger | |
| and larger ecosystem of reusable components. | |
| ## Custom Scripts and Styling | |
| There are many popular JavaScript and CSS libraries that can be used via | |
| a simple [`Script`](https://www.fastht.ml/docs/api/xtend.html#script) or | |
| [`Style`](https://www.fastht.ml/docs/api/xtend.html#style) tag. But in | |
| some cases you will need to write more custom code. FastHTML’s | |
| [js.py](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/js.py) | |
| contains a few examples that may be useful as reference. | |
| For example, to use the [marked.js](https://marked.js.org/) library to | |
| render markdown in a div, including in components added after the page | |
| has loaded via htmx, we do something like this: | |
| ``` javascript | |
| import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; | |
| proc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent)); | |
| ``` | |
| `proc_htmx` is a shortcut that we wrote to apply a function to elements | |
| matching a selector, including the element that triggered the event. | |
| Here’s the code for reference: | |
| ``` javascript | |
| export function proc_htmx(sel, func) { | |
| htmx.onLoad(elt => { | |
| const elements = htmx.findAll(elt, sel); | |
| if (elt.matches(sel)) elements.unshift(elt) | |
| elements.forEach(func); | |
| }); | |
| } | |
| ``` | |
| The [AI Pictionary | |
| example](https://github.com/AnswerDotAI/fasthtml-example/tree/main/03_pictionary) | |
| uses a larger chunk of custom JavaScript to handle the drawing canvas. | |
| It’s a good example of the type of application where running code on the | |
| client side makes the most sense, but still shows how you can integrate | |
| it with FastHTML on the server side to add functionality (like the AI | |
| responses) easily. | |
| Adding styling with custom CSS and libraries such as tailwind is done | |
| the same way we add custom JavaScript. The [doodle | |
| example](https://github.com/AnswerDotAI/fasthtml-example/tree/main/doodle) | |
| uses [Doodle.CSS](https://github.com/chr15m/DoodleCSS) to style the page | |
| in a quirky way. | |
| ## Deploying Your App | |
| We can deploy FastHTML almost anywhere you can deploy python apps. We’ve | |
| tested Railway, Replit, | |
| [HuggingFace](https://github.com/AnswerDotAI/fasthtml-hf), and | |
| [PythonAnywhere](https://github.com/AnswerDotAI/fasthtml-example/blob/main/deploying-to-pythonanywhere.md). | |
| ### Railway | |
| 1. [Install the Railway CLI](https://docs.railway.app/guides/cli) and | |
| sign up for an account. | |
| 2. Set up a folder with our app as `main.py` | |
| 3. In the folder, run `railway login`. | |
| 4. Use the `fh_railway_deploy` script to deploy our project: | |
| ``` bash | |
| fh_railway_deploy MY_APP_NAME | |
| ``` | |
| What the script does for us: | |
| 4. Do we have an existing railway project? | |
| - Yes: Link the project folder to our existing Railway project. | |
| - No: Create a new Railway project. | |
| 5. Deploy the project. We’ll see the logs as the service is built and | |
| run! | |
| 6. Fetches and displays the URL of our app. | |
| 7. By default, mounts a `/app/data` folder on the cloud to our app’s | |
| root folder. The app is run in `/app` by default, so from our app | |
| anything we store in `/data` will persist across restarts. | |
| A final note about Railway: We can add secrets like API keys that can be | |
| accessed as environment variables from our apps via | |
| [‘Variables’](https://docs.railway.app/guides/variables). For example, | |
| for the [image generation | |
| app](https://github.com/AnswerDotAI/fasthtml-example/tree/main/image_app_simple), | |
| we can add a `REPLICATE_API_KEY` variable, and then in `main.py` we can | |
| access it as `os.environ['REPLICATE_API_KEY']`. | |
| ### Replit | |
| Fork [this repl](https://replit.com/@johnowhitaker/FastHTML-Example) for | |
| a minimal example you can edit to your heart’s content. `.replit` has | |
| been edited to add the right run command | |
| (`run = ["uvicorn", "main:app", "--reload"]`) and to set up the ports | |
| correctly. FastHTML was installed with `poetry add python-fasthtml`, you | |
| can add additional packages as needed in the same way. Running the app | |
| in Replit will show you a webview, but you may need to open in a new tab | |
| for all features (such as cookies) to work. When you’re ready, you can | |
| deploy your app by clicking the ‘Deploy’ button. You pay for usage - for | |
| an app that is mostly idle the cost is usually a few cents per month. | |
| You can store secrets like API keys via the ‘Secrets’ tab in the Replit | |
| project settings. | |
| ### HuggingFace | |
| Follow the instructions in [this | |
| repository](https://github.com/AnswerDotAI/fasthtml-hf) to deploy to | |
| HuggingFace spaces. | |
| ## Where Next? | |
| We’ve covered a lot of ground here! Hopefully this has given you plenty | |
| to work with in building your own FastHTML apps. If you have any | |
| questions, feel free to ask in the \#fasthtml Discord channel (in the | |
| fastai Discord community). You can look through the other examples in | |
| the [fasthtml-example | |
| repository](https://github.com/AnswerDotAI/fasthtml-example) for more | |
| ideas, and keep an eye on Jeremy’s [YouTube | |
| channel](https://www.youtube.com/@howardjeremyp) where we’ll be | |
| releasing a number of “dev chats” related to FastHTML in the near | |
| future. | |