[ { "objectID": "index.html", "href": "index.html", "title": "FastHTML", "section": "", "text": "Welcome to the official FastHTML documentation.\nFastHTML is a new next-generation web framework for fast, scalable web applications with minimal, compact code. It’s designed to be:\nFastHTML apps are just Python code, so you can use FastHTML with the full power of the Python language and ecosystem. FastHTML’s functionality maps 1:1 directly to HTML and HTTP, but allows them to be encapsulated using good software engineering practices—so you’ll need to understand these foundations to use this library fully. To understand how and why this works, please read this first: fastht.ml/about.", "crumbs": [ "Home", "Get Started" ] }, { "objectID": "index.html#installation", "href": "index.html#installation", "title": "FastHTML", "section": "Installation", "text": "Installation\nSince fasthtml is a Python library, you can install it with:\npip install python-fasthtml\nIn the near future, we hope to add component libraries that can likewise be installed via pip.", "crumbs": [ "Home", "Get Started" ] }, { "objectID": "index.html#usage", "href": "index.html#usage", "title": "FastHTML", "section": "Usage", "text": "Usage\nFor a minimal app, create a file “main.py” as follows:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\napp,rt = fast_app()\n\n@rt('/')\ndef get(): return Div(P('Hello World!'), hx_get=\"/change\")\n\nserve()\n\nRunning the app with python main.py prints out a link to your running app: http://localhost:5001. Visit that link in your browser and you should see a page with the text “Hello World!”. Congratulations, you’ve just created your first FastHTML app!\nAdding interactivity is surprisingly easy, thanks to HTMX. Modify the file to add this function:\n\n\nmain.py\n\n@rt('/change')\ndef get(): return P('Nice to be here!')\n\nYou now have a page with a clickable element that changes the text when clicked. When clicking on this link, the server will respond with an “HTML partial”—that is, just a snippet of HTML which will be inserted into the existing page. In this case, the returned element will replace the original P element (since that’s the default behavior of HTMX) with the new version returned by the second route.\nThis “hypermedia-based” approach to web development is a powerful way to build web applications.\n\nGetting help from AI\nBecause FastHTML is newer than most LLMs, AI systems like Cursor, ChatGPT, Claude, and Copilot won’t give useful answers about it. To fix that problem, we’ve provided an LLM-friendly guide that teaches them how to use FastHTML. To use it, add this link for your AI helper to use:\n\n/llms-ctx.txt\n\nThis example is in a format based on recommendations from Anthropic for use with Claude Projects. This works so well that we’ve actually found that Claude can provide even better information than our own documentation! For instance, read through this annotated Claude chat for some great getting-started information, entirely generated from a project using the above text file as context.\nIf you use Cursor, type @doc then choose “Add new doc”, and use the /llms-ctx.txt link above. The context file is auto-generated from our llms.txt (our proposed standard for providing AI-friendly information)—you can generate alternative versions suitable for other models as needed.", "crumbs": [ "Home", "Get Started" ] }, { "objectID": "index.html#next-steps", "href": "index.html#next-steps", "title": "FastHTML", "section": "Next Steps", "text": "Next Steps\nStart with the official sources to learn more about FastHTML:\n\nAbout: Learn about the core ideas behind FastHTML\nDocumentation: Learn from examples how to write FastHTML code\nIdiomatic app: Heavily commented source code walking through a complete application, including custom authentication, JS library connections, and database use.\n\nWe also have a 1-hour intro video:\n\nThe capabilities of FastHTML are vast and growing, and not all the features and patterns have been documented yet. Be prepared to invest time into studying and modifying source code, such as the main FastHTML repo’s notebooks and the official FastHTML examples repo:\n\nFastHTML Examples Repo on GitHub\nFastHTML Repo on GitHub\n\nThen explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:\n\nFastHTML Gallery: Learn from minimal examples of components (ie chat bubbles, click-to-edit, infinite scroll, etc)\nCreating Custom FastHTML Tags for Markdown Rendering by Isaac Flath\nHow to Build a Simple Login System in FastHTML by Marius Vach\nYour tutorial here!\n\nFinally, join the FastHTML community to ask questions, share your work, and learn from others:\n\nDiscord", "crumbs": [ "Home", "Get Started" ] }, { "objectID": "index.html#other-languages-and-related-projects", "href": "index.html#other-languages-and-related-projects", "title": "FastHTML", "section": "Other languages and related projects", "text": "Other languages and related projects\nIf you’re not a Python user, or are keen to try out a new language, we’ll list here other projects that have a similar approach to FastHTML. (Please reach out if you know of any other projects that you’d like to see added.)\n\nhtmgo (Go): “htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx. By combining the speed & simplicity of go + hypermedia attributes (htmx) to add interactivity to websites, all conveniently wrapped in pure go, you can build simple, fast, interactive websites without touching javascript. All compiled to a single deployable binary”\n\nIf you’re just interested in functional HTML components, rather than a full HTMX server solution, consider:\n\nfastcore.xml.FT: This is actually what FastHTML uses behind the scenes\nhtpy: Similar to fastcore.xml.FT, but with a somewhat different syntax\nelm-html: Elm’s built-in HTML library with a type-safe functional approach\nhiccup: Popular library for representing HTML in Clojure using vectors\nhiccl: HTML generation library for Common Lisp inspired by Clojure’s Hiccup\nFalco.Markup: F# HTML DSL and web framework with type-safe HTML generation\nLucid: Type-safe HTML generation for Haskell using monad transformers\ndream-html: Part of the Dream web framework for OCaml, provides type-safe HTML templating\n\nFor other hypermedia application platforms, not based on HTMX, take a look at:\n\nHotwire/Turbo: Rails-oriented framework that similarly uses HTML-over-the-wire\nLiveView: Phoenix framework’s solution for building interactive web apps with minimal JavaScript\nUnpoly: Another HTML-over-the-wire framework with progressive enhancement\nLivewire: Laravel’s take on building dynamic interfaces with minimal JavaScript", "crumbs": [ "Home", "Get Started" ] }, { "objectID": "tutorials/index.html", "href": "tutorials/index.html", "title": "Tutorials", "section": "", "text": "Click through to any of these tutorials to get started with FastHTML’s features.\n\n\n\n\n\n\n\n\n\nTitle\n\n\nDescription\n\n\n\n\n\n\nFastHTML By Example\n\n\nAn introduction to FastHTML from the ground up, with four complete examples\n\n\n\n\nWeb Devs Quickstart\n\n\nA fast introduction to FastHTML for experienced web developers.\n\n\n\n\nJS App Walkthrough\n\n\nHow to build a website with custom JavaScript in FastHTML step-by-step\n\n\n\n\nFastHTML Best Practices\n\n\n\n\n\n\n\nUsing Jupyter to write FastHTML\n\n\nWriting FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\n\n\n\n\n\nNo matching items", "crumbs": [ "Home", "Tutorials" ] }, { "objectID": "tutorials/jupyter_and_fasthtml.html", "href": "tutorials/jupyter_and_fasthtml.html", "title": "Using Jupyter to write FastHTML", "section": "", "text": "Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\nThe first step is to import necessary libraries. As using FastHTML inside a Jupyter notebook is a special case, it remains a special import.\nfrom fasthtml.common import *\nfrom fasthtml.jupyter import JupyUvi, HTMX\nLet’s create an app with fast_app.\napp, rt = fast_app(pico=True)\nDefine a route to test the application.\n@rt\ndef index():\n return Titled('Hello, Jupyter',\n P('Welcome to the FastHTML + Jupyter example'),\n Button('Click', hx_get='/click', hx_target='#dest'),\n Div(id='dest')\n )\nCreate a server object using JupyUvi, which also starts Uvicorn. The server runs in a separate thread from Jupyter, so it can use normal HTTP client functions in a notebook.\nserver = JupyUvi(app)\nThe HTMX callable displays the server’s HTMX application in an iframe which can be displayed by Jupyter notebook. Pass in the same port variable used in the JupyUvi callable above or leave it blank to use the default (8000).\n# This doesn't display in the docs - uncomment and run it to see it in action\n# HTMX()\nWe didn’t define the /click route, but that’s fine - we can define (or change) it any time, and it’s dynamically inserted into the running app. No need to restart or reload anything!\n@rt\ndef click(): return P('You clicked me!')", "crumbs": [ "Home", "Tutorials", "Using Jupyter to write FastHTML" ] }, { "objectID": "tutorials/jupyter_and_fasthtml.html#full-screen-view", "href": "tutorials/jupyter_and_fasthtml.html#full-screen-view", "title": "Using Jupyter to write FastHTML", "section": "Full screen view", "text": "Full screen view\nYou can view your app outside of Jupyter by going to localhost:PORT, where PORT is usually the default 8000, so in most cases just click this link.", "crumbs": [ "Home", "Tutorials", "Using Jupyter to write FastHTML" ] }, { "objectID": "tutorials/jupyter_and_fasthtml.html#graceful-shutdowns", "href": "tutorials/jupyter_and_fasthtml.html#graceful-shutdowns", "title": "Using Jupyter to write FastHTML", "section": "Graceful shutdowns", "text": "Graceful shutdowns\nUse the server.stop() function displayed below. If you restart Jupyter without calling this line the thread may not be released and the HTMX callable above may throw errors. If that happens, a quick temporary fix is to specify a different port number in JupyUvi and HTMX with the port parameter.\nCleaner solutions to the dangling thread are to kill the dangling thread (dependant on each operating system) or restart the computer.\n\nserver.stop()", "crumbs": [ "Home", "Tutorials", "Using Jupyter to write FastHTML" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html", "href": "tutorials/quickstart_for_web_devs.html", "title": "Web Devs Quickstart", "section": "", "text": "pip install python-fasthtml", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#installation", "href": "tutorials/quickstart_for_web_devs.html#installation", "title": "Web Devs Quickstart", "section": "", "text": "pip install python-fasthtml", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#a-minimal-application", "href": "tutorials/quickstart_for_web_devs.html#a-minimal-application", "title": "Web Devs Quickstart", "section": "A Minimal Application", "text": "A Minimal Application\nA minimal FastHTML application looks something like this:\n\n\nmain.py\n\n1from fasthtml.common import *\n\n2app, rt = fast_app()\n\n3@rt(\"/\")\n4def get():\n5 return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n6serve()\n\n\n1\n\nWe 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.\n\n2\n\nWe 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.\n\n3\n\nWe use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.\n\n4\n\nWe connect this route to HTTP GET requests by defining a view function called get().\n\n5\n\nA 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.\n\n6\n\nThe serve() utility configures and runs FastHTML using a library called uvicorn.\n\n\nRun the code:\npython main.py\nThe terminal will look like this:\nINFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [58058] using WatchFiles\nINFO: Started server process [58060]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nConfirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:\n\n\n\n\n\n\n\nNote\n\n\n\nWhile 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.\nIf you want to learn more about how FastHTML handles imports, we cover that here.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#a-minimal-charting-application", "href": "tutorials/quickstart_for_web_devs.html#a-minimal-charting-application", "title": "Web Devs Quickstart", "section": "A Minimal Charting Application", "text": "A Minimal Charting Application\nThe Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:\nimport json\nfrom fasthtml.common import * \n\napp, rt = fast_app(hdrs=(Script(src=\"https://cdn.plot.ly/plotly-2.32.0.min.js\"),))\n\ndata = json.dumps({\n \"data\": [{\"x\": [1, 2, 3, 4],\"type\": \"scatter\"},\n {\"x\": [1, 2, 3, 4],\"y\": [16, 5, 11, 9],\"type\": \"scatter\"}],\n \"title\": \"Plotly chart in FastHTML \",\n \"description\": \"This is a demo dashboard\",\n \"type\": \"scatter\"\n})\n\n\n@rt(\"/\")\ndef get():\n return Titled(\"Chart Demo\", Div(id=\"myDiv\"),\n Script(f\"var data = {data}; Plotly.newPlot('myDiv', data);\"))\n\nserve()", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#debug-mode", "href": "tutorials/quickstart_for_web_devs.html#debug-mode", "title": "Web Devs Quickstart", "section": "Debug Mode", "text": "Debug Mode\nWhen 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.\nfrom fasthtml.common import *\n\n1app, rt = fast_app(debug=True)\n\n@rt(\"/\")\ndef get():\n2 1/0\n return Titled(\"FastHTML Error!\", P(\"Let's error!\"))\n\nserve()\n\n1\n\ndebug=True sets debug mode on.\n\n2\n\nPython throws an error when it tries to divide an integer by zero.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#routing", "href": "tutorials/quickstart_for_web_devs.html#routing", "title": "Web Devs Quickstart", "section": "Routing", "text": "Routing\nFastHTML builds upon FastAPI’s friendly decorator pattern for specifying URLs, with extra features:\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n1@rt(\"/\")\ndef get():\n return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n2@rt(\"/hello\")\ndef get():\n return Titled(\"Hello, world!\")\n\nserve()\n\n\n1\n\nThe “/” URL on line 5 is the home of a project. This would be accessed at 127.0.0.1:5001.\n\n2\n\n“/hello” URL on line 9 will be found by the project if the user visits 127.0.0.1:5001/hello.\n\n\n\n\n\n\n\n\nTip\n\n\n\nIt 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.\n\n\nYou can do more! Read on to learn what we can do to make parts of the URL dynamic.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#variables-in-urls", "href": "tutorials/quickstart_for_web_devs.html#variables-in-urls", "title": "Web Devs Quickstart", "section": "Variables in URLs", "text": "Variables in URLs\nYou 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:\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n1@rt(\"/{name}/{age}\")\n2def get(name: str, age: int):\n3 return Titled(f\"Hello {name.title()}, age {age}\")\n\nserve()\n\n\n1\n\nWe specify two variable names, name and age.\n\n2\n\nWe define two function arguments named identically to the variables. You will note that we specify the Python types to be passed.\n\n3\n\nWe use these functions in our project.\n\n\nTry it out by going to this address: 127.0.0.1:5001/uma/5. You should get a page that says,\n\n“Hello Uma, age 5”.\n\n\nWhat happens if we enter incorrect data?\nThe 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, then FastHTML will return an error instead of a web page.\n\n\n\n\n\n\nFastHTML URL routing supports more complex types\n\n\n\nThe 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, pydantic, and even sqlmodel.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#http-methods", "href": "tutorials/quickstart_for_web_devs.html#http-methods", "title": "Web Devs Quickstart", "section": "HTTP Methods", "text": "HTTP Methods\nFastHTML 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.\nForm 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.\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app()\n\n@rt(\"/\") \n1def get():\n return Titled(\"HTTP GET\", P(\"Handle GET\"))\n\n@rt(\"/\") \n2def post():\n return Titled(\"HTTP POST\", P(\"Handle POST\"))\n\nserve()\n\n\n1\n\nOn line 6 because the get() function name is used, this will handle HTTP GETs going to the / URI.\n\n2\n\nOn line 10 because the post() function name is used, this will handle HTTP POSTs going to the / URI.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#css-files-and-inline-styles", "href": "tutorials/quickstart_for_web_devs.html#css-files-and-inline-styles", "title": "Web Devs Quickstart", "section": "CSS Files and Inline Styles", "text": "CSS Files and Inline Styles\nHere we modify default headers to demonstrate how to use the Sakura CSS microframework instead of FastHTML’s default of Pico CSS.\n\n\nmain.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app(\n1 pico=False,\n hdrs=(\n Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),\n2 Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),\n3 Style(\"p {color: red;}\")\n))\n\n@app.get(\"/\")\ndef home():\n return Titled(\"FastHTML\",\n P(\"Let's do this!\"),\n )\n\nserve()\n\n\n1\n\nBy setting pico to False, FastHTML will not include pico.min.css.\n\n2\n\nThis will generate an HTML <link> tag for sourcing the css for Sakura.\n\n3\n\nIf you want an inline styles, the Style() function will put the result into the HTML.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#other-static-media-file-locations", "href": "tutorials/quickstart_for_web_devs.html#other-static-media-file-locations", "title": "Web Devs Quickstart", "section": "Other Static Media File Locations", "text": "Other Static Media File Locations\nAs you saw, 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 function.\napp, rt = fast_app(static_path='public')\nFastHTML 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.\n@rt(\"/{fname:path}.{ext:static}\")\nasync def get(fname:str, ext:str): \n return FileResponse(f'public/{fname}.{ext}')", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#rendering-markdown", "href": "tutorials/quickstart_for_web_devs.html#rendering-markdown", "title": "Web Devs Quickstart", "section": "Rendering Markdown", "text": "Rendering Markdown\nfrom fasthtml.common import *\n\nhdrs = (MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']), )\n\napp, rt = fast_app(hdrs=hdrs)\n\ncontent = \"\"\"\nHere are some _markdown_ elements.\n\n- This is a list item\n- This is another list item\n- And this is a third list item\n\n**Fenced code blocks work here.**\n\"\"\"\n\n@rt('/')\ndef get(req):\n return Titled(\"Markdown rendering example\", Div(content,cls=\"marked\"))\n\nserve()", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#code-highlighting", "href": "tutorials/quickstart_for_web_devs.html#code-highlighting", "title": "Web Devs Quickstart", "section": "Code highlighting", "text": "Code highlighting\nHere’s how to highlight code without any markdown configuration.\nfrom fasthtml.common import *\n\n# Add the HighlightJS built-in header\nhdrs = (HighlightJS(langs=['python', 'javascript', 'html', 'css']),)\n\napp, rt = fast_app(hdrs=hdrs)\n\ncode_example = \"\"\"\nimport datetime\nimport time\n\nfor i in range(10):\n print(f\"{datetime.datetime.now()}\")\n time.sleep(1)\n\"\"\"\n\n@rt('/')\ndef get(req):\n return Titled(\"Markdown rendering example\",\n Div(\n # The code example needs to be surrounded by\n # Pre & Code elements\n Pre(Code(code_example))\n ))\n\nserve()", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#defining-new-ft-components", "href": "tutorials/quickstart_for_web_devs.html#defining-new-ft-components", "title": "Web Devs Quickstart", "section": "Defining new ft components", "text": "Defining new ft components\nWe can build our own ft components and combine them with other components. The simplest method is defining them as a function.\n\nfrom fasthtml.common import *\n\n\ndef hero(title, statement):\n return Div(H1(title),P(statement), cls=\"hero\")\n\n# usage example\nMain(\n hero(\"Hello World\", \"This is a hero statement\")\n)\n\n<main> <div class=\"hero\">\n <h1>Hello World</h1>\n <p>This is a hero statement</p>\n </div>\n</main>\n\n\n\nPass through components\nFor 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.\n\ndef layout(*args, **kwargs):\n \"\"\"Dashboard layout for all our dashboard views\"\"\"\n return Main(\n H1(\"Dashboard\"),\n Div(*args, **kwargs),\n cls=\"dashboard\",\n )\n\n# usage example\nlayout(\n Ul(*[Li(o) for o in range(3)]),\n P(\"Some content\", cls=\"description\"),\n)\n\n<main class=\"dashboard\"> <h1>Dashboard</h1>\n <div>\n <ul>\n <li>0</li>\n <li>1</li>\n <li>2</li>\n </ul>\n <p class=\"description\">Some content</p>\n </div>\n</main>\n\n\n\n\nDataclasses as ft components\nWhile functions are easy to read, for more complex components some might find it easier to use a dataclass.\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Hero:\n title: str\n statement: str\n \n def __ft__(self):\n \"\"\" The __ft__ method renders the dataclass at runtime.\"\"\"\n return Div(H1(self.title),P(self.statement), cls=\"hero\")\n \n# usage example\nMain(\n Hero(\"Hello World\", \"This is a hero statement\")\n)\n\n<main> <div class=\"hero\">\n <h1>Hello World</h1>\n <p>This is a hero statement</p>\n </div>\n</main>", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#testing-views-in-notebooks", "href": "tutorials/quickstart_for_web_devs.html#testing-views-in-notebooks", "title": "Web Devs Quickstart", "section": "Testing views in notebooks", "text": "Testing views in notebooks\nBecause 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.\n\n# First we instantiate our app, in this case we remove the\n# default headers to reduce the size of the output.\napp, rt = fast_app(default_hdrs=False)\n\n# Setting up the Starlette test client\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\n\n# Usage example\n@rt(\"/\")\ndef get():\n return Titled(\"FastHTML is awesome\", \n P(\"The fastest way to create web apps in Python\"))\n\nprint(client.get(\"/\").text)\n\n <!doctype html>\n <html>\n <head>\n<title>FastHTML is awesome</title> </head>\n <body>\n<main class=\"container\"> <h1>FastHTML is awesome</h1>\n <p>The fastest way to create web apps in Python</p>\n</main> </body>\n </html>", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#forms", "href": "tutorials/quickstart_for_web_devs.html#forms", "title": "Web Devs Quickstart", "section": "Forms", "text": "Forms\nTo validate data coming from users, first define a dataclass representing the data you want to check. Here’s an example representing a signup form.\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Profile: email:str; phone:str; age:int\n\nCreate an FT component representing an empty version of that form. Don’t pass in any value to fill the form, that gets handled later.\n\nprofile_form = Form(method=\"post\", action=\"/profile\")(\n Fieldset(\n Label('Email', Input(name=\"email\")),\n Label(\"Phone\", Input(name=\"phone\")),\n Label(\"Age\", Input(name=\"age\")),\n ),\n Button(\"Save\", type=\"submit\"),\n )\nprofile_form\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\">\n</label><label>Phone <input name=\"phone\">\n</label><label>Age <input name=\"age\">\n</label></fieldset><button type=\"submit\">Save</button></form>\n\n\nOnce the dataclass and form function are completed, we can add data to the form. To do that, instantiate the profile dataclass:\n\nprofile = Profile(email='john@example.com', phone='123456789', age=5)\nprofile\n\nProfile(email='john@example.com', phone='123456789', age=5)\n\n\nThen add that data to the profile_form using FastHTML’s fill_form class:\n\nfill_form(profile_form, profile)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form>\n\n\n\nForms with views\nThe 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:\n\ndb = database(\"profiles.db\")\nprofiles = db.create(Profile, pk=\"email\")\n\nNow we insert a record into the database:\n\nprofiles.insert(profile)\n\nProfile(email='john@example.com', phone='123456789', age=5)\n\n\nAnd we can then demonstrate in the code that form is filled and displayed to the user.\n\n@rt(\"/profile/{email}\")\ndef profile(email:str):\n1 profile = profiles[email]\n2 filled_profile_form = fill_form(profile_form, profile)\n return Titled(f'Profile for {profile.email}', filled_profile_form)\n\nprint(client.get(f\"/profile/john@example.com\").text)\n\n\n1\n\nFetch the profile using the profile table’s email primary key\n\n2\n\nFill the form for display.\n\n\n\n\n <!doctype html>\n <html>\n <head>\n<title>Profile for john@example.com</title> </head>\n <body>\n<main class=\"container\"> <h1>Profile for john@example.com</h1>\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form></main> </body>\n </html>\n\n\n\nAnd now let’s demonstrate making a change to the data.\n\n@rt(\"/profile\")\n1def post(profile: Profile):\n2 profiles.update(profile)\n3 return RedirectResponse(url=f\"/profile/{profile.email}\")\n\nnew_data = dict(email='john@example.com', phone='7654321', age=25)\n4print(client.post(\"/profile\", data=new_data).text)\n\n\n1\n\nWe use the Profile dataclass definition to set the type for the incoming profile content. This validates the field types for the incoming data\n\n2\n\nTaking our validated data, we updated the profiles table\n\n3\n\nWe redirect the user back to their profile view\n\n4\n\nThe display is of the profile form view showing the changes in data.\n\n\n\n\n <!doctype html>\n <html>\n <head>\n<title>Profile for john@example.com</title> </head>\n <body>\n<main class=\"container\"> <h1>Profile for john@example.com</h1>\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"7654321\">\n</label><label>Age <input name=\"age\" value=\"25\">\n</label></fieldset><button type=\"submit\">Save</button></form></main> </body>\n </html>", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#strings-and-conversion-order", "href": "tutorials/quickstart_for_web_devs.html#strings-and-conversion-order", "title": "Web Devs Quickstart", "section": "Strings and conversion order", "text": "Strings and conversion order\nThe 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\nAs 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())).\nAbove we saw how a dataclass behaves with the __ft__ method defined. On a plain dataclass, str() will be called (but not escaped).\n\nfrom dataclasses import dataclass\n\n@dataclass\nclass Hero:\n title: str\n statement: str\n \n# rendering the dataclass with the default method\nMain(\n Hero(\"<h1>Hello World</h1>\", \"This is a hero statement\")\n)\n\n<main>Hero(title='<h1>Hello World</h1>', statement='This is a hero statement')</main>\n\n\n\n# This will display the HTML as text on your page\nDiv(\"Let's include some HTML here: <div>Some HTML</div>\")\n\n<div>Let's include some HTML here: <div>Some HTML</div></div>\n\n\n\n# Keep the string untouched, will be rendered on the page\nDiv(NotStr(\"<div><h1>Some HTML</h1></div>\"))\n\n<div><div><h1>Some HTML</h1></div></div>", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#custom-exception-handlers", "href": "tutorials/quickstart_for_web_devs.html#custom-exception-handlers", "title": "Web Devs Quickstart", "section": "Custom exception handlers", "text": "Custom exception handlers\nFastHTML allows customization of exception handlers, but does so gracefully. What this means is by default it includes all the <html> tags needed to display attractive content. Try it out!\nfrom fasthtml.common import *\n\ndef not_found(req, exc): return Titled(\"404: I don't exist!\")\n\nexception_handlers = {404: not_found}\n\napp, rt = fast_app(exception_handlers=exception_handlers)\n\n@rt('/')\ndef get():\n return (Titled(\"Home page\", P(A(href=\"/oops\")(\"Click to generate 404 error\"))))\n\nserve()\nWe can also use lambda to make things more terse:\nfrom fasthtml.common import *\n\nexception_handlers={\n 404: lambda req, exc: Titled(\"404: I don't exist!\"),\n 418: lambda req, exc: Titled(\"418: I'm a teapot!\")\n}\n\napp, rt = fast_app(exception_handlers=exception_handlers)\n\n@rt('/')\ndef get():\n return (Titled(\"Home page\", P(A(href=\"/oops\")(\"Click to generate 404 error\"))))\n\nserve()", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#cookies", "href": "tutorials/quickstart_for_web_devs.html#cookies", "title": "Web Devs Quickstart", "section": "Cookies", "text": "Cookies\nWe can set cookies using the cookie() function. In our example, we’ll create a timestamp cookie.\n\nfrom datetime import datetime\nfrom IPython.display import HTML\n\n\n@rt(\"/settimestamp\")\ndef get(req):\n now = datetime.now()\n return P(f'Set to {now}'), cookie('now', datetime.now())\n\nHTML(client.get('/settimestamp').text)\n\n \n \n \nFastHTML page \n \n Set to 2024-09-26 15:33:48.141869\n \n \n\n\nNow let’s get it back using the same name for our parameter as the cookie name.\n\n@rt('/gettimestamp')\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nclient.get('/gettimestamp').text\n\n'Cookie was set at time 15:33:48.141903'", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#sessions", "href": "tutorials/quickstart_for_web_devs.html#sessions", "title": "Web Devs Quickstart", "section": "Sessions", "text": "Sessions\nFor 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.\n\n@rt('/adder/{num}')\ndef get(session, num: int):\n session.setdefault('sum', 0)\n session['sum'] = session.get('sum') + num\n return Response(f'The sum is {session[\"sum\"]}.')", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#toasts-also-known-as-messages", "href": "tutorials/quickstart_for_web_devs.html#toasts-also-known-as-messages", "title": "Web Devs Quickstart", "section": "Toasts (also known as Messages)", "text": "Toasts (also known as Messages)\nToasts, sometimes called “Messages” are small notifications usually in colored boxes used to notify users that something has happened. Toasts can be of four types:\n\ninfo\nsuccess\nwarning\nerror\n\nExamples toasts might include:\n\n“Payment accepted”\n“Data submitted”\n“Request approved”\n\nToasts require the use of the setup_toasts() function plus every view needs these two features:\n\nThe session argument\nMust return FT components\n\n1setup_toasts(app)\n\n@rt('/toasting')\n2def get(session):\n # Normally one toast is enough, this allows us to see\n # different toast types in action.\n add_toast(session, f\"Toast is being cooked\", \"info\")\n add_toast(session, f\"Toast is ready\", \"success\")\n add_toast(session, f\"Toast is getting a bit crispy\", \"warning\")\n add_toast(session, f\"Toast is burning!\", \"error\")\n3 return Titled(\"I like toast\")\n\n1\n\nsetup_toasts is a helper function that adds toast dependencies. Usually this would be declared right after fast_app()\n\n2\n\nToasts require sessions\n\n3\n\nViews with Toasts must return FT or FtResponse components.\n\n\n💡 setup_toasts takes a duration input that allows you to specify how long a toast will be visible before disappearing. For example setup_toasts(duration=5) sets the toasts duration to 5 seconds. By default toasts disappear after 10 seconds.\n⚠️ Toasts don’t work with SPA like navigation that replaces the entire body such as this navigation trigger A('About', hx_get=\"/about\", hx_swap=\"outerHTML\", hx_push_url=\"true\", hx_target=\"body\"). As an alternative, wrap the content of your route in an element containing an id and set this id as the target for your navigation trigger (i.e. hx_target='#container_id').", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#authentication-and-authorization", "href": "tutorials/quickstart_for_web_devs.html#authentication-and-authorization", "title": "Web Devs Quickstart", "section": "Authentication and authorization", "text": "Authentication and authorization\nIn 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.\nFirst, we write a function that accepts a request and session arguments:\n\n# Status code 303 is a redirect that can change POST to GET,\n# so it's appropriate for a login page.\nlogin_redir = RedirectResponse('/login', status_code=303)\n\ndef user_auth_before(req, sess):\n # The `auth` key in the request scope is automatically provided\n # to any handler which requests it, and can not be injected\n # by the user using query params, cookies, etc, so it should\n # be secure to use. \n auth = req.scope['auth'] = sess.get('auth', None)\n # If the session key is not there, it redirects to the login page.\n if not auth: return login_redir\n\nNow we pass our user_auth_before function as the first argument into a 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.\n\nbeforeware = Beforeware(\n user_auth_before,\n skip=[r'/favicon\\.ico', r'/static/.*', r'.*\\.css', r'.*\\.js', '/login', '/']\n)\n\napp, rt = fast_app(before=beforeware)", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#server-sent-events-sse", "href": "tutorials/quickstart_for_web_devs.html#server-sent-events-sse", "title": "Web Devs Quickstart", "section": "Server-sent events (SSE)", "text": "Server-sent events (SSE)\nWith 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.\nFastHTML 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.\n\nimport random\nfrom asyncio import sleep\nfrom fasthtml.common import *\n\n1hdrs=(Script(src=\"https://unpkg.com/htmx-ext-sse@2.2.1/sse.js\"),)\napp,rt = fast_app(hdrs=hdrs)\n\n@rt\ndef index():\n return Titled(\"SSE Random Number Generator\",\n P(\"Generate pairs of random numbers, as the list grows scroll downwards.\"),\n2 Div(hx_ext=\"sse\",\n3 sse_connect=\"/number-stream\",\n4 hx_swap=\"beforeend show:bottom\",\n5 sse_swap=\"message\"))\n\n6shutdown_event = signal_shutdown()\n\n7async def number_generator():\n8 while not shutdown_event.is_set():\n data = Article(random.randint(1, 100))\n9 yield sse_message(data)\n await sleep(1)\n\n@rt(\"/number-stream\")\n10async def get(): return EventStream(number_generator())\n\n\n1\n\nImport the HTMX SSE extension\n\n2\n\nTell HTMX to load the SSE extension\n\n3\n\nLook at the /number-stream endpoint for SSE content\n\n4\n\nWhen 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\n\n5\n\nSpecify 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\n\n6\n\nSet up the asyncio event loop\n\n7\n\nDon’t forget to make this an async function!\n\n8\n\nIterate through the asyncio event loop\n\n9\n\nWe yield the data. Data ideally should be comprised of FT components as that plugs nicely into HTMX in the browser\n\n10\n\nThe endpoint view needs to be an async function that returns a EventStream", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#websockets", "href": "tutorials/quickstart_for_web_devs.html#websockets", "title": "Web Devs Quickstart", "section": "Websockets", "text": "Websockets\nWith websockets we can have bi-directional communications between a browser and client. Websockets are useful for things like chat and certain types of games. While websockets can be used for single direction messages from the server (i.e. telling users that a process is finished), that task is arguably better suited for SSE.\nFastHTML provides useful tools for adding websockets to your pages.\n\nfrom fasthtml.common import *\nfrom asyncio import sleep\n\n1app, rt = fast_app(exts='ws')\n\n2def mk_inp(): return Input(id='msg', autofocus=True)\n\n@rt('/')\nasync def get(request):\n cts = Div(\n Div(id='notifications'),\n3 Form(mk_inp(), id='form', ws_send=True),\n4 hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\n\n5async def on_connect(send):\n print('Connected!')\n6 await send(Div('Hello, you have connected', id=\"notifications\"))\n\n7async def on_disconnect(ws):\n print('Disconnected!')\n\n8@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\n9async def ws(msg:str, send):\n10 await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n11 return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\n\n\n1\n\nTo use websockets in FastHTML, you must instantiate the app with exts set to ‘ws’\n\n2\n\nAs we want to use websockets to reset the form, we define the mk_input function that can be called from multiple locations\n\n3\n\nWe create the form and mark it with the ws_send attribute, which is documented here in the HTMX websocket specification. This tells HTMX to send a message to the nearest websocket based on the trigger for the form element, which for forms is pressing the enter key, an action considered to be a form submission\n\n4\n\nThis is where the HTMX extension is loaded (hx_ext='ws') and the nearest websocket is defined (ws_connect='/ws')\n\n5\n\nWhen a websocket first connects we can optionally have it call a function that accepts a send argument. The send argument will push a message to the browser.\n\n6\n\nHere we use the send function that was passed into the on_connect function to send a Div with an id of notifications that HTMX assigns to the element in the page that already has an id of notifications\n\n7\n\nWhen a websocket disconnects we can call a function which takes no arguments. Typically the role of this function is to notify the server to take an action. In this case, we print a simple message to the console\n\n8\n\nWe use the app.ws decorator to mark that /ws is the route for our websocket. We also pass in the two optional conn and disconn parameters to this decorator. As a fun experiment, remove the conn and disconn arguments and see what happens\n\n9\n\nDefine the ws function as async. This is necessary for ASGI to be able to serve websockets. The function accepts two arguments, a msg that is user input from the browser, and a send function for pushing data back to the browser\n\n10\n\nThe send function is used here to send HTML back to the page. As the HTML has an id of notifications, HTMX will overwrite what is already on the page with the same ID\n\n11\n\nThe websocket function can also be used to return a value. In this case, it is a tuple of two HTML elements. HTMX will take the elements and replace them where appropriate. As both have id specified (notifications and msg respectively), they will replace their predecessor on the page.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "tutorials/quickstart_for_web_devs.html#file-uploads", "href": "tutorials/quickstart_for_web_devs.html#file-uploads", "title": "Web Devs Quickstart", "section": "File Uploads", "text": "File Uploads\nA common task in web development is uploading files. The examples below are for uploading files to the hosting server, with information about the uploaded file presented to the user.\n\n\n\n\n\n\nFile uploads in production can be dangerous\n\n\n\nFile uploads can be the target of abuse, accidental or intentional. That means users may attempt to upload files that are too large or present a security risk. This is especially of concern for public facing apps. File upload security is outside the scope of this tutorial, for now we suggest reading the OWASP File Upload Cheat Sheet.\n\n\n\nSingle File Uploads\nfrom fasthtml.common import *\nfrom pathlib import Path\n\napp, rt = fast_app()\n\nupload_dir = Path(\"filez\")\nupload_dir.mkdir(exist_ok=True)\n\n@rt('/')\ndef get():\n return Titled(\"File Upload Demo\",\n Article(\n1 Form(hx_post=upload, hx_target=\"#result-one\")(\n2 Input(type=\"file\", name=\"file\"),\n Button(\"Upload\", type=\"submit\", cls='secondary'),\n ),\n Div(id=\"result-one\")\n )\n )\n\ndef FileMetaDataCard(file):\n return Article(\n Header(H3(file.filename)),\n Ul(\n Li('Size: ', file.size), \n Li('Content Type: ', file.content_type),\n Li('Headers: ', file.headers),\n )\n ) \n\n@rt\n3async def upload(file: UploadFile):\n4 card = FileMetaDataCard(file)\n5 filebuffer = await file.read()\n6 (upload_dir / file.filename).write_bytes(filebuffer)\n return card\n\nserve()\n\n1\n\nEvery form rendered with the Form FT component defaults to enctype=\"multipart/form-data\"\n\n2\n\nDon’t forget to set the Input FT Component’s type to file\n\n3\n\nThe upload view should receive a Starlette UploadFile type. You can add other form variables\n\n4\n\nWe can access the metadata of the card (filename, size, content_type, headers), a quick and safe process. We set that to the card variable\n\n5\n\nIn order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata\n\n6\n\nThis step shows how to use Python’s built-in pathlib.Path library to write the file to disk.\n\n\n\n\nMultiple File Uploads\nfrom fasthtml.common import *\nfrom pathlib import Path\n\napp, rt = fast_app()\n\nupload_dir = Path(\"filez\")\nupload_dir.mkdir(exist_ok=True)\n\n@rt('/')\ndef get():\n return Titled(\"Multiple File Upload Demo\",\n Article(\n1 Form(hx_post=upload_many, hx_target=\"#result-many\")(\n2 Input(type=\"file\", name=\"files\", multiple=True),\n Button(\"Upload\", type=\"submit\", cls='secondary'),\n ),\n Div(id=\"result-many\")\n )\n )\n\ndef FileMetaDataCard(file):\n return Article(\n Header(H3(file.filename)),\n Ul(\n Li('Size: ', file.size), \n Li('Content Type: ', file.content_type),\n Li('Headers: ', file.headers),\n )\n ) \n\n@rt\n3async def upload_many(files: list[UploadFile]):\n cards = []\n4 for file in files:\n5 cards.append(FileMetaDataCard(file))\n6 filebuffer = await file.read()\n7 (upload_dir / file.filename).write_bytes(filebuffer)\n return cards\n\nserve()\n\n1\n\nEvery form rendered with the Form FT component defaults to enctype=\"multipart/form-data\"\n\n2\n\nDon’t forget to set the Input FT Component’s type to file and assign the multiple attribute to True\n\n3\n\nThe upload view should receive a list containing the Starlette UploadFile type. You can add other form variables\n\n4\n\nIterate through the files\n\n5\n\nWe can access the metadata of the card (filename, size, content_type, headers), a quick and safe process. We add that to the cards variable\n\n6\n\nIn order to access the contents contained within a file we use the await method to read() it. As files may be quite large or contain bad data, this is a seperate step from accessing metadata\n\n7\n\nThis step shows how to use Python’s built-in pathlib.Path library to write the file to disk.", "crumbs": [ "Home", "Tutorials", "Web Devs Quickstart" ] }, { "objectID": "api/components.html", "href": "api/components.html", "title": "Components", "section": "", "text": "from collections import UserDict\nfrom lxml import html as lx\nfrom pprint import pprint", "crumbs": [ "Home", "Source", "Components" ] }, { "objectID": "api/components.html#tests", "href": "api/components.html#tests", "title": "Components", "section": "Tests", "text": "Tests\n\ntest_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">', attr1st=True)\ntest_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">')\ntest_html2ft('<div id=\"foo\"></div>')\ntest_html2ft('<div id=\"foo\">hi</div>')\ntest_html2ft('<div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>')\ntest_html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>')\n\n\nassert html2ft('<div id=\"foo\">hi</div>', attr1st=True) == \"Div(id='foo')('hi')\"\nassert html2ft(\"\"\"\n <div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>\n\"\"\") == \"Div('Hello 👋', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})\"\nassert html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>') == \"Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})\"\nassert html2ft(\"<img alt=' ' />\") == \"Img(alt=' ')\"", "crumbs": [ "Home", "Source", "Components" ] }, { "objectID": "api/js.html", "href": "api/js.html", "title": "Javascript examples", "section": "", "text": "To expedite fast development, FastHTML comes with several built-in Javascript and formatting components. These are largely provided to demonstrate FastHTML JS patterns. There’s far too many JS libs for FastHTML to wrap them all, and as shown here the code to add FastHTML support is very simple anyway.\n\nsource\n\nlight_media\n\n light_media (css:str)\n\nRender light media for day mode views\n\n\n\n\nType\nDetails\n\n\n\n\ncss\nstr\nCSS to be included in the light media query\n\n\n\n\nlight_media('.body {color: green;}')\n\n<style>@media (prefers-color-scheme: light) {.body {color: green;}}</style>\n\n\n\nsource\n\n\ndark_media\n\n dark_media (css:str)\n\nRender dark media for night mode views\n\n\n\n\nType\nDetails\n\n\n\n\ncss\nstr\nCSS to be included in the dark media query\n\n\n\n\ndark_media('.body {color: white;}')\n\n<style>@media (prefers-color-scheme: dark) {.body {color: white;}}</style>\n\n\n\nsource\n\n\nMarkdownJS\n\n MarkdownJS (sel='.marked')\n\nImplements browser-based markdown rendering.\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.marked\nCSS selector for markdown elements\n\n\n\nUsage example here.\n\n__file__ = '../../fasthtml/katex.js'\n\n\nsource\n\n\nKatexMarkdownJS\n\n KatexMarkdownJS (sel='.marked', inline_delim='$', display_delim='$$',\n math_envs=None)\n\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.marked\nCSS selector for markdown elements\n\n\ninline_delim\nstr\n$\nDelimiter for inline math\n\n\ndisplay_delim\nstr\n$$\nDelimiter for long math\n\n\nmath_envs\nNoneType\nNone\nList of environments to render as display math\n\n\n\nKatexMarkdown usage example:\nlongexample = r\"\"\"\nLong example:\n\n$$\\begin{array}{c}\n\n\\nabla \\times \\vec{\\mathbf{B}} -\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{E}}}{\\partial t} &\n= \\frac{4\\pi}{c}\\vec{\\mathbf{j}} \\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\\n\n\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\\n\n\\nabla \\cdot \\vec{\\mathbf{B}} & = 0\n\n\\end{array}$$\n\"\"\"\n\napp, rt = fast_app(hdrs=[KatexMarkdownJS()])\n\n@rt('/')\ndef get():\n return Titled(\"Katex Examples\", \n # Assigning 'marked' class to components renders content as markdown\n P(cls='marked')(\"Inline example: $\\sqrt{3x-1}+(1+x)^2$\"),\n Div(cls='marked')(longexample)\n )\n\nsource\n\n\nHighlightJS\n\n HighlightJS (sel='pre code:not([data-highlighted=\"yes\"])',\n langs:str|list|tuple='python', light='atom-one-light',\n dark='atom-one-dark')\n\nImplements browser-based syntax highlighting. Usage example here.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\npre code:not([data-highlighted=“yes”])\nCSS selector for code elements. Default is industry standard, be careful before adjusting it\n\n\nlangs\nstr | list | tuple\npython\nLanguage(s) to highlight\n\n\nlight\nstr\natom-one-light\nLight theme\n\n\ndark\nstr\natom-one-dark\nDark theme\n\n\n\n\nsource\n\n\nSortableJS\n\n SortableJS (sel='.sortable', ghost_class='blue-background-class')\n\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.sortable\nCSS selector for sortable elements\n\n\nghost_class\nstr\nblue-background-class\nWhen an element is being dragged, this is the class used to distinguish it from the rest\n\n\n\n\nsource\n\n\nMermaidJS\n\n MermaidJS (sel='.language-mermaid', theme='base')\n\nImplements browser-based Mermaid diagram rendering.\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nsel\nstr\n.language-mermaid\nCSS selector for mermaid elements\n\n\ntheme\nstr\nbase\nMermaid theme to use\n\n\n\napp, rt = fast_app(hdrs=[MermaidJS()])\n@rt('/')\ndef get():\n return Titled(\"Mermaid Examples\", \n # Assigning 'marked' class to components renders content as markdown\n Pre(Code(cls =\"language-mermaid\")('''flowchart TD\n A[main] --> B[\"fact(5)\"] --> C[\"fact(4)\"] --> D[\"fact(3)\"] --> E[\"fact(2)\"] --> F[\"fact(1)\"] --> G[\"fact(0)\"]\n ''')))\nIn a markdown file, just like a code cell you can define\n```mermaid\n graph TD\n A --> B \n B --> C \n C --> E\n```", "crumbs": [ "Home", "Source", "Javascript examples" ] }, { "objectID": "api/cli.html", "href": "api/cli.html", "title": "Command Line Tools", "section": "", "text": "source\n\nrailway_link\n\n railway_link ()\n\nLink the current directory to the current project’s Railway service\n\nsource\n\n\nrailway_deploy\n\n railway_deploy (name:str, mount:<function bool_arg>=True)\n\nDeploy a FastHTML app to Railway\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nname\nstr\n\nThe project name to deploy\n\n\nmount\nbool_arg\nTrue\nCreate a mounted volume at /app/data?", "crumbs": [ "Home", "Source", "Command Line Tools" ] }, { "objectID": "api/xtend.html", "href": "api/xtend.html", "title": "Component extensions", "section": "", "text": "from pprint import pprint\nsource\npara", "crumbs": [ "Home", "Source", "Component extensions" ] }, { "objectID": "api/xtend.html#forms", "href": "api/xtend.html#forms", "title": "Component extensions", "section": "Forms", "text": "Forms\n\nsource\n\nForm\n\n Form (*c, enctype='multipart/form-data', target_id=None, hx_vals=None,\n hx_target=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA Form tag; identical to plain ft_hx version except default enctype='multipart/form-data'\n\nsource\n\n\nHidden\n\n Hidden (value:Any='', id:Any=None, target_id=None, hx_vals=None,\n hx_target=None, cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nAn Input of type ‘hidden’\n\nsource\n\n\nCheckboxX\n\n CheckboxX (checked:bool=False, label=None, value='1', id=None, name=None,\n target_id=None, hx_vals=None, hx_target=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)\n\nA Checkbox optionally inside a Label, preceded by a Hidden with matching name\n\nshow(CheckboxX(True, 'Check me out!'))\n\n\n \nCheck me out!\n\n\n\nsource\n\n\nScript\n\n Script (code:str='', id=None, cls=None, title=None, style=None,\n attrmap=None, valmap=None, ft_cls=None, **kwargs)\n\nA Script tag that doesn’t escape its code\n\nsource\n\n\nStyle\n\n Style (*c, id=None, cls=None, title=None, style=None, attrmap=None,\n valmap=None, ft_cls=None, **kwargs)\n\nA Style tag that doesn’t escape its code", "crumbs": [ "Home", "Source", "Component extensions" ] }, { "objectID": "api/xtend.html#style-and-script-templates", "href": "api/xtend.html#style-and-script-templates", "title": "Component extensions", "section": "Style and script templates", "text": "Style and script templates\n\nsource\n\ndouble_braces\n\n double_braces (s)\n\nConvert single braces to double braces if next to special chars or newline\n\nsource\n\n\nundouble_braces\n\n undouble_braces (s)\n\nConvert double braces to single braces if next to special chars or newline\n\nsource\n\n\nloose_format\n\n loose_format (s, **kw)\n\nString format s using kw, without being strict about braces outside of template params\n\nsource\n\n\nScriptX\n\n ScriptX (fname, src=None, nomodule=None, type=None, _async=None,\n defer=None, charset=None, crossorigin=None, integrity=None,\n **kw)\n\nA script element with contents read from fname\n\nsource\n\n\nreplace_css_vars\n\n replace_css_vars (css, pre='tpl', **kwargs)\n\nReplace var(--) CSS variables with kwargs if name prefix matches pre\n\nsource\n\n\nStyleX\n\n StyleX (fname, **kw)\n\nA style element with contents read from fname and variables replaced from kw\n\nsource\n\n\nNbsp\n\n Nbsp ()\n\nA non-breaking space", "crumbs": [ "Home", "Source", "Component extensions" ] }, { "objectID": "api/xtend.html#surreal-and-js", "href": "api/xtend.html#surreal-and-js", "title": "Component extensions", "section": "Surreal and JS", "text": "Surreal and JS\n\nsource\n\nSurreal\n\n Surreal (code:str)\n\nWrap code in domReadyExecute and set m=me() and p=me('-')\n\nsource\n\n\nOn\n\n On (code:str, event:str='click', sel:str='', me=True)\n\nAn async surreal.js script block event handler for event on selector sel,p, making available parent p, event ev, and target e\n\nsource\n\n\nPrev\n\n Prev (code:str, event:str='click')\n\nAn async surreal.js script block event handler for event on previous sibling, with same vars as On\n\nsource\n\n\nNow\n\n Now (code:str, sel:str='')\n\nAn async surreal.js script block on selector me(sel)\n\nsource\n\n\nAnyNow\n\n AnyNow (sel:str, code:str)\n\nAn async surreal.js script block on selector any(sel)\n\nsource\n\n\nrun_js\n\n run_js (js, id=None, **kw)\n\nRun js script, auto-generating id based on name of caller if needed, and js-escaping any kw params\n\nsource\n\n\nHtmxOn\n\n HtmxOn (eventname:str, code:str)\n\n\nsource\n\n\njsd\n\n jsd (org, repo, root, path, prov='gh', typ='script', ver=None, esm=False,\n **kwargs)\n\njsdelivr Script or CSS Link tag, or URL", "crumbs": [ "Home", "Source", "Component extensions" ] }, { "objectID": "api/xtend.html#other-helpers", "href": "api/xtend.html#other-helpers", "title": "Component extensions", "section": "Other helpers", "text": "Other helpers\n\nsource\n\nFragment\n\n Fragment (*c)\n\nAn empty tag, used as a container\n\nfts = Fragment(P('1st'), P('2nd'))\nprint(to_xml(fts))\n\n <p>1st</p>\n <p>2nd</p>\n\n\n\n\nsource\n\n\nTitled\n\n Titled (title:str='FastHTML app', *args, cls='container', target_id=None,\n hx_vals=None, hx_target=None, id=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nAn HTML partial containing a Title, and H1, and any provided children\n\nshow(Titled('my page', P('para')))\n\nmy page\n my page\n \n\n\n\n\nsource\n\n\nSocials\n\n Socials (title, site_name, description, image, url=None, w=1200, h=630,\n twitter_site=None, creator=None, card='summary')\n\nOG and Twitter social card headers\n\nsource\n\n\nYouTubeEmbed\n\n YouTubeEmbed (video_id:str, width:int=560, height:int=315,\n start_time:int=0, no_controls:bool=False,\n title:str='YouTube video player', cls:str='', **kwargs)\n\nEmbed a YouTube video\n\nsource\n\n\nFavicon\n\n Favicon (light_icon, dark_icon)\n\nLight and dark favicon headers\n\nsource\n\n\nclear\n\n clear (id)\n\n\nsource\n\n\nwith_sid\n\n with_sid (app, dest, path='/')", "crumbs": [ "Home", "Source", "Component extensions" ] }, { "objectID": "explains/websockets.html", "href": "explains/websockets.html", "title": "WebSockets", "section": "", "text": "Websockets are a protocol for two-way, persistent communication between a client and server. This is different from HTTP, which uses a request/response model where the client sends a request and the server responds. With websockets, either party can send messages at any time, and the other party can respond.\nThis allows for different applications to be built, including things like chat apps, live-updating dashboards, and real-time collaborative tools, which would require constant polling of the server for updates with HTTP.\nIn FastHTML, you can create a websocket route using the @app.ws decorator. This decorator takes a route path, and optional conn and disconn parameters representing the on_connect and on_disconnect callbacks in websockets, respectively. The function decorated by @app.ws is the main function that is called when a message is received.\nHere’s an example of a basic websocket route:\nThe on_message function is the main function that is called when a message is received and can be named however you like. Similar to standard routes, the arguments to on_message are automatically parsed from the websocket payload for you, so you don’t need to manually parse the message content. However, certain argument names are reserved for special purposes. Here are the most important ones:\nFor example, we can send a message to the client that just connected like this:\nOr if we receive a message from the client, we can send a message back to them:\nOn the client side, we can use HTMX’s websocket extension to open a websocket connection and send/receive messages. For example:\nThis will create a websocket connection to the server on route /ws, and send any form submissions to the server via the websocket. The server will then respond by sending a message back to the client. The client will then update the message div with the message from the server using Out of Band Swaps, which means that the content is swapped with the same id without reloading the page.\nPutting it all together, the code for the client and server should look like this:\nThis is a fairly simple example and could be done just as easily with standard HTTP requests, but it illustrates the basic idea of how websockets work. Let’s look at a more complex example next.", "crumbs": [ "Home", "Explanations", "WebSockets" ] }, { "objectID": "explains/websockets.html#session-data-in-websockets", "href": "explains/websockets.html#session-data-in-websockets", "title": "WebSockets", "section": "Session data in Websockets", "text": "Session data in Websockets\nSession data is shared between standard HTTP routes and Websockets. This means you can access, for example, logged in user ID inside websocket handler:\nfrom fasthtml.common import *\n\napp = FastHTML(exts='ws')\nrt = app.route\n\n@rt('/login')\ndef get(session):\n session[\"person\"] = \"Bob\"\n return \"ok\"\n\n@app.ws('/ws')\nasync def ws(msg:str, send, session):\n await send(Div(f'Hello {session.get(\"person\")}' + msg, id='notifications'))\n\nserve()", "crumbs": [ "Home", "Explanations", "WebSockets" ] }, { "objectID": "explains/websockets.html#real-time-chat-app", "href": "explains/websockets.html#real-time-chat-app", "title": "WebSockets", "section": "Real-Time Chat App", "text": "Real-Time Chat App\nLet’s put our new websocket knowledge to use by building a simple chat app. We will create a chat app where multiple users can send and receive messages in real time.\nLet’s start by defining the app and the home page:\nfrom fasthtml.common import *\n\napp = FastHTML(exts='ws')\nrt = app.route\n\nmsgs = []\n@rt('/')\ndef home(): return Div(\n Div(Ul(*[Li(m) for m in msgs], id='msg-list')),\n Form(Input(id='msg'), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\nNow, let’s handle the websocket connection. We’ll add a new route for this along with an on_conn and on_disconn function to keep track of the users currently connected to the websocket. Finally, we will handle the logic for sending messages to all connected users.\nusers = {}\ndef on_conn(ws, send): users[str(id(ws))] = send\ndef on_disconn(ws): users.pop(str(id(ws)), None)\n\n@app.ws('/ws', conn=on_conn, disconn=on_disconn)\nasync def ws(msg:str):\n msgs.append(msg)\n # Use associated `send` function to send message to each user\n for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))\n\nserve()\nWe can now run this app with python chat_ws.py and open multiple browser tabs to http://localhost:5001. You should be able to send messages in one tab and see them appear in the other tabs.\n\nA Work in Progress\nThis page (and Websocket support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!", "crumbs": [ "Home", "Explanations", "WebSockets" ] }, { "objectID": "explains/explaining_xt_components.html", "href": "explains/explaining_xt_components.html", "title": "FT Components", "section": "", "text": "FT, or ‘FastTags’, are the display components of FastHTML. In fact, the word “components” in the context of FastHTML is often synonymous with FT.\nFor example, when we look at a FastHTML app, in particular the views, as well as various functions and other objects, we see something like the code snippet below. It’s the return statement that we want to pay attention to:\nfrom fasthtml.common import *\n\ndef example():\n # The code below is a set of ft components\n return Div(\n H1(\"FastHTML APP\"),\n P(\"Let's do this\"),\n cls=\"go\"\n )\nLet’s go ahead and call our function and print the result:\nexample()\n\n<div class=\"go\">\n <h1>FastHTML APP</h1>\n <p>Let's do this</p>\n</div>\nAs you can see, when returned to the user from a Python callable, like a function, the ft components are transformed into their string representations of XML or XML-like content such as HTML. More concisely, ft turns Python objects into HTML.\nNow that we know what ft components look and behave like we can begin to understand them. At their most fundamental level, ft components:", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/explaining_xt_components.html#how-fasthtml-names-ft-components", "href": "explains/explaining_xt_components.html#how-fasthtml-names-ft-components", "title": "FT Components", "section": "How FastHTML names ft components", "text": "How FastHTML names ft components\nWhen it comes to naming ft components, FastHTML appears to break from PEP8. Specifically, PEP8 specifies that when naming variables, functions and instantiated classes we use the snake_case_pattern. That is to say, lowercase with words separated by underscores. However, FastHTML uses PascalCase for ft components.\nThere’s a couple of reasons for this:\n\nft components can be made from any callable type, so adhering to any one pattern doesn’t make much sense\nIt makes for easier reading of FastHTML code, as anything that is PascalCase is probably an ft component", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/explaining_xt_components.html#default-ft-components", "href": "explains/explaining_xt_components.html#default-ft-components", "title": "FT Components", "section": "Default FT components", "text": "Default FT components\nFastHTML has over 150 FT components designed to accelerate web development. Most of these mirror HTML tags such as <div>, <p>, <a>, <title>, and more. However, there are some extra tags added, including:\n\nTitled, a combination of the Title() and H1() tags\nSocials, renders popular social media tags", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/explaining_xt_components.html#the-fasthtml.ft-namespace", "href": "explains/explaining_xt_components.html#the-fasthtml.ft-namespace", "title": "FT Components", "section": "The fasthtml.ft Namespace", "text": "The fasthtml.ft Namespace\nSome people prefer to write code using namespaces while adhering to PEP8. If that’s a preference, projects can be coded using the fasthtml.ft namespace.\n\nfrom fasthtml import ft\n\nft.Ul(\n ft.Li(\"one\"),\n ft.Li(\"two\"),\n ft.Li(\"three\")\n)\n\n<ul>\n <li>one</li>\n <li>two</li>\n <li>three</li>\n</ul>", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/explaining_xt_components.html#attributes", "href": "explains/explaining_xt_components.html#attributes", "title": "FT Components", "section": "Attributes", "text": "Attributes\nThis example demonstrates many important things to know about how ft components handle attributes.\n#| echo: False\n1Label(\n \"Choose an option\", \n Select(\n2 Option(\"one\", value=\"1\", selected=True),\n3 Option(\"two\", value=\"2\", selected=False),\n4 Option(\"three\", value=3),\n5 cls=\"selector\",\n6 _id=\"counter\",\n7 **{'@click':\"alert('Clicked');\"},\n ),\n8 _for=\"counter\",\n)\n\n1\n\nLine 2 demonstrates that FastHTML appreciates Labels surrounding their fields.\n\n2\n\nOn line 5, we can see that attributes set to the boolean value of True are rendered with just the name of the attribute.\n\n3\n\nOn line 6, we demonstrate that attributes set to the boolean value of False do not appear in the rendered output.\n\n4\n\nLine 7 is an example of how integers and other non-string values in the rendered output are converted to strings.\n\n5\n\nLine 8 is where we set the HTML class using the cls argument. We use cls here as class is a reserved word in Python. During the rendering process this will be converted to the word “class”.\n\n6\n\nLine 9 demonstrates that any named argument passed into an ft component will have the leading underscore stripped away before rendering. Useful for handling reserved words in Python.\n\n7\n\nOn line 10 we have an attribute name that cannot be represented as a python variable. In cases like these, we can use an unpacked dict to represent these values.\n\n8\n\nThe use of _for on line 12 is another demonstration of an argument having the leading underscore stripped during render. We can also use fr as that will be expanded to for.\n\n\nThis renders the following HTML snippet:\n\nLabel(\n \"Choose an option\", \n Select(\n Option(\"one\", value=\"1\", selected=True),\n Option(\"two\", value=\"2\", selected=False),\n Option(\"three\", value=3), # <4>,\n cls=\"selector\",\n _id=\"counter\",\n **{'@click':\"alert('Clicked');\"},\n ),\n _for=\"counter\",\n)\n\n<label for=\"counter\">\nChoose an option\n <select id=\"counter\" @click=\"alert('Clicked');\" class=\"selector\" name=\"counter\">\n <option value=\"1\" selected>one</option>\n <option value=\"2\" >two</option>\n <option value=\"3\">three</option>\n </select>\n</label>", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/explaining_xt_components.html#defining-new-ft-components", "href": "explains/explaining_xt_components.html#defining-new-ft-components", "title": "FT Components", "section": "Defining new ft components", "text": "Defining new ft components\nIt is possible and sometimes useful to create your own ft components that generate non-standard tags that are not in the FastHTML library. FastHTML supports created and defining those new tags flexibly.\nFor more information, see the Defining new ft components reference page.", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/explaining_xt_components.html#ft-components-and-type-hints", "href": "explains/explaining_xt_components.html#ft-components-and-type-hints", "title": "FT Components", "section": "FT components and type hints", "text": "FT components and type hints\nIf you use type hints, we strongly suggest that FT components be treated as the Any type.\nThe reason is that FastHTML leverages python’s dynamic features to a great degree. Especially when it comes to FT components, which can evaluate out to be FT|str|None|tuple as well as anything that supports the __ft__, __html__, and __str__ method. That’s enough of the Python stack that assigning anything but Any to be the FT type will prove an exercise in frustation.", "crumbs": [ "Home", "Explanations", "**FT** Components" ] }, { "objectID": "explains/stripe.html", "href": "explains/stripe.html", "title": "Stripe", "section": "", "text": "This guide will walk through a minimal example of working with a Stripe one-time payment link and webhook for secure reconciliation of payments.\nTo get started we can import the stripe library and authenticate with a Stripe API key that you can get from the stripe web UI.\nExported source\nfrom fasthtml.common import *\nimport os", "crumbs": [ "Home", "Explanations", "Stripe" ] }, { "objectID": "explains/stripe.html#stripe-authentication", "href": "explains/stripe.html#stripe-authentication", "title": "Stripe", "section": "Stripe Authentication", "text": "Stripe Authentication\nYou can install stripe python sdk directly from pypi:\npip install stripe\nAdditionally, you need to install the stripe cli. You can find how to install it on your specific system in their docs here\n\n# uncomment and execute if needed\n#!pip install stripe\n\n\n\nExported source\nimport stripe\n\n\n\n\nExported source\nstripe.api_key = os.environ.get(\"STRIPE_SECRET_KEY\")\nDOMAIN_URL = os.environ.get(\"DOMAIN_URL\", \"http://localhost:5001\")\n\n\nYou can get this API key from the Stripe Dashboard by going to this url.\n\n\n\n\n\n\nNote\n\n\n\nNote: Make sure you have Test mode turned on in the dashboard.\n\n\n\nMake sure you are using a test key for this tutorial\n\nassert 'test_' in stripe.api_key", "crumbs": [ "Home", "Explanations", "Stripe" ] }, { "objectID": "explains/stripe.html#pre-app-setup", "href": "explains/stripe.html#pre-app-setup", "title": "Stripe", "section": "Pre-app setup", "text": "Pre-app setup\n\n\n\n\n\n\nTip\n\n\n\nEverything in the pre-app setup sections is a run once and not to be included in your web-app.\n\n\n\nCreate a product\nYou can run this to programatically create a Stripe Product with a Price. Typically, this is not something you do dynamically in your FastHTML app, but rather something you set up one time. You can also optionally do this on the Stripe Dashboard UI.\n\n\nExported source\ndef _search_app(app_nm:str, limit=1): \n \"Checks for product based on app_nm and returns the product if it exists\"\n return stripe.Product.search(query=f\"name:'{app_nm}' AND active:'True'\", limit=limit).data\n\ndef create_price(app_nm:str, amt:int, currency=\"usd\") -> list[stripe.Price]:\n \"Create a product and bind it to a price object. If product already exist just return the price list.\"\n existing_product = _search_app(app_nm)\n if existing_product: \n return stripe.Price.list(product=existing_product[0].id).data\n else:\n product = stripe.Product.create(name=f\"{app_nm}\")\n return [stripe.Price.create(product=product.id, unit_amount=amt, currency=currency)]\n\ndef archive_price(app_nm:str):\n \"Archive a price - useful for cleanup if testing.\"\n existing_products = _search_app(app_nm, limit=50)\n for product in existing_products:\n for price in stripe.Price.list(product=product.id).data: \n stripe.Price.modify(price.id, active=False)\n stripe.Product.modify(product.id, active=False)\n\n\n\n\n\n\n\n\nTip\n\n\n\nTo do recurring payment, you would use recurring={\"interval\": \"year\"} or recurring={\"interval\": \"month\"} when creating your stripe price.\n\n\n\n\nExported source\napp_nm = \"[FastHTML Docs] Demo Product\"\nprice_list = create_price(app_nm, amt=1999)\nassert len(price_list) == 1, 'For this tutorial, we only have one price bound to our product.'\nprice = price_list[0]\n\n\n\nprint(f\"Price ID = {price.id}\")\n\nPrice ID = price_1R1ZzcFrdmWPkpOp9M28ykjy\n\n\n\n\nCreate a webook\nA webhook is simply a URL where your app listens for messages from Stripe. It provides a way for Stripe, the payment processor, to notify your application when something happens with a payment. Think of it like a delivery notification: when a customer completes a payment, Stripe needs to tell your application so you can update your records, send confirmation emails, or provide access to purchased content. It is simply a URL,\nBut your app needs to be sure every webhook event is actually coming from Stripe. That is, it needs to authenticate the notification. To do that, your app will need a webhook signing secret, which it uses to confirm that the notifications were signed by Stripe.\nThis secret is different from your Stripe API key. The Stripe API key lets you prove who you are to Stripe. The webhook signing secret lets you be sure messages from Stripe are coming from Stripe.\nYou will need a webhook signing secret whether your app is is running locally in test mode, or whether it is a real production app on running on a server. Here is how you get the webhook signing secret in these two cases.\n\nLocal Webhook\nWhen your application runs locally during development it can be reached only from your computer, so Stripe can’t make an HTTP request against the webhook. To workaround this in development, the Stripe CLI tool creates a secure tunnel which forwards these webhook notifications from Stripe’s servers to your local application.\nRun this command to start that tunnel:\nstripe listen --forward-to http://localhost:5001/webhook\nOn success, that command will also tell you the webhook signing secret. Take the secret it gives you and set it as an environment variable.\nexport STRIPE_LOCAL_TEST_WEBHOOK_SECRET=<your-secret>\n\n\nProduction Webhook\nFor a deployed app, you configure a permanent webhook connection in your Stripe Dashboard. This establishes an official notification channel where Stripe will send real-time updates about payments to your application’s /webhook URL.\nOn the dashboard, you can configure which specific payment event notifications will go to this webhook (e.g., completed checkouts, successful payments, failed payments, etc..). Your app provides the webhook signing secret to the Stripe library, to authenticate that these notifications come from the Stripe service. This is essential for production environments where your app needs to automatically respond to payment activities without manual intervention.\nTo configure the permanent webhook connection, you need to do the following steps:\n\nMake sure you are in Test mode like before\nGo to https://dashboard.stripe.com/test/webhooks\nClick “+ Add endpoint” to create create a new webhook (or, if that is missing, click “Create an event destination”).\nOn the primary screen shown below, “Listen to Stripe events”, fill out the details. Your Endpoint URL will be https://YOURDOMAIN/webhook\nSave your webhook signing scret. On the “Listen to Stripe events” screen, you can find it in the app sample code on the right hand side as the “endpoint secret”. You can also retrieve it later from the dashboard.\n\n\nYou also need to configure which events should generate webhook notifications:\n\nClick “+ Select events” to open the secondary control screen, “Select events to send”, which is shown below. In on our case we want to listen for checkout.session.completed.\nClick the “Add Events” button, to confirm which events to send.\n\n\n\n\n\n\n\n\nTip\n\n\n\nFor subscriptions you may also want to enable additional events for your webhook such as: customer.subscription.created, customer.subscription.deleted, and others based on your use-case.\n\n\n\nFinally, click “Add Endpoint”, to finish configuring the endpoint.", "crumbs": [ "Home", "Explanations", "Stripe" ] }, { "objectID": "explains/stripe.html#app", "href": "explains/stripe.html#app", "title": "Stripe", "section": "App", "text": "App\n\n\n\n\n\n\nTip\n\n\n\nEverything after this point is going to be included in your actual application. The application created in this tutorial can be found here\n\n\n\nSetup to have the right information\nIn order to accept a payment, you need to know who is making the payment.\nThere are many ways to accomplish this, for example using oauth or a form. For this example we will start by hardcoding an email address into a session to simulate what it would look like with oauth.\nWe save the email address into the session object, under the key auth. By putting this logic into beforeware, which runs before every request is processed, we ensure that every route handler will be able to read that address from the session object.\n\n\nExported source\ndef before(sess): sess['auth'] = 'hamel@hamel.com'\nbware = Beforeware(before, skip=['/webhook'])\napp, rt = fast_app(before=bware)\n\n\nWe will need our webhook secret that was created. For this tutorial, we will be using the local development environment variable that was created above. For your deployed production environment, you will need to get the secret for your webhook from the Stripe Dashboard.\n\n\nExported source\nWEBHOOK_SECRET = os.getenv(\"STRIPE_LOCAL_TEST_WEBHOOK_SECRET\")\n\n\n\n\nPayment Setup\nWe need 2 things first:\n\nA button for users to click to pay\nA route that gives stripe the information it needs to process the payment\n\n\n\nExported source\n@rt(\"/\")\ndef home(sess):\n auth = sess['auth']\n return Titled(\n \"Buy Now\", \n Div(H2(\"Demo Product - $19.99\"),\n P(f\"Welcome, {auth}\"),\n Button(\"Buy Now\", hx_post=\"/create-checkout-session\", hx_swap=\"none\"),\n A(\"View Account\", href=\"/account\")))\n\n\nWe are only allowing card payments (payment_method_types=['card']). For additional options see the Stripe docs.\n\n\nExported source\n@rt(\"/create-checkout-session\", methods=[\"POST\"])\nasync def create_checkout_session(sess):\n checkout_session = stripe.checkout.Session.create(\n line_items=[{'price': price.id, 'quantity': 1}],\n mode='payment',\n payment_method_types=['card'],\n customer_email=sess['auth'],\n metadata={'app_name': app_nm, \n 'AnyOther': 'Metadata',},\n # CHECKOUT_SESSION_ID is a special variable Stripe fills in for you\n success_url=DOMAIN_URL + '/success?checkout_sid={CHECKOUT_SESSION_ID}',\n cancel_url=DOMAIN_URL + '/cancel')\n return Redirect(checkout_session.url)\n\n\n\n\n\n\n\n\nTip\n\n\n\nFor subscriptions the mode would typically be subscription instead of payment\n\n\nThis section creates two key components: a simple webpage with a “Buy Now” button, and a function that handles what happens when that button is clicked.\nWhen a customer clicks “Buy Now,” the app creates a Stripe checkout session (essentially a payment page) with product details, price, and customer information. Stripe then takes over the payment process, showing the customer a secure payment form. After payment is completed or canceled, Stripe redirects the customer back to your app using the success or cancel URLs you specified. This approach keeps sensitive payment details off your server, as Stripe handles the actual transaction.\n\n\nPost-Payment Processing\nAfter a customer initiates payment, there are two parallel processes:\n\nUser Experience Flow: The customer is redirected to Stripe’s checkout page, completes payment, and is then redirected back to your application (either the success or cancel page).\nBackend Processing Flow: Stripe sends webhook notifications to your server about payment events, allowing your application to update records, provision access, or trigger other business logic.\n\nThis dual-track approach ensures both a smooth user experience and reliable payment processing.\nThe webhook notification is critical as it’s a reliable way to confirm payment completion.\n\nBackend Processing Flow\nCreate a database schema with the information you’d like to store.\n\n\nExported source\n# Database Table\nclass Payment:\n checkout_session_id: str # Stripe checkout session ID (primary key)\n email: str\n amount: int # Amount paid in cents\n payment_status: str # paid, pending, failed\n created_at: int # Unix timestamp\n metadata: str # Additional payment metadata as JSON\n\n\nConnect to the database\n\n\nExported source\ndb = Database(\"stripe_payments.db\")\npayments = db.create(Payment, pk='checkout_session_id', transform=True)\n\n\nIn our webhook we can execute any business logic and database updating we need to.\n\n\nExported source\n@rt(\"/webhook\")\nasync def post(req):\n payload = await req.body()\n # Verify the event came from Stripe\n try:\n event = stripe.Webhook.construct_event(\n payload, req.headers.get(\"stripe-signature\"), WEBHOOK_SECRET)\n except Exception as e:\n print(f\"Webhook error: {e}\")\n return\n if event and event.type == \"checkout.session.completed\":\n event_data = event.data.object\n if event_data.metadata.get('app_name') == app_nm:\n payment = Payment(\n checkout_session_id=event_data.id,\n email=event_data.customer_email,\n amount=event_data.amount_total,\n payment_status=event_data.payment_status,\n created_at=event_data.created,\n metadata=str(event_data.metadata))\n payments.insert(payment)\n print(f\"Payment recorded for user: {event_data.customer_email}\")\n \n # Do not worry about refunds yet, we will cover how to do this later in the tutorial\n elif event and event.type == \"charge.refunded\":\n event_data = event.data.object\n payment_intent_id = event_data.payment_intent\n sessions = stripe.checkout.Session.list(payment_intent=payment_intent_id)\n if sessions and sessions.data:\n checkout_sid = sessions.data[0].id\n payments.update(Payment(checkout_session_id= checkout_sid, payment_status=\"refunded\"))\n print(f\"Refund recorded for payment: {checkout_sid}\")\n\n\nThe webhook route is where Stripe sends automated notifications about payment events. When a payment is completed, Stripe sends a secure notification to this endpoint. The code verifies this notification is legitimate using the webhook secret, then processes the event data - extracting information like the customer’s email and payment status. This allows your application to automatically update user accounts, trigger fulfillment processes, or record transaction details without manual intervention.\nNote that in this route, our code extracts the user’s email address from the Stripe event, not from the session object. That is the because this route will be hit by a request from Stripe’s servers, not from the user’s browser.\n\n\n\n\n\n\nTip\n\n\n\nWhen doing a subscription, often you would add additional event types in an if statement to update your database appropriately with the subscription status.\nif event.type == \"payment_intent.succeeded\":\n ...\nelif event.type == \"customer.subscription.created\":\n ...\nelif event.type == \"customer.subscription.deleted\":\n ...\n\n\n\n\nUser Experience Flow\nThe /success route is where Stripe will redirect the user after the payment completes successfully, which will also be after Stripe has called the webhook to inform your app of the transaction.\nStripe knows to send the user here, because you provided Stripe with this route when you created a checkout session.\nBut you want to verify this is the case. So in this route, you should verify the user’s payment status, by checking your database for the entry which your app saved when it received that webhook notification.\n\n\nExported source\n@rt(\"/success\")\ndef success(sess, checkout_sid:str): \n # Get payment record from database (saved in the webhook)\n payment = payments[checkout_sid]\n\n if not payment or payment.payment_status != 'paid': \n return Titled(\"Error\", P(\"Payment not found\"))\n\n return Titled(\n \"Success\",\n Div(H2(\"Payment Successful!\"),\n P(f\"Thank you for your purchase, {sess['auth']}\"),\n P(f\"Amount Paid: ${payment.amount / 100:.2f}\"),\n P(f\"Status: {payment.payment_status}\"),\n P(f\"Transaction ID: {payment.checkout_session_id}\"),\n A(\"Back to Home\", href=\"/\")))\n\n\nThere is also a /cancel route, where Stripe will redirect the user if they canceled the checkout.\n\n\nExported source\n@rt(\"/cancel\")\ndef cancel():\n return Titled(\n \"Cancelled\",\n Div(H2(\"Payment Cancelled\"),\n P(\"Your payment was cancelled.\"),\n A(\"Back to Home\", href=\"/\")))\n\n\nThis image shows Stripe’s payment page that customers see after clicking the “Buy Now” button. When your app redirects to the Stripe checkout URL, Stripe displays this secure payment form where customers enter their card details. For testing purposes, you can use Stripe’s test card number (4242 4242 4242 4242) with any future expiration date and any 3-digit CVC code. This test card will successfully process payments in test mode without charging real money. The form shows the product name and price that were configured in your Stripe session, providing a seamless transition from your app to the payment processor and back again after completion.\n\nOnce you have processed the payments you can see each record in the sqlite database that was stored in the webhook.\nNext, we can see how to add the refund route\nIn order to use a refund capability we need an account management page where users can request refunds for their payments.\nWhen you initiate a refund, you can see the status of the refund in your Stripe dasbhoard at https://dashboard.stripe.com/payments, or https://dashboard.stripe.com/test/payments if you are in Test mode\nIt will look like this with a Refunded icon:", "crumbs": [ "Home", "Explanations", "Stripe" ] }, { "objectID": "explains/faq.html", "href": "explains/faq.html", "title": "FAQ", "section": "", "text": "Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.\nTo avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):\n\nOpen your FastHTML project\nPress Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette\nType “Preferences: Open Workspace Settings (JSON)” and select it\nIn the JSON file that opens, add the following lines:\n\n{\n \"python.analysis.diagnosticSeverityOverrides\": {\n \"reportGeneralTypeIssues\": \"none\",\n \"reportOptionalMemberAccess\": \"none\",\n \"reportWildcardImportFromLibrary\": \"none\",\n \"reportRedeclaration\": \"none\",\n \"reportAttributeAccessIssue\": \"none\",\n \"reportInvalidTypeForm\": \"none\",\n \"reportAssignmentType\": \"none\",\n }\n}\n\nSave the file\n\nEven with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:\n{\n \"python.analysis.ignore\": [ \"*\" ]\n}", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#why-does-my-editor-say-that-i-have-errors-in-my-fasthtml-code", "href": "explains/faq.html#why-does-my-editor-say-that-i-have-errors-in-my-fasthtml-code", "title": "FAQ", "section": "", "text": "Many editors, including Visual Studio Code, use PyLance to provide error checking for Python. However, PyLance’s error checking is just a guess – it can’t actually know whether your code is correct or not. PyLance particularly struggles with FastHTML’s syntax, which leads to it often reporting false error messages in FastHTML projects.\nTo avoid these misleading error messages, it’s best to disable some PyLance error checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the same approach should also work in other editors based on vscode, such as Cursor and GitHub Codespaces):\n\nOpen your FastHTML project\nPress Ctrl+Shift+P (or Cmd+Shift+P on Mac) to open the Command Palette\nType “Preferences: Open Workspace Settings (JSON)” and select it\nIn the JSON file that opens, add the following lines:\n\n{\n \"python.analysis.diagnosticSeverityOverrides\": {\n \"reportGeneralTypeIssues\": \"none\",\n \"reportOptionalMemberAccess\": \"none\",\n \"reportWildcardImportFromLibrary\": \"none\",\n \"reportRedeclaration\": \"none\",\n \"reportAttributeAccessIssue\": \"none\",\n \"reportInvalidTypeForm\": \"none\",\n \"reportAssignmentType\": \"none\",\n }\n}\n\nSave the file\n\nEven with PyLance diagnostics turned off, your FastHTML code will still run correctly. If you’re still seeing some false errors from PyLance, you can disable it entirely by adding this to your settings:\n{\n \"python.analysis.ignore\": [ \"*\" ]\n}", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#why-the-distinctive-coding-style", "href": "explains/faq.html#why-the-distinctive-coding-style", "title": "FAQ", "section": "Why the distinctive coding style?", "text": "Why the distinctive coding style?\nFastHTML coding style is the fastai coding style.\nIf you are coming from a data science background the fastai coding style may already be your preferred style.\nIf you are coming from a PEP-8 background where the use of ruff is encouraged, there is a learning curve. However, once you get used to the fastai coding style you may discover yourself appreciating the concise nature of this style. It also encourages using more functional programming tooling, which is both productive and fun. Having said that, it’s entirely optional!", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#why-not-jsx", "href": "explains/faq.html#why-not-jsx", "title": "FAQ", "section": "Why not JSX?", "text": "Why not JSX?\nMany have asked! We think there’s no benefit… Python’s positional and kw args precisely 1:1 map already to html/xml children and attrs, so there’s no need for a new syntax.\nWe wrote some more thoughts on Why Python HTML components over Jinja2, Mako, or JSX here.", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#why-use-import", "href": "explains/faq.html#why-use-import", "title": "FAQ", "section": "Why use import *", "text": "Why use import *\nFirst, through the use of the __all__ attribute in our Python modules we control what actually gets imported. So there’s no risk of namespace pollution.\nSecond, our style lends itself to working in rather compact Jupyter notebooks and small Python modules. Hence we know about the source code whose libraries we import * from. This terseness means we can develop faster. We’re a small team, and any edge we can gain is important to us.\nThird, for external libraries, be it core Python, SQLAlchemy, or other things we do tend to use explicit imports. In part to avoid namespace collisions, and also as reference to know where things are coming from.\nWe’ll finish by saying a lot of our users employ explicit imports. If that’s the path you want to take, we encourage the use of from fasthtml import common as fh. The acronym of fh makes it easy to recognize that a symbol is from the FastHTML library.", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#can-fasthtml-be-used-for-dashboards", "href": "explains/faq.html#can-fasthtml-be-used-for-dashboards", "title": "FAQ", "section": "Can FastHTML be used for dashboards?", "text": "Can FastHTML be used for dashboards?\nYes it can. In fact, it excels at building dashboards. In addition to being great for building static dashboards, because of its foundation in ASGI and tech stack, FastHTML natively supports Websockets. That means using FastHTML we can create dashboards that autoupdate.", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#why-is-fasthtml-developed-using-notebooks", "href": "explains/faq.html#why-is-fasthtml-developed-using-notebooks", "title": "FAQ", "section": "Why is FastHTML developed using notebooks?", "text": "Why is FastHTML developed using notebooks?\nSome people are under the impression that writing software in notebooks is bad.\nWatch this video. We’ve used Jupyter notebooks exported via nbdev to write a wide range of “very serious” software projects over the last three years. This includes deep learning libraries, API clients, Python language extensions, terminal user interfaces, web frameworks, and more!\nnbdev is a Jupyter-powered tool for writing software. Traditional programming environments throw away the result of your exploration in REPLs or notebooks. nbdev makes exploration an integral part of your workflow, all while promoting software engineering best practices.", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "explains/faq.html#why-not-pyproject.toml-for-packaging", "href": "explains/faq.html#why-not-pyproject.toml-for-packaging", "title": "FAQ", "section": "Why not pyproject.toml for packaging?", "text": "Why not pyproject.toml for packaging?\nFastHTML uses a setup.py module instead of a pyproject.toml file to configure itself for installation. The reason for this is pyproject.toml is not compatible with nbdev, which is what is used to write and build FastHTML.\nThe nbdev project spent around a year trying to move to pyproject.toml but there was insufficient functionality in the toml-based approach to complete the transition.", "crumbs": [ "Home", "Explanations", "FAQ" ] }, { "objectID": "unpublished/tutorial_for_web_devs.html", "href": "unpublished/tutorial_for_web_devs.html", "title": "BYO Blog", "section": "", "text": "Caution\n\n\n\nThis document is a work in progress.\nIn this tutorial we’re going to write a blog by example. Blogs are a good way to learn a web framework as they start simple yet can get surprisingly sophistated. The wikipedia definition of a blog is “an informational website consisting of discrete, often informal diary-style text entries (posts) informal diary-style text entries (posts)”, which means we need to provide these basic features:\nWe’ll also add in these features, so the blog can become a working site:" }, { "objectID": "unpublished/tutorial_for_web_devs.html#how-to-best-use-this-tutorial", "href": "unpublished/tutorial_for_web_devs.html#how-to-best-use-this-tutorial", "title": "BYO Blog", "section": "How to best use this tutorial", "text": "How to best use this tutorial\nWe could copy/paste every code example in sequence and have a finished blog at the end. However, it’s debatable how much we will learn through the copy/paste method. We’re not saying its impossible to learn through copy/paste, we’re just saying it’s not that of an efficient way to learn. It’s analogous to learning how to play a musical instrument or sport or video game by watching other people do it - you can learn some but its not the same as doing.\nA better approach is to type out every line of code in this tutorial. This forces us to run the code through our brains, giving us actual practice in how to write FastHTML and Pythoncode and forcing us to debug our own mistakes. In some cases we’ll repeat similar tasks - a key component in achieving mastery in anything. Coming back to the instrument/sport/video game analogy, it’s exactly like actually practicing an instrument, sport, or video game. Through practice and repetition we eventually achieve mastery." }, { "objectID": "unpublished/tutorial_for_web_devs.html#installing-fasthtml", "href": "unpublished/tutorial_for_web_devs.html#installing-fasthtml", "title": "BYO Blog", "section": "Installing FastHTML", "text": "Installing FastHTML\nFastHTML is just Python. Installation is often done with pip:\npip install python-fasthtml" }, { "objectID": "unpublished/tutorial_for_web_devs.html#a-minimal-fasthtml-app", "href": "unpublished/tutorial_for_web_devs.html#a-minimal-fasthtml-app", "title": "BYO Blog", "section": "A minimal FastHTML app", "text": "A minimal FastHTML app\nFirst, create the directory for our project using Python’s pathlib module:\nimport pathlib\npathlib.Path('blog-system').mkdir()\nNow that we have our directory, let’s create a minimal FastHTML site in it.\n\n\nblog-system/minimal.py\n\nfrom fasthtml.common import * \n\napp, rt = fast_app() \n\n@rt(\"/\") \ndef get():\n return Titled(\"FastHTML\", P(\"Let's do this!\")) \n\nserve()\n\nRun that with python minimal.py and you should get something like this:\npython minimal.py \nLink: http://localhost:5001\nINFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']\nINFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [46572] using WatchFiles\nINFO: Started server process [46576]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nConfirm FastHTML is running by opening your web browser to 127.0.0.1:5001. You should see something like the image below:\n\n\n\n\n\n\n\nWhat about the import *?\n\n\n\nFor those worried about the use of import * rather than a PEP8-style declared namespace, understand that __all__ is defined in FastHTML’s common module. That means that only the symbols (functions, classes, and other things) the framework wants us to have will be brought into our own code via import *. Read importing from a package) for more information.\nNevertheless, if we want to use a defined namespace we can do so. Here’s an example:\nfrom fasthtml import common as fh\n\n\napp, rt = fh.fast_app() \n\n@rt(\"/\") \ndef get():\n return fh.Titled(\"FastHTML\", fh.P(\"Let's do this!\")) \n\nfh.serve()" }, { "objectID": "unpublished/tutorial_for_web_devs.html#looking-more-closely-at-our-app", "href": "unpublished/tutorial_for_web_devs.html#looking-more-closely-at-our-app", "title": "BYO Blog", "section": "Looking more closely at our app", "text": "Looking more closely at our app\nLet’s look more closely at our application. Every line is packed with powerful features of FastHTML:\n\n\nblog-system/minimal.py\n\n1from fasthtml.common import *\n\n2app, rt = fast_app()\n\n3@rt(\"/\")\n4def get():\n5 return Titled(\"FastHTML\", P(\"Let's do this!\"))\n\n6serve()\n\n\n1\n\nThe top level namespace of Fast HTML (fasthtml.common) contains everything we need from FastHTML to build applications. A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.\n\n2\n\nWe instantiate a FastHTML app with the fast_app() utility function. This provides a number of really useful defaults that we’ll modify or take advantage of later in the tutorial.\n\n3\n\nWe use the rt() decorator to tell FastHTML what to return when a user visits / in their browser.\n\n4\n\nWe connect this route to HTTP GET requests by defining a view function called get().\n\n5\n\nA 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.\n\n6\n\nThe serve() utility configures and runs FastHTML using a library called uvicorn. Any changes to this module will be reloaded into the browser." }, { "objectID": "unpublished/tutorial_for_web_devs.html#adding-dynamic-content-to-our-minimal-app", "href": "unpublished/tutorial_for_web_devs.html#adding-dynamic-content-to-our-minimal-app", "title": "BYO Blog", "section": "Adding dynamic content to our minimal app", "text": "Adding dynamic content to our minimal app\nOur page is great, but we’ll make it better. Let’s add a randomized list of letters to the page. Every time the page reloads, a new list of varying length will be generated.\n\n\nblog-system/random_letters.py\n\nfrom fasthtml.common import *\n1import string, random\n\napp, rt = fast_app()\n\n@rt(\"/\")\ndef get():\n2 letters = random.choices(string.ascii_uppercase, k=random.randint(5, 20))\n3 items = [Li(c) for c in letters]\n return Titled(\"Random lists of letters\",\n4 Ul(*items)\n ) \n\nserve()\n\n\n1\n\nThe string and random libraries are part of Python’s standard library\n\n2\n\nWe use these libraries to generate a random length list of random letters called letters\n\n3\n\nUsing letters as the base we use list comprehension to generate a list of Li ft display components, each with their own letter and save that to the variable items\n\n4\n\nInside a call to the Ul() ft component we use Python’s *args special syntax on the items variable. Therefore *list is treated not as one argument but rather a set of them.\n\n\nWhen this is run, it will generate something like this with a different random list of letters for each page load:" }, { "objectID": "unpublished/tutorial_for_web_devs.html#storing-the-articles", "href": "unpublished/tutorial_for_web_devs.html#storing-the-articles", "title": "BYO Blog", "section": "Storing the articles", "text": "Storing the articles\nThe most basic component of a blog is a series of articles sorted by date authored. Rather than a database we’re going to use our computer’s harddrive to store a set of markdown files in a directory within our blog called posts. First, let’s create the directory and some test files we can use to search for:\n\nfrom fastcore.utils import *\n\n\n# Create some dummy posts\nposts = Path(\"posts\")\nposts.mkdir(exist_ok=True)\nfor i in range(10): (posts/f\"article_{i}.md\").write_text(f\"This is article {i}\")\n\nSearching for these files can be done with pathlib.\n\nimport pathlib\nposts.ls()\n\n(#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')]\n\n\n\n\n\n\n\n\nTip\n\n\n\nPython’s pathlib library is quite useful and makes file search and manipulation much easier. There’s many uses for it and is compatible across operating systems." }, { "objectID": "unpublished/tutorial_for_web_devs.html#creating-the-blog-home-page", "href": "unpublished/tutorial_for_web_devs.html#creating-the-blog-home-page", "title": "BYO Blog", "section": "Creating the blog home page", "text": "Creating the blog home page\nWe now have enough tools that we can create the home page. Let’s create a new Python file and write out our simple view to list the articles in our blog.\n\n\nblog-system/main.py\n\nfrom fasthtml.common import *\nimport pathlib\n\napp, rt = fast_app()\n\n@rt(\"/\")\ndef get():\n fnames = pathlib.Path(\"posts\").rglob(\"*.md\")\n items = [Li(A(fname, href=fname)) for fname in fnames] \n return Titled(\"My Blog\",\n Ul(*items)\n ) \n\nserve()\n\n\nfor p in posts.ls(): p.unlink()" }, { "objectID": "ref/response_types.html", "href": "ref/response_types.html", "title": "Response Types", "section": "", "text": "FastHTML provides multiple HTTP response types that automatically set the appropriate HTTP content type and handle serialization. The main response types are:", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/response_types.html#configuration", "href": "ref/response_types.html#configuration", "title": "Response Types", "section": "Configuration", "text": "Configuration\n\nfrom fasthtml.common import *\n\n\napp,rt = fast_app()\n\napp and rt are the common FastHTML route handler decorators. We instantiate them with the fast_app function.\n\ncli = Client(app)\n\nFastHTML comes with the test client named Client. It allows us to test handlers via a simple interface where .get() is a HTTP GET request, .post() is a HTTP POST request.", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/response_types.html#ft-component-response", "href": "ref/response_types.html#ft-component-response", "title": "Response Types", "section": "FT Component Response", "text": "FT Component Response\n\n@rt('/ft')\ndef get(): return Html(Div('FT Component Response'))\n\nThis is the response type you’re probably most familiar with. Here the route handler returns an FT component, which FastHTML wraps in an HTML document with a head and body.\n\nprint(cli.get('/ft').text)\n\n <!doctype html>\n <html>\n <div>FT Component Response</div>\n </html>", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/response_types.html#redirect-response", "href": "ref/response_types.html#redirect-response", "title": "Response Types", "section": "Redirect Response", "text": "Redirect Response\n\n@rt('/rr')\ndef get(): return Redirect('https://fastht.ml/')\n\nHere in this route handler, Redirect redirects the user’s browser to the new URL ‘https://fastht.ml/’\n\nresp = cli.get('/rr')\nprint(resp.url)\nprint(resp.status_code)\n\nhttp://testserver/rr\n303\n\n\nYou can see the URL in the response headers and url attribute, as well as a status code of 303.", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/response_types.html#json-response", "href": "ref/response_types.html#json-response", "title": "Response Types", "section": "JSON Response", "text": "JSON Response\n\n@rt('/json')\ndef get(): return {'hello': 'world'}\n\nThis route handler returns a JSON response, where the content-type has been set to .\n\nresp = cli.get('/json')\nprint(resp.headers)\nprint(resp.json())\n\nHeaders({'content-length': '17', 'content-type': 'application/json'})\n{'hello': 'world'}\n\n\nYou can see that the Content-Type header has been set to application/json, and that the response is simply the JSON without any HTML wrapping it.", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/response_types.html#eventstream", "href": "ref/response_types.html#eventstream", "title": "Response Types", "section": "EventStream", "text": "EventStream\n\nfrom time import sleep\n\ndef counter():\n \"\"\"Counter is an generator that\n publishes a number every second.\n \"\"\"\n for i in range(3):\n yield sse_message(f\"Event {i}\")\n sleep(1)\n\n@rt('/stream')\ndef get():\n return EventStream(counter())\n\nWith 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.\n\nresp = cli.get('/stream')\nprint(resp.text)\n\nevent: message\ndata: Event 0\n\nevent: message\ndata: Event 1\n\nevent: message\ndata: Event 2\n\n\n\n\nEach one of the message events above arrived one second after the previous message event.", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/response_types.html#plaintext-response", "href": "ref/response_types.html#plaintext-response", "title": "Response Types", "section": "Plaintext Response", "text": "Plaintext Response\n\n@rt('/text')\ndef get(): return 'Hello world'\n\nWhen you return a string from a route handler, you get a plain-text response.\n\nprint(cli.get('/text').text)\n\nHello world\n\n\nHere you can see that the response text is simply the string you returned, without any HTML wrapping it.", "crumbs": [ "Home", "Reference", "Response Types" ] }, { "objectID": "ref/live_reload.html", "href": "ref/live_reload.html", "title": "Live Reloading", "section": "", "text": "When building your app it can be useful to view your changes in a web browser as you make them. FastHTML supports live reloading which means that it watches for any changes to your code and automatically refreshes the webpage in your browser.\nTo enable live reloading simply replace FastHTML in your app with FastHTMLWithLiveReload.\nThen in your terminal run uvicorn with reloading enabled.\n⚠️ Gotchas - A reload is only triggered when you save your changes. - FastHTMLWithLiveReload should only be used during development. - If your app spans multiple directories you might need to use the --reload-dir flag to watch all files in each directory. See the uvicorn docs for more info. - The live reload script is only injected into the page when rendering ft components.", "crumbs": [ "Home", "Reference", "Live Reloading" ] }, { "objectID": "ref/live_reload.html#live-reloading-with-fast_app", "href": "ref/live_reload.html#live-reloading-with-fast_app", "title": "Live Reloading", "section": "Live reloading with fast_app", "text": "Live reloading with fast_app\nIn development the fast_app function provides the same functionality. It instantiates the FastHTMLWithLiveReload class if you pass live=True:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\n1app, rt = fast_app(live=True)\n\n2serve()\n\n\n1\n\nfast_app() instantiates the FastHTMLWithLiveReload class.\n\n2\n\nserve() is a wrapper around a uvicorn call.\n\n\nTo run main.py in live reload mode, just do python main.py. We recommend turning off live reload when deploying your app to production.", "crumbs": [ "Home", "Reference", "Live Reloading" ] }, { "objectID": "ref/best_practice.html", "href": "ref/best_practice.html", "title": "FastHTML Best Practices", "section": "", "text": "FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that’s both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss:", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#database-table-creation", "href": "ref/best_practice.html#database-table-creation", "title": "FastHTML Best Practices", "section": "Database Table Creation", "text": "Database Table Creation\nBefore:\ntodos = db.t.todos\nif not todos.exists():\ntodos.create(id=int, task=str, completed=bool, created=str, pk='id')\nAfter:\nclass Todo: id:int; task:str; completed:bool; created:str\ntodos = db.create(Todo)\nFastLite’s create() is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The id field is automatically the primary key.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#route-naming-conventions", "href": "ref/best_practice.html#route-naming-conventions", "title": "FastHTML Best Practices", "section": "Route Naming Conventions", "text": "Route Naming Conventions\nBefore:\n@rt(\"/\")\ndef get(): return Titled(\"Todo List\", ...)\n\n@rt(\"/add\")\ndef post(task: str): ...\nAfter:\n@rt\ndef index(): return Titled(\"Todo List\", ...) # Special name for \"/\"\n@rt\ndef add(task: str): ... # Function name becomes route\nUse @rt without arguments and let the function name define the route. The special name index maps to /.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#query-parameters-over-path-parameters", "href": "ref/best_practice.html#query-parameters-over-path-parameters", "title": "FastHTML Best Practices", "section": "Query Parameters over Path Parameters", "text": "Query Parameters over Path Parameters\nBefore:\n@rt(\"/toggle/{todo_id}\")\ndef post(todo_id: int): ...\n# URL: /toggle/123\nAfter:\n@rt\ndef toggle(id: int): ...\n# URL: /toggle?id=123\nQuery parameters are more idiomatic in FastHTML and avoid duplicating param names in the path.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#leverage-return-values", "href": "ref/best_practice.html#leverage-return-values", "title": "FastHTML Best Practices", "section": "Leverage Return Values", "text": "Leverage Return Values\n\nBefore:\n@rt\ndef add(task: str):\n new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())\n return todo_item(todos[new_todo])\n\n@rt\ndef toggle(id: int):\n todo = todos[id]\n todos.update(completed=not todo.completed, id=id)\n return todo_item(todos[id])\nAfter:\n@rt\ndef add(task: str):\n return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))\n\n@rt\ndef toggle(id: int):\n return todo_item(todos.update(completed=not todos[id].completed, id=id))\nBoth insert() and update() return the affected object, enabling functional chaining.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#use-.to-for-url-generation", "href": "ref/best_practice.html#use-.to-for-url-generation", "title": "FastHTML Best Practices", "section": "Use .to() for URL Generation", "text": "Use .to() for URL Generation\nBefore:\nhx_post=f\"/toggle?id={todo.id}\"\nAfter:\nhx_post=toggle.to(id=todo.id)\nThe .to() method generates URLs with type safety and is refactoring-friendly.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#picocss-comes-free", "href": "ref/best_practice.html#picocss-comes-free", "title": "FastHTML Best Practices", "section": "PicoCSS comes free", "text": "PicoCSS comes free\nBefore:\nstyle = Style(\"\"\"\n.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }\n/* ... many more lines ... */\n\"\"\")\nAfter:\n# Just use semantic HTML - Pico styles it automatically\nContainer(...), Article(...), Card(...), Group(...)\nfast_app() includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#smart-defaults", "href": "ref/best_practice.html#smart-defaults", "title": "FastHTML Best Practices", "section": "Smart Defaults", "text": "Smart Defaults\nBefore:\nreturn Titled(\"Todo List\", Container(...))\n\nif __name__ == \"__main__\":\n serve()\nAfter:\nreturn Titled(\"Todo List\", ...) # Container is automatic\n\nserve() # No need for if __name__ guard\nTitled already wraps content in a Container, and serve() handles the main check internally.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#fasthtml-handles-iterables", "href": "ref/best_practice.html#fasthtml-handles-iterables", "title": "FastHTML Best Practices", "section": "FastHTML Handles Iterables", "text": "FastHTML Handles Iterables\nBefore:\nSection(*[todo_item(todo) for todo in all_todos], id=\"todo-list\")\nAfter:\nSection(map(todo_item, all_todos), id=\"todo-list\")\nFastHTML components accept iterables directly - no need to unpack with *.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#functional-patterns", "href": "ref/best_practice.html#functional-patterns", "title": "FastHTML Best Practices", "section": "Functional Patterns", "text": "Functional Patterns\nList comprehensions are great, but map() is often cleaner for simple transformations, especially when combined with FastHTML’s iterable handling.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#minimal-code", "href": "ref/best_practice.html#minimal-code", "title": "FastHTML Best Practices", "section": "Minimal Code", "text": "Minimal Code\nBefore:\n@rt\ndef delete(id: int):\n # Delete from database\n todos.delete(id)\n # Return empty response\n return \"\"\nAfter:\n@rt\ndef delete(id: int): todos.delete(id)\n\nSkip comments when code is self-documenting\nDon’t return empty strings - None is returned by default\nUse a single line for a single idea.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#use-post-for-all-mutations", "href": "ref/best_practice.html#use-post-for-all-mutations", "title": "FastHTML Best Practices", "section": "Use POST for All Mutations", "text": "Use POST for All Mutations\nBefore:\nhx_delete=f\"/delete?id={todo.id}\"\nAfter:\nhx_post=delete.to(id=todo.id)\nFastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler.", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/best_practice.html#modern-htmx-event-syntax", "href": "ref/best_practice.html#modern-htmx-event-syntax", "title": "FastHTML Best Practices", "section": "Modern HTMX Event Syntax", "text": "Modern HTMX Event Syntax\nBefore:\nhx_on=\"htmx:afterRequest: this.reset()\"\nAfter:\nhx_on__after_request=\"this.reset()\"\nThis works because:\n\nhx-on=\"event: code\" is deprecated; hx-on-event=\"code\" is preferred\nFastHTML converts _ to - (so hx_on__after_request becomes hx-on--after-request)\n:: in HTMX can be used as a shortcut for :htmx:.\nHTMX natively accepts - instead of : (so -htmx- works like :htmx:)\nHTMX accepts e.g after-request as an alternative to camelCase afterRequest", "crumbs": [ "Home", "Reference", "FastHTML Best Practices" ] }, { "objectID": "ref/concise_guide.html", "href": "ref/concise_guide.html", "title": "Concise reference", "section": "", "text": "from fasthtml.common import *\n\nFastHTML is a python library which brings together Starlette, Uvicorn, HTMX, and fastcore’s FT “FastTags” into a library for creating server-rendered hypermedia applications. The FastHTML class itself inherits from Starlette, and adds decorator-based routing with many additions, Beforeware, automatic FT to HTML rendering, and much more.\nThings to remember when writing FastHTML apps:\n\nNot compatible with FastAPI syntax; FastHTML is for HTML-first apps, not API services (although it can implement APIs too)\nFastHTML 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. MonsterUI is a richer FastHTML-first component framework with similar capabilities to shadcn\nFastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte\nUse serve() for running uvicorn (if __name__ == \"__main__\" is not needed since it’s automatic)\nWhen 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.", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#about-fasthtml", "href": "ref/concise_guide.html#about-fasthtml", "title": "Concise reference", "section": "", "text": "from fasthtml.common import *\n\nFastHTML is a python library which brings together Starlette, Uvicorn, HTMX, and fastcore’s FT “FastTags” into a library for creating server-rendered hypermedia applications. The FastHTML class itself inherits from Starlette, and adds decorator-based routing with many additions, Beforeware, automatic FT to HTML rendering, and much more.\nThings to remember when writing FastHTML apps:\n\nNot compatible with FastAPI syntax; FastHTML is for HTML-first apps, not API services (although it can implement APIs too)\nFastHTML 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. MonsterUI is a richer FastHTML-first component framework with similar capabilities to shadcn\nFastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte\nUse serve() for running uvicorn (if __name__ == \"__main__\" is not needed since it’s automatic)\nWhen 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.", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#minimal-app", "href": "ref/concise_guide.html#minimal-app", "title": "Concise reference", "section": "Minimal App", "text": "Minimal App\nThe code examples here use fast.ai style: prefer ternary op, 1-line docstring, minimize vertical space, etc. (Normally fast.ai style uses few if any comments, but they’re added here as documentation.)\nA minimal FastHTML app looks something like this:\n# Meta-package with all key symbols from FastHTML and Starlette. Import it like this at the start of every FastHTML app.\nfrom fasthtml.common import *\n# The FastHTML app object and shortcut to `app.route`\napp,rt = fast_app()\n\n# Enums constrain the values accepted for a route parameter\nname = str_enum('names', 'Alice', 'Bev', 'Charlie')\n\n# Passing a path to `rt` is optional. If not passed (recommended), the function name is the route ('/foo')\n# Both GET and POST HTTP methods are handled by default\n# Type-annotated params are passed as query params (recommended) unless a path param is defined (which it isn't here)\n@rt\ndef foo(nm: name):\n # `Title` and `P` here are FastTags: direct m-expression mappings of HTML tags to Python functions with positional and named parameters. All standard HTML tags are included in the common wildcard import.\n # When a tuple is returned, this returns concatenated HTML partials. HTMX by default will use a title HTML partial to set the current page name. HEAD tags (e.g. Meta, Link, etc) in the returned tuple are automatically placed in HEAD; everything else is placed in BODY.\n # FastHTML will automatically return a complete HTML document with appropriate headers if a normal HTTP request is received. For an HTMX request, however, just the partials are returned.\n return Title(\"FastHTML\"), H1(\"My web app\"), P(f\"Hello, {name}!\")\n# By default `serve` runs uvicorn on port 5001. Never write `if __name__ == \"__main__\"` since `serve` checks it internally.\nserve()\nTo run this web app:\npython main.py # access via localhost:5001", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#fasttags-aka-ft-components-or-fts", "href": "ref/concise_guide.html#fasttags-aka-ft-components-or-fts", "title": "Concise reference", "section": "FastTags (aka FT Components or FTs)", "text": "FastTags (aka FT Components or FTs)\nFTs are m-expressions plus simple sugar. Positional params map to children. Named parameters map to attributes. Aliases must be used for Python reserved words.\n\ntags = Title(\"FastHTML\"), H1(\"My web app\"), P(f\"Let's do this!\", cls=\"myclass\")\ntags\n\n(title(('FastHTML',),{}),\n h1(('My web app',),{}),\n p((\"Let's do this!\",),{'class': 'myclass'}))\n\n\nThis example shows key aspects of how FTs handle attributes:\nLabel(\n \"Choose an option\", \n Select(\n Option(\"one\", value=\"1\", selected=True), # True renders just the attribute name\n Option(\"two\", value=2, selected=False), # Non-string values are converted to strings. False omits the attribute entirely\n cls=\"selector\", id=\"counter\", # 'cls' becomes 'class'\n **{'@click':\"alert('Clicked');\"}, # Dict unpacking for attributes with special chars\n ),\n _for=\"counter\", # '_for' becomes 'for' (can also use 'fr')\n)\nClasses with __ft__ defined are rendered using that method.\n\nclass FtTest:\n def __ft__(self): return P('test')\n \nto_xml(FtTest())\n\n'<p>test</p>\\n'\n\n\nYou can create new FTs by importing the new component from fasthtml.components. If the FT doesn’t exist within that module, FastHTML will create it.\n\nfrom fasthtml.components import Some_never_before_used_tag\n\nSome_never_before_used_tag()\n\n<some-never-before-used-tag></some-never-before-used-tag>\n\n\nFTs can be combined by defining them as a function.\n\ndef Hero(title, statement): return Div(H1(title),P(statement), cls=\"hero\")\nto_xml(Hero(\"Hello World\", \"This is a hero statement\"))\n\n'<div class=\"hero\">\\n <h1>Hello World</h1>\\n <p>This is a hero statement</p>\\n</div>\\n'\n\n\nWhen handling a response, FastHTML will automatically render FTs using the to_xml function.\n\nto_xml(tags)\n\n'<title>FastHTML</title>\\n<h1>My web app</h1>\\n<p class=\"myclass\">Let's do this!</p>\\n'", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#js", "href": "ref/concise_guide.html#js", "title": "Concise reference", "section": "JS", "text": "JS\nThe Script function allows you to include JavaScript. You can use Python to generate parts of your JS or JSON like this:\n# In future snippets this import will not be shown, but is required\nfrom fasthtml.common import * \napp,rt = fast_app(hdrs=[Script(src=\"https://cdn.plot.ly/plotly-2.32.0.min.js\")])\n# `index` is a special function name which maps to the `/` route. \n@rt\ndef index():\n data = {'somedata':'fill me in…'}\n # `Titled` returns a title tag and an h1 tag with the 1st param, with remaining params as children in a `Main` parent.\n return Titled(\"Chart Demo\", Div(id=\"myDiv\"), Script(f\"var data = {data}; Plotly.newPlot('myDiv', data);\"))\n# In future snippets `serve() will not be shown, but is required\nserve()\nPrefer Python whenever possible over JS. Never use React or shadcn.", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#fast_app-hdrs", "href": "ref/concise_guide.html#fast_app-hdrs", "title": "Concise reference", "section": "fast_app hdrs", "text": "fast_app hdrs\n# In future snippets we'll skip showing the `fast_app` call if it has no params\napp, rt = fast_app(\n pico=False, # The Pico CSS framework is included by default, so pass `False` to disable it if needed. No other CSS frameworks are included.\n # These are added to the `head` part of the page for non-HTMX requests.\n hdrs=(\n Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),\n Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),\n Style(\"p {color: red;}\"),\n # `MarkdownJS` and `HighlightJS` are available via concise functions\n MarkdownJS(), HighlightJS(langs=['python', 'javascript', 'html', 'css']),\n # by default, all standard static extensions are served statically from the web app dir,\n # which can be modified using e.g `static_path='public'`\n )\n)\n\n@rt\ndef index(req): return Titled(\"Markdown rendering example\",\n # This will be client-side rendered to HTML with highlight-js\n Div(\"*hi* there\",cls=\"marked\"),\n # This will be syntax highlighted\n Pre(Code(\"def foo(): pass\")))", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#responses", "href": "ref/concise_guide.html#responses", "title": "Concise reference", "section": "Responses", "text": "Responses\nRoutes can return various types:\n\nFastTags or tuples of FastTags (automatically rendered to HTML)\nStandard Starlette responses (used directly)\nJSON-serializable types (returned as JSON in a plain text response)\n\n@rt(\"/{fname:path}.{ext:static}\")\nasync def serve_static_file(fname:str, ext:str): return FileResponse(f'public/{fname}.{ext}')\n\napp, rt = fast_app(hdrs=(MarkdownJS(), HighlightJS(langs=['python', 'javascript'])))\n@rt\ndef index(): \n return Titled(\"Example\",\n Div(\"*markdown* here\", cls=\"marked\"),\n Pre(Code(\"def foo(): pass\")))\nRoute functions can be used in attributes like href or action and will be converted to paths. Use .to() to generate paths with query parameters.\n@rt\ndef profile(email:str): return fill_form(profile_form, profiles[email])\n\nprofile_form = Form(action=profile)(\n Label(\"Email\", Input(name=\"email\")),\n Button(\"Save\", type=\"submit\")\n)\n\nuser_profile_path = profile.to(email=\"user@example.com\") # '/profile?email=user%40example.com'\n\nfrom dataclasses import dataclass\n\napp,rt = fast_app()\n\nWhen a route handler function is used as a fasttag attribute (such as href, hx_get, or action) it is converted to that route’s path. fill_form is used to copy an object’s matching attrs into matching-name form fields.\n\n@dataclass\nclass Profile: email:str; phone:str; age:int\nemail = 'john@example.com'\nprofiles = {email: Profile(email=email, phone='123456789', age=5)}\n@rt\ndef profile(email:str): return fill_form(profile_form, profiles[email])\n\nprofile_form = Form(method=\"post\", action=profile)(\n Fieldset(\n Label('Email', Input(name=\"email\")),\n Label(\"Phone\", Input(name=\"phone\")),\n Label(\"Age\", Input(name=\"age\"))),\n Button(\"Save\", type=\"submit\"))", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#testing", "href": "ref/concise_guide.html#testing", "title": "Concise reference", "section": "Testing", "text": "Testing\nWe can use TestClient for testing.\n\nfrom starlette.testclient import TestClient\n\n\npath = \"/profile?email=john@example.com\"\nclient = TestClient(app)\nhtmx_req = {'HX-Request':'1'}\nprint(client.get(path, headers=htmx_req).text)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"123456789\">\n</label><label>Age <input name=\"age\" value=\"5\">\n</label></fieldset><button type=\"submit\">Save</button></form>", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#form-handling-and-data-binding", "href": "ref/concise_guide.html#form-handling-and-data-binding", "title": "Concise reference", "section": "Form Handling and Data Binding", "text": "Form Handling and Data Binding\nWhen a dataclass, namedtuple, etc. is used as a type annotation, the form body will be unpacked into matching attribute names automatically.\n\n@rt\ndef edit_profile(profile: Profile):\n profiles[email]=profile\n return RedirectResponse(url=path)\n\nnew_data = dict(email='john@example.com', phone='7654321', age=25)\nprint(client.post(\"/edit_profile\", data=new_data, headers=htmx_req).text)\n\n<form enctype=\"multipart/form-data\" method=\"post\" action=\"/profile\"><fieldset><label>Email <input name=\"email\" value=\"john@example.com\">\n</label><label>Phone <input name=\"phone\" value=\"7654321\">\n</label><label>Age <input name=\"age\" value=\"25\">\n</label></fieldset><button type=\"submit\">Save</button></form>", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#fasttag-rendering-rules", "href": "ref/concise_guide.html#fasttag-rendering-rules", "title": "Concise reference", "section": "fasttag Rendering Rules", "text": "fasttag Rendering Rules\nThe general rules for rendering children inside tuples or fasttag children 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\nIf 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 Safe(...), e.g to show a data frame use Div(NotStr(df.to_html())).", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#exceptions", "href": "ref/concise_guide.html#exceptions", "title": "Concise reference", "section": "Exceptions", "text": "Exceptions\nFastHTML allows customization of exception handlers.\ndef not_found(req, exc): return Titled(\"404: I don't exist!\")\nexception_handlers = {404: not_found}\napp, rt = fast_app(exception_handlers=exception_handlers)", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#cookies", "href": "ref/concise_guide.html#cookies", "title": "Concise reference", "section": "Cookies", "text": "Cookies\nWe can set cookies using the cookie() function.\n\n@rt\ndef setcook(): return P(f'Set'), cookie('mycookie', 'foobar')\nprint(client.get('/setcook', headers=htmx_req).text)\n\n <p>Set</p>\n\n\n\n\n@rt\ndef getcook(mycookie:str): return f'Got {mycookie}'\n# If handlers return text instead of FTs, then a plaintext response is automatically created\nprint(client.get('/getcook').text)\n\nGot foobar\n\n\nFastHTML provide access to Starlette’s request object automatically using special request parameter name (or any prefix of that name).\n\n@rt\ndef headers(req): return req.headers['host']", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#request-and-session-objects", "href": "ref/concise_guide.html#request-and-session-objects", "title": "Concise reference", "section": "Request and Session Objects", "text": "Request and Session Objects\nFastHTML provides access to Starlette’s session middleware automatically using the special session parameter name (or any prefix of that name).\n\n@rt\ndef profile(req, sess, user_id: int=None):\n ip = req.client.host\n sess['last_visit'] = datetime.now().isoformat()\n visits = sess.setdefault('visit_count', 0) + 1\n sess['visit_count'] = visits\n user = get_user(user_id or sess.get('user_id'))\n return Titled(f\"Profile: {user.name}\", \n P(f\"Visits: {visits}\"), \n P(f\"IP: {ip}\"),\n Button(\"Logout\", hx_post=logout))\n\nHandler functions can return the HtmxResponseHeaders object to set HTMX-specific response headers.\n\n@rt\ndef htmlredirect(app): return HtmxResponseHeaders(location=\"http://example.org\")", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#apirouter", "href": "ref/concise_guide.html#apirouter", "title": "Concise reference", "section": "APIRouter", "text": "APIRouter\nAPIRouter lets you organize routes across multiple files in a FastHTML app.\n# products.py\nar = APIRouter()\n\n@ar\ndef details(pid: int): return f\"Here are the product details for ID: {pid}\"\n\n@ar\ndef all_products(req):\n return Div(\n Div(\n Button(\"Details\",hx_get=details.to(pid=42),hx_target=\"#products_list\",hx_swap=\"outerHTML\",),\n ), id=\"products_list\")\n# main.py\nfrom products import ar,all_products\n\napp, rt = fast_app()\nar.to_app(app)\n\n@rt\ndef index():\n return Div(\n \"Products\",\n hx_get=all_products, hx_swap=\"outerHTML\")", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#toasts", "href": "ref/concise_guide.html#toasts", "title": "Concise reference", "section": "Toasts", "text": "Toasts\nToasts can be of four types:\n\ninfo\nsuccess\nwarning\nerror\n\nToasts require the use of the setup_toasts() function, plus every handler needs:\n\nThe session argument\nMust return FT components\n\nsetup_toasts(app)\n\n@rt\ndef toasting(session):\n add_toast(session, f\"cooked\", \"info\")\n add_toast(session, f\"ready\", \"success\")\n return Titled(\"toaster\")\nsetup_toasts(duration) allows you to specify how long a toast will be visible before disappearing.10 seconds.\nAuthentication and authorization are handled with Beforeware, which functions that run before the route handler is called.", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#auth", "href": "ref/concise_guide.html#auth", "title": "Concise reference", "section": "Auth", "text": "Auth\n\ndef user_auth_before(req, sess):\n # `auth` key in the request scope is automatically provided to any handler which requests it and can not be injected\n auth = req.scope['auth'] = sess.get('auth', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n\nbeforeware = Beforeware(\n user_auth_before,\n skip=[r'/favicon\\.ico', r'/static/.*', r'.*\\.css', r'.*\\.js', '/login', '/']\n)\n\napp, rt = fast_app(before=beforeware)", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#server-side-events-sse", "href": "ref/concise_guide.html#server-side-events-sse", "title": "Concise reference", "section": "Server-Side Events (SSE)", "text": "Server-Side Events (SSE)\nFastHTML supports the HTMX SSE extension.\n\nimport random\nhdrs=(Script(src=\"https://unpkg.com/htmx-ext-sse@2.2.3/sse.js\"),)\napp,rt = fast_app(hdrs=hdrs)\n\n@rt\ndef index(): return Div(hx_ext=\"sse\", sse_connect=\"/numstream\", hx_swap=\"beforeend show:bottom\", sse_swap=\"message\")\n\n# `signal_shutdown()` gets an event that is set on shutdown\nshutdown_event = signal_shutdown()\n\nasync def number_generator():\n while not shutdown_event.is_set():\n data = Article(random.randint(1, 100))\n yield sse_message(data)\n\n@rt\nasync def numstream(): return EventStream(number_generator())", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#websockets", "href": "ref/concise_guide.html#websockets", "title": "Concise reference", "section": "Websockets", "text": "Websockets\nFastHTML provides useful tools for HTMX’s websockets extension.\n\n# These HTMX extensions are available through `exts`:\n# head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer\napp, rt = fast_app(exts='ws')\n\ndef mk_inp(): return Input(id='msg', autofocus=True)\n\n@rt\nasync def index(request):\n # `ws_send` tells HTMX to send a message to the nearest websocket based on the trigger for the form element\n cts = Div(\n Div(id='notifications'),\n Form(mk_inp(), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\n\nasync def on_connect(send): await send(Div('Hello, you have connected', id=\"notifications\"))\nasync def on_disconnect(ws): print('Disconnected!')\n\n@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send):\n # websocket hander returns/sends are treated as OOB swaps\n await send(Div('Hello ' + msg, id=\"notifications\"))\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\n\nSample chatbot that uses FastHTML’s setup_ws function:\napp = FastHTML(exts='ws')\nrt = app.route\nmsgs = []\n\n@rt('/')\ndef home():\n return Div(hx_ext='ws', ws_connect='/ws')(\n Div(Ul(*[Li(m) for m in msgs], id='msg-list')),\n Form(Input(id='msg'), id='form', ws_send=True)\n )\n\nasync def ws(msg:str):\n msgs.append(msg)\n await send(Ul(*[Li(m) for m in msgs], id='msg-list'))\n\nsend = setup_ws(app, ws)\n\nSingle File Uploads\nForm defaults to “multipart/form-data”. A Starlette UploadFile is passed to the handler.\nupload_dir = Path(\"filez\")\n\n@rt\ndef index():\n return (\n Form(hx_post=upload, hx_target=\"#result\")(\n Input(type=\"file\", name=\"file\"),\n Button(\"Upload\", type=\"submit\")),\n Div(id=\"result\")\n )\n\n# Use `async` handlers where IO is used to avoid blocking other clients\n@rt\nasync def upload(file: UploadFile):\n filebuffer = await file.read()\n (upload_dir / file.filename).write_bytes(filebuffer)\n return P('Size: ', file.size)\nFor multi-file, use Input(..., multiple=True), and a type annotation of list[UploadFile] in the handler.", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#fastlite", "href": "ref/concise_guide.html#fastlite", "title": "Concise reference", "section": "Fastlite", "text": "Fastlite\nFastlite and the MiniDataAPI specification it’s built on are a CRUD-oriented API for working with SQLite. APSW and apswutils is used to connect to SQLite, optimized for speed and clean error handling.\n\nfrom fastlite import *\n\n\ndb = database(':memory:') # or database('data/app.db')\n\nTables are normally constructed with classes, field types are specified as type hints.\n\nclass Book: isbn: str; title: str; pages: int; userid: int\n# The transform arg instructs fastlite to change the db schema when fields change.\n# Create only creates a table if the table doesn't exist.\nbooks = db.create(Book, pk='isbn', transform=True)\n \nclass User: id: int; name: str; active: bool = True\n# If no pk is provided, id is used as the primary key.\nusers = db.create(User, transform=True)\nusers\n\n<Table user (id, name, active)>\n\n\n\nFastlite CRUD operations\nEvery operation in fastlite returns a full superset of dataclass functionality.\n\nuser = users.insert(name='Alex',active=False)\nuser\n\nUser(id=1, name='Alex', active=0)\n\n\n\n# List all records\nusers()\n\n[User(id=1, name='Alex', active=0)]\n\n\n\n# Limit, offset, and order results:\nusers(order_by='name', limit=2, offset=1)\n\n# Filter on the results\nusers(where=\"name='Alex'\")\n\n# Placeholder for avoiding injection attacks\nusers(\"name=?\", ('Alex',))\n\n# A single record by pk\nusers[user.id]\n\nUser(id=1, name='Alex', active=0)\n\n\nTest if a record exists by using in keyword on primary key:\n\n1 in users\n\nTrue\n\n\nUpdates (which take a dict or a typed object) return the updated record.\n\nuser.name='Lauren'\nuser.active=True\nusers.update(user)\n\nUser(id=1, name='Lauren', active=1)\n\n\n.xtra() to automatically constrain queries, updates, and inserts from there on:\n\nusers.xtra(active=True)\nusers()\n\n[User(id=1, name='Lauren', active=1)]\n\n\nDeleting by pk:\n\nusers.delete(user.id)\n\n<Table user (id, name, active)>\n\n\nNotFoundError is raised by pk [], updates, and deletes.\n\ntry: users['Amy']\nexcept NotFoundError: print('User not found')\n\nUser not found", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/concise_guide.html#monsterui", "href": "ref/concise_guide.html#monsterui", "title": "Concise reference", "section": "MonsterUI", "text": "MonsterUI\nMonsterUI is a shadcn-like component library for FastHTML. It adds the Tailwind-based libraries FrankenUI and DaisyUI to FastHTML, as well as Python’s mistletoe for Markdown, HighlightJS for code highlighting, and Katex for latex support, following semantic HTML patterns when possible. It is recommended for when you wish to go beyond the basics provided by FastHTML’s built-in pico support.\nA minimal app:\n\nfrom fasthtml.common import *\nfrom monsterui.all import *\n\napp, rt = fast_app(hdrs=Theme.blue.headers(highlightjs=True)) # Use MonsterUI blue theme and highlight code in markdown\n\n@rt\ndef index():\n socials = (('github','https://github.com/AnswerDotAI/MonsterUI'),)\n return Titled(\"App\",\n Card(\n P(\"App\", cls=TextPresets.muted_sm),\n # LabelInput, DivLAigned, and UkIconLink are non-semantic MonsterUI FT Components,\n LabelInput('Email', type='email', required=True),\n footer=DivLAligned(*[UkIconLink(icon,href=url) for icon,url in socials])))\n\nMonsterUI recommendations:\n\nUse defaults as much as possible, for example Container in monsterui already has defaults for margins\nUse *T for button styling consistency, for example cls=ButtonT.destructive for a red delete button or cls=ButtonT.primary for a CTA button\nUse Label* functions for forms as much as possible (e.g. LabelInput, LabelRange) which creates and links both the FormLabel and user input appropriately to avoid boiler plate.\n\nFlex Layout Elements (such as DivLAligned and DivFullySpaced) can be used to create layouts concisely\n\ndef TeamCard(name, role, location=\"Remote\"):\n icons = (\"mail\", \"linkedin\", \"github\")\n return Card(\n DivLAligned(\n DiceBearAvatar(name, h=24, w=24),\n Div(H3(name), P(role))),\n footer=DivFullySpaced(\n DivHStacked(UkIcon(\"map-pin\", height=16), P(location)),\n DivHStacked(*(UkIconLink(icon, height=16) for icon in icons))))\n\nForms are styled and spaced for you without significant additional classes.\n\ndef MonsterForm():\n relationship = [\"Parent\",'Sibling', \"Friend\", \"Spouse\", \"Significant Other\", \"Relative\", \"Child\", \"Other\"]\n return Div(\n DivCentered(\n H3(\"Emergency Contact Form\"),\n P(\"Please fill out the form completely\", cls=TextPresets.muted_sm)),\n Form(\n Grid(LabelInput(\"Name\",id='name'),LabelInput(\"Email\", id='email')),\n H3(\"Relationship to patient\"),\n Grid(*[LabelCheckboxX(o) for o in relationship], cols=4, cls='space-y-3'),\n DivCentered(Button(\"Submit Form\", cls=ButtonT.primary))),\n cls='space-y-4')\n\nText can be styled with markdown automatically with MonsterUI\n\nrender_md(\"\"\"\n# My Document\n\n> Important note here\n\n+ List item with **bold**\n+ Another with `code`\n\n```python\ndef hello():\n print(\"world\")\n```\n\"\"\")\n\n'<div><h1 class=\"uk-h1 text-4xl font-bold mt-12 mb-6\">My Document</h1>\\n<blockquote class=\"uk-blockquote pl-4 border-l-4 border-primary italic mb-6\">\\n<p class=\"text-lg leading-relaxed mb-6\">Important note here</p>\\n</blockquote>\\n<ul class=\"uk-list uk-list-bullet space-y-2 mb-6 ml-6 text-lg\">\\n<li class=\"leading-relaxed\">List item with <strong>bold</strong></li>\\n<li class=\"leading-relaxed\">Another with <code class=\"uk-codespan px-1\">code</code></li>\\n</ul>\\n<pre class=\"bg-base-200 rounded-lg p-4 mb-6\"><code class=\"language-python uk-codespan px-1 uk-codespan px-1 block overflow-x-auto\">def hello():\\n print(\"world\")\\n</code></pre>\\n</div>'\n\n\nOr using semantic HTML:\n\ndef SemanticText():\n return Card(\n H1(\"MonsterUI's Semantic Text\"),\n P(\n Strong(\"MonsterUI\"), \" brings the power of semantic HTML to life with \",\n Em(\"beautiful styling\"), \" and \", Mark(\"zero configuration\"), \".\"),\n Blockquote(\n P(\"Write semantic HTML in pure Python, get modern styling for free.\"),\n Cite(\"MonsterUI Team\")),\n footer=Small(\"Released February 2025\"),)", "crumbs": [ "Home", "Reference", "Concise reference" ] }, { "objectID": "ref/handlers.html", "href": "ref/handlers.html", "title": "Handling handlers", "section": "", "text": "from fasthtml.common import *\nfrom collections import namedtuple\nfrom typing import TypedDict\nfrom datetime import datetime\nimport json,time\napp = FastHTML()\nThe FastHTML class is the main application class for FastHTML apps.\nrt = app.route\napp.route is used to register route handlers. It is a decorator, which means we place it before a function that is used as a handler. Because it’s used frequently in most FastHTML applications, we often alias it as rt, as we do here.", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/handlers.html#basic-route-handling", "href": "ref/handlers.html#basic-route-handling", "title": "Handling handlers", "section": "Basic Route Handling", "text": "Basic Route Handling\n\n@rt(\"/hi\")\ndef get(): return 'Hi there'\n\nHandler functions can return strings directly. These strings are sent as the response body to the client.\n\ncli = Client(app)\n\nClient is a test client for FastHTML applications. It allows you to simulate requests to your app without running a server.\n\ncli.get('/hi').text\n\n'Hi there'\n\n\nThe get method on a Client instance simulates GET requests to the app. It returns a response object that has a .text attribute, which you can use to access the body of the response. It calls httpx.get internally – all httpx HTTP verbs are supported.\n\n@rt(\"/hi\")\ndef post(): return 'Postal'\ncli.post('/hi').text\n\n'Postal'\n\n\nHandler functions can be defined for different HTTP methods on the same route. Here, we define a post handler for the /hi route. The Client instance can simulate different HTTP methods, including POST requests.", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/handlers.html#request-and-response-objects", "href": "ref/handlers.html#request-and-response-objects", "title": "Handling handlers", "section": "Request and Response Objects", "text": "Request and Response Objects\n\n@app.get(\"/hostie\")\ndef show_host(req): return req.headers['host']\ncli.get('/hostie').text\n\n'testserver'\n\n\nHandler functions can accept a req (or request) parameter, which represents the incoming request. This object contains information about the request, including headers. In this example, we return the host header from the request. The test client uses ‘testserver’ as the default host.\nIn this example, we use @app.get(\"/hostie\") instead of @rt(\"/hostie\"). The @app.get() decorator explicitly specifies the HTTP method (GET) for the route, while @rt() by default handles both GET and POST requests.\n\n@rt\ndef yoyo(): return 'a yoyo'\ncli.post('/yoyo').text\n\n'a yoyo'\n\n\nIf the @rt decorator is used without arguments, it uses the function name as the route path. Here, the yoyo function becomes the handler for the /yoyo route. This handler responds to GET and POST methods, since a specific method wasn’t provided.\n\n@rt\ndef ft1(): return Html(Div('Text.'))\nprint(cli.get('/ft1').text)\n\n <!doctype html>\n <html>\n <div>Text.</div>\n </html>\n\n\n\nHandler functions can return FT objects, which are automatically converted to HTML strings. The FT class can take other FT components as arguments, such as Div. This allows for easy composition of HTML elements in your responses.\n\n@app.get\ndef autopost(): return Html(Div('Text.', hx_post=yoyo.to()))\nprint(cli.get('/autopost').text)\n\n <!doctype html>\n <html>\n <div hx-post=\"/yoyo\">Text.</div>\n </html>\n\n\n\nThe rt decorator modifies the yoyo function by adding a to() method. This method returns the route path associated with the handler. It’s a convenient way to reference the route of a handler function dynamically.\nIn the example, yoyo.to() is used as the value for hx_post. This means when the div is clicked, it will trigger an HTMX POST request to the route of the yoyo handler. This approach allows for flexible, DRY code by avoiding hardcoded route strings and automatically updating if the route changes.\nThis pattern is particularly useful in larger applications where routes might change, or when building reusable components that need to reference their own routes dynamically.\n\n@app.get\ndef autoget(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))\nprint(cli.get('/autoget').text)\n\n <!doctype html>\n <html>\n <body>\n <div hx-post=\"/hostie?a=b\" class=\"px-2\">Text.</div>\n </body>\n </html>\n\n\n\nThe rt() method of handler functions can also accept parameters. When called with parameters, it returns the route path with a query string appended. In this example, show_host.to(a='b') generates the path /hostie?a=b.\nThe Body component is used here to demonstrate nesting of FT components. Div is nested inside Body, showcasing how you can create more complex HTML structures.\nThe cls parameter is used to add a CSS class to the Div. This translates to the class attribute in the rendered HTML. (class can’t be used as a parameter name directly in Python since it’s a reserved word.)\n\n@rt('/ft2')\ndef get(): return Title('Foo'),H1('bar')\nprint(cli.get('/ft2').text)\n\n <!doctype html>\n <html>\n <head>\n <title>Foo</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body>\n <h1>bar</h1>\n </body>\n </html>\n\n\n\nHandler functions can return multiple FT objects as a tuple. The first item is treated as the Title, and the rest are added to the Body. When the request is not an HTMX request, FastHTML automatically adds necessary HTML boilerplate, including default head content with required scripts.\nWhen using app.route (or rt), if the function name matches an HTTP verb (e.g., get, post, put, delete), that HTTP method is automatically used for the route. In this case, a path must be explicitly provided as an argument to the decorator.\n\nhxhdr = {'headers':{'hx-request':\"1\"}}\nprint(cli.get('/ft2', **hxhdr).text)\n\n <title>Foo</title>\n <h1>bar</h1>\n\n\n\nFor HTMX requests (indicated by the hx-request header), FastHTML returns only the specified components without the full HTML structure. This allows for efficient partial page updates in HTMX applications.\n\n@rt('/ft3')\ndef get(): return H1('bar')\nprint(cli.get('/ft3', **hxhdr).text)\n\n <h1>bar</h1>\n\n\n\nWhen a handler function returns a single FT object for an HTMX request, it’s rendered as a single HTML partial.\n\n@rt('/ft4')\ndef get(): return Html(Head(Title('hi')), Body(P('there')))\n\nprint(cli.get('/ft4').text)\n\n <!doctype html>\n <html>\n <head>\n <title>hi</title>\n </head>\n <body>\n <p>there</p>\n </body>\n </html>\n\n\n\nHandler functions can return a complete Html structure, including Head and Body components. When a full HTML structure is returned, FastHTML doesn’t add any additional boilerplate. This gives you full control over the HTML output when needed.\n\n@rt\ndef index(): return \"welcome!\"\nprint(cli.get('/').text)\n\nwelcome!\n\n\nThe index function is a special handler in FastHTML. When defined without arguments to the @rt decorator, it automatically becomes the handler for the root path ('/'). This is a convenient way to define the main page or entry point of your application.", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/handlers.html#path-and-query-parameters", "href": "ref/handlers.html#path-and-query-parameters", "title": "Handling handlers", "section": "Path and Query Parameters", "text": "Path and Query Parameters\n\n@rt('/user/{nm}', name='gday')\ndef get(nm:str=''): return f\"Good day to you, {nm}!\"\ncli.get('/user/Alexis').text\n\n'Good day to you, Alexis!'\n\n\nHandler functions can use path parameters, defined using curly braces in the route – this is implemented by Starlette directly, so all Starlette path parameters can be used. These parameters are passed as arguments to the function.\nThe name parameter in the decorator allows you to give the route a name, which can be used for URL generation.\nIn this example, {nm} in the route becomes the nm parameter in the function. The function uses this parameter to create a personalized greeting.\n\n@app.get\ndef autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))\nprint(cli.get('/autolink').text)\n\n <!doctype html>\n <html>\n <div href=\"/user/Alexis\">Text.</div>\n </html>\n\n\n\nThe uri function is used to generate URLs for named routes. It takes the route name as its first argument, followed by any path or query parameters needed for that route.\nIn this example, uri('gday', nm='Alexis') generates the URL for the route named ‘gday’ (which we defined earlier as ‘/user/{nm}’), with ‘Alexis’ as the value for the ‘nm’ parameter.\nThe link parameter in FT components sets the href attribute of the rendered HTML element. By using uri(), we can dynamically generate correct URLs even if the underlying route structure changes.\nThis approach promotes maintainable code by centralizing route definitions and avoiding hardcoded URLs throughout the application.\n\n@rt('/link')\ndef get(req): return f\"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}\"\n\ncli.get('/link').text\n\n'http://testserver/user/Alexis; http://testserver/hostie'\n\n\nThe url_for method of the request object can be used to generate URLs for named routes. It takes the route name as its first argument, followed by any path parameters needed for that route.\nIn this example, req.url_for('gday', nm='Alexis') generates the full URL for the route named ‘gday’, including the scheme and host. Similarly, req.url_for('show_host') generates the URL for the ‘show_host’ route.\nThis method is particularly useful when you need to generate absolute URLs, such as for email links or API responses. It ensures that the correct host and scheme are included, even if the application is accessed through different domains or protocols.\n\napp.url_path_for('gday', nm='Jeremy')\n\n'/user/Jeremy'\n\n\nThe url_path_for method of the application can be used to generate URL paths for named routes. Unlike url_for, it returns only the path component of the URL, without the scheme or host.\nIn this example, app.url_path_for('gday', nm='Jeremy') generates the path ‘/user/Jeremy’ for the route named ‘gday’.\nThis method is useful when you need relative URLs or just the path component, such as for internal links or when constructing URLs in a host-agnostic manner.\n\n@rt('/oops')\ndef get(nope): return nope\nr = cli.get('/oops?nope=1')\nprint(r)\nr.text\n\n<Response [200 OK]>\n\n\n/Users/iflath/git/AnswerDotAI/fasthtml/build/__editable__.python_fasthtml-0.12.1-py3-none-any/fasthtml/core.py:188: UserWarning: `nope has no type annotation and is not a recognised special name, so is ignored.\n if arg!='resp': warn(f\"`{arg} has no type annotation and is not a recognised special name, so is ignored.\")\n\n\n''\n\n\nHandler functions can include parameters, but they must be type-annotated or have special names (like req) to be recognized. In this example, the nope parameter is not annotated, so it’s ignored, resulting in a warning.\nWhen a parameter is ignored, it doesn’t receive the value from the query string. This can lead to unexpected behavior, as the function attempts to return nope, which is undefined.\nThe cli.get('/oops?nope=1') call succeeds with a 200 OK status because the handler doesn’t raise an exception, but it returns an empty response, rather than the intended value.\nTo fix this, you should either add a type annotation to the parameter (e.g., def get(nope: str):) or use a recognized special name like req.\n\n@rt('/html/{idx}')\ndef get(idx:int): return Body(H4(f'Next is {idx+1}.'))\nprint(cli.get('/html/1', **hxhdr).text)\n\n <body>\n <h4>Next is 2.</h4>\n </body>\n\n\n\nPath parameters can be type-annotated, and FastHTML will automatically convert them to the specified type if possible. In this example, idx is annotated as int, so it’s converted from the string in the URL to an integer.\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@rt(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\nprint(cli.get('/static/foo/jph.ico').text)\n\nGetting jph.ico from /foo/\n\n\nThe reg_re_param function is used to register custom path parameter types using regular expressions. Here, we define a new path parameter type called “imgext” that matches common image file extensions.\nHandler functions can use complex path patterns with multiple parameters and custom types. In this example, the route pattern r'/static/{path:path}{fn}.{ext:imgext}' uses three path parameters:\n\npath: A Starlette built-in type that matches any path segments\nfn: The filename without extension\next: Our custom “imgext” type that matches specific image extensions\n\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\n\n@rt(\"/models/{nm}\")\ndef get(nm:ModelName): return nm\n\nprint(cli.get('/models/alexnet').text)\n\nalexnet\n\n\nWe define ModelName as an enum with three possible values: “alexnet”, “resnet”, and “lenet”. Handler functions can use these enum types as parameter annotations. In this example, the nm parameter is annotated with ModelName, which ensures that only valid model names are accepted.\nWhen a request is made with a valid model name, the handler function returns that name. This pattern is useful for creating type-safe APIs with a predefined set of valid values.\n\n@rt(\"/files/{path}\")\nasync def get(path: Path): return path.with_suffix('.txt')\nprint(cli.get('/files/foo').text)\n\nfoo.txt\n\n\nHandler functions can use Path objects as parameter types. The Path type is from Python’s standard library pathlib module, which provides an object-oriented interface for working with file paths. In this example, the path parameter is annotated with Path, so FastHTML automatically converts the string from the URL to a Path object.\nThis approach is particularly useful when working with file-related routes, as it provides a convenient and platform-independent way to handle file paths.\n\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n@rt(\"/items/\")\ndef get(idx:int|None = 0): return fake_db[idx]\nprint(cli.get('/items/?idx=1').text)\n\n{\"name\":\"Bar\"}\n\n\nHandler functions can use query parameters, which are automatically parsed from the URL. In this example, idx is a query parameter with a default value of 0. It’s annotated as int|None, allowing it to be either an integer or None.\nThe function uses this parameter to index into a fake database (fake_db). When a request is made with a valid idx query parameter, the handler returns the corresponding item from the database.\n\nprint(cli.get('/items/').text)\n\n{\"name\":\"Foo\"}\n\n\nWhen no idx query parameter is provided, the handler function uses the default value of 0. This results in returning the first item from the fake_db list, which is {\"name\":\"Foo\"}.\nThis behavior demonstrates how default values for query parameters work in FastHTML. They allow the API to have a sensible default behavior when optional parameters are not provided.\n\nprint(cli.get('/items/?idx=g'))\n\n<Response [404 Not Found]>\n\n\nWhen an invalid value is provided for a typed query parameter, FastHTML returns a 404 Not Found response. In this example, ‘g’ is not a valid integer for the idx parameter, so the request fails with a 404 status.\nThis behavior ensures type safety and prevents invalid inputs from reaching the handler function.\n\n@app.get(\"/booly/\")\ndef _(coming:bool=True): return 'Coming' if coming else 'Not coming'\nprint(cli.get('/booly/?coming=true').text)\nprint(cli.get('/booly/?coming=no').text)\n\nComing\nNot coming\n\n\nHandler functions can use boolean query parameters. In this example, coming is a boolean parameter with a default value of True. FastHTML automatically converts string values like ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’, and ‘no’ to their corresponding boolean values.\nThe underscore _ is used as the function name in this example to indicate that the function’s name is not important or won’t be referenced elsewhere. This is a common Python convention for throwaway or unused variables, and it works here because FastHTML uses the route decorator parameter, when provided, to determine the URL path, not the function name. By default, both get and post methods can be used in routes that don’t specify an http method (by either using app.get, def get, or the methods parameter to app.route).\n\n@app.get(\"/datie/\")\ndef _(d:parsed_date): return d\ndate_str = \"17th of May, 2024, 2p\"\nprint(cli.get(f'/datie/?d={date_str}').text)\n\n2024-05-17 14:00:00\n\n\nHandler functions can use date objects as parameter types. FastHTML uses dateutil.parser library to automatically parse a wide variety of date string formats into date objects.\n\n@app.get(\"/ua\")\nasync def _(user_agent:str): return user_agent\nprint(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)\n\nFastHTML\n\n\nHandler functions can access HTTP headers by using parameter names that match the header names. In this example, user_agent is used as a parameter name, which automatically captures the value of the ‘User-Agent’ header from the request.\nThe Client instance allows setting custom headers for test requests. Here, we set the ‘User-Agent’ header to ‘FastHTML’ in the test request.\n\n@app.get(\"/hxtest\")\ndef _(htmx): return htmx.request\nprint(cli.get('/hxtest', headers={'HX-Request':'1'}).text)\n\n@app.get(\"/hxtest2\")\ndef _(foo:HtmxHeaders, req): return foo.request\nprint(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)\n\n1\n1\n\n\nHandler functions can access HTMX-specific headers using either the special htmx parameter name, or a parameter annotated with HtmxHeaders. Both approaches provide access to HTMX-related information.\nIn these examples, the htmx.request attribute returns the value of the ‘HX-Request’ header.\n\napp.chk = 'foo'\n@app.get(\"/app\")\ndef _(app): return app.chk\nprint(cli.get('/app').text)\n\nfoo\n\n\nHandler functions can access the FastHTML application instance using the special app parameter name. This allows handlers to access application-level attributes and methods.\nIn this example, we set a custom attribute chk on the application instance. The handler function then uses the app parameter to access this attribute and return its value.\n\n@app.get(\"/app2\")\ndef _(foo:FastHTML): return foo.chk,HttpHeader(\"mykey\", \"myval\")\nr = cli.get('/app2', **hxhdr)\nprint(r.text)\nprint(r.headers)\n\nfoo\nHeaders({'mykey': 'myval', 'content-length': '3', 'content-type': 'text/html; charset=utf-8'})\n\n\nHandler functions can access the FastHTML application instance using a parameter annotated with FastHTML. This allows handlers to access application-level attributes and methods, just like using the special app parameter name.\nHandlers can return tuples containing both content and HttpHeader objects. HttpHeader allows setting custom HTTP headers in the response.\nIn this example:\n\nWe define a handler that returns both the chk attribute from the application and a custom header.\nThe HttpHeader(\"mykey\", \"myval\") sets a custom header in the response.\nWe use the test client to make a request and examine both the response text and headers.\nThe response includes the custom header “mykey” along with standard headers like content-length and content-type.\n\n\n@app.get(\"/app3\")\ndef _(foo:FastHTML): return HtmxResponseHeaders(location=\"http://example.org\")\nr = cli.get('/app3')\nprint(r.headers)\n\nHeaders({'hx-location': 'http://example.org', 'content-length': '0', 'content-type': 'text/html; charset=utf-8'})\n\n\nHandler functions can return HtmxResponseHeaders objects to set HTMX-specific response headers. This is useful for HTMX-specific behaviors like client-side redirects.\nIn this example we define a handler that returns an HtmxResponseHeaders object with a location parameter, which sets the HX-Location header in the response. HTMX uses this for client-side redirects.\n\n@app.get(\"/app4\")\ndef _(foo:FastHTML): return Redirect(\"http://example.org\")\ncli.get('/app4', follow_redirects=False)\n\n<Response [303 See Other]>\n\n\nHandler functions can return Redirect objects to perform HTTP redirects. This is useful for redirecting users to different pages or external URLs.\nIn this example:\n\nWe define a handler that returns a Redirect object with the URL “http://example.org”.\nThe cli.get('/app4', follow_redirects=False) call simulates a GET request to the ‘/app4’ route without following redirects.\nThe response has a 303 See Other status code, indicating a redirect.\n\nThe follow_redirects=False parameter is used to prevent the test client from automatically following the redirect, allowing us to inspect the redirect response itself.\n\nRedirect.__response__\n\n<function fasthtml.core.Redirect.__response__(self, req)>\n\n\nThe Redirect class in FastHTML implements a __response__ method, which is a special method recognized by the framework. When a handler returns a Redirect object, FastHTML internally calls this __response__ method to replace the original response.\nThe __response__ method takes a req parameter, which represents the incoming request. This allows the method to access request information if needed when constructing the redirect response.\n\n@rt\ndef meta(): \n return ((Title('hi'),H1('hi')),\n (Meta(property='image'), Meta(property='site_name')))\n\nprint(cli.post('/meta').text)\n\n <!doctype html>\n <html>\n <head>\n <title>hi</title>\n <meta property=\"image\">\n <meta property=\"site_name\">\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body>\n <h1>hi</h1>\n </body>\n </html>\n\n\n\nFastHTML automatically identifies elements typically placed in the <head> (like Title and Meta) and positions them accordingly, while other elements go in the <body>.\nIn this example: - (Title('hi'), H1('hi')) defines the title and main heading. The title is placed in the head, and the H1 in the body. - (Meta(property='image'), Meta(property='site_name')) defines two meta tags, which are both placed in the head.", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/handlers.html#apirouter", "href": "ref/handlers.html#apirouter", "title": "Handling handlers", "section": "APIRouter", "text": "APIRouter\nAPIRouter is useful when you want to split your application routes across multiple .py files that are part of a single FastHTMl application. It accepts an optional prefix argument that will be applied to all routes within that instance of APIRouter.\nBelow we define several hypothetical product related routes in a products.py and then demonstrate how they can seamlessly be incorporated into a FastHTML app instance.\n\n# products.py\nar = APIRouter(prefix=\"/products\")\n\n@ar(\"/all\")\ndef all_products(req):\n return Div(\n \"Welcome to the Products Page! Click the button below to look at the details for product 42\",\n Div(\n Button(\n \"Details\",\n hx_get=req.url_for(\"details\", pid=42),\n hx_target=\"#products_list\",\n hx_swap=\"outerHTML\",\n ),\n ),\n id=\"products_list\",\n )\n\n\n@ar.get(\"/{pid}\", name=\"details\")\ndef details(pid: int):\n return f\"Here are the product details for ID: {pid}\"\n\nSince we specified the prefix=/products in our hypothetical products.py file, all routes defined in that file will be found under /products.\n\nprint(str(ar.rt_funcs.all_products))\nprint(str(ar.rt_funcs.details))\n\n/products/all\n/products/{pid}\n\n\n\n# main.py\n# from products import ar\n\napp, rt = fast_app()\nar.to_app(app)\n\n@rt\ndef index():\n return Div(\n \"Click me for a look at our products\",\n hx_get=ar.rt_funcs.all_products,\n hx_swap=\"outerHTML\",\n )\n\nNote how you can reference our python route functions via APIRouter.rt_funcs in your hx_{http_method} calls like normal.", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/handlers.html#form-data-and-json-handling", "href": "ref/handlers.html#form-data-and-json-handling", "title": "Handling handlers", "section": "Form Data and JSON Handling", "text": "Form Data and JSON Handling\n\napp = FastHTML()\nrt = app.route\ncli = Client(app)\n\n\n@app.post('/profile/me')\ndef profile_update(username: str): return username\n\nr = cli.post('/profile/me', data={'username' : 'Alexis'}).text\nassert r == 'Alexis'\nprint(r)\n\nAlexis\n\n\nHandler functions can accept form data parameters, without needing to manually extract it from the request. In this example, username is expected to be sent as form data.\nThe data parameter in the cli.post() method simulates sending form data in the request.\n\nr = cli.post('/profile/me', data={})\nassert r.status_code == 400\nprint(r.text)\nr\n\nMissing required field: username\n\n\n<Response [400 Bad Request]>\n\n\nIf required form data is missing, FastHTML automatically returns a 400 Bad Request response with an error message.\n\n@app.post('/pet/dog')\ndef pet_dog(dogname: str = None): return dogname or 'unknown name'\nr = cli.post('/pet/dog', data={}).text\nr\n\n'unknown name'\n\n\nHandlers can have optional form data parameters with default values. In this example, dogname is an optional parameter with a default value of None.\nHere, if the form data doesn’t include the dogname field, the function uses the default value. The function returns either the provided dogname or ‘unknown name’ if dogname is None.\n\n@dataclass\nclass Bodie: a:int;b:str\n\n@rt(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\nprint(cli.post('/bodie/me', data=dict(a=1, b='foo', nm='me')).text)\n\n{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}\n\n\nYou can use dataclasses to define structured form data. In this example, Bodie is a dataclass with a (int) and b (str) fields.\nFastHTML automatically converts the incoming form data to a Bodie instance where attribute names match parameter names. Other form data elements are matched with parameters with the same names (in this case, nm).\nHandler functions can return dictionaries, which FastHTML automatically JSON-encodes.\n\n@app.post(\"/bodied/\")\ndef bodied(data:dict): return data\n\nd = dict(a=1, b='foo')\nprint(cli.post('/bodied/', data=d).text)\n\n{\"a\":\"1\",\"b\":\"foo\"}\n\n\ndict parameters capture all form data as a dictionary. In this example, the data parameter is annotated with dict, so FastHTML automatically converts all incoming form data into a dictionary.\nNote that when form data is converted to a dictionary, all values become strings, even if they were originally numbers. This is why the ‘a’ key in the response has a string value “1” instead of the integer 1.\n\nnt = namedtuple('Bodient', ['a','b'])\n\n@app.post(\"/bodient/\")\ndef bodient(data:nt): return asdict(data)\nprint(cli.post('/bodient/', data=d).text)\n\n{\"a\":\"1\",\"b\":\"foo\"}\n\n\nHandler functions can use named tuples to define structured form data. In this example, Bodient is a named tuple with a and b fields.\nFastHTML automatically converts the incoming form data to a Bodient instance where field names match parameter names. As with the previous example, all form data values are converted to strings in the process.\n\nclass BodieTD(TypedDict): a:int;b:str='foo'\n\n@app.post(\"/bodietd/\")\ndef bodient(data:BodieTD): return data\nprint(cli.post('/bodietd/', data=d).text)\n\n{\"a\":1,\"b\":\"foo\"}\n\n\nYou can use TypedDict to define structured form data with type hints. In this example, BodieTD is a TypedDict with a (int) and b (str) fields, where b has a default value of ‘foo’.\nFastHTML automatically converts the incoming form data to a BodieTD instance where keys match the defined fields. Unlike with regular dictionaries or named tuples, FastHTML respects the type hints in TypedDict, converting values to the specified types when possible (e.g., converting ‘1’ to the integer 1 for the ‘a’ field).\n\nclass Bodie2:\n a:int|None; b:str\n def __init__(self, a, b='foo'): store_attr()\n\n@app.post(\"/bodie2/\")\ndef bodie(d:Bodie2): return f\"a: {d.a}; b: {d.b}\"\nprint(cli.post('/bodie2/', data={'a':1}).text)\n\na: 1; b: foo\n\n\nCustom classes can be used to define structured form data. Here, Bodie2 is a custom class with a (int|None) and b (str) attributes, where b has a default value of ‘foo’. The store_attr() function (from fastcore) automatically assigns constructor parameters to instance attributes.\nFastHTML automatically converts the incoming form data to a Bodie2 instance, matching form fields to constructor parameters. It respects type hints and default values.\n\n@app.post(\"/b\")\ndef index(it: Bodie): return Titled(\"It worked!\", P(f\"{it.a}, {it.b}\"))\n\ns = json.dumps({\"b\": \"Lorem\", \"a\": 15})\nprint(cli.post('/b', headers={\"Content-Type\": \"application/json\", 'hx-request':\"1\"}, data=s).text)\n\n <title>It worked!</title>\n<main class=\"container\"> <h1>It worked!</h1>\n <p>15, Lorem</p>\n</main>\n\n\nHandler functions can accept JSON data as input, which is automatically parsed into the specified type. In this example, it is of type Bodie, and FastHTML converts the incoming JSON data to a Bodie instance.\nThe Titled component is used to create a page with a title and main content. It automatically generates an <h1> with the provided title, wraps the content in a <main> tag with a “container” class, and adds a title to the head.\nWhen making a request with JSON data: - Set the “Content-Type” header to “application/json” - Provide the JSON data as a string in the data parameter of the request", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/handlers.html#cookies-sessions-file-uploads-and-more", "href": "ref/handlers.html#cookies-sessions-file-uploads-and-more", "title": "Handling handlers", "section": "Cookies, Sessions, File Uploads, and more", "text": "Cookies, Sessions, File Uploads, and more\n\n@rt(\"/setcookie\")\ndef get(): return cookie('now', datetime.now())\n\n@rt(\"/getcookie\")\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nprint(cli.get('/setcookie').text)\ntime.sleep(0.01)\ncli.get('/getcookie').text\n\n\n\n\n'Cookie was set at time 16:19:27.811570'\n\n\nHandler functions can set and retrieve cookies. In this example:\n\nThe /setcookie route sets a cookie named ‘now’ with the current datetime.\nThe /getcookie route retrieves the ‘now’ cookie and returns its value.\n\nThe cookie() function is used to create a cookie response. FastHTML automatically converts the datetime object to a string when setting the cookie, and parses it back to a date object when retrieving it.\n\ncookie('now', datetime.now())\n\nHttpHeader(k='set-cookie', v='now=\"2025-01-30 16:19:29.997374\"; Path=/; SameSite=lax')\n\n\nThe cookie() function returns an HttpHeader object with the ‘set-cookie’ key. You can return it in a tuple along with FT elements, along with anything else FastHTML supports in responses.\n\napp = FastHTML(secret_key='soopersecret')\ncli = Client(app)\nrt = app.route\n\n\n@rt(\"/setsess\")\ndef get(sess, foo:str=''):\n now = datetime.now()\n sess['auth'] = str(now)\n return f'Set to {now}'\n\n@rt(\"/getsess\")\ndef get(sess): return f'Session time: {sess[\"auth\"]}'\n\nprint(cli.get('/setsess').text)\ntime.sleep(0.01)\n\ncli.get('/getsess').text\n\nSet to 2025-01-30 16:19:31.078650\n\n\n'Session time: 2025-01-30 16:19:31.078650'\n\n\nSessions store and retrieve data across requests. To use sessions, you should to initialize the FastHTML application with a secret_key. This is used to cryptographically sign the cookie used by the session.\nThe sess parameter in handler functions provides access to the session data. You can set and get session variables using dictionary-style access.\n\n@rt(\"/upload\")\nasync def post(uf:UploadFile): return (await uf.read()).decode()\n\nwith open('../../CHANGELOG.md', 'rb') as f:\n print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])\n\n# Release notes\n\n\nHandler functions can accept file uploads using Starlette’s UploadFile type. In this example:\n\nThe /upload route accepts a file upload named uf.\nThe UploadFile object provides an asynchronous read() method to access the file contents.\nWe use await to read the file content asynchronously and decode it to a string.\n\nWe added async to the handler function because it uses await to read the file content asynchronously. In Python, any function that uses await must be declared as async. This allows the function to be run asynchronously, potentially improving performance by not blocking other operations while waiting for the file to be read.\n\napp.static_route('.md', static_path='../..')\nprint(cli.get('/README.md').text[:10])\n\n# FastHTML\n\n\nThe static_route method of the FastHTML application allows serving static files with specified extensions from a given directory. In this example:\n\n.md files are served from the ../.. directory (two levels up from the current directory).\nAccessing /README.md returns the contents of the README.md file from that directory.\n\n\nhelp(app.static_route_exts)\n\nHelp on method static_route_exts in module fasthtml.core:\n\nstatic_route_exts(prefix='/', static_path='.', exts='static') method of fasthtml.core.FastHTML instance\n Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`\n\n\n\n\napp.static_route_exts()\nassert cli.get('/README.txt').status_code == 404\nprint(cli.get('/README.txt').text[:50])\n\n404 Not Found\n\n\nThe static_route_exts method of the FastHTML application allows serving static files with specified extensions from a given directory. By default:\n\nIt serves files from the current directory (‘.’).\nIt uses the ‘static’ regex, which includes common static file extensions like ‘ico’, ‘gif’, ‘jpg’, ‘css’, ‘js’, etc.\nThe URL prefix is set to ‘/’.\n\nThe ‘static’ regex is defined by FastHTML using this code:\nreg_re_param(\"static\", \"ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html|map\")\n\n@rt(\"/form-submit/{list_id}\")\ndef options(list_id: str):\n headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': '*',\n }\n return Response(status_code=200, headers=headers)\n\nprint(cli.options('/form-submit/2').headers)\n\nHeaders({'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST', 'access-control-allow-headers': '*', 'content-length': '0', 'set-cookie': 'session_=eyJhdXRoIjogIjIwMjUtMDEtMzAgMTY6MTk6MzEuMDc4NjUwIn0=.Z5vtZA.1ooY2RCWopWAbLYDy6660g_LlHI; path=/; Max-Age=31536000; httponly; samesite=lax'})\n\n\nFastHTML handlers can handle OPTIONS requests and set custom headers. In this example:\n\nThe /form-submit/{list_id} route handles OPTIONS requests.\nCustom headers are set to allow cross-origin requests (CORS).\nThe function returns a Starlette Response object with a 200 status code and the custom headers.\n\nYou can return any Starlette Response type from a handler function, giving you full control over the response when needed.\n\ndef _not_found(req, exc): return Div('nope')\n\napp = FastHTML(exception_handlers={404:_not_found})\ncli = Client(app)\nrt = app.route\n\nr = cli.get('/')\nprint(r.text)\n\n <!doctype html>\n <html>\n <head>\n <title>FastHTML page</title>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body>\n <div>nope</div>\n </body>\n </html>\n\n\n\nFastHTML allows you to define custom exception handlers – in this case, a custom 404 (Not Found) handler function _not_found, which returns a Div component with the text ‘nope’.", "crumbs": [ "Home", "Reference", "Handling handlers" ] }, { "objectID": "ref/defining_xt_component.html", "href": "ref/defining_xt_component.html", "title": "Custom Components", "section": "", "text": "The majority of the time the default ft components are all you need (for example Div, P, H1, etc.).\nHowever, there are many situations where you need a custom ft component that creates a unique HTML tag (for example <zero-md></zero-md>). There are many options in FastHTML to do this, and this section will walk through them. Generally you want to use the highest level option that fits your needs.", "crumbs": [ "Home", "Reference", "Custom Components" ] }, { "objectID": "ref/defining_xt_component.html#notstr", "href": "ref/defining_xt_component.html#notstr", "title": "Custom Components", "section": "NotStr", "text": "NotStr\nThe first way is to use the NotStr class to use an HTML tag as a string. It works as a one-off but quickly becomes harder to work with as complexity grows. However we can see that you can genenrate the same xml using NotStr as the out-of-the-box components.\n\nfrom fasthtml.common import NotStr,Div, to_xml\n\n\ndiv_NotStr = NotStr('<div></div>') \nprint(div_NotStr)\n\n<div></div>", "crumbs": [ "Home", "Reference", "Custom Components" ] }, { "objectID": "ref/defining_xt_component.html#automatic-creation", "href": "ref/defining_xt_component.html#automatic-creation", "title": "Custom Components", "section": "Automatic Creation", "text": "Automatic Creation\nThe next (and better) approach is to let FastHTML generate the component function for you. As you can see in our assert this creates a function that creates the HTML just as we wanted. This works even though there is not a Some_never_before_used_tag function in the fasthtml.components source code (you can verify this yourself by looking at the source code).\n\n\n\n\n\n\nTip\n\n\n\nTypically these tags are needed because a CSS or Javascript library created a new XML tag that isn’t default HTML. For example the zero-md javascript library looks for a <zero-md></zero-md> tag to know what to run its javascript code on. Most CSS libraries work by creating styling based on the class attribute, but they can also apply styling to an arbitrary HTML tag that they made up.\n\n\n\nfrom fasthtml.components import Some_never_before_used_tag\n\nSome_never_before_used_tag()\n\n<some-never-before-used-tag></some-never-before-used-tag>", "crumbs": [ "Home", "Reference", "Custom Components" ] }, { "objectID": "ref/defining_xt_component.html#manual-creation", "href": "ref/defining_xt_component.html#manual-creation", "title": "Custom Components", "section": "Manual Creation", "text": "Manual Creation\nThe automatic creation isn’t magic. It’s just calling a python function __getattr__ and you can call it yourself to get the same result.\n\nimport fasthtml\n\nauto_called = fasthtml.components.Some_never_before_used_tag()\nmanual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()\n\n# Proving they generate the same xml\nassert to_xml(auto_called) == to_xml(manual_called)\n\nKnowing that, we know that it’s possible to create a different function that has different behavior than FastHTMLs default behavior by modifying how the ___getattr__ function creates the components! It’s only a few lines of code and reading that what it does is a great way to understand components more deeply.\n\n\n\n\n\n\nTip\n\n\n\nDunder methods and functions are special functions that have double underscores at the beginning and end of their name. They are called at specific times in python so you can use them to cause customized behavior that makes sense for your specific use case. They can appear magical if you don’t know how python works, but they are extremely commonly used to modify python’s default behavior (__init__ is probably the most common one).\nIn a module __getattr__ is called to get an attribute. In fasthtml.components, this is defined to create components automatically for you.\n\n\nFor example if you want a component that creates <path></path> that doesn’t conflict names with pathlib.Path you can do that. FastHTML automatically creates new components with a 1:1 mapping and a consistent name, which is almost always what you want. But in some cases you may want to customize that and you can use the ft_hx function to do that differently than the default.\n\nfrom fasthtml.common import ft_hx\n\ndef ft_path(*c, target_id=None, **kwargs): \n return ft_hx('path', *c, target_id=target_id, **kwargs)\n\nft_path()\n\n<path></path>\n\n\nWe can add any behavior in that function that we need to, so let’s go through some progressively complex examples that you may need in some of your projects.\n\nUnderscores in tags\nNow that we understand how FastHTML generates components, we can create our own in all kinds of ways. For example, maybe we need a weird HTML tag that uses underscores. FastHTML replaces _ with - in tags because underscores in tags are highly unusual and rarely what you want, though it does come up rarely.\n\ndef tag_with_underscores(*c, target_id=None, **kwargs): \n return ft_hx('tag_with_underscores', *c, target_id=target_id, **kwargs)\n\ntag_with_underscores()\n\n<tag_with_underscores></tag_with_underscores>\n\n\n\n\nSymbols (ie @) in tags\nSometimes you may need to use a tag that uses characters that are not allowed in function names in python (again, very unusual).\n\ndef tag_with_AtSymbol(*c, target_id=None, **kwargs): \n return ft_hx('tag-with-@symbol', *c, target_id=target_id, **kwargs)\n\ntag_with_AtSymbol()\n\n<tag-with-@symbol></tag-with-@symbol>\n\n\n\n\nSymbols (ie @) in tag attributes\nIt also may be that an argument in an HTML tag uses characters that can’t be used in python arguments. To handle these you can define those args using a dictionary.\n\nDiv(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'})\n\n<div normal-arg=\"normal stuff\" notnormal:arg:with_varing@symbols!=\"123\"></div>", "crumbs": [ "Home", "Reference", "Custom Components" ] }, { "objectID": "explains/minidataapi.html", "href": "explains/minidataapi.html", "title": "MiniDataAPI Spec", "section": "", "text": "The MiniDataAPI is a persistence API specification that designed to be small and relatively easy to implement across a wide range of datastores. While early implementations have been SQL-based, the specification can be quickly implemented in key/value stores, document databases, and more.", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/minidataapi.html#why", "href": "explains/minidataapi.html#why", "title": "MiniDataAPI Spec", "section": "Why?", "text": "Why?\nThe MiniDataAPI specification allows us to use the same API for many different database engines. Any application using the MiniDataAPI spec for interacting with its database requires no modification beyond import and configuration changes to switch database engines. For example, to convert an application from Fastlite running SQLite to FastSQL running PostgreSQL, should require only changing these two lines:\n\n\nFastLite version\nfrom fastlite import *\ndb = database('test.db')\n\nFastSQL version\nfrom fastsql import *\ndb = Database('postgres:...')\n\n\nAs both libraries adhere to the MiniDataAPI specification, the rest of the code in the application should remain the same. The advantage of the MiniDataAPI spec is that it allows people to use whatever datastores they have access to or prefer.\n\n\n\n\n\n\nNote\n\n\n\nSwitching databases won’t migrate any existing data between databases.\n\n\n\nEasy to learn, quick to implement\nThe MiniDataAPI specification is designed to be easy-to-learn and quick to implement. It focuses on straightforward Create, Read, Update, and Delete (CRUD) operations.\nMiniDataAPI databases aren’t limited to just row-based systems. In fact, the specification is closer in design to a key/value store than a set of records. What’s exciting about this is we can write implementations for tools like Python dict stored as JSON, Redis, and even the venerable ZODB.\n\n\nLimitations of the MiniDataAPI Specification\n\n“Mini refers to the lightweightness of specification, not the data.”\n– Jeremy Howard\n\nThe advantages of the MiniDataAPI come at a cost. The MiniDataAPI specification focuses a very small set of features compared to what can be found in full-fledged ORMs and query languages. It intentionally avoids nuances or sophisticated features.\nThis means the specification does not include joins or formal foreign keys. Complex data stored over multiple tables that require joins isn’t handled well. For this kind of scenario it’s probably for the best to use more sophisticated ORMs or even direct database queries.\n\n\nSummary of the MiniDataAPI Design\n\nEasy-to-learn\nRelative quick to implement for new database engines\nAn API for CRUD operations\nFor many different types of databases including row- and key/value-based designs\nIntentionally small in terms of features: no joins, no foreign keys, no database specific features\nBest for simpler designs, complex architectures will need more sophisticated tools.", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/minidataapi.html#connectconstruct-the-database", "href": "explains/minidataapi.html#connectconstruct-the-database", "title": "MiniDataAPI Spec", "section": "Connect/construct the database", "text": "Connect/construct the database\nWe connect or construct the database by passing in a string connecting to the database endpoint or a filepath representing the database’s location. While this example is for SQLite running in memory, other databases such as PostgreSQL, Redis, MongoDB, might instead use a URI pointing at the database’s filepath or endpoint. The method of connecting to a DB is not part of this API, but part of the underlying library. For instance, for fastlite:\n\ndb = database(':memory:')\n\nHere’s a complete list of the available methods in the API, all documented below (assuming db is a database and t is a table):\n\ndb.create\nt.insert\nt.delete\nt.update\nt[key]\nt(...)\nt.xtra", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/minidataapi.html#tables", "href": "explains/minidataapi.html#tables", "title": "MiniDataAPI Spec", "section": "Tables", "text": "Tables\nFor the sake of expediency, this document uses a SQL example. However, tables can represent anything, not just the fundamental construct of a SQL databases. They might represent keys within a key/value structure or files on a hard-drive.\n\nCreating tables\nWe use a create() method attached to Database object (db in our example) to create the tables.\n\nclass User: name:str; email: str; year_started:int\nusers = db.create(User, pk='name')\nusers\n\n<Table user (name, email, year_started)>\n\n\n\nclass User: name:str; email: str; year_started:int\nusers = db.create(User, pk='name')\nusers\n\n<Table user (name, email, year_started)>\n\n\nIf no pk is provided, id is assumed to be the primary key. Regardless of whether you mark a class as a dataclass or not, it will be turned into one – specifically into a flexiclass.\n\n@dataclass\nclass Todo: id: int; title: str; detail: str; status: str; name: str\ntodos = db.create(Todo) \ntodos\n\n<Table todo (id, title, detail, status, name)>\n\n\n\n\nCompound primary keys\nThe MiniData API spec supports compound primary keys, where more than one column is used to identify records. We’ll also use this example to demonstrate creating a table using a dict of keyword arguments.\n\nclass Publication: authors: str; year: int; title: str\npublications = db.create(Publication, pk=('authors', 'year'))\n\n\n\nTransforming tables\nDepending on the database type, this method can include transforms - the ability to modify the tables. Let’s go ahead and add a password field for our table called pwd.\n\nclass User: name:str; email: str; year_started:int; pwd:str\nusers = db.create(User, pk='name', transform=True)\nusers\n\n<Table user (name, email, year_started, pwd)>", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/minidataapi.html#manipulating-data", "href": "explains/minidataapi.html#manipulating-data", "title": "MiniDataAPI Spec", "section": "Manipulating data", "text": "Manipulating data\nThe specification is designed to provide as straightforward CRUD API (Create, Read, Update, and Delete) as possible. Additional features like joins are out of scope.\n\n.insert()\nAdd a new record to the database. We want to support as many types as possible, for now we have tests for Python classes, dataclasses, and dicts. Returns an instance of the new record.\nHere’s how to add a record using a Python class:\n\nusers.insert(User(name='Braden', email='b@example.com', year_started=2018))\n\nUser(name='Braden', email='b@example.com', year_started=2018, pwd=None)\n\n\nWe can also use keyword arguments directly:\n\nusers.insert(name='Alma', email='a@example.com', year_started=2019)\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\nAnd now Charlie gets added via a Python dict.\n\nusers.insert({'name': 'Charlie', 'email': 'c@example.com', 'year_started': 2018})\n\nUser(name='Charlie', email='c@example.com', year_started=2018, pwd=None)\n\n\nAnd now TODOs. Note that the inserted row is returned:\n\ntodos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden'))\ntodos.insert(title='Implement SSE in FastHTML', status='open', name='Alma')\ntodo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie'))\ntodo\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nLet’s do the same with the Publications table.\n\npublications.insert(Publication(authors='Alma', year=2019, title='FastHTML'))\npublications.insert(authors='Alma', year=2030, title='FastHTML and beyond')\npublication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years')))\npublication\n\nPublication(authors='Alma', year=2035, title='FastHTML, the early years')\n\n\n\n\nSquare bracket search []\nGet a single record by entering a primary key into a table object within square brackets. Let’s see if we can find Alma.\n\nuser = users['Alma']\nuser\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\nIf no record is found, a NotFoundError error is raised. Here we look for David, who hasn’t yet been added to our users table.\n\ntry: users['David']\nexcept NotFoundError: print(f'User not found')\n\nUser not found\n\n\nHere’s a demonstration of a ticket search, demonstrating how this works with non-string primary keys.\n\ntodos[1]\n\nTodo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden')\n\n\nCompound primary keys can be supplied in lists or tuples, in the order they were defined. In this case it is the authors and year columns.\nHere’s a query by compound primary key done with a list:\n\npublications[['Alma', 2019]]\n\nPublication(authors='Alma', year=2019, title='FastHTML')\n\n\nHere’s the same query done directly with index args.\n\npublications['Alma', 2030]\n\nPublication(authors='Alma', year=2030, title='FastHTML and beyond')\n\n\n\n\nParentheses search ()\nGet zero to many records by entering values with parentheses searches. If nothing is in the parentheses, then everything is returned.\n\nusers()\n\n[User(name='Braden', email='b@example.com', year_started=2018, pwd=None),\n User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\nWe can order the results.\n\nusers(order_by='name')\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Braden', email='b@example.com', year_started=2018, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\nWe can filter on the results:\n\nusers(where=\"name='Alma'\")\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]\n\n\nGenerally you probably want to use placeholders, to avoid SQL injection attacks:\n\nusers(\"name=?\", ('Alma',))\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None)]\n\n\nWe can limit results with the limit keyword:\n\nusers(limit=1)\n\n[User(name='Braden', email='b@example.com', year_started=2018, pwd=None)]\n\n\nIf we’re using the limit keyword, we can also use the offset keyword to start the query later.\n\nusers(limit=5, offset=1)\n\n[User(name='Alma', email='a@example.com', year_started=2019, pwd=None),\n User(name='Charlie', email='c@example.com', year_started=2018, pwd=None)]\n\n\n\n\n.update()\nUpdate an existing record of the database. Must accept Python dict, dataclasses, and standard classes. Uses the primary key for identifying the record to be changed. Returns an instance of the updated record.\nHere’s with a normal Python class:\n\nuser\n\nUser(name='Alma', email='a@example.com', year_started=2019, pwd=None)\n\n\n\nuser.year_started = 2099\nusers.update(user)\n\nUser(name='Alma', email='a@example.com', year_started=2099, pwd=None)\n\n\nOr use a dict:\n\nusers.update(dict(name='Alma', year_started=2199, email='a@example.com'))\n\nUser(name='Alma', email='a@example.com', year_started=2199, pwd=None)\n\n\nOr use kwargs:\n\nusers.update(name='Alma', year_started=2149)\n\nUser(name='Alma', email='a@example.com', year_started=2149, pwd=None)\n\n\nIf the primary key doesn’t match a record, raise a NotFoundError.\nJohn hasn’t started with us yet so doesn’t get the chance yet to travel in time.\n\ntry: users.update(User(name='John', year_started=2024, email='j@example.com'))\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\n\n\n.delete()\nDelete a record of the database. Uses the primary key for identifying the record to be removed. Returns a table object.\nCharlie decides to not travel in time. He exits our little group.\n\nusers.delete('Charlie')\n\n<Table user (name, email, year_started, pwd)>\n\n\nIf the primary key value can’t be found, raises a NotFoundError.\n\ntry: users.delete('Charlies')\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\nIn John’s case, he isn’t time travelling with us yet so can’t be removed.\n\ntry: users.delete('John')\nexcept NotFoundError: print('User not found')\n\nUser not found\n\n\nDeleting records with compound primary keys requires providing the entire key.\n\npublications.delete(['Alma' , 2035])\n\n<Table publication (authors, year, title)>\n\n\n\n\nin keyword\nAre Alma and John contained in the Users table? Or, to be technically precise, is the item with the specified primary key value in this table?\n\n'Alma' in users, 'John' in users\n\n(True, False)\n\n\nAlso works with compound primary keys, as shown below. You’ll note that the operation can be done with either a list or tuple.\n\n['Alma', 2019] in publications\n\nTrue\n\n\nAnd now for a False result, where John has no publications.\n\n('John', 1967) in publications\n\nFalse\n\n\n\n\n.xtra()\nIf we set fields within the .xtra function to a particular value, then indexing is also filtered by those. This applies to every database method except for record creation. This makes it easier to limit users (or other objects) access to only things for which they have permission. This is a one-way operation, once set it can’t be undone for a particular table object.\nFor example, if we query all our records below without setting values via the .xtra function, we can see todos for everyone. Pay special attention to the id values of all three records, as we are about to filter most of them away.\n\ntodos()\n\n[Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),\n Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),\n Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]\n\n\nLet’s use .xtra to constrain results just to Charlie. We set the name field in Todos, but it could be any field defined for this table.\n\ntodos.xtra(name='Charlie')\n\nWe’ve now set a field to a value with .xtra, if we loop over all the records again, only those assigned to records with a name of Charlie will be displayed.\n\ntodos()\n\n[Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]\n\n\nThe in keyword is also affected. Only records with a name of Charlie will evaluate to be True. Let’s demonstrate by testing it with a Charlie record:\n\nct = todos[3]\nct\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nCharlie’s record has an ID of 3. Here we demonstrate that Charlie’s TODO can be found in the list of todos:\n\nct.id in todos\n\nTrue\n\n\nIf we try in with the other IDs the query fails because the filtering is now set to just records with a name of Charlie.\n\n1 in todos, 2 in todos\n\n(False, False)\n\n\n\ntry: todos[2]\nexcept NotFoundError: print('Record not found')\n\nRecord not found\n\n\nWe are also constrained by what records we can update. In the following example we try to update a TODO not named ‘Charlie’. Because the name is wrong, the .update function will raise a NotFoundError.\n\ntry: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))\nexcept NotFoundError as e: print('Record not updated')\n\nRecord not updated\n\n\nUnlike poor Braden, Charlie isn’t filtered out. Let’s update his TODO.\n\ntodos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))\n\nTodo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')\n\n\nFinally, once constrained by .xtra, only records with Charlie as the name can be deleted.\n\ntry: todos.delete(1)\nexcept NotFoundError as e: print('Record not updated')\n\nRecord not updated\n\n\nCharlie’s TODO was to finish development of FastHTML. While the framework will stabilize, like any good project it will see new features added and the odd bug corrected for many years to come. Therefore, Charlie’s TODO is nonsensical. Let’s delete it.\n\ntodos.delete(ct.id)\n\n<Table todo (id, title, detail, status, name)>\n\n\nWhen a TODO is inserted, the xtra fields are automatically set. This ensures that we don’t accidentally, for instance, insert items for others users. Note that here we don’t set the name field, but it’s still included in the resultant row:\n\nct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))\nct\n\nTodo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')\n\n\nIf we try to change the username to someone else, the change is ignored, due to xtra:\n\nct.name = 'Braden'\ntodos.update(ct)\n\nTodo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/minidataapi.html#sql-first-design", "href": "explains/minidataapi.html#sql-first-design", "title": "MiniDataAPI Spec", "section": "SQL-first design", "text": "SQL-first design\n\nusers = None\nUser = None\n\n\nusers = db.t.user\nusers\n\n<Table user (name, email, year_started, pwd)>\n\n\n(This section needs to be documented properly.)\nFrom the table objects we can extract a Dataclass version of our tables. Usually this is given an singular uppercase version of our table name, which in this case is User.\n\nUser = users.dataclass()\n\n\nUser(name='Braden', email='b@example.com', year_started=2018)\n\nUser(name='Braden', email='b@example.com', year_started=2018, pwd=UNSET)", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/minidataapi.html#implementations", "href": "explains/minidataapi.html#implementations", "title": "MiniDataAPI Spec", "section": "Implementations", "text": "Implementations\n\nImplementing MiniDataAPI for a new datastore\nFor creating new implementations, the code examples in this specification are the test case for the API. New implementations should pass the tests in order to be compliant with the specification.\n\n\nImplementations\n\nfastlite - The original implementation, only for Sqlite\nfastsql - An SQL database agnostic implementation based on the excellent SQLAlchemy library.", "crumbs": [ "Home", "Explanations", "MiniDataAPI Spec" ] }, { "objectID": "explains/routes.html", "href": "explains/routes.html", "title": "Routes", "section": "", "text": "Behaviour in FastHTML apps is defined by routes. The syntax is largely the same as the wonderful FastAPI (which is what you should be using instead of this if you’re creating a JSON service. FastHTML is mainly for making HTML web apps, not APIs).\nNote that you need to include the types of your parameters, so that FastHTML knows what to pass to your function. Here, we’re just expecting a string:\nfrom fasthtml.common import *\napp = FastHTML()\n\n@app.get('/user/{nm}')\ndef get_nm(nm:str): return f\"Good day to you, {nm}!\"\nNormally you’d save this into a file such as main.py, and then run it in uvicorn using:\nHowever, for testing, we can use Starlette’s TestClient to try it out:\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\nr = client.get('/user/Jeremy')\nr\n\n<Response [200 OK]>\nTestClient uses httpx behind the scenes, so it returns a httpx.Response, which has a text attribute with our response body:\nr.text\n\n'Good day to you, Jeremy!'\nIn the previous example, the function name (get_nm) didn’t actually matter – we could have just called it _, for instance, since we never actually call it directly. It’s just called through HTTP. In fact, we often do call our functions _ when using this style of route, since that’s one less thing we have to worry about, naming.\nAn alternative approach to creating a route is to use app.route instead, in which case, you make the function name the HTTP method you want. Since this is such a common pattern, you might like to give a shorter name to app.route – we normally use rt:\nrt = app.route\n\n@rt('/')\ndef post(): return \"Going postal!\"\n\nclient.post('/').text\n\n'Going postal!'", "crumbs": [ "Home", "Explanations", "Routes" ] }, { "objectID": "explains/routes.html#combining-routes", "href": "explains/routes.html#combining-routes", "title": "Routes", "section": "Combining Routes", "text": "Combining Routes\nSometimes a FastHTML project can grow so weildy that putting all the routes into main.py becomes unweildy. Or, we install a FastHTML- or Starlette-based package that requires us to add routes.\nFirst let’s create a books.py module, that represents all the user-related views:\n\n# books.py\nbooks_app, rt = fast_app()\n\nbooks = ['A Guide to FastHTML', 'FastHTML Cookbook', 'FastHTML in 24 Hours']\n\n@rt(\"/\", name=\"list\")\ndef get():\n return Titled(\"Books\", *[P(book) for book in books])\n\nLet’s mount it in our main module:\nfrom books import books_app\n\n1app, rt = fast_app(routes=[Mount(\"/books\", books_app, name=\"books\")])\n\n@rt(\"/\")\ndef get():\n return Titled(\"Dashboard\",\n2 P(A(href=\"/books\")(\"Books\")),\n Hr(),\n3 P(A(link=uri(\"books:list\"))(\"Books\")),\n )\n\nserve()\n\n1\n\nWe use starlette.Mount to add the route to our routes list. We provide the name of books to make discovery and management of the links easier. More on that in items 2 and 3 of this annotations list\n\n2\n\nThis example link to the books list view is hand-crafted. Obvious in purpose, it makes changing link patterns in the future harder\n\n3\n\nThis example link uses the named URL route for the books. The advantage of this approach is it makes management of large numbers of link items easier.", "crumbs": [ "Home", "Explanations", "Routes" ] }, { "objectID": "explains/background_tasks.html", "href": "explains/background_tasks.html", "title": "Background Tasks", "section": "", "text": "Useful for operations where the user gets a response quickly but doesn’t need to wait for the operation to finish. Typical scenarios include:", "crumbs": [ "Home", "Explanations", "Background Tasks" ] }, { "objectID": "explains/background_tasks.html#a-simple-background-task-example", "href": "explains/background_tasks.html#a-simple-background-task-example", "title": "Background Tasks", "section": "A simple background task example", "text": "A simple background task example\nIn this example we are attaching a task to FtResponse by assigning it via the background argument. When the page is visited, it will display ‘Simple Background Task Example’ almost instantly, while in the terminal it will slowly count upward from 0.\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom starlette.background import BackgroundTask\nfrom time import sleep\n\napp, rt = fast_app()\n\n1def counter(loops:int):\n \"Slowly print integers to the terminal\"\n for i in range(loops):\n print(i)\n sleep(i)\n\n@rt\ndef index():\n2 task = BackgroundTask(counter, loops=5)\n3 return Titled('Simple Background Task Example'), task\n\nserve()\n\n\n1\n\ncounter is our task function. There is nothing special about it, although it is a good practice for its arguments to be serializable as JSON\n\n2\n\nWe use starlette.background.BackgroundTask to turn counter() into a background task\n\n3\n\nTo add a background task to a handler, we add it to the return values at the top level of the response.", "crumbs": [ "Home", "Explanations", "Background Tasks" ] }, { "objectID": "explains/background_tasks.html#a-more-realistic-example", "href": "explains/background_tasks.html#a-more-realistic-example", "title": "Background Tasks", "section": "A more realistic example", "text": "A more realistic example\nLet’s imagine that we are accessing a slow-to-process critical service. We don’t want our users to have to wait. While we could set up SSE to notify on completion, instead we decide to periodically check to see if the status of their record has changed.\n\nSimulated Slow API Service\nFirst, create a very simple slow timestamp API. All it does is stall requests for a few seconds before returning JSON containing timestamps.\n# slow_api.py\nfrom fasthtml.common import *\nfrom time import sleep, time\n\napp, rt = fast_app()\n\n@rt('/slow')\ndef slow(ts: int):\n1 sleep(3)\n2 return dict(request_time=ts, response_time=int(time()))\n\nserve(port=8123)\n\n1\n\nThis represents slow processing.\n\n2\n\nReturns both the task’s original timestamp and the time after completion\n\n\n\n\nMain FastHTML app\nNow let’s create a user-facing app that uses this API to fetch the timestamp from the glacially slow service.\n# main.py\nfrom fasthtml.common import *\nfrom starlette.background import BackgroundTask\nimport time\nimport httpx\n\napp, rt = fast_app()\n\ndb = database(':memory:')\n\n1class TStamp: request_time: int; response_time: int\n\ntstamps = db.create(TStamp, pk='request_time')\n\n2def task_submit(request_time: int):\n client = httpx.Client()\n3 response = client.post(f'http://127.0.0.1:8123/slow?ts={request_time}')\n4 tstamps.insert(**response.json())\n\n@rt\ndef submit():\n \"Route that initiates a background task and returns immediately.\"\n request_time = int(time.time())\n5 task = BackgroundTask(task_submit, request_time=request_time)\n6 return P(f'Request submitted at: {request_time}'), task\n\n@rt\n7def show_tstamps(): return Ul(map(Li, tstamps()))\n\n@rt\ndef index():\n return Titled('Background Task Dashboard',\n8 P(Button('Press to call slow service',\n hx_post=submit, hx_target='#res')),\n H2('Responses from Tasks'),\n P('', id='res'),\n Div(Ul(map(Li, tstamps())),\n9 hx_get=show_tstamps, hx_trigger='every 5s'),\n )\n\nserve()\n\n1\n\nTracks when requests are sent and responses received\n\n2\n\nTask function calling slow service to be run in the background of a route handler. It is common but not necessary to prefix task functions with ‘task_’\n\n3\n\nCall the slow API service (simulating a time-consuming operation)\n\n4\n\nStore both timestamps in our database\n\n5\n\nCreate a background task by passing in the function to a BackgroundTask object, followed by any arguments.\n\n6\n\nIn FtResponse, use the background keyword argument to set the task to be run after the HTTP response is generated.\n\n7\n\nEndpoint that displays all recorded timestamp pairs.\n\n8\n\nWhen this button is pressed, the ‘submit’ handler will respond instantly. The task_submit function will insert the slow API response into the db later.\n\n9\n\nEvery 5 seconds get the tstamps stored in the DB.\n\n\n\n\n\n\n\n\nTip\n\n\n\nIn the example above we use a synchronous background task function set in the FtResponse of a synchronous handler. However, we can also use asynchronous functions and handlers.", "crumbs": [ "Home", "Explanations", "Background Tasks" ] }, { "objectID": "explains/background_tasks.html#multiple-background-tasks-in-a-handler", "href": "explains/background_tasks.html#multiple-background-tasks-in-a-handler", "title": "Background Tasks", "section": "Multiple background tasks in a handler", "text": "Multiple background tasks in a handler\nIt is possible to add multiple background tasks to an FtResponse.\n\n\n\n\n\n\nWarning\n\n\n\nMultiple background tasks on a background task are executed in order. In the case a task raises an exception, following tasks will not get the opportunity to be executed.\n\n\nfrom starlette.background import BackgroundTasks\n\n@rt\nasync def signup(email, username):\n tasks = BackgroundTasks()\n tasks.add_task(send_welcome_email, to_address=email)\n tasks.add_task(send_admin_notification, username=username)\n return Titled('Signup successful!'), tasks\n\nasync def send_welcome_email(to_address):\n ...\n\nasync def send_admin_notification(username):\n ...", "crumbs": [ "Home", "Explanations", "Background Tasks" ] }, { "objectID": "explains/background_tasks.html#background-tasks-at-scale", "href": "explains/background_tasks.html#background-tasks-at-scale", "title": "Background Tasks", "section": "Background tasks at scale", "text": "Background tasks at scale\nBackground tasks enhance application performance both for users and apps by handling blocking processes asynchronously, even when defined as synchronous functions.\nWhen FastHTML’s background tasks aren’t enough and your app runs slow on a server, manually offloading processes to the multiprocessing library is an option. By doing so you can leverage multiple cores and bypass the GIL, significantly improving speed and performance at the cost of added complexity.\nSometimes a server reaches its processing limits, and this is where distributed task queue systems like Celery and Dramatiq come into play. They are designed to distribute tasks across multiple servers, offering improved observability, retry mechanisms, and persistence, at the cost of substantially increased complexity.\nHowever most applications work well with built-in background tasks like those in FastHTML, which we recommend trying first. Writing these functions with JSON-serializable arguments ensures straightforward conversion to other concurrency methods if needed.", "crumbs": [ "Home", "Explanations", "Background Tasks" ] }, { "objectID": "explains/oauth.html", "href": "explains/oauth.html", "title": "OAuth", "section": "", "text": "OAuth is an open standard for ‘access delegation’, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords. It is the mechanism that enables “Log in with Google” on many sites, saving you from having to remember and manage yet another password. Like many auth-related topics, there’s a lot of depth and complexity to the OAuth standard, but once you understand the basic usage it can be a very convenient alternative to managing your own user accounts.\nOn this page you’ll see how to use OAuth with FastHTML to implement some common pieces of functionality.", "crumbs": [ "Home", "Explanations", "OAuth" ] }, { "objectID": "explains/oauth.html#creating-an-client", "href": "explains/oauth.html#creating-an-client", "title": "OAuth", "section": "Creating an Client", "text": "Creating an Client\nFastHTML has Client classes for managing settings and state for different OAuth providers. Currently implemented are: GoogleAppClient, GitHubAppClient, HuggingFaceClient and DiscordAppClient - see the source if you need to add other providers. You’ll need a client_id and client_secret from the provider (see the from-scratch example later in this page for an example of registering with GitHub) to create the client. We recommend storing these in environment variables, rather than hardcoding them in your code.\n\nimport os\nfrom fasthtml.oauth import GoogleAppClient\nclient = GoogleAppClient(os.getenv(\"AUTH_CLIENT_ID\"),\n os.getenv(\"AUTH_CLIENT_SECRET\"))\n\nThe client is used to obtain a login link and to manage communications between your app and the OAuth provider (client.login_link(redirect_uri=\"/redirect\")).", "crumbs": [ "Home", "Explanations", "OAuth" ] }, { "objectID": "explains/oauth.html#using-the-oauth-class", "href": "explains/oauth.html#using-the-oauth-class", "title": "OAuth", "section": "Using the OAuth class", "text": "Using the OAuth class\nOnce you’ve set up a client, adding OAuth to a FastHTML app can be as simple as:\n\nfrom fasthtml.oauth import OAuth\nfrom fasthtml.common import FastHTML, RedirectResponse\n\nclass Auth(OAuth):\n def get_auth(self, info, ident, session, state):\n email = info.email or ''\n if info.email_verified and email.split('@')[-1]=='answer.ai':\n return RedirectResponse('/', status_code=303)\n\napp = FastHTML()\noauth = Auth(app, client)\n\n@app.get('/')\ndef home(auth): return P('Logged in!'), A('Log out', href='/logout')\n\n@app.get('/login')\ndef login(req): return Div(P(\"Not logged in\"), A('Log in', href=oauth.login_link(req)))\n\nThere’s a fair bit going on here, so let’s unpack what’s happening in that code:\n\nOAuth (and by extension our custom Auth class) has a number of default arguments, including some key URLs: redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login'. It will create and handle the redirect and logout paths, and it’s up to you to handle /login (where unsuccessful login attempts will be redirected) and /error (for oauth errors).\nWhen we run oauth = Auth(app, client) it adds the redirect and logout paths to the app and also adds some beforeware. This beforeware runs on any requests (apart from any specified with the skip parameter).\n\nThe added beforeware specifies some app behaviour:\n\nIf someone who isn’t logged in attempts to visit our homepage (/) here, they will be redirected to /login.\nIf they are logged in, it calls a check_invalid method. This defaults to False, which let’s the user continue to the page they requested. The behaviour can be modified by defining your own check_invalid method in the Auth class - for example, you could have this forcibly log out users who have recently been banned.\n\nSo how does someone log in? If they visit (or are redirected to) the login page at /login, we show them a login link. This sends them to the OAuth provider, where they’ll go through the steps of selecting their account, giving permissions etc. Once done they will be redirected back to /redirect. Behind the scenes a code that comes as part of their request gets turned into user info, which is then passed to the key function get_auth(self, info, ident, session, state). Here is where you’d handle looking up or adding a user in a database, checking for some condition (for example, this code checks if the email is an answer.ai email address) or choosing the destination based on state. The arguments are:\n\nself: the Auth object, which you can use to access the client (self.cli)\ninfo: the information provided by the OAuth provider, typically including a unique user id, email address, username and other metadata.\nident: a unique identifier for this user. What this looks like varies between providers. This is useful for managing a database of users, for example.\nsession: the current session, that you can store information in securely\nstate: you can optionally pass in some state when creating the login link. This persists and is returned after the user goes through the Oath steps, which is useful for returning them to the same page they left. It can also be used as added security against CSRF attacks.\n\nIn our example, we check the email in info (we use a GoogleAppClient, not all providers will include an email). If we aren’t happy, and get_auth returns False or nothing (as in the case here for non-answerai people) then the user is redirected back to the login page. But if everything looks good we return a redirect to the homepage, and an auth key is added to the session and the scope containing the users identity ident. So, for example, in the homepage route we could use auth to look up this particular user’s profile info and customize the page accordingly. This auth will persist in their session until they clear the browser cache, so by default they’ll stay logged in. To log them out, remove it ( session.pop('auth', None)) or send them to /logout which will do that for you.", "crumbs": [ "Home", "Explanations", "OAuth" ] }, { "objectID": "explains/oauth.html#explaining-oauth-with-a-from-scratch-implementation", "href": "explains/oauth.html#explaining-oauth-with-a-from-scratch-implementation", "title": "OAuth", "section": "Explaining OAuth with a from-scratch implementation", "text": "Explaining OAuth with a from-scratch implementation\nHopefully the example above is enough to get you started. You can also check out the (fairly minimal) source code where this is implemented, and the examples here.\nIf you’re wanting to learn more about how this works, and to see where you might add additional functionality, the rest of this page will walk through some examples without the OAuth convenience class, to illustrate the concepts. This was written before said OAuth class was available, and is kept here for educational purposes - we recommend you stick with the new approach shown above in most cases.", "crumbs": [ "Home", "Explanations", "OAuth" ] }, { "objectID": "explains/oauth.html#a-minimal-login-flow-github", "href": "explains/oauth.html#a-minimal-login-flow-github", "title": "OAuth", "section": "A Minimal Login Flow (GitHub)", "text": "A Minimal Login Flow (GitHub)\nLet’s begin by building a minimal ‘Sign in with GitHub’ flow. This will demonstrate the basic steps of OAuth.\nOAuth requires a “provider” (in this case, GitHub) to authenticate the user. So the first step when setting up our app is to register with GitHub to set things up.\nGo to https://github.com/settings/developers and click “New OAuth App”. Fill in the form with the following values, then click ‘Register application’.\n\nApplication name: Your app name\nHomepage URL: http://localhost:8000 (or whatever URL you’re using - you can change this later)\nAuthorization callback URL: http://localhost:8000/auth_redirect (you can modify this later too)\n\n\n\n\nAfter you register, you’ll see a screen where you can view the client ID and generate a client secret. Store these values in a safe place. You’ll use them to create a GitHubAppClient object in FastHTML.\nThis client object is responsible for handling the parts of the OAuth flow which depend on direct communication between your app and GitHub, as opposed to interactions which go through the user’s browser via redirects.\nHere is how to setup the client object:\nclient = GitHubAppClient(\n client_id=\"your_client_id\",\n client_secret=\"your_client_secret\"\n)\nYou should also save the path component of the authorization callback URL which you provided on registration.\nThis route is where GitHub will redirect the user’s browser in order to send an authorization code to your app. You should save only the URL’s path component rather than the entire URL because you want your code to work automatically in deployment, when the host and port part of the URL change from localhost:8000 to your real DNS name.\nSave the special authorization callback path under an obvious name:\nauth_callback_path = \"/auth_redirect\"\n\n\n\n\n\n\nNote\n\n\n\nIt’s recommended to store the client ID, and secret, in environment variables, rather than hardcoding them in your code.\n\n\nWhen the user visit a normal page of your app, if they are not already logged in, then you’ll want to redirect them to your app’s login page, which will live at the /login path. We accomplish that by using this piece of “beforeware”, which defines logic which runs before other work for all routes except ones we specify to be skipped:\ndef before(req, session):\n auth = req.scope['auth'] = session.get('user_id', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n counts.xtra(name=auth)\nbware = Beforeware(before, skip=['/login', auth_callback_path])\nWe configure the beforeware to skip /login because that’s where the user goes to login, and we also skip the special authorization callback path because that is used by OAuth itself to receive information from GitHub.\nIt’s only at your login page that we start the OAuth flow. To start the OAuth flow, you need to give the user a link to GitHub’s login for your app. You’ll need the client object to generate that link, and the client object will in turn need the full authorization callback URL, which we need to build from the authorization callback path, so it is a multi-step process to produce this GitHub login link.\nHere is an implementation of your own /login route handler. It generates the GitHub login link and presents it to the user:\n@app.get('/login')\ndef login(request)\n redir = redir_url(request,auth_callback_path)\n login_link = client.login_link(redir)\n return P(A('Login with GitHub', href=login_link)) \nOnce the user follows that link, GitHub will ask them to grant permission to your app to access their GitHub account. If they agree, GitHub will redirect them back to your app’s authorization callback URL, carrying an authorization code which your app can use to generate an access token. To receive this code, you need to set up a route in FastHTML that listens for requests at the authorization callback path. For example:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str):\n return P(f\"code: {code}\")\nThis authorization code is temporary, and is used by your app to directly ask the provider for user information like an access token.\nTo recap, you can think of the exchange so far as:\n\nUser to us: “I want to log in with you, app.”\nUs to User: “Okay but first, here’s a special link to log in with GitHub”\nUser to GitHub: “I want to log in with you, GitHub, to use this app.”\nGitHub to User: “OK, redirecting you back to the app’s URL (with an auth code)”\nUser to Us: “Hi again, app. Here’s the GitHub auth code you need to ask GitHub for info about me” (delivered via /auth_redirect?code=...)\n\nThe final steps we need to implement are as follows:\n\nUs to GitHUb: “A user just gave me this auth code. May I have the user info (e.g., an access token)?”\nGitHub to us: “Since you have an auth code, here’s the user info”\n\nIt’s critical for us to derive the user info from the auth code immediately in the authorization callback, because the auth code may be used only once. So we use it that once in order to get information like an access token, which will remain valid for longer.\nTo go from the auth code to user info, you use info = client.retr_info(code,redirect_uri). From the user info, you can extract the user_id, which is a unique identifier for the user:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = info[client.id_key]\n return P(f\"User id: {user_id}\")\nBut we want the user ID not to print it but to remember the user.\nSo let us store it in the session object, to remember who is logged in:\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request, session):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = user_info[client.id_key] # get their ID\n session['user_id'] = user_id # save ID in the session\n return RedirectResponse('/', status_code=303)\nThe session object is derived from values visible to the user’s browser, but it is cryptographically signed so the user can’t read it themselves. This makes it safe to store even information we don’t want to expose to the user.\nFor larger quantities of data, we’d want to save that information in a database and use the session to hold keys to lookup information from that database.\nHere’s a minimal app that puts all these pieces together. It uses the user info to get the user_id. It stores that in the session object. It then uses the user_id as a key into a database, which tracks how frequently every user has hit an increment button.\nimport os\nfrom fasthtml.common import *\nfrom fasthtml.oauth import GitHubAppClient, redir_url\n\ndb = database('data/counts.db')\ncounts = db.t.counts\nif counts not in db.t: counts.create(dict(name=str, count=int), pk='name')\nCount = counts.dataclass()\n\n# Auth client setup for GitHub\nclient = GitHubAppClient(os.getenv(\"AUTH_CLIENT_ID\"), \n os.getenv(\"AUTH_CLIENT_SECRET\"))\nauth_callback_path = \"/auth_redirect\"\n\ndef before(req, session):\n # if not logged in, we send them to our login page\n # logged in means:\n # - 'user_id' in the session object, \n # - 'auth' in the request object\n auth = req.scope['auth'] = session.get('user_id', None)\n if not auth: return RedirectResponse('/login', status_code=303)\n counts.xtra(name=auth)\nbware = Beforeware(before, skip=['/login', auth_callback_path])\n\napp = FastHTML(before=bware)\n\n# User asks us to Login\n@app.get('/login')\ndef login(request):\n redir = redir_url(request,auth_callback_path)\n login_link = client.login_link(redir)\n # we tell user to login at github\n return P(A('Login with GitHub', href=login_link)) \n\n# User comes back to us with an auth code from Github\n@app.get(auth_callback_path)\ndef auth_redirect(code:str, request, session):\n redir = redir_url(request, auth_callback_path)\n user_info = client.retr_info(code, redir)\n user_id = user_info[client.id_key] # get their ID\n session['user_id'] = user_id # save ID in the session\n # create a db entry for the user\n if user_id not in counts: counts.insert(name=user_id, count=0)\n return RedirectResponse('/', status_code=303)\n\n@app.get('/')\ndef home(auth):\n return Div(\n P(\"Count demo\"),\n P(f\"Count: \", Span(counts[auth].count, id='count')),\n Button('Increment', hx_get='/increment', hx_target='#count'),\n P(A('Logout', href='/logout'))\n )\n\n@app.get('/increment')\ndef increment(auth):\n c = counts[auth]\n c.count += 1\n return counts.upsert(c).count\n\n@app.get('/logout')\ndef logout(session):\n session.pop('user_id', None)\n return RedirectResponse('/login', status_code=303)\n\nserve()\nSome things to note:\n\nThe before function is used to check if the user is authenticated. If not, they are redirected to the login page.\nTo log the user out, we remove the user ID from the session.\nCalling counts.xtra(name=auth) ensures that only the row corresponding to the current user is accessible when responding to a request. This is often nicer than trying to remember to filter the data in every route, and lowers the risk of accidentally leaking data.\nIn the auth_redirect route, we store the user ID in the session and create a new row in the user_counts table if it doesn’t already exist.\n\nYou can find more heavily-commented version of this code in the oauth directory in fasthtml-example, along with an even more minimal example. More examples may be added in the future.\n\nRevoking Tokens (Google)\nWhen the user in the example above logs out, we remove their user ID from the session. However, the user is still logged in to GitHub. If they click ‘Login with GitHub’ again, they’ll be redirected back to our site without having to log in again. This is because GitHub remembers that they’ve already granted our app permission to access their account. Most of the time this is convenient, but for testing or security purposes you may want a way to revoke this permission.\nAs a user, you can usually revoke access to an app from the provider’s website (for example, https://github.com/settings/applications). But as a developer, you can also revoke access programmatically - at least with some providers. This requires keeping track of the access token (stored in client.token[\"access_token\"] after you call retr_info), and sending a request to the provider’s revoke URL:\nauth_revoke_url = \"https://accounts.google.com/o/oauth2/revoke\"\ndef revoke_token(token):\n response = requests.post(auth_revoke_url, params={\"token\": token})\n return response.status_code == 200 # True if successful\nNot all providers support token revocation, and it is not built into FastHTML clients at the moment.\n\n\nUsing State (Hugging Face)\nImagine a user (not logged in) comes to your AI image editing site, starts testing things out, and then realizes they need to sign in before they can click “Run (Pro)” on the edit they’re working on. They click “Sign in with Hugging Face”, log in, and are redirected back to your site. But now they’ve lost their in-progress edit and are left just looking at the homepage! This is an example of a case where you might want to keep track of some additional state. Another strong use case for being able to pass some uniqie state through the OAuth flow is to prevent something called a CSRF attack. To add a state string to the OAuth flow, include a state argument when creating the login link:\n# in login page:\nlink = A('Login with GitHub', href=client.login_link(state='current_prompt: add a unicorn'))\n\n# in auth_redirect:\n@app.get('/auth_redirect')\ndef auth_redirect(code:str, session, state:str=None):\n print(f\"state: {state}\") # Use as needed\n ...\nThe state string is passed through the OAuth flow and back to your site.\n\n\nA Work in Progress\nThis page (and OAuth support in FastHTML) is a work in progress. Questions, PRs, and feedback are welcome!", "crumbs": [ "Home", "Explanations", "OAuth" ] }, { "objectID": "api/core.html", "href": "api/core.html", "title": "Core", "section": "", "text": "This is the source code to fasthtml. You won’t need to read this unless you want to understand how things are built behind the scenes, or need full details of a particular API. The notebook is converted to the Python module fasthtml/core.py using nbdev.", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#imports-and-utils", "href": "api/core.html#imports-and-utils", "title": "Core", "section": "Imports and utils", "text": "Imports and utils\n\nimport time\n\nfrom IPython import display\nfrom enum import Enum\nfrom pprint import pprint\n\nfrom fastcore.test import *\nfrom starlette.testclient import TestClient\nfrom starlette.requests import Headers\nfrom starlette.datastructures import UploadFile\n\nWe write source code first, and then tests come after. The tests serve as both a means to confirm that the code works and also serves as working examples. The first exported function, parsed_date, is an example of this pattern.\n\nsource\n\nparsed_date\n\n parsed_date (s:str)\n\nConvert s to a datetime\n\nparsed_date('2pm')\n\ndatetime.datetime(2025, 5, 29, 14, 0)\n\n\n\nisinstance(date.fromtimestamp(0), date)\n\nTrue\n\n\n\nsource\n\n\nsnake2hyphens\n\n snake2hyphens (s:str)\n\nConvert s from snake case to hyphenated and capitalised\n\nsnake2hyphens(\"snake_case\")\n\n'Snake-Case'\n\n\n\nsource\n\n\nHtmxHeaders\n\n HtmxHeaders (boosted:str|None=None, current_url:str|None=None,\n history_restore_request:str|None=None, prompt:str|None=None,\n request:str|None=None, target:str|None=None,\n trigger_name:str|None=None, trigger:str|None=None)\n\n\ndef test_request(url: str='/', headers: dict={}, method: str='get') -> Request:\n scope = {\n 'type': 'http',\n 'method': method,\n 'path': url,\n 'headers': Headers(headers).raw,\n 'query_string': b'',\n 'scheme': 'http',\n 'client': ('127.0.0.1', 8000),\n 'server': ('127.0.0.1', 8000),\n }\n receive = lambda: {\"body\": b\"\", \"more_body\": False}\n return Request(scope, receive)\n\n\nh = test_request(headers=Headers({'HX-Request':'1'}))\n_get_htmx(h.headers)\n\nHtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None)", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#request-and-response", "href": "api/core.html#request-and-response", "title": "Core", "section": "Request and response", "text": "Request and response\n\ntest_eq(_fix_anno(Union[str,None], 'a'), 'a')\ntest_eq(_fix_anno(float, 0.9), 0.9)\ntest_eq(_fix_anno(int, '1'), 1)\ntest_eq(_fix_anno(int, ['1','2']), 2)\ntest_eq(_fix_anno(list[int], ['1','2']), [1,2])\ntest_eq(_fix_anno(list[int], '1'), [1])\n\n\nd = dict(k=int, l=List[int])\ntest_eq(_form_arg('k', \"1\", d), 1)\ntest_eq(_form_arg('l', \"1\", d), [1])\ntest_eq(_form_arg('l', [\"1\",\"2\"], d), [1,2])\n\n\nsource\n\nHttpHeader\n\n HttpHeader (k:str, v:str)\n\n\n_to_htmx_header('trigger_after_settle')\n\n'HX-Trigger-After-Settle'\n\n\n\nsource\n\n\nHtmxResponseHeaders\n\n HtmxResponseHeaders (location=None, push_url=None, redirect=None,\n refresh=None, replace_url=None, reswap=None,\n retarget=None, reselect=None, trigger=None,\n trigger_after_settle=None, trigger_after_swap=None)\n\nHTMX response headers\n\nHtmxResponseHeaders(trigger_after_settle='hi')\n\nHttpHeader(k='HX-Trigger-After-Settle', v='hi')\n\n\n\nsource\n\n\nform2dict\n\n form2dict (form:starlette.datastructures.FormData)\n\nConvert starlette form data to a dict\n\nd = [('a',1),('a',2),('b',0)]\nfd = FormData(d)\nres = form2dict(fd)\ntest_eq(res['a'], [1,2])\ntest_eq(res['b'], 0)\n\n\nsource\n\n\nparse_form\n\n parse_form (req:starlette.requests.Request)\n\nStarlette errors on empty multipart forms, so this checks for that situation\n\nsource\n\n\nJSONResponse\n\n JSONResponse (content:Any, status_code:int=200,\n headers:Optional[Mapping[str,str]]=None,\n media_type:str|None=None,\n background:starlette.background.BackgroundTask|None=None)\n\nSame as starlette’s version, but auto-stringifies non serializable types\n\nasync def f(req):\n def _f(p:HttpHeader): ...\n p = first(_params(_f).values())\n result = await _from_body(req, p)\n return JSONResponse(result.__dict__)\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\n\nd = dict(k='value1',v=['value2','value3'])\nresponse = client.post('/', data=d)\nprint(response.json())\n\n{'k': 'value1', 'v': 'value3'}\n\n\n\nasync def f(req): return Response(str(req.query_params.getlist('x')))\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['GET'])]))\nclient.get('/?x=1&x=2').text\n\n\"['1', '2']\"\n\n\n\ndef g(req, this:Starlette, a:str, b:HttpHeader): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/?a=1', data=d)\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]\n\n\n\ndef g(req, this:Starlette, a:str, b:HttpHeader): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/?a=1', data=d)\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]\n\n\nMissing Request Params\nIf a request param has a default value (e.g. a:str=''), the request is valid even if the user doesn’t include the param in their request.\n\ndef g(req, this:Starlette, a:str=''): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/', json={}) # no param in request\nprint(response.text)\n\n[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '']\n\n\nIf we remove the default value and re-run the request, we should get the following error Missing required field: a.\n\ndef g(req, this:Starlette, a:str): ...\n\nasync def f(req):\n a = await _wrap_req(req, _params(g))\n return Response(str(a))\n\nclient = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))\nresponse = client.post('/', json={}) # no param in request\nprint(response.text)\n\nMissing required field: a\n\n\n\nsource\n\n\nflat_xt\n\n flat_xt (lst)\n\nFlatten lists\n\nx = ft('a',1)\ntest_eq(flat_xt([x, x, [x,x]]), (x,)*4)\ntest_eq(flat_xt(x), (x,))\n\n\nsource\n\n\nBeforeware\n\n Beforeware (f, skip=None)\n\nInitialize self. See help(type(self)) for accurate signature.", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#websockets-sse", "href": "api/core.html#websockets-sse", "title": "Core", "section": "Websockets / SSE", "text": "Websockets / SSE\n\ndef on_receive(self, msg:str): return f\"Message text was: {msg}\"\nc = _ws_endp(on_receive)\ncli = TestClient(Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))]))\nwith cli.websocket_connect('/') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\nsource\n\nEventStream\n\n EventStream (s)\n\nCreate a text/event-stream response from s\n\nsource\n\n\nsignal_shutdown\n\n signal_shutdown ()", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#routing-and-application", "href": "api/core.html#routing-and-application", "title": "Core", "section": "Routing and application", "text": "Routing and application\n\nsource\n\nuri\n\n uri (_arg, **kwargs)\n\n\nsource\n\n\ndecode_uri\n\n decode_uri (s)\n\n\nsource\n\n\nStringConvertor.to_string\n\n StringConvertor.to_string (value:str)\n\n\nsource\n\n\nHTTPConnection.url_path_for\n\n HTTPConnection.url_path_for (name:str, **path_params)\n\n\nsource\n\n\nflat_tuple\n\n flat_tuple (o)\n\nFlatten lists\n\nsource\n\n\nnoop_body\n\n noop_body (c, req)\n\nDefault Body wrap function which just returns the content\n\nsource\n\n\nrespond\n\n respond (req, heads, bdy)\n\nDefault FT response creation function\nRender fragment if HX-Request header is present and HX-History-Restore-Request header is absent.\n\nsource\n\n\nis_full_page\n\n is_full_page (req, resp)\n\n\nsource\n\n\nRedirect\n\n Redirect (loc)\n\nUse HTMX or Starlette RedirectResponse as required to redirect to loc\nThe FastHTML exts param supports the following:\n\nprint(' '.join(htmx_exts))\n\nmorph head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer\n\n\n\nsource\n\n\nget_key\n\n get_key (key=None, fname='.sesskey')\n\n\nsource\n\n\nqp\n\n qp (p:str, **kw)\n\nAdd parameters kw to path p\nqp adds query parameters to route path strings\n\nvals = {'a':5, 'b':False, 'c':[1,2], 'd':'bar', 'e':None, 'ab':42}\n\n\nres = qp('/foo', **vals)\ntest_eq(res, '/foo?a=5&b=&c=1&c=2&d=bar&e=&ab=42')\n\nqp checks to see if each param should be sent as a query parameter or as part of the route, and encodes that properly.\n\npath = '/foo/{a}/{d}/{ab:int}'\nres = qp(path, **vals)\ntest_eq(res, '/foo/5/bar/42?b=&c=1&c=2&e=')\n\n\nsource\n\n\ndef_hdrs\n\n def_hdrs (htmx=True, surreal=True)\n\nDefault headers for a FastHTML app\n\nsource\n\n\nFastHTML\n\n FastHTML (debug=False, routes=None, middleware=None, title:str='FastHTML\n page', exception_handlers=None, on_startup=None,\n on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,\n exts=None, before=None, after=None, surreal=True, htmx=True,\n default_hdrs=True, sess_cls=<class\n 'starlette.middleware.sessions.SessionMiddleware'>,\n secret_key=None, session_cookie='session_', max_age=31536000,\n sess_path='/', same_site='lax', sess_https_only=False,\n sess_domain=None, key_fname='.sesskey', body_wrap=<function\n noop_body>, htmlkw=None, nb_hdrs=False, canonical=True,\n **bodykw)\n\nCreates an Starlette application.\n\nsource\n\n\nFastHTML.ws\n\n FastHTML.ws (path:str, conn=None, disconn=None, name=None,\n middleware=None)\n\nAdd a websocket route at path\n\nsource\n\n\nnested_name\n\n nested_name (f)\n\n*Get name of function f using ’_’ to join nested function names*\n\ndef f():\n def g(): ...\n return g\n\n\nfunc = f()\nnested_name(func)\n\n'f_g'\n\n\n\nsource\n\n\nFastHTML.route\n\n FastHTML.route (path:str=None, methods=None, name=None,\n include_in_schema=True, body_wrap=None)\n\nAdd a route at path\n\napp = FastHTML()\n@app.get\ndef foo(a:str, b:list[int]): ...\n\nfoo.to(a='bar', b=[1,2])\n\n'/foo?a=bar&b=1&b=2'\n\n\n\n@app.get('/foo/{a}')\ndef foo(a:str, b:list[int]): ...\n\nfoo.to(a='bar', b=[1,2])\n\n'/foo/bar?b=1&b=2'\n\n\n\nsource\n\n\nserve\n\n serve (appname=None, app='app', host='0.0.0.0', port=None, reload=True,\n reload_includes:list[str]|str|None=None,\n reload_excludes:list[str]|str|None=None)\n\nRun the app in an async server, with live reload set as the default.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nappname\nNoneType\nNone\nName of the module\n\n\napp\nstr\napp\nApp instance to be served\n\n\nhost\nstr\n0.0.0.0\nIf host is 0.0.0.0 will convert to localhost\n\n\nport\nNoneType\nNone\nIf port is None it will default to 5001 or the PORT environment variable\n\n\nreload\nbool\nTrue\nDefault is to reload the app upon code changes\n\n\nreload_includes\nlist[str] | str | None\nNone\nAdditional files to watch for changes\n\n\nreload_excludes\nlist[str] | str | None\nNone\nFiles to ignore for changes\n\n\n\n\nsource\n\n\nClient\n\n Client (app, url='http://testserver')\n\nA simple httpx ASGI client that doesn’t require async\n\napp = FastHTML(routes=[Route('/', lambda _: Response('test'))])\ncli = Client(app)\n\ncli.get('/').text\n\n'test'\n\n\nNote that you can also use Starlette’s TestClient instead of FastHTML’s Client. They should be largely interchangable.", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#fasthtml-tests", "href": "api/core.html#fasthtml-tests", "title": "Core", "section": "FastHTML Tests", "text": "FastHTML Tests\n\ndef get_cli(app): return app,TestClient(app),app.route\n\n\napp,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))\n\n\napp,cli,rt = get_cli(FastHTML(title=\"My Custom Title\"))\n@app.get\ndef foo(): return Div(\"Hello World\")\n\nprint(app.routes)\n\nresponse = cli.get('/foo')\nassert '<title>My Custom Title</title>' in response.text\n\nfoo.to(param='value')\n\n[Route(path='/foo', name='foo', methods=['GET', 'HEAD'])]\n\n\n'/foo?param=value'\n\n\n\napp,cli,rt = get_cli(FastHTML())\n\n@rt('/xt2')\ndef get(): return H1('bar')\n\ntxt = cli.get('/xt2').text\nassert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\n\n@rt(\"/hi\")\ndef get(): return 'Hi there'\n\nr = cli.get('/hi')\nr.text\n\n'Hi there'\n\n\n\n@rt(\"/hi\")\ndef post(): return 'Postal'\n\ncli.post('/hi').text\n\n'Postal'\n\n\n\n@app.get(\"/hostie\")\ndef show_host(req): return req.headers['host']\n\ncli.get('/hostie').text\n\n'testserver'\n\n\n\n@app.get(\"/setsess\")\ndef set_sess(session):\n session['foo'] = 'bar'\n return 'ok'\n\n@app.ws(\"/ws\")\ndef ws(self, msg:str, ws:WebSocket, session): return f\"Message text was: {msg} with session {session.get('foo')}, from client: {ws.client}\"\n\ncli.get('/setsess')\nwith cli.websocket_connect('/ws') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\nassert 'Message text was: Hi! with session bar' in data\nprint(data)\n\nMessage text was: Hi! with session bar, from client: Address(host='testclient', port=50000)\n\n\n\n@rt\ndef yoyo(): return 'a yoyo'\n\ncli.post('/yoyo').text\n\n'a yoyo'\n\n\n\n@app.get\ndef autopost(): return Html(Div('Text.', hx_post=yoyo()))\nprint(cli.get('/autopost').text)\n\n <!doctype html>\n <html>\n <div hx-post=\"a yoyo\">Text.</div>\n </html>\n\n\n\n\n@app.get\ndef autopost2(): return Html(Body(Div('Text.', cls='px-2', hx_post=show_host.to(a='b'))))\nprint(cli.get('/autopost2').text)\n\n <!doctype html>\n <html>\n <body>\n <div class=\"px-2\" hx-post=\"/hostie?a=b\">Text.</div>\n </body>\n </html>\n\n\n\n\n@app.get\ndef autoget2(): return Html(Div('Text.', hx_get=show_host))\nprint(cli.get('/autoget2').text)\n\n <!doctype html>\n <html>\n <div hx-get=\"/hostie\">Text.</div>\n </html>\n\n\n\n\n@rt('/user/{nm}', name='gday')\ndef get(nm:str=''): return f\"Good day to you, {nm}!\"\ncli.get('/user/Alexis').text\n\n'Good day to you, Alexis!'\n\n\n\n@app.get\ndef autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))\nprint(cli.get('/autolink').text)\n\n <!doctype html>\n <html>\n <div href=\"/user/Alexis\">Text.</div>\n </html>\n\n\n\n\n@rt('/link')\ndef get(req): return f\"{req.url_for('gday', nm='Alexis')}; {req.url_for('show_host')}\"\n\ncli.get('/link').text\n\n'http://testserver/user/Alexis; http://testserver/hostie'\n\n\n\n@app.get(\"/background\")\nasync def background_task(request):\n async def long_running_task():\n await asyncio.sleep(0.1)\n print(\"Background task completed!\")\n return P(\"Task started\"), BackgroundTask(long_running_task)\n\nresponse = cli.get(\"/background\")\n\nBackground task completed!\n\n\n\ntest_eq(app.router.url_path_for('gday', nm='Jeremy'), '/user/Jeremy')\n\n\nhxhdr = {'headers':{'hx-request':\"1\"}}\n\n@rt('/ft')\ndef get(): return Title('Foo'),H1('bar')\n\ntxt = cli.get('/ft').text\nassert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\n@rt('/xt2')\ndef get(): return H1('bar')\n\ntxt = cli.get('/xt2').text\nassert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\nassert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'\n\n@rt('/xt3')\ndef get(): return Html(Head(Title('hi')), Body(P('there')))\n\ntxt = cli.get('/xt3').text\nassert '<title>FastHTML page</title>' not in txt and '<title>hi</title>' in txt and '<p>there</p>' in txt\n\n\n@rt('/oops')\ndef get(nope): return nope\ntest_warns(lambda: cli.get('/oops?nope=1'))\n\n\ndef test_r(cli, path, exp, meth='get', hx=False, **kwargs):\n if hx: kwargs['headers'] = {'hx-request':\"1\"}\n test_eq(getattr(cli, meth)(path, **kwargs).text, exp)\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n\n@rt('/html/{idx}')\nasync def get(idx:int): return Body(H4(f'Next is {idx+1}.'))\n\n\n@rt(\"/models/{nm}\")\ndef get(nm:ModelName): return nm\n\n@rt(\"/files/{path}\")\nasync def get(path: Path): return path.with_suffix('.txt')\n\n@rt(\"/items/\")\ndef get(idx:int|None = 0): return fake_db[idx]\n\n@rt(\"/idxl/\")\ndef get(idx:list[int]): return str(idx)\n\n\nr = cli.get('/html/1', headers={'hx-request':\"1\"})\nassert '<h4>Next is 2.</h4>' in r.text\ntest_r(cli, '/models/alexnet', 'alexnet')\ntest_r(cli, '/files/foo', 'foo.txt')\ntest_r(cli, '/items/?idx=1', '{\"name\":\"Bar\"}')\ntest_r(cli, '/items/', '{\"name\":\"Foo\"}')\nassert cli.get('/items/?idx=g').text=='404 Not Found'\nassert cli.get('/items/?idx=g').status_code == 404\ntest_r(cli, '/idxl/?idx=1&idx=2', '[1, 2]')\nassert cli.get('/idxl/?idx=1&idx=g').status_code == 404\n\n\napp = FastHTML()\nrt = app.route\ncli = TestClient(app)\n@app.route(r'/static/{path:path}.jpg')\ndef index(path:str): return f'got {path}'\ncli.get('/static/sub/a.b.jpg').text\n\n'got sub/a.b'\n\n\n\napp.chk = 'foo'\n\n\n@app.get(\"/booly/\")\ndef _(coming:bool=True): return 'Coming' if coming else 'Not coming'\n\n@app.get(\"/datie/\")\ndef _(d:parsed_date): return d\n\n@app.get(\"/ua\")\nasync def _(user_agent:str): return user_agent\n\n@app.get(\"/hxtest\")\ndef _(htmx): return htmx.request\n\n@app.get(\"/hxtest2\")\ndef _(foo:HtmxHeaders, req): return foo.request\n\n@app.get(\"/app\")\ndef _(app): return app.chk\n\n@app.get(\"/app2\")\ndef _(foo:FastHTML): return foo.chk,HttpHeader(\"mykey\", \"myval\")\n\n@app.get(\"/app3\")\ndef _(foo:FastHTML): return HtmxResponseHeaders(location=\"http://example.org\")\n\n@app.get(\"/app4\")\ndef _(foo:FastHTML): return Redirect(\"http://example.org\")\n\n\ntest_r(cli, '/booly/?coming=true', 'Coming')\ntest_r(cli, '/booly/?coming=no', 'Not coming')\ndate_str = \"17th of May, 2024, 2p\"\ntest_r(cli, f'/datie/?d={date_str}', '2024-05-17 14:00:00')\ntest_r(cli, '/ua', 'FastHTML', headers={'User-Agent':'FastHTML'})\ntest_r(cli, '/hxtest' , '1', headers={'HX-Request':'1'})\ntest_r(cli, '/hxtest2', '1', headers={'HX-Request':'1'})\ntest_r(cli, '/app' , 'foo')\n\n\nr = cli.get('/app2', **hxhdr)\ntest_eq(r.text, 'foo')\ntest_eq(r.headers['mykey'], 'myval')\n\n\nr = cli.get('/app3')\ntest_eq(r.headers['HX-Location'], 'http://example.org')\n\n\nr = cli.get('/app4', follow_redirects=False)\ntest_eq(r.status_code, 303)\n\n\nr = cli.get('/app4', headers={'HX-Request':'1'})\ntest_eq(r.headers['HX-Redirect'], 'http://example.org')\n\n\n@rt\ndef meta():\n return ((Title('hi'),H1('hi')),\n (Meta(property='image'), Meta(property='site_name'))\n )\n\nt = cli.post('/meta').text\nassert re.search(r'<body>\\s*<h1>hi</h1>\\s*</body>', t)\nassert '<meta' in t\n\n\n@app.post('/profile/me')\ndef profile_update(username: str): return username\n\ntest_r(cli, '/profile/me', 'Alexis', 'post', data={'username' : 'Alexis'})\ntest_r(cli, '/profile/me', 'Missing required field: username', 'post', data={})\n\n\n# Example post request with parameter that has a default value\n@app.post('/pet/dog')\ndef pet_dog(dogname: str = None): return dogname\n\n# Working post request with optional parameter\ntest_r(cli, '/pet/dog', '', 'post', data={})\n\n\n@dataclass\nclass Bodie: a:int;b:str\n\n@rt(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\n@app.post(\"/bodied/\")\ndef bodied(data:dict): return data\n\nnt = namedtuple('Bodient', ['a','b'])\n\n@app.post(\"/bodient/\")\ndef bodient(data:nt): return asdict(data)\n\nclass BodieTD(TypedDict): a:int;b:str='foo'\n\n@app.post(\"/bodietd/\")\ndef bodient(data:BodieTD): return data\n\nclass Bodie2:\n a:int|None; b:str\n def __init__(self, a, b='foo'): store_attr()\n\n@rt(\"/bodie2/\", methods=['get','post'])\ndef bodie(d:Bodie2): return f\"a: {d.a}; b: {d.b}\"\n\n\nfrom fasthtml.xtend import Titled\n\n\nd = dict(a=1, b='foo')\n\ntest_r(cli, '/bodie/me', '{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}', 'post', data=dict(a=1, b='foo', nm='me'))\ntest_r(cli, '/bodied/', '{\"a\":\"1\",\"b\":\"foo\"}', 'post', data=d)\ntest_r(cli, '/bodie2/', 'a: 1; b: foo', 'post', data={'a':1})\ntest_r(cli, '/bodie2/?a=1&b=foo&nm=me', 'a: 1; b: foo')\ntest_r(cli, '/bodient/', '{\"a\":\"1\",\"b\":\"foo\"}', 'post', data=d)\ntest_r(cli, '/bodietd/', '{\"a\":1,\"b\":\"foo\"}', 'post', data=d)\n\n\n# Testing POST with Content-Type: application/json\n@app.post(\"/\")\ndef index(it: Bodie): return Titled(\"It worked!\", P(f\"{it.a}, {it.b}\"))\n\ns = json.dumps({\"b\": \"Lorem\", \"a\": 15})\nresponse = cli.post('/', headers={\"Content-Type\": \"application/json\"}, data=s).text\nassert \"<title>It worked!</title>\" in response and \"<p>15, Lorem</p>\" in response\n\n\n# Testing POST with Content-Type: application/json\n@app.post(\"/bodytext\")\ndef index(body): return body\n\nresponse = cli.post('/bodytext', headers={\"Content-Type\": \"application/json\"}, data=s).text\ntest_eq(response, '{\"b\": \"Lorem\", \"a\": 15}')\n\n\nfiles = [ ('files', ('file1.txt', b'content1')),\n ('files', ('file2.txt', b'content2')) ]\n\n\n@rt(\"/uploads\")\nasync def post(files:list[UploadFile]):\n return ','.join([(await file.read()).decode() for file in files])\n\nres = cli.post('/uploads', files=files)\nprint(res.status_code)\nprint(res.text)\n\n200\ncontent1,content2\n\n\n\nres = cli.post('/uploads', files=[files[0]])\nprint(res.status_code)\nprint(res.text)\n\n200\ncontent1\n\n\n\n@rt(\"/setsess\")\ndef get(sess, foo:str=''):\n now = datetime.now()\n sess['auth'] = str(now)\n return f'Set to {now}'\n\n@rt(\"/getsess\")\ndef get(sess): return f'Session time: {sess[\"auth\"]}'\n\nprint(cli.get('/setsess').text)\ntime.sleep(0.01)\n\ncli.get('/getsess').text\n\nSet to 2025-05-29 08:31:48.235262\n\n\n'Session time: 2025-05-29 08:31:48.235262'\n\n\n\n@rt(\"/sess-first\")\ndef post(sess, name: str):\n sess[\"name\"] = name\n return str(sess)\n\ncli.post('/sess-first', data={'name': 2})\n\n@rt(\"/getsess-all\")\ndef get(sess): return sess['name']\n\ntest_eq(cli.get('/getsess-all').text, '2')\n\n\n@rt(\"/upload\")\nasync def post(uf:UploadFile): return (await uf.read()).decode()\n\nwith open('../../CHANGELOG.md', 'rb') as f:\n print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])\n\n# Release notes\n\n\n\n@rt(\"/form-submit/{list_id}\")\ndef options(list_id: str):\n headers = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'POST',\n 'Access-Control-Allow-Headers': '*',\n }\n return Response(status_code=200, headers=headers)\n\n\nh = cli.options('/form-submit/2').headers\ntest_eq(h['Access-Control-Allow-Methods'], 'POST')\n\n\nfrom fasthtml.authmw import user_pwd_auth\n\n\ndef _not_found(req, exc): return Div('nope')\n\napp,cli,rt = get_cli(FastHTML(exception_handlers={404:_not_found}))\n\ntxt = cli.get('/').text\nassert '<div>nope</div>' in txt\nassert '<!doctype html>' in txt\n\n\napp,cli,rt = get_cli(FastHTML())\n\n@rt(\"/{name}/{age}\")\ndef get(name: str, age: int):\n return Titled(f\"Hello {name.title()}, age {age}\")\n\nassert '<title>Hello Uma, age 5</title>' in cli.get('/uma/5').text\nassert '404 Not Found' in cli.get('/uma/five').text\n\n\nauth = user_pwd_auth(testuser='spycraft')\napp,cli,rt = get_cli(FastHTML(middleware=[auth]))\n\n@rt(\"/locked\")\ndef get(auth): return 'Hello, ' + auth\n\ntest_eq(cli.get('/locked').text, 'not authenticated')\ntest_eq(cli.get('/locked', auth=(\"testuser\",\"spycraft\")).text, 'Hello, testuser')\n\n\nauth = user_pwd_auth(testuser='spycraft')\napp,cli,rt = get_cli(FastHTML(middleware=[auth]))\n\n@rt(\"/locked\")\ndef get(auth): return 'Hello, ' + auth\n\ntest_eq(cli.get('/locked').text, 'not authenticated')\ntest_eq(cli.get('/locked', auth=(\"testuser\",\"spycraft\")).text, 'Hello, testuser')", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#apirouter", "href": "api/core.html#apirouter", "title": "Core", "section": "APIRouter", "text": "APIRouter\n\nsource\n\nRouteFuncs\n\n RouteFuncs ()\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nAPIRouter\n\n APIRouter (prefix:str|None=None, body_wrap=<function noop_body>)\n\nAdd routes to an app\n\nar = APIRouter()\n\n\n@ar(\"/hi\")\ndef get(): return 'Hi there'\n@ar(\"/hi\")\ndef post(): return 'Postal'\n@ar\ndef ho(): return 'Ho ho'\n@ar(\"/hostie\")\ndef show_host(req): return req.headers['host']\n@ar\ndef yoyo(): return 'a yoyo'\n@ar\ndef index(): return \"home page\"\n\n@ar.ws(\"/ws\")\ndef ws(self, msg:str): return f\"Message text was: {msg}\"\n\n\napp,cli,_ = get_cli(FastHTML())\nar.to_app(app)\n\n\nassert str(yoyo) == '/yoyo'\n# ensure route functions are properly discoverable on `APIRouter` and `APIRouter.rt_funcs`\nassert ar.prefix == ''\nassert str(ar.rt_funcs.index) == '/'\nassert str(ar.index) == '/'\nwith ExceptionExpected(): ar.blah()\nwith ExceptionExpected(): ar.rt_funcs.blah()\n# ensure any route functions named using an HTTPMethod are not discoverable via `rt_funcs`\nassert \"get\" not in ar.rt_funcs._funcs.keys()\n\n\ntest_eq(cli.get('/hi').text, 'Hi there')\ntest_eq(cli.post('/hi').text, 'Postal')\ntest_eq(cli.get('/hostie').text, 'testserver')\ntest_eq(cli.post('/yoyo').text, 'a yoyo')\n\ntest_eq(cli.get('/ho').text, 'Ho ho')\ntest_eq(cli.post('/ho').text, 'Ho ho')\n\n\nwith cli.websocket_connect('/ws') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\nar2 = APIRouter(\"/products\")\n\n\n@ar2(\"/hi\")\ndef get(): return 'Hi there'\n@ar2(\"/hi\")\ndef post(): return 'Postal'\n@ar2\ndef ho(): return 'Ho ho'\n@ar2(\"/hostie\")\ndef show_host(req): return req.headers['host']\n@ar2\ndef yoyo(): return 'a yoyo'\n@ar2\ndef index(): return \"home page\"\n\n@ar2.ws(\"/ws\")\ndef ws(self, msg:str): return f\"Message text was: {msg}\"\n\n\napp,cli,_ = get_cli(FastHTML())\nar2.to_app(app)\n\n\nassert str(yoyo) == '/products/yoyo'\nassert ar2.prefix == '/products'\nassert str(ar2.rt_funcs.index) == '/products/'\nassert str(ar2.index) == '/products/'\nassert str(ar.index) == '/'\nwith ExceptionExpected(): ar2.blah()\nwith ExceptionExpected(): ar2.rt_funcs.blah()\nassert \"get\" not in ar2.rt_funcs._funcs.keys()\n\n\ntest_eq(cli.get('/products/hi').text, 'Hi there')\ntest_eq(cli.post('/products/hi').text, 'Postal')\ntest_eq(cli.get('/products/hostie').text, 'testserver')\ntest_eq(cli.post('/products/yoyo').text, 'a yoyo')\n\ntest_eq(cli.get('/products/ho').text, 'Ho ho')\ntest_eq(cli.post('/products/ho').text, 'Ho ho')\n\n\nwith cli.websocket_connect('/products/ws') as ws:\n ws.send_text('{\"msg\":\"Hi!\"}')\n data = ws.receive_text()\n assert data == 'Message text was: Hi!'\n\n\n@ar.get\ndef hi2(): return 'Hi there'\n@ar.get(\"/hi3\")\ndef _(): return 'Hi there'\n@ar.post(\"/post2\")\ndef _(): return 'Postal'\n\n@ar2.get\ndef hi2(): return 'Hi there'\n@ar2.get(\"/hi3\")\ndef _(): return 'Hi there'\n@ar2.post(\"/post2\")\ndef _(): return 'Postal'", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/core.html#extras", "href": "api/core.html#extras", "title": "Core", "section": "Extras", "text": "Extras\n\napp,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))\n\n\nsource\n\ncookie\n\n cookie (key:str, value='', max_age=None, expires=None, path='/',\n domain=None, secure=False, httponly=False, samesite='lax')\n\nCreate a ‘set-cookie’ HttpHeader\n\n@rt(\"/setcookie\")\ndef get(req): return cookie('now', datetime.now())\n\n@rt(\"/getcookie\")\ndef get(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\nprint(cli.get('/setcookie').text)\ntime.sleep(0.01)\ncli.get('/getcookie').text\n\n <!doctype html>\n <html>\n <head>\n <title>FastHTML page</title>\n <link rel=\"canonical\" href=\"http://testserver/setcookie\">\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<script src=\"https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js\"></script><script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script><script>\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };</script> </head>\n <body></body>\n </html>\n\n\n\n'Cookie was set at time 08:31:49.013668'\n\n\n\nsource\n\n\nreg_re_param\n\n reg_re_param (m, s)\n\n\nsource\n\n\nFastHTML.static_route_exts\n\n FastHTML.static_route_exts (prefix='/', static_path='.', exts='static')\n\nAdd a static route at URL path prefix with files from static_path and exts defined by reg_re_param()\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm|pdf\")\n\n@rt(r'/static/{path:path}{fn}.{ext:imgext}')\ndef get(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\ntest_r(cli, '/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/')\n\n\napp.static_route_exts()\nassert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text\n\n\nsource\n\n\nFastHTML.static_route\n\n FastHTML.static_route (ext='', prefix='/', static_path='.')\n\nAdd a static route at URL path prefix with files from static_path and single ext (including the ‘.’)\n\napp.static_route('.md', static_path='../..')\nassert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text\n\n\nsource\n\n\nMiddlewareBase\n\n MiddlewareBase ()\n\nInitialize self. See help(type(self)) for accurate signature.\n\nsource\n\n\nFtResponse\n\n FtResponse (content, status_code:int=200, headers=None, cls=<class\n 'starlette.responses.HTMLResponse'>,\n media_type:str|None=None,\n background:starlette.background.BackgroundTask|None=None)\n\nWrap an FT response with any Starlette Response\n\n@rt('/ftr')\ndef get():\n cts = Title('Foo'),H1('bar')\n return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'})\n\nr = cli.get('/ftr')\n\ntest_eq(r.status_code, 201)\ntest_eq(r.headers['location'], '/foo/1')\ntxt = r.text\nassert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n\nTest on a single background task:\n\ndef my_slow_task():\n print('Starting slow task') \n time.sleep(0.001)\n print('Finished slow task') \n\n@rt('/background')\ndef get():\n return P('BG Task'), BackgroundTask(my_slow_task)\n\nr = cli.get('/background')\n\ntest_eq(r.status_code, 200)\n\nStarting slow task\nFinished slow task\n\n\nTest multiple background tasks:\n\ndef increment(amount):\n amount = amount/1000\n print(f'Sleeping for {amount}s') \n time.sleep(amount)\n print(f'Slept for {amount}s')\n\n\n@rt\ndef backgrounds():\n tasks = BackgroundTasks()\n for i in range(3): tasks.add_task(increment, i)\n return P('BG Tasks'), tasks\n\nr = cli.get('/backgrounds')\ntest_eq(r.status_code, 200)\n\nSleeping for 0.0s\nSlept for 0.0s\nSleeping for 0.001s\nSlept for 0.001s\nSleeping for 0.002s\nSlept for 0.002s\n\n\n\n@rt\ndef backgrounds2():\n tasks = [BackgroundTask(increment,i) for i in range(3)]\n return P('BG Tasks'), *tasks\n\nr = cli.get('/backgrounds2')\ntest_eq(r.status_code, 200)\n\nSleeping for 0.0s\nSlept for 0.0s\nSleeping for 0.001s\nSlept for 0.001s\nSleeping for 0.002s\nSlept for 0.002s\n\n\n\n@rt\ndef backgrounds3():\n tasks = [BackgroundTask(increment,i) for i in range(3)]\n return {'status':'done'}, *tasks\n\nr = cli.get('/backgrounds3')\ntest_eq(r.status_code, 200)\nr.json()\n\nSleeping for 0.0s\nSlept for 0.0s\nSleeping for 0.001s\nSlept for 0.001s\nSleeping for 0.002s\nSlept for 0.002s\n\n\n{'status': 'done'}\n\n\n\nsource\n\n\nunqid\n\n unqid (seeded=False)\n\n\nsource\n\n\nsetup_ws\n\n setup_ws (app, f=<function noop>)\n\n\nsource\n\n\nFastHTML.devtools_json\n\n FastHTML.devtools_json (path=None, uuid=None)", "crumbs": [ "Home", "Source", "Core" ] }, { "objectID": "api/pico.html", "href": "api/pico.html", "title": "Pico.css components", "section": "", "text": "picocondlink is the class-conditional css link tag, and picolink is the regular tag.\n\nshow(picocondlink)\n\n\n\n\n\n\nsource\n\nset_pico_cls\n\n set_pico_cls ()\n\nRun this to make jupyter outputs styled with pico:\n\nset_pico_cls()\n\n\n\n\n\nsource\n\n\nCard\n\n Card (*c, header=None, footer=None, target_id=None, hx_vals=None,\n hx_target=None, id=None, cls=None, title=None, style=None,\n accesskey=None, contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Card, implemented as an Article with optional Header and Footer\n\nshow(Card('body', header=P('head'), footer=P('foot')))\n\n\n head\n\nbody\n foot\n\n\n\n\n\nsource\n\n\nGroup\n\n Group (*c, target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None, enterkeyhint=None,\n hidden=None, inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Group, implemented as a Fieldset with role ‘group’\n\nshow(Group(Input(), Button(\"Save\")))\n\n\n \n Save\n\n\n\n\nsource\n\n\nSearch\n\n Search (*c, target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Search, implemented as a Form with role ‘search’\n\nshow(Search(Input(type=\"search\"), Button(\"Search\")))\n\n\n \n Search\n\n\n\n\nsource\n\n\nGrid\n\n Grid (*c, cls='grid', target_id=None, hx_vals=None, hx_target=None,\n id=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None, enterkeyhint=None,\n hidden=None, inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Grid, implemented as child Divs in a Div with class ‘grid’\n\ncolors = [Input(type=\"color\", value=o) for o in ('#e66465', '#53d2c5', '#f6b73c')]\nshow(Grid(*colors))\n\n\n \n\n \n\n \n\n\n\n\n\nsource\n\n\nDialogX\n\n DialogX (*c, open=None, header=None, footer=None, id=None,\n target_id=None, hx_vals=None, hx_target=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None,\n hx_include=None, hx_select=None, hx_select_oob=None,\n hx_indicator=None, hx_push_url=None, hx_confirm=None,\n hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,\n hx_ext=None, hx_headers=None, hx_history=None,\n hx_history_elt=None, hx_inherit=None, hx_params=None,\n hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,\n hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None,\n hx_on_mouseout=None, hx_on_mouseover=None, hx_on_mouseup=None,\n hx_on_wheel=None, hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Dialog, with children inside a Card\n\nhdr = Div(Button(aria_label=\"Close\", rel=\"prev\"), P('confirm'))\nftr = Div(Button('Cancel', cls=\"secondary\"), Button('Confirm'))\nd = DialogX('thank you!', header=hdr, footer=ftr, open=None, id='dlgtest')\n# use js or htmx to display modal\n\n\nsource\n\n\nContainer\n\n Container (*args, target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs)\n\nA PicoCSS Container, implemented as a Main with class ‘container’\n\nsource\n\n\nPicoBusy\n\n PicoBusy ()", "crumbs": [ "Home", "Source", "Pico.css components" ] }, { "objectID": "api/oauth.html", "href": "api/oauth.html", "title": "OAuth", "section": "", "text": "See the docs page for an explanation of how to use this.\n\nfrom IPython.display import Markdown\n\n\nsource\n\nGoogleAppClient\n\n GoogleAppClient (client_id, client_secret, code=None, scope=None,\n project_id=None, **kwargs)\n\nA WebApplicationClient for Google oauth2\n\nsource\n\n\nGitHubAppClient\n\n GitHubAppClient (client_id, client_secret, code=None, scope=None,\n **kwargs)\n\nA WebApplicationClient for GitHub oauth2\n\nsource\n\n\nHuggingFaceClient\n\n HuggingFaceClient (client_id, client_secret, code=None, scope=None,\n state=None, **kwargs)\n\nA WebApplicationClient for HuggingFace oauth2\n\nsource\n\n\nDiscordAppClient\n\n DiscordAppClient (client_id, client_secret, is_user=False, perms=0,\n scope=None, **kwargs)\n\nA WebApplicationClient for Discord oauth2\n\nsource\n\n\nAuth0AppClient\n\n Auth0AppClient (domain, client_id, client_secret, code=None, scope=None,\n redirect_uri='', **kwargs)\n\nA WebApplicationClient for Auth0 OAuth2\n\n# cli = GoogleAppClient.from_file('client_secret.json')\n\n\nsource\n\n\nWebApplicationClient.login_link\n\n WebApplicationClient.login_link (redirect_uri, scope=None, state=None,\n **kwargs)\n\nGet a login link for this client\nGenerating a login link that sends the user to the OAuth provider is done with client.login_link().\nIt can sometimes be useful to pass state to the OAuth provider, so that when the user returns you can pick up where they left off. This can be done by passing the state parameter.\n\nfrom fasthtml.jupyter import *\n\n\nredir_path = '/redirect'\nport = 8000\n\n\napp,rt = fast_app()\nserver = JupyUvi(app, port=port)\n\n\n\n\n\n\nsource\n\n\nredir_url\n\n redir_url (request, redir_path, scheme=None)\n\nGet the redir url for the host in request\n\n@rt\ndef index(request):\n redir = redir_url(request, redir_path)\n return A('login', href=cli.login_link(redir), target='_blank')\n\n\nsource\n\n\n_AppClient.parse_response\n\n _AppClient.parse_response (code, redirect_uri)\n\nGet the token from the oauth2 server response\n\nsource\n\n\n_AppClient.get_info\n\n _AppClient.get_info (token=None)\n\nGet the info for authenticated user\n\nsource\n\n\n_AppClient.retr_info\n\n _AppClient.retr_info (code, redirect_uri)\n\nCombines parse_response and get_info\n\n@rt(redir_path)\ndef get(request, code:str):\n redir = redir_url(request, redir_path)\n info = cli.retr_info(code, redir)\n return P(f'Login successful for {info[\"name\"]}!')\n\n\n# HTMX()\n\n\nserver.stop()\n\n\nsource\n\n\n_AppClient.retr_id\n\n _AppClient.retr_id (code, redirect_uri)\n\nCall retr_info and then return id/subscriber value\nAfter logging in via the provider, the user will be redirected back to the supplied redirect URL. The request to this URL will contain a code parameter, which is used to get an access token and fetch the user’s profile information. See the explanation here for a worked example. You can either:\n\nUse client.retr_info(code) to get all the profile information, or\nUse client.retr_id(code) to get just the user’s ID.\n\nAfter either of these calls, you can also access the access token (used to revoke access, for example) with client.token[\"access_token\"].\n\nsource\n\n\nurl_match\n\n url_match (url, patterns=('^(localhost|127\\\\.0\\\\.0\\\\.1)(:\\\\d+)?$',))\n\n\nsource\n\n\nOAuth\n\n OAuth (app, cli, skip=None, redir_path='/redirect', error_path='/error',\n logout_path='/logout', login_path='/login', https=True,\n http_patterns=('^(localhost|127\\\\.0\\\\.0\\\\.1)(:\\\\d+)?$',))\n\nInitialize self. See help(type(self)) for accurate signature.\n\n\nGoogle helpers\n\nsource\n\n\nGoogleAppClient.consent_url\n\n GoogleAppClient.consent_url (proj=None)\n\nGet Google OAuth consent screen URL\n\nsource\n\n\nGoogleAppClient.consent_url\n\n GoogleAppClient.consent_url (proj=None)\n\nGet Google OAuth consent screen URL\n\nsource\n\n\nCredentials.update\n\n Credentials.update ()\n\nRefresh the credentials if they are expired, and return them\n\nsource\n\n\nCredentials.update\n\n Credentials.update ()\n\nRefresh the credentials if they are expired, and return them\n\nsource\n\n\nCredentials.save\n\n Credentials.save (fname)\n\nSave credentials to fname\n\nsource\n\n\nCredentials.save\n\n Credentials.save (fname)\n\nSave credentials to fname\n\nsource\n\n\nload_creds\n\n load_creds (fname)\n\nLoad credentials from fname\n\nsource\n\n\nGoogleAppClient.creds\n\n GoogleAppClient.creds ()\n\nCreate Credentials from the client, refreshing if needed\n\nsource\n\n\nGoogleAppClient.creds\n\n GoogleAppClient.creds ()\n\nCreate Credentials from the client, refreshing if needed", "crumbs": [ "Home", "Source", "OAuth" ] }, { "objectID": "api/svg.html", "href": "api/svg.html", "title": "SVG", "section": "", "text": "from nbdev.showdoc import show_doc\nYou can create SVGs directly from strings, for instance (as always, use NotStr or Safe to tell FastHTML to not escape the text):\nsvg = '<svg width=\"50\" height=\"50\"><circle cx=\"20\" cy=\"20\" r=\"15\" fill=\"red\"></circle></svg>'\nshow(NotStr(svg))\nYou can also use libraries such as fa6-icons.\nTo create and modify SVGs using a Python API, use the FT elements in fasthtml.svg, discussed below.\nNote: fasthtml.common does NOT automatically export SVG elements. To get access to them, you need to import fasthtml.svg like so\nsource", "crumbs": [ "Home", "Source", "SVG" ] }, { "objectID": "api/svg.html#basic-shapes", "href": "api/svg.html#basic-shapes", "title": "SVG", "section": "Basic shapes", "text": "Basic shapes\nWe’ll define a simple function to display SVG shapes in this notebook:\n\ndef demo(el, h=50, w=50): return show(Svg(h=h,w=w)(el))\n\n\nsource\n\nRect\n\n Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None,\n rx=None, ry=None, transform=None, opacity=None, clip=None,\n mask=None, filter=None, vector_effect=None, pointer_events=None,\n target_id=None, hx_vals=None, hx_target=None, id=None, cls=None,\n title=None, style=None, accesskey=None, contenteditable=None,\n dir=None, draggable=None, enterkeyhint=None, hidden=None,\n inert=None, inputmode=None, lang=None, popover=None,\n spellcheck=None, tabindex=None, translate=None, hx_get=None,\n hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,\n hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,\n hx_select=None, hx_select_oob=None, hx_indicator=None,\n hx_push_url=None, hx_confirm=None, hx_disable=None,\n hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,\n hx_headers=None, hx_history=None, hx_history_elt=None,\n hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG rect element\nAll our shapes just create regular FT elements. The only extra functionality provided by most of them is to add additional defined kwargs to improve auto-complete in IDEs and notebooks, and re-order parameters so that positional args can also be used to save a bit of typing, e.g:\n\ndemo(Rect(30, 30, fill='blue', rx=8, ry=8))\n\n\n\n\n\nsource\n\n\nCircle\n\n Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None,\n hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None,\n hx_on_input=None, hx_on_invalid=None, hx_on_reset=None,\n hx_on_select=None, hx_on_submit=None, hx_on_keydown=None,\n hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None,\n hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None,\n hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG circle element\n\ndemo(Circle(20, 25, 25, stroke='red', stroke_width=3))\n\n\n\n\n\nsource\n\n\nEllipse\n\n Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)\n\nA standard SVG ellipse element\n\ndemo(Ellipse(20, 10, 25, 25))\n\n\n\n\n\nsource\n\n\ntransformd\n\n transformd (translate=None, scale=None, rotate=None, skewX=None,\n skewY=None, matrix=None)\n\nCreate an SVG transform kwarg dict\n\nrot = transformd(rotate=(45, 25, 25))\nrot\n\n{'transform': 'rotate(45,25,25)'}\n\n\n\ndemo(Ellipse(20, 10, 25, 25, **rot))\n\n\n\n\n\nsource\n\n\nLine\n\n Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG line element\n\ndemo(Line(20, 30, w=3))\n\n\n\n\n\nsource\n\n\nPolyline\n\n Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None,\n filter=None, vector_effect=None, pointer_events=None,\n target_id=None, hx_vals=None, hx_target=None, id=None,\n cls=None, title=None, style=None, accesskey=None,\n contenteditable=None, dir=None, draggable=None,\n enterkeyhint=None, hidden=None, inert=None, inputmode=None,\n lang=None, popover=None, spellcheck=None, tabindex=None,\n translate=None, hx_get=None, hx_post=None, hx_put=None,\n hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,\n hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None,\n hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None,\n hx_on__abort=None, hx_on__after_on_load=None,\n hx_on__after_process_node=None, hx_on__after_request=None,\n hx_on__after_settle=None, hx_on__after_swap=None,\n hx_on__before_cleanup_element=None, hx_on__before_on_load=None,\n hx_on__before_process_node=None, hx_on__before_request=None,\n hx_on__before_swap=None, hx_on__before_send=None,\n hx_on__before_transition=None, hx_on__config_request=None,\n hx_on__confirm=None, hx_on__history_cache_error=None,\n hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)\n\nA standard SVG polyline element\n\ndemo(Polyline((0,0), (10,10), (20,0), (30,10), (40,0),\n fill='yellow', stroke='blue', stroke_width=2))\n\n\n\n\n\ndemo(Polyline(points='0,0 10,10 20,0 30,10 40,0', fill='purple', stroke_width=2))\n\n\n\n\n\nsource\n\n\nPolygon\n\n Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None,\n transform=None, opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None,\n hx_request=None, hx_sync=None, hx_validate=None,\n hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None,\n hx_on_focus=None, hx_on_input=None, hx_on_invalid=None,\n hx_on_reset=None, hx_on_select=None, hx_on_submit=None,\n hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None,\n hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None,\n hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None,\n hx_on__history_restore=None, hx_on__before_history_save=None,\n hx_on__load=None, hx_on__no_sse_source_error=None,\n hx_on__on_load_error=None, hx_on__oob_after_swap=None,\n hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None,\n hx_on__prompt=None, hx_on__pushed_into_history=None,\n hx_on__replaced_in_history=None, hx_on__response_error=None,\n hx_on__send_abort=None, hx_on__send_error=None,\n hx_on__sse_error=None, hx_on__sse_open=None,\n hx_on__swap_error=None, hx_on__target_error=None,\n hx_on__timeout=None, hx_on__validation_validate=None,\n hx_on__validation_failed=None, hx_on__validation_halted=None,\n hx_on__xhr_abort=None, hx_on__xhr_loadend=None,\n hx_on__xhr_loadstart=None, hx_on__xhr_progress=None)\n\nA standard SVG polygon element\n\ndemo(Polygon((25,5), (43.3,15), (43.3,35), (25,45), (6.7,35), (6.7,15), \n fill='lightblue', stroke='navy', stroke_width=2))\n\n\n\n\n\ndemo(Polygon(points='25,5 43.3,15 43.3,35 25,45 6.7,35 6.7,15',\n fill='lightgreen', stroke='darkgreen', stroke_width=2))\n\n\n\n\n\nsource\n\n\nText\n\n Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None,\n text_anchor=None, dominant_baseline=None, font_weight=None,\n font_style=None, text_decoration=None, transform=None,\n opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nA standard SVG text element\n\ndemo(Text(\"Hello!\", x=10, y=30))\n\nHello!", "crumbs": [ "Home", "Source", "SVG" ] }, { "objectID": "api/svg.html#paths", "href": "api/svg.html#paths", "title": "SVG", "section": "Paths", "text": "Paths\nPaths in SVGs are more complex, so we add a small (optional) fluent interface for constructing them:\n\nsource\n\nPathFT\n\n PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)\n\nA ‘Fast Tag’ structure, containing tag,children,and attrs\n\nsource\n\n\nPath\n\n Path (d='', fill=None, stroke=None, stroke_width=None, transform=None,\n opacity=None, clip=None, mask=None, filter=None,\n vector_effect=None, pointer_events=None, target_id=None,\n hx_vals=None, hx_target=None, id=None, cls=None, title=None,\n style=None, accesskey=None, contenteditable=None, dir=None,\n draggable=None, enterkeyhint=None, hidden=None, inert=None,\n inputmode=None, lang=None, popover=None, spellcheck=None,\n tabindex=None, translate=None, hx_get=None, hx_post=None,\n hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,\n hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,\n hx_select_oob=None, hx_indicator=None, hx_push_url=None,\n hx_confirm=None, hx_disable=None, hx_replace_url=None,\n hx_disabled_elt=None, hx_ext=None, hx_headers=None,\n hx_history=None, hx_history_elt=None, hx_inherit=None,\n hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,\n hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None,\n hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None,\n hx_on_invalid=None, hx_on_reset=None, hx_on_select=None,\n hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None,\n hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None,\n hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None,\n hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None,\n hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None,\n hx_on__after_on_load=None, hx_on__after_process_node=None,\n hx_on__after_request=None, hx_on__after_settle=None,\n hx_on__after_swap=None, hx_on__before_cleanup_element=None,\n hx_on__before_on_load=None, hx_on__before_process_node=None,\n hx_on__before_request=None, hx_on__before_swap=None,\n hx_on__before_send=None, hx_on__before_transition=None,\n hx_on__config_request=None, hx_on__confirm=None,\n hx_on__history_cache_error=None, hx_on__history_cache_miss=None,\n hx_on__history_cache_miss_error=None,\n hx_on__history_cache_miss_load=None, hx_on__history_restore=None,\n hx_on__before_history_save=None, hx_on__load=None,\n hx_on__no_sse_source_error=None, hx_on__on_load_error=None,\n hx_on__oob_after_swap=None, hx_on__oob_before_swap=None,\n hx_on__oob_error_no_target=None, hx_on__prompt=None,\n hx_on__pushed_into_history=None, hx_on__replaced_in_history=None,\n hx_on__response_error=None, hx_on__send_abort=None,\n hx_on__send_error=None, hx_on__sse_error=None,\n hx_on__sse_open=None, hx_on__swap_error=None,\n hx_on__target_error=None, hx_on__timeout=None,\n hx_on__validation_validate=None, hx_on__validation_failed=None,\n hx_on__validation_halted=None, hx_on__xhr_abort=None,\n hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None,\n hx_on__xhr_progress=None)\n\nCreate a standard path SVG element. This is a special object\nLet’s create a square shape, but using Path instead of Rect:\n\nM(10, 10): Move to starting point (10, 10)\nL(40, 10): Line to (40, 10) - top edge\nL(40, 40): Line to (40, 40) - right edge\nL(10, 40): Line to (10, 40) - bottom edge\nZ(): Close path - connects back to start\n\nM = Move to, L = Line to, Z = Close path\n\ndemo(Path(fill='none', stroke='purple', stroke_width=2\n ).M(10, 10).L(40, 10).L(40, 40).L(10, 40).Z())\n\n\n\n\nUsing curves we can create a spiral:\n\np = (Path(fill='none', stroke='purple', stroke_width=2)\n .M(25, 25)\n .C(25, 25, 20, 20, 30, 20)\n .C(40, 20, 40, 30, 30, 30)\n .C(20, 30, 20, 15, 35, 15)\n .C(50, 15, 50, 35, 25, 35)\n .C(0, 35, 0, 10, 40, 10)\n .C(80, 10, 80, 40, 25, 40))\ndemo(p, 50, 100)\n\n\n\n\nUsing arcs and curves we can create a map marker icon:\n\np = (Path(fill='red')\n .M(25,45)\n .C(25,45,10,35,10,25)\n .A(15,15,0,1,1,40,25)\n .C(40,35,25,45,25,45)\n .Z())\ndemo(p)\n\n\n\n\nBehind the scenes it’s just creating regular SVG path d attr – you can pass d in directly if you prefer.\n\nprint(p.d)\n\n M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z\n\n\n\ndemo(Path(d='M25 45 C25 45 10 35 10 25 A15 15 0 1 1 40 25 C40 35 25 45 25 45 Z'))\n\n\n\n\n\nsource\n\n\nPathFT.M\n\n PathFT.M (x, y)\n\nMove to.\n\nsource\n\n\nPathFT.L\n\n PathFT.L (x, y)\n\nLine to.\n\nsource\n\n\nPathFT.H\n\n PathFT.H (x)\n\nHorizontal line to.\n\nsource\n\n\nPathFT.V\n\n PathFT.V (y)\n\nVertical line to.\n\nsource\n\n\nPathFT.Z\n\n PathFT.Z ()\n\nClose path.\n\nsource\n\n\nPathFT.C\n\n PathFT.C (x1, y1, x2, y2, x, y)\n\nCubic Bézier curve.\n\nsource\n\n\nPathFT.S\n\n PathFT.S (x2, y2, x, y)\n\nSmooth cubic Bézier curve.\n\nsource\n\n\nPathFT.Q\n\n PathFT.Q (x1, y1, x, y)\n\nQuadratic Bézier curve.\n\nsource\n\n\nPathFT.T\n\n PathFT.T (x, y)\n\nSmooth quadratic Bézier curve.\n\nsource\n\n\nPathFT.A\n\n PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)\n\nElliptical Arc.", "crumbs": [ "Home", "Source", "SVG" ] }, { "objectID": "api/svg.html#htmx-helpers", "href": "api/svg.html#htmx-helpers", "title": "SVG", "section": "HTMX helpers", "text": "HTMX helpers\n\nsource\n\nSvgOob\n\n SvgOob (*args, **kwargs)\n\nWraps an SVG shape as required for an HTMX OOB swap\nWhen returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap it with SvgOob to have it appear correctly. (SvgOob is just a shortcut for Template(Svg(...)), which is the trick that makes SVG OOB swaps work.)\n\nsource\n\n\nSvgInb\n\n SvgInb (*args, **kwargs)\n\nWraps an SVG shape as required for an HTMX inband swap\nWhen returning an SVG shape in-band in HTMX, either have the calling element include hx_select='svg>*', or **svg_inb (which are two ways of saying the same thing), or wrap the response with SvgInb to have it appear correctly. (SvgInb is just a shortcut for the tuple (Svg(...), HtmxResponseHeaders(hx_reselect='svg>*')), which is the trick that makes SVG in-band swaps work.)", "crumbs": [ "Home", "Source", "SVG" ] }, { "objectID": "api/jupyter.html", "href": "api/jupyter.html", "title": "Jupyter compatibility", "section": "", "text": "from httpx import get, AsyncClient", "crumbs": [ "Home", "Source", "Jupyter compatibility" ] }, { "objectID": "api/jupyter.html#helper-functions", "href": "api/jupyter.html#helper-functions", "title": "Jupyter compatibility", "section": "Helper functions", "text": "Helper functions\n\nsource\n\nnb_serve\n\n nb_serve (app, log_level='error', port=8000, host='0.0.0.0', **kwargs)\n\nStart a Jupyter compatible uvicorn server with ASGI app on port with log_level\n\nsource\n\n\nnb_serve_async\n\n nb_serve_async (app, log_level='error', port=8000, host='0.0.0.0',\n **kwargs)\n\nAsync version of nb_serve\n\nsource\n\n\nis_port_free\n\n is_port_free (port, host='localhost')\n\nCheck if port is free on host\n\nsource\n\n\nwait_port_free\n\n wait_port_free (port, host='localhost', max_wait=3)\n\nWait for port to be free on host", "crumbs": [ "Home", "Source", "Jupyter compatibility" ] }, { "objectID": "api/jupyter.html#using-fasthtml-in-jupyter", "href": "api/jupyter.html#using-fasthtml-in-jupyter", "title": "Jupyter compatibility", "section": "Using FastHTML in Jupyter", "text": "Using FastHTML in Jupyter\n\nsource\n\nshow\n\n show (*s, iframe=False, height='auto', style=None)\n\nSame as fasthtml.components.show, but also adds htmx.process()\n\nsource\n\n\nrender_ft\n\n render_ft ()\n\n\nsource\n\n\nhtmx_config_port\n\n htmx_config_port (port=8000)\n\n\nsource\n\n\nJupyUvi\n\n JupyUvi (app, log_level='error', host='0.0.0.0', port=8000, start=True,\n **kwargs)\n\nStart and stop a Jupyter compatible uvicorn server with ASGI app on port with log_level\nCreating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook.\n\napp = FastHTML()\nrt = app.route\n\n@app.route\ndef index(): return 'hi'\n\nport = 8000\nserver = JupyUvi(app, port=port)\n\n\n\n\n\n\nget(f'http://localhost:{port}').text\n\n'hi'\n\n\nYou can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application.\n\nserver.stop()\n\n\napp = FastHTML()\nrt = app.route\n\n@app.route\nasync def index(): return 'hi'\n\nserver = JupyUvi(app, port=port, start=False)\nawait server.start_async()\n\n\n\n\n\n\nprint((await AsyncClient().get(f'http://localhost:{port}')).text)\n\nhi\n\n\n\nsource\n\n\nJupyUviAsync\n\n JupyUviAsync (app, log_level='error', host='0.0.0.0', port=8000,\n **kwargs)\n\nStart and stop an async Jupyter compatible uvicorn server with ASGI app on port with log_level\n\nserver = JupyUviAsync(app, port=port)\nawait server.start()\n\n\n\n\n\n\nasync with AsyncClient() as client:\n r = await client.get(f'http://localhost:{port}')\nprint(r.text)\n\nhi\n\n\n\nserver.stop()\n\n\n\nUsing a notebook as a web app\nYou can also run an HTMX web app directly in a notebook. To make this work, you have to add the default FastHTML headers to the DOM of the notebook with show(*def_hdrs()). Additionally, you might find it convenient to use auto_id mode, in which the ID of an FT object is automatically generated if not provided.\n\nfh_cfg['auto_id' ]=True\n\nAfter importing fasthtml.jupyter and calling render_ft(), FT components render directly in the notebook.\n\nshow(*def_hdrs())\nrender_ft()\n\n\n\n\n\n\n\n(c := Div('Cogito ergo sum'))\n\n\n\nCogito ergo sum\n\n\n\n\n\nHandlers are written just like a regular web app:\n\nserver = JupyUvi(app, port=port)\n\n\n\n\n\n\n@rt\ndef hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true')\n\nAll the usual hx_* attributes can be used:\n\nP('not loaded', hx_get=hoho, hx_trigger='load')\n\n\n\nnot loaded\n\n\n\n\n\nFT components can be used directly both as id values and as hx_target values.\n\n(c := Div(''))\n\n\n\n\n\n\n\n\n\n\n@rt\ndef foo(): return Div('foo bar')\nP('hi', hx_get=foo, hx_trigger='load', hx_target=c)\n\n\n\nhi\n\n\n\n\n\n\nserver.stop()\n\n\n\nRunning apps in an IFrame\nUsing an IFrame can be a good idea to get complete isolation of the styles and scripts in an app. The HTMX function creates an auto-sizing IFrame for a web app.\n\nsource\n\n\nHTMX\n\n HTMX (path='', app=None, host='localhost', port=8000, height='auto',\n link=False, iframe=True)\n\nAn iframe which displays the HTMX application in a notebook.\n\n@rt\ndef index():\n return Div(\n P(A('Click me', hx_get=update, hx_target='#result')),\n P(A('No me!', hx_get=update, hx_target='#result')),\n Div(id='result'))\n\n@rt\ndef update(): return Div(P('Hi!'),P('There!'))\n\n\nserver.start()\n\n\n# Run the notebook locally to see the HTMX iframe in action\nHTMX()\n\n \n\n\n\nserver.stop()\n\n\nsource\n\n\nws_client\n\n ws_client (app, nm='', host='localhost', port=8000, ws_connect='/ws',\n frame=True, link=True, **kwargs)", "crumbs": [ "Home", "Source", "Jupyter compatibility" ] }, { "objectID": "tutorials/e2e.html", "href": "tutorials/e2e.html", "title": "JS App Walkthrough", "section": "", "text": "You’ll need the following software to complete the tutorial, read on for specific installation instructions:\n\nPython\nA Python package manager such as pip (which normally comes with Python) or uv\nFastHTML\nWeb browser\nRailway.app account\n\nIf you haven’t worked with Python before, we recommend getting started with Miniconda.\nNote that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.\n\n\nFor Mac, Windows and Linux, enter:\npip install python-fasthtml", "crumbs": [ "Home", "Tutorials", "JS App Walkthrough" ] }, { "objectID": "tutorials/e2e.html#installation", "href": "tutorials/e2e.html#installation", "title": "JS App Walkthrough", "section": "", "text": "You’ll need the following software to complete the tutorial, read on for specific installation instructions:\n\nPython\nA Python package manager such as pip (which normally comes with Python) or uv\nFastHTML\nWeb browser\nRailway.app account\n\nIf you haven’t worked with Python before, we recommend getting started with Miniconda.\nNote that you will only need to follow the steps in the installation section once per environment. If you create a new repo, you won’t need to redo these.\n\n\nFor Mac, Windows and Linux, enter:\npip install python-fasthtml", "crumbs": [ "Home", "Tutorials", "JS App Walkthrough" ] }, { "objectID": "tutorials/e2e.html#first-steps", "href": "tutorials/e2e.html#first-steps", "title": "JS App Walkthrough", "section": "First steps", "text": "First steps\nBy the end of this section you’ll have your own FastHTML website with tests deployed to railway.app.\n\nCreate a hello world\nCreate a new folder to organize all the files for your project. Inside this folder, create a file called main.py and add the following code to it:\n\n\nmain.py\n\nfrom fasthtml.common import *\n\napp = FastHTML()\nrt = app.route\n\n@rt('/')\ndef get():\n return 'Hello, world!'\n\nserve()\n\nFinally, run python main.py in your terminal and open your browser to the ‘Link’ that appears.\n\n\nQuickDraw: A FastHTML Adventure 🎨✨\nThe end result of this tutorial will be QuickDraw, a real-time collaborative drawing app using FastHTML. Here is what the final site will look like:\n\n\n\nQuickDraw\n\n\n\nDrawing Rooms\nDrawing rooms are the core concept of our application. Each room represents a separate drawing space where a user can let their inner Picasso shine. Here’s a detailed breakdown:\n\nRoom Creation and Storage\n\n\n\nmain.py\n\ndb = database('data/drawapp.db')\nrooms = db.t.rooms\nif rooms not in db.t:\n rooms.create(id=int, name=str, created_at=str, pk='id')\nRoom = rooms.dataclass()\n\n@patch\ndef __ft__(self:Room):\n return Li(A(self.name, href=f\"/rooms/{self.id}\"))\n\nOr you can use our fast_app function to create a FastHTML app with a SQLite database and dataclass in one line:\n\n\nmain.py\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')\n\nWe are specifying a render function to convert our dataclass into HTML, which is the same as extending the __ft__ method from the patch decorator we used before. We will use this method for the rest of the tutorial since it is a lot cleaner and easier to read.\n\nWe’re using a SQLite database (via FastLite) to store our rooms.\nEach room has an id (integer), a name (string), and a created_at timestamp (string).\nThe Room dataclass is automatically generated based on this structure.\n\n\nCreating a room\n\n\n\nmain.py\n\n@rt(\"/\")\ndef get():\n # The 'Input' id defaults to the same as the name, so you can omit it if you wish\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"DrawCollab\", \n H1(\"DrawCollab\"),\n create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n\nWhen a user submits the “Create Room” form, this route is called.\nIt creates a new Room object, sets the creation time, and inserts it into the database.\nIt returns an HTML list item with a link to the new room, which is dynamically added to the room list on the homepage thanks to HTMX.\n\n\nLet’s give our rooms shape\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n return Titled(f\"Room: {room.name}\", H1(f\"Welcome to {room.name}\"), A(Button(\"Leave Room\"), href=\"/\"))\n\n\nThis route renders the interface for a specific room.\nIt fetches the room from the database and renders a title, heading, and paragraph.\n\nHere is the full code so far:\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom datetime import datetime\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id')\n\n@rt(\"/\")\ndef get():\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"DrawCollab\", create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n return Titled(f\"Room: {room.name}\", H1(f\"Welcome to {room.name}\"), A(Button(\"Leave Room\"), href=\"/\"))\n\nserve()\n\nNow run python main.py in your terminal and open your browser to the ‘Link’ that appears. You should see a page with a form to create a new room and a list of existing rooms.\n\n\nThe Canvas - Let’s Get Drawing! 🖌️\nTime to add the actual drawing functionality. We’ll use Fabric.js for this:\n\n\nmain.py\n\n# ... (keep the previous imports and database setup)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#3CDD8C\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n \n js = \"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#3CDD8C';\n canvas.freeDrawingBrush.width = 10;\n \n document.getElementById('color-picker').onchange = function() {\n canvas.freeDrawingBrush.color = this.value;\n };\n \n document.getElementById('brush-size').oninput = function() {\n canvas.freeDrawingBrush.width = parseInt(this.value, 10);\n };\n \"\"\"\n \n return Titled(f\"Room: {room.name}\",\n A(Button(\"Leave Room\"), href=\"/\"),\n canvas,\n Div(color_picker, brush_size),\n Script(src=\"https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js\"),\n Script(js))\n\n# ... (keep the serve() part)\n\nNow we’ve got a drawing canvas! FastHTML makes it easy to include external libraries and add custom JavaScript.\n\n\nSaving and Loading Canvases 💾\nNow that we have a working drawing canvas, let’s add the ability to save and load drawings. We’ll modify our database schema to include a canvas_data field, and add new routes for saving and loading canvas data. Here’s how we’ll update our code:\n\nModify the database schema:\n\n\n\nmain.py\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')\n\n\nAdd a save button that grabs the canvas’ state and sends it to the server:\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#3CDD8C\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n save_button = Button(\"Save Canvas\", id=\"save-canvas\", hx_post=f\"/rooms/{id}/save\", hx_vals=\"js:{canvas_data: JSON.stringify(canvas.toJSON())}\")\n # ... (rest of the function remains the same)\n\n\nAdd routes for saving and loading canvas data:\n\n\n\nmain.py\n\n@rt(\"/rooms/{id}/save\")\nasync def post(id:int, canvas_data:str):\n rooms.update({'canvas_data': canvas_data}, id)\n return \"Canvas saved successfully\"\n\n@rt(\"/rooms/{id}/load\")\nasync def get(id:int):\n room = rooms[id]\n return room.canvas_data if room.canvas_data else \"{}\"\n\n\nUpdate the JavaScript to load existing canvas data:\n\n\n\nmain.py\n\njs = f\"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#3CDD8C';\n canvas.freeDrawingBrush.width = 10;\n // Load existing canvas data\n fetch(`/rooms/{id}/load`)\n .then(response => response.json())\n .then(data => {{\n if (data && Object.keys(data).length > 0) {{\n canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));\n }}\n }});\n \n // ... (rest of the JavaScript remains the same)\n\"\"\"\n\nWith these changes, users can now save their drawings and load them when they return to the room. The canvas data is stored as a JSON string in the database, allowing for easy serialization and deserialization. Try it out! Create a new room, make a drawing, save it, and then reload the page. You should see your drawing reappear, ready for further editing.\nHere is the completed code:\n\n\nmain.py\n\nfrom fasthtml.common import *\nfrom datetime import datetime\n\ndef render(room):\n return Li(A(room.name, href=f\"/rooms/{room.id}\"))\n\napp,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id')\n\n@rt(\"/\")\ndef get():\n create_room = Form(Input(id=\"name\", name=\"name\", placeholder=\"New Room Name\"),\n Button(\"Create Room\"),\n hx_post=\"/rooms\", hx_target=\"#rooms-list\", hx_swap=\"afterbegin\")\n rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')\n return Titled(\"QuickDraw\", \n create_room, rooms_list)\n\n@rt(\"/rooms\")\nasync def post(room:Room):\n room.created_at = datetime.now().isoformat()\n return rooms.insert(room)\n\n@rt(\"/rooms/{id}\")\nasync def get(id:int):\n room = rooms[id]\n canvas = Canvas(id=\"canvas\", width=\"800\", height=\"600\")\n color_picker = Input(type=\"color\", id=\"color-picker\", value=\"#000000\")\n brush_size = Input(type=\"range\", id=\"brush-size\", min=\"1\", max=\"50\", value=\"10\")\n save_button = Button(\"Save Canvas\", id=\"save-canvas\", hx_post=f\"/rooms/{id}/save\", hx_vals=\"js:{canvas_data: JSON.stringify(canvas.toJSON())}\")\n\n js = f\"\"\"\n var canvas = new fabric.Canvas('canvas');\n canvas.isDrawingMode = true;\n canvas.freeDrawingBrush.color = '#000000';\n canvas.freeDrawingBrush.width = 10;\n\n // Load existing canvas data\n fetch(`/rooms/{id}/load`)\n .then(response => response.json())\n .then(data => {{\n if (data && Object.keys(data).length > 0) {{\n canvas.loadFromJSON(data, canvas.renderAll.bind(canvas));\n }}\n }});\n \n document.getElementById('color-picker').onchange = function() {{\n canvas.freeDrawingBrush.color = this.value;\n }};\n \n document.getElementById('brush-size').oninput = function() {{\n canvas.freeDrawingBrush.width = parseInt(this.value, 10);\n }};\n \"\"\"\n \n return Titled(f\"Room: {room.name}\",\n A(Button(\"Leave Room\"), href=\"/\"),\n canvas,\n Div(color_picker, brush_size, save_button),\n Script(src=\"https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js\"),\n Script(js))\n\n@rt(\"/rooms/{id}/save\")\nasync def post(id:int, canvas_data:str):\n rooms.update({'canvas_data': canvas_data}, id)\n return \"Canvas saved successfully\"\n\n@rt(\"/rooms/{id}/load\")\nasync def get(id:int):\n room = rooms[id]\n return room.canvas_data if room.canvas_data else \"{}\"\n\nserve()\n\n\n\n\nDeploying to Railway\nYou can deploy your website to a number of hosting providers, for this tutorial we’ll be using Railway. To get started, make sure you create an account and install the Railway CLI. Once installed, make sure to run railway login to log in to your account.\nTo make deploying your website as easy as possible, FastHTMl comes with a built in CLI tool that will handle most of the deployment process for you. To deploy your website, run the following command in your terminal in the root directory of your project:\nfh_railway_deploy quickdraw\n\n\n\n\n\n\nNote\n\n\n\nYour app must be located in a main.py file for this to work.\n\n\n\n\nConclusion: You’re a FastHTML Artist Now! 🎨🚀\nCongratulations! You’ve just built a sleek, interactive web application using FastHTML. Let’s recap what we’ve learned:\n\nFastHTML allows you to create dynamic web apps with minimal code.\nWe used FastHTML’s routing system to handle different pages and actions.\nWe integrated with a SQLite database to store room information and canvas data.\nWe utilized Fabric.js to create an interactive drawing canvas.\nWe implemented features like color picking, brush size adjustment, and canvas saving.\nWe used HTMX for seamless, partial page updates without full reloads.\nWe learned how to deploy our FastHTML application to Railway for easy hosting.\n\nYou’ve taken your first steps into the world of FastHTML development. From here, the possibilities are endless! You could enhance the drawing app further by adding features like:\n\nImplementing different drawing tools (e.g., shapes, text)\nAdding user authentication\nCreating a gallery of saved drawings\nImplementing real-time collaborative drawing using WebSockets\n\nWhatever you choose to build next, FastHTML has got your back. Now go forth and create something awesome! Happy coding! 🖼️🚀", "crumbs": [ "Home", "Tutorials", "JS App Walkthrough" ] }, { "objectID": "tutorials/best_practice.html", "href": "tutorials/best_practice.html", "title": "FastHTML Best Practices", "section": "", "text": "FastHTML applications are different to applications using FastAPI/react, Django, etc. Don’t assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that’s both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss:", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#database-table-creation---use-dataclasses-and-idempotent-patterns", "href": "tutorials/best_practice.html#database-table-creation---use-dataclasses-and-idempotent-patterns", "title": "FastHTML Best Practices", "section": "Database Table Creation - Use dataclasses and idempotent patterns", "text": "Database Table Creation - Use dataclasses and idempotent patterns\nBefore:\ntodos = db.t.todos\nif not todos.exists():\ntodos.create(id=int, task=str, completed=bool, created=str, pk='id')\nAfter:\nclass Todo: id:int; task:str; completed:bool; created:str\ntodos = db.create(Todo)\nFastLite’s create() is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The id field is automatically the primary key.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#route-naming-conventions---let-function-names-define-routes", "href": "tutorials/best_practice.html#route-naming-conventions---let-function-names-define-routes", "title": "FastHTML Best Practices", "section": "Route Naming Conventions - Let function names define routes", "text": "Route Naming Conventions - Let function names define routes\nBefore:\n@rt(\"/\")\ndef get(): return Titled(\"Todo List\", ...)\n\n@rt(\"/add\")\ndef post(task: str): ...\nAfter:\n@rt\ndef index(): return Titled(\"Todo List\", ...) # Special name for \"/\"\n@rt\ndef add(task: str): ... # Function name becomes route\nUse @rt without arguments and let the function name define the route. The special name index maps to /.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#query-parameters-over-path-parameters---cleaner-url-patterns", "href": "tutorials/best_practice.html#query-parameters-over-path-parameters---cleaner-url-patterns", "title": "FastHTML Best Practices", "section": "Query Parameters over Path Parameters - Cleaner URL patterns", "text": "Query Parameters over Path Parameters - Cleaner URL patterns\nBefore:\n@rt(\"/toggle/{todo_id}\")\ndef post(todo_id: int): ...\n# URL: /toggle/123\nAfter:\n@rt\ndef toggle(id: int): ...\n# URL: /toggle?id=123\nQuery parameters are more idiomatic in FastHTML and avoid duplicating param names in the path.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#leverage-return-values---chain-operations-in-one-line", "href": "tutorials/best_practice.html#leverage-return-values---chain-operations-in-one-line", "title": "FastHTML Best Practices", "section": "Leverage Return Values - Chain operations in one line", "text": "Leverage Return Values - Chain operations in one line\n\nBefore:\n@rt\ndef add(task: str):\n new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat())\n return todo_item(todos[new_todo])\n\n@rt\ndef toggle(id: int):\n todo = todos[id]\n todos.update(completed=not todo.completed, id=id)\n return todo_item(todos[id])\nAfter:\n@rt\ndef add(task: str):\n return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat()))\n\n@rt\ndef toggle(id: int):\n return todo_item(todos.update(completed=not todos[id].completed, id=id))\nBoth insert() and update() return the affected object, enabling functional chaining.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#use-.to-for-url-generation---type-safe-route-references", "href": "tutorials/best_practice.html#use-.to-for-url-generation---type-safe-route-references", "title": "FastHTML Best Practices", "section": "Use .to() for URL Generation - Type-safe route references", "text": "Use .to() for URL Generation - Type-safe route references\nBefore:\nhx_post=f\"/toggle?id={todo.id}\"\nAfter:\nhx_post=toggle.to(id=todo.id)\nThe .to() method generates URLs with type safety and is refactoring-friendly.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#built-in-css-frameworks---picocss-comes-free-with-fast_app", "href": "tutorials/best_practice.html#built-in-css-frameworks---picocss-comes-free-with-fast_app", "title": "FastHTML Best Practices", "section": "Built-in CSS Frameworks - PicoCSS comes free with fast_app()", "text": "Built-in CSS Frameworks - PicoCSS comes free with fast_app()\nBefore:\nstyle = Style(\"\"\"\n.todo-container { max-width: 600px; margin: 0 auto; padding: 20px; }\n/* ... many more lines ... */\n\"\"\")\nAfter:\n# Just use semantic HTML - Pico styles it automatically\nContainer(...), Article(...), Card(...), Group(...)\nfast_app() includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#smart-defaults---titled-creates-container-serve-handles-main", "href": "tutorials/best_practice.html#smart-defaults---titled-creates-container-serve-handles-main", "title": "FastHTML Best Practices", "section": "Smart Defaults - Titled creates Container, serve() handles main", "text": "Smart Defaults - Titled creates Container, serve() handles main\nBefore:\nreturn Titled(\"Todo List\", Container(...))\n\nif __name__ == \"__main__\":\n serve()\nAfter:\nreturn Titled(\"Todo List\", ...) # Container is automatic\n\nserve() # No need for if __name__ guard\nTitled already wraps content in a Container, and serve() handles the main check internally.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#fasthtml-handles-iterables---no-unpacking-needed-for-generators", "href": "tutorials/best_practice.html#fasthtml-handles-iterables---no-unpacking-needed-for-generators", "title": "FastHTML Best Practices", "section": "FastHTML Handles Iterables - No unpacking needed for generators", "text": "FastHTML Handles Iterables - No unpacking needed for generators\nBefore:\nSection(*[todo_item(todo) for todo in all_todos], id=\"todo-list\")\nAfter:\nSection(map(todo_item, all_todos), id=\"todo-list\")\nFastHTML components accept iterables directly - no need to unpack with *.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#functional-patterns---use-map-over-list-comprehensions", "href": "tutorials/best_practice.html#functional-patterns---use-map-over-list-comprehensions", "title": "FastHTML Best Practices", "section": "Functional Patterns - Use map() over list comprehensions", "text": "Functional Patterns - Use map() over list comprehensions\nList comprehensions are great, but map() is often cleaner for simple transformations, especially when combined with FastHTML’s iterable handling.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#minimal-code---remove-comments-and-unnecessary-returns", "href": "tutorials/best_practice.html#minimal-code---remove-comments-and-unnecessary-returns", "title": "FastHTML Best Practices", "section": "Minimal Code - Remove comments and unnecessary returns", "text": "Minimal Code - Remove comments and unnecessary returns\nBefore:\n@rt\ndef delete(id: int):\n # Delete from database\n todos.delete(id)\n # Return empty response\n return \"\"\nAfter:\n@rt\ndef delete(id: int): todos.delete(id)\n\nSkip comments when code is self-documenting\nDon’t return empty strings - None is returned by default\nUse a single line for a single idea.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#use-post-for-all-mutations", "href": "tutorials/best_practice.html#use-post-for-all-mutations", "title": "FastHTML Best Practices", "section": "Use POST for All Mutations", "text": "Use POST for All Mutations\nBefore:\nhx_delete=f\"/delete?id={todo.id}\"\nAfter:\nhx_post=delete.to(id=todo.id)\nFastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler.", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/best_practice.html#modern-htmx-event-syntax", "href": "tutorials/best_practice.html#modern-htmx-event-syntax", "title": "FastHTML Best Practices", "section": "Modern HTMX Event Syntax", "text": "Modern HTMX Event Syntax\nBefore:\nhx_on=\"htmx:afterRequest: this.reset()\"\nAfter:\nhx_on__after_request=\"this.reset()\"\nThis works because:\n\nhx-on=\"event: code\" is deprecated; hx-on-event=\"code\" is preferred\nFastHTML converts _ to - (so hx_on__after_request becomes hx-on--after-request)\n:: in HTMX can be used as a shortcut for :htmx:.\nHTMX natively accepts - instead of : (so -htmx- works like :htmx:)\nHTMX accepts e.g after-request as an alternative to camelCase afterRequest", "crumbs": [ "Home", "Tutorials", "FastHTML Best Practices" ] }, { "objectID": "tutorials/by_example.html", "href": "tutorials/by_example.html", "title": "FastHTML By Example", "section": "", "text": "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.\nLet’s get started.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#fasthtml-basics", "href": "tutorials/by_example.html#fasthtml-basics", "title": "FastHTML By Example", "section": "FastHTML Basics", "text": "FastHTML Basics\nFastHTML 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.\nThe core usage of FastHTML is to define routes, and then to define what to do at each route. This is similar to the FastAPI 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.\nHere’s a simple FastHTML app that returns a “Hello, World” message:\n\nfrom fasthtml.common import FastHTML, serve\n\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n return \"<h1>Hello, World</h1>\"\n\nserve()\n\nTo run this app, place it in a file, say app.py, and then run it with python app.py.\nINFO: Will watch for changes in these directories: ['/home/jonathan/fasthtml-example']\nINFO: Uvicorn running on http://127.0.0.1:5001 (Press CTRL+C to quit)\nINFO: Started reloader process [871942] using WatchFiles\nINFO: Started server process [871945]\nINFO: Waiting for application startup.\nINFO: Application startup complete.\nIf 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.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#constructing-html", "href": "tutorials/by_example.html#constructing-html", "title": "FastHTML By Example", "section": "Constructing HTML", "text": "Constructing HTML\nNotice 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 has all we need for constructing HTML from Python, and FastHTML includes all the tags you need to get started. For example:\n\nfrom fasthtml.common import *\npage = Html(\n Head(Title('Some page')),\n Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src=\"https://placehold.co/200\"), cls='myclass')))\nprint(to_xml(page))\n\n<!doctype html></!doctype>\n\n<html>\n <head>\n <title>Some page</title>\n </head>\n <body>\n <div class=\"myclass\">\nSome text, \n <a href=\"https://example.com\">A link</a>\n <img src=\"https://placehold.co/200\">\n </div>\n </body>\n</html>\n\n\n\n\nshow(page)\n\n\n\n\n \n Some page\n \n \n \nSome text, \n A link\n \n \n \n\n\n\nIf that import * worries you, you can always import only the tags you need.\nFastHTML 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:\n\nfrom fasthtml.common import *\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n page = Html(\n Head(Title('Some page')),\n Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src=\"https://placehold.co/200\"), cls='myclass')))\n return page\n\nserve()\n\nThis will render the HTML in the browser.\nFor 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.\n\n\n\n\n\n\nLive Reloading\n\n\n\nYou can also enable live reloading so you don’t have to manually refresh your browser to view updates.\n\n\nYou can also use Starlette’s TestClient to try it out in a notebook:\n\nfrom starlette.testclient import TestClient\nclient = TestClient(app)\nr = client.get(\"/\")\nprint(r.text)\n\n<html>\n <head><title>Some page</title>\n</head>\n <body><div class=\"myclass\">\nSome text, \n <a href=\"https://example.com\">A link</a>\n <img src=\"https://placehold.co/200\">\n</div>\n</body>\n</html>\n\n\n\nFastHTML 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 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:\n\napp = FastHTML()\n\n@app.get(\"/\")\ndef home():\n return Title(\"Page Demo\"), Div(H1('Hello, World'), P('Some text'), P('Some more text'))\n\nclient = TestClient(app)\nprint(client.get(\"/\").text)\n\n<!doctype html></!doctype>\n\n<html>\n <head>\n <title>Page Demo</title>\n <meta charset=\"utf-8\"></meta>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\"></meta>\n <script src=\"https://unpkg.com/htmx.org@next/dist/htmx.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js\"></script>\n </head>\n <body>\n<div>\n <h1>Hello, World</h1>\n <p>Some text</p>\n <p>Some more text</p>\n</div>\n </body>\n</html>\n\n\n\nWe’ll use this pattern often in the examples to follow.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#defining-routes", "href": "tutorials/by_example.html#defining-routes", "title": "FastHTML By Example", "section": "Defining Routes", "text": "Defining Routes\nThe 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:\n@app.route(\"/\", methods='get')\ndef home():\n return H1('Hello, World')\n\n@app.route(\"/\", methods=['post', 'put'])\ndef post_or_put():\n return \"got a POST or PUT request\"\nThis 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”.\n\n\n\n\n\n\nTest the POST request\n\n\n\nYou 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.\n\n\nThere are a few other ways you can specify the route+method - FastHTML has .get, .post, etc. as shorthand for route(..., methods=['get']), etc.\n\n@app.get(\"/\")\ndef my_function():\n return \"Hello World from a GET request\"\n\nOr you can use the @rt decorator without a method but specify the method with the name of the function. For example:\n\nrt = app.route\n\n@rt(\"/\")\ndef post():\n return \"Hello World from a POST request\"\n\n\nclient.post(\"/\").text\n\n'Hello World from a POST request'\n\n\nYou’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:\n\n@app.get@rt\n\n\n\n@app.get(\"/greet/{nm}\")\ndef greet(nm:str):\n return f\"Good day to you, {nm}!\"\n\nclient.get(\"/greet/Dave\").text\n\n'Good day to you, Dave!'\n\n\n\n\n\n@rt(\"/greet/{nm}\")\ndef get(nm:str):\n return f\"Good day to you, {nm}!\"\n\nclient.get(\"/greet/Dave\").text\n\n'Good day to you, Dave!'\n\n\n\n\n\nMore on this in the More on Routing and Request Parameters section, which goes deeper into the different ways to get information from a request.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#styling-basics", "href": "tutorials/by_example.html#styling-basics", "title": "FastHTML By Example", "section": "Styling Basics", "text": "Styling Basics\nPlain 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. A common way to add CSS files to web pages is to use a <link> tag inside your HTML header, like this:\n<header>\n ...\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\">\n</header>\nFor convenience, FastHTML already defines a Pico component for you with picolink:\n\nprint(to_xml(picolink))\n\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\">\n\n<style>:root { --pico-font-size: 100%; }</style>\n\n\n\n\n\n\n\n\n\nNote\n\n\n\npicolink 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.\n\n\nSince 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:\n\nfrom fasthtml.common import *\n1css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}')\n2app = FastHTML(hdrs=(picolink, css))\n\n@app.route(\"/\")\ndef get():\n return (Title(\"Hello World\"), \n3 Main(H1('Hello, World'), cls=\"container\"))\n\n\n1\n\nCustom styling to override the pico defaults\n\n2\n\nDefine shared headers for all pages\n\n3\n\nAs per the pico docs, we put all of our content inside a <main> tag with a class of container:\n\n\n\n\n\n\n\n\n\n\nReturning Tuples\n\n\n\nWe’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).\n\n\nYou can check out the Pico 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.\nIf you want to override the default styles 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.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#web-page---web-app", "href": "tutorials/by_example.html#web-page---web-app", "title": "FastHTML By Example", "section": "Web Page -> Web App", "text": "Web Page -> Web App\nShowing 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:\n\napp = FastHTML()\nmessages = [\"This is a message, which will get rendered as a paragraph\"]\n\n@app.get(\"/\")\ndef home():\n return Main(H1('Messages'), \n *[P(msg) for msg in messages],\n A(\"Link to Page 2 (to add messages)\", href=\"/page2\"))\n\n@app.get(\"/page2\")\ndef page2():\n return Main(P(\"Add a message with the form below:\"),\n Form(Input(type=\"text\", name=\"data\"),\n Button(\"Submit\"),\n action=\"/\", method=\"post\"))\n\n@app.post(\"/\")\ndef add_message(data:str):\n messages.append(data)\n return home()\n\nWe 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.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#htmx", "href": "tutorials/by_example.html#htmx", "title": "FastHTML By Example", "section": "HTMX", "text": "HTMX\nHTMX 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.\nIt 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:\n\napp = FastHTML()\n\ncount = 0\n\n@app.get(\"/\")\ndef home():\n return Title(\"Count Demo\"), Main(\n H1(\"Count Demo\"),\n P(f\"Count is set to {count}\", id=\"count\"),\n Button(\"Increment\", hx_post=\"/increment\", hx_target=\"#count\", hx_swap=\"innerHTML\")\n )\n\n@app.post(\"/increment\")\ndef increment():\n print(\"incrementing\")\n global count\n count += 1\n return f\"Count is set to {count}\"\n\nThe 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:\n\ninnerHTML: Replace the target element’s content with the result.\nouterHTML: Replace the target element with the result.\nbeforebegin: Insert the result before the target element.\nbeforeend: Insert the result inside the target element, after its last child.\nafterbegin: Insert the result inside the target element, before its first child.\nafterend: Insert the result after the target element.\n\nYou can also use an hx_swap of delete to delete the target element regardless of response, or of none to do nothing.\nBy 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 for more.\nThis 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.\n\nReplacing Elements Besides the Target\nSometimes 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.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#full-example-1---todo-app", "href": "tutorials/by_example.html#full-example-1---todo-app", "title": "FastHTML By Example", "section": "Full Example #1 - ToDo App", "text": "Full Example #1 - ToDo App\nThe 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:\n\n\n\n\nimage.png\n\n\nWe’ve made a number of variants of this app - so in addition to the version shown in the video you can browse this series of examples with increasing complexity, the heavily-commented “idiomatic” version here, and the example linked from the FastHTML homepage.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#full-example-2---image-generation-app", "href": "tutorials/by_example.html#full-example-2---image-generation-app", "title": "FastHTML By Example", "section": "Full Example #2 - Image Generation App", "text": "Full Example #2 - Image Generation App\nLet’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 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:\n# Main page\n@app.get(\"/\")\ndef get():\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_list = Div(id='gen-list')\n return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')\nSubmitting 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:\ndef generation_preview(id):\n if os.path.exists(f\"gens/{id}.png\"):\n return Div(Img(src=f\"/gens/{id}.png\"), id=f'gen-{id}')\n else:\n return Div(\"Generating...\", id=f'gen-{id}', \n hx_post=f\"/generations/{id}\",\n hx_trigger='every 1s', hx_swap='outerHTML')\n \n@app.post(\"/generations/{id}\")\ndef get(id:int): return generation_preview(id)\n\n@app.post(\"/\")\ndef post(prompt:str):\n id = len(generations)\n generate_and_save(prompt, id)\n generations.append(prompt)\n clear_input = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\", hx_swap_oob='true')\n return generation_preview(id), clear_input\n\n@threaded\ndef generate_and_save(prompt, id): ... \nThe form sends the prompt to the / route, which starts the generation in a separate thread then returns two things:\n\nA 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)\nAn 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.\n\nThe 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.\nThis 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.\n\nAgain, with Style\nThe app is functional, but can be improved. The next version 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:\n\n\n\nimage.png\n\n\nStep 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.\nTo 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).\n<div class=\"row\">\n <div class=\"col-xs-12\">\n <div class=\"box\">This takes up the full width</div>\n </div>\n</div>\nThis 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:\n\ngrid = Html(\n Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\"),\n Div(\n Div(Div(\"This takes up the full width\", cls=\"box\", style=\"background-color: #800000;\"), cls=\"col-xs-12\"),\n Div(Div(\"This takes up half\", cls=\"box\", style=\"background-color: #008000;\"), cls=\"col-xs-6\"),\n Div(Div(\"This takes up half\", cls=\"box\", style=\"background-color: #0000B0;\"), cls=\"col-xs-6\"),\n cls=\"row\", style=\"color: #fff;\"\n )\n)\nshow(grid)\n\n\n\n\n \n \n \n This takes up the full width\n \n \n This takes up half\n \n \n This takes up half\n \n \n\n\n\nAside: when in doubt with CSS stuff, add a background color or a border so you can see what’s happening!\nTranslating 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.\ngridlink = Link(rel=\"stylesheet\", href=\"https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css\", type=\"text/css\")\napp = FastHTML(hdrs=(picolink, gridlink))\n\n# Main page\n@app.get(\"/\")\ndef get():\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with last 10\n gen_list = Div(*gen_containers[::-1], id='gen-list', cls=\"row\") # flexbox container: class = row\n return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add, gen_list, cls='container')\n\n# Show the image (if available) and prompt for a generation\ndef generation_preview(g):\n grid_cls = \"box col-xs-12 col-sm-6 col-md-4 col-lg-3\"\n image_path = f\"{g.folder}/{g.id}.png\"\n if os.path.exists(image_path):\n return Div(Card(\n Img(src=image_path, alt=\"Card image\", cls=\"card-img-top\"),\n Div(P(B(\"Prompt: \"), g.prompt, cls=\"card-text\"),cls=\"card-body\"),\n ), id=f'gen-{g.id}', cls=grid_cls)\n return Div(f\"Generating gen {g.id} with prompt {g.prompt}\", \n id=f'gen-{g.id}', hx_get=f\"/gens/{g.id}\", \n hx_trigger=\"every 2s\", hx_swap=\"outerHTML\", cls=grid_cls)\nYou can see the final result in 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. Now for the next question: how do we keep track of different users?\n\n\nAgain, with Sessions\nAt 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:\n@app.get(\"/\")\ndef get(session):\n if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())\n return H1(f\"Session ID: {session['session_id']}\")\nRefresh 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.\nIn the image app example, we can add a session_id column to our database, and modify our homepage like so:\n@app.get(\"/\")\ndef get(session):\n if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())\n inp = Input(id=\"new-prompt\", name=\"prompt\", placeholder=\"Enter a prompt\")\n add = Form(Group(inp, Button(\"Generate\")), hx_post=\"/\", target_id='gen-list', hx_swap=\"afterbegin\")\n gen_containers = [generation_preview(g) for g in gens(limit=10, where=f\"session_id == '{session['session_id']}'\")]\n ...\nSo 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. 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.\n\n\nAgain, with Credits!\nGenerating 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 :)\nTaking payments with Stripe is intimidating but very doable. Here’s a 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!\nFor the finished example we add the bare minimum:\n\nA way to create a Stripe checkout session and redirect the user to the session URL\n‘Success’ and ‘Cancel’ routes to handle the result of the checkout\nA route that listens for a webhook from Stripe to update the number of credits when a payment is made.\n\nIn 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!", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#more-on-routing-and-request-parameters", "href": "tutorials/by_example.html#more-on-routing-and-request-parameters", "title": "FastHTML By Example", "section": "More on Routing and Request Parameters", "text": "More on Routing and Request Parameters\nThere 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\n\nThe path parameters\nThe query parameters\nThe cookies\nThe headers\nThe session\nForm data\n\nThere are also a few special arguments\n\nrequest (or any prefix like req): gets the raw Starlette Request object\nsession (or any prefix like sess): gets the session object\nauth\nhtmx\napp\n\nIn this section let’s quickly look at some of these in action.\n\nfrom fasthtml.common import *\nfrom starlette.testclient import TestClient\n\napp = FastHTML()\ncli = TestClient(app)\n\nPart of the route (path parameters):\n\n@app.get('/user/{nm}')\ndef _(nm:str): return f\"Good day to you, {nm}!\"\n\ncli.get('/user/jph').text\n\n'Good day to you, jph!'\n\n\nMatching with a regex:\n\nreg_re_param(\"imgext\", \"ico|gif|jpg|jpeg|webm\")\n\n@app.get(r'/static/{path:path}/{fn}.{ext:imgext}')\ndef get_img(fn:str, path:str, ext:str): return f\"Getting {fn}.{ext} from /{path}\"\n\ncli.get('/static/foo/jph.ico').text\n\n'Getting jph.ico from /foo/'\n\n\nUsing an enum (try using a string that isn’t in the enum):\n\nModelName = str_enum('ModelName', \"alexnet\", \"resnet\", \"lenet\")\n\n@app.get(\"/models/{nm}\")\ndef model(nm:ModelName): return nm\n\nprint(cli.get('/models/alexnet').text)\n\nalexnet\n\n\nCasting to a Path:\n\n@app.get(\"/files/{path}\")\ndef txt(path: Path): return path.with_suffix('.txt')\n\nprint(cli.get('/files/foo').text)\n\nfoo.txt\n\n\nAn integer with a default value:\n\nfake_db = [{\"name\": \"Foo\"}, {\"name\": \"Bar\"}]\n\n@app.get(\"/items/\")\ndef read_item(idx: int = 0): return fake_db[idx]\n\nprint(cli.get('/items/?idx=1').text)\n\n{\"name\":\"Bar\"}\n\n\n\n# Equivalent to `/items/?idx=0`.\nprint(cli.get('/items/').text)\n\n{\"name\":\"Foo\"}\n\n\nBoolean values (takes anything “truthy” or “falsy”):\n\n@app.get(\"/booly/\")\ndef booly(coming:bool=True): return 'Coming' if coming else 'Not coming'\n\nprint(cli.get('/booly/?coming=true').text)\n\nComing\n\n\n\nprint(cli.get('/booly/?coming=no').text)\n\nNot coming\n\n\nGetting dates:\n\n@app.get(\"/datie/\")\ndef datie(d:parsed_date): return d\n\ndate_str = \"17th of May, 2024, 2p\"\nprint(cli.get(f'/datie/?d={date_str}').text)\n\n2024-05-17 14:00:00\n\n\nMatching a dataclass:\n\nfrom dataclasses import dataclass, asdict\n\n@dataclass\nclass Bodie:\n a:int;b:str\n\n@app.route(\"/bodie/{nm}\")\ndef post(nm:str, data:Bodie):\n res = asdict(data)\n res['nm'] = nm\n return res\n\ncli.post('/bodie/me', data=dict(a=1, b='foo')).text\n\n'{\"a\":1,\"b\":\"foo\",\"nm\":\"me\"}'\n\n\n\nCookies\nCookies can be set via a Starlette Response object, and can be read back by specifying the name:\n\nfrom datetime import datetime\n\n@app.get(\"/setcookie\")\ndef setc(req):\n now = datetime.now()\n res = Response(f'Set to {now}')\n res.set_cookie('now', str(now))\n return res\n\ncli.get('/setcookie').text\n\n'Set to 2024-07-20 23:14:54.364793'\n\n\n\n@app.get(\"/getcookie\")\ndef getc(now:parsed_date): return f'Cookie was set at time {now.time()}'\n\ncli.get('/getcookie').text\n\n'Cookie was set at time 23:14:54.364793'\n\n\n\n\nUser Agent and HX-Request\nAn 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.\n\n@app.get(\"/ua\")\nasync def ua(user_agent:str): return user_agent\n\ncli.get('/ua', headers={'User-Agent':'FastHTML'}).text\n\n'FastHTML'\n\n\n\n@app.get(\"/hxtest\")\ndef hxtest(htmx): return htmx.request\n\ncli.get('/hxtest', headers={'HX-Request':'1'}).text\n\n'1'\n\n\n\n\nStarlette Requests\nIf 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:\n@app.get(\"/form\")\nasync def form(request:Request):\n form_data = await request.form()\n a = form_data.get('a')\nSee the Starlette docs for more information on the Request object.\n\n\nStarlette Responses\nYou can return a Starlette Response object from a route to control the response. For example:\n@app.get(\"/redirect\")\ndef redirect():\n return RedirectResponse(url=\"/\")\nWe used this to set cookies in the previous example. See the Starlette docs for more information on the Response object.\n\n\nStatic Files\nWe 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:\n# For images, CSS, etc.\n@app.get(\"/{fname:path}.{ext:static}\")\ndef static(fname: str, ext: str):\n return FileResponse(f'{fname}.{ext}')\nYou 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!\n\n\nWebSockets\nFor 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:\napp = FastHTML(exts='ws')\nrt = app.route\nWith 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:\ndef mk_inp(): return Input(id='msg')\n\n@rt('/')\nasync def get(request):\n cts = Div(\n Div(id='notifications'),\n Form(mk_inp(), id='form', ws_send=True),\n hx_ext='ws', ws_connect='/ws')\n return Titled('Websocket Test', cts)\nAnd 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:\n@app.ws('/ws')\nasync def ws(msg:str, send):\n await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()\nOne 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!\nNow, 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:\nasync def on_connect(send):\n print('Connected!')\n await send(Div('Hello, you have connected', id=\"notifications\"))\n\nasync def on_disconnect(ws):\n print('Disconnected!')\n\n@app.ws('/ws', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send):\n await send(Div('Hello ' + msg, id=\"notifications\"))\n await sleep(2)\n return Div('Goodbye ' + msg, id=\"notifications\"), mk_inp()", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#full-example-3---chatbot-example-with-daisyui-components", "href": "tutorials/by_example.html#full-example-3---chatbot-example-with-daisyui-components", "title": "FastHTML By Example", "section": "Full Example #3 - Chatbot Example with DaisyUI Components", "text": "Full Example #3 - Chatbot Example with DaisyUI Components\nLet’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. The final result will look like this:\n\n\n\nimage.png\n\n\nAt first glance, DaisyUI’s chat component looks quite intimidating. The examples look like this:\n<div class=\"chat chat-start\">\n <div class=\"chat-image avatar\">\n <div class=\"w-10 rounded-full\">\n <img alt=\"Tailwind CSS chat bubble component\" src=\"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg\" />\n </div>\n </div>\n <div class=\"chat-header\">\n Obi-Wan Kenobi\n <time class=\"text-xs opacity-50\">12:45</time>\n </div>\n <div class=\"chat-bubble\">You were the Chosen One!</div>\n <div class=\"chat-footer opacity-50\">\n Delivered\n </div>\n</div>\n<div class=\"chat chat-end\">\n <div class=\"chat-image avatar\">\n <div class=\"w-10 rounded-full\">\n <img alt=\"Tailwind CSS chat bubble component\" src=\"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.jpg\" />\n </div>\n </div>\n <div class=\"chat-header\">\n Anakin\n <time class=\"text-xs opacity-50\">12:46</time>\n </div>\n <div class=\"chat-bubble\">I hate you!</div>\n <div class=\"chat-footer opacity-50\">\n Seen at 12:46\n </div>\n</div>\nWe have several things going for us however.\n\nChatGPT knows DaisyUI and Tailwind (DaisyUI is a Tailwind component library)\nWe can build things up piece by piece with AI standing by to help.\n\nhttps://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.\nWe can strip out some unnecessary bits and try to get the simplest possible example working in a notebook first:\n\n# Loading tailwind and daisyui\nheaders = (Script(src=\"https://cdn.tailwindcss.com\"),\n Link(rel=\"stylesheet\", href=\"https://cdn.jsdelivr.net/npm/daisyui@4.11.1/dist/full.min.css\"))\n\n# Displaying a single message\nd = Div(\n Div(\"Chat header here\", cls=\"chat-header\"),\n Div(\"My message goes here\", cls=\"chat-bubble chat-bubble-primary\"),\n cls=\"chat chat-start\"\n)\n# show(Html(*headers, d)) # uncomment to view\n\nNow 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:\n\nmessages = [\n {\"role\":\"user\", \"content\":\"Hello\"},\n {\"role\":\"assistant\", \"content\":\"Hi, how can I assist you?\"}\n]\n\ndef ChatMessage(msg):\n return Div(\n Div(msg['role'], cls=\"chat-header\"),\n Div(msg['content'], cls=f\"chat-bubble chat-bubble-{'primary' if msg['role'] == 'user' else 'secondary'}\"),\n cls=f\"chat chat-{'end' if msg['role'] == 'user' else 'start'}\")\n\nchatbox = Div(*[ChatMessage(msg) for msg in messages], cls=\"chat-box\", id=\"chatlist\")\n\n# show(Html(*headers, chatbox)) # Uncomment to view\n\nNext, it was back to the ChatGPT to tweak the chat box so it wouldn’t grow as messages were added. I asked:\n\"I have something like this (it's working now) \n[code]\nThe messages are added to this div so it grows over time. \nIs there a way I can set it's height to always be 80% of the total window height with a scroll bar if needed?\"\nBased 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.”\nTo 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!\nThe actual chat functionality of the app is based on our claudette 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 for implementations of both, along with further details.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#full-example-4---multiplayer-game-of-life-example-with-websockets", "href": "tutorials/by_example.html#full-example-4---multiplayer-game-of-life-example-with-websockets", "title": "FastHTML By Example", "section": "Full Example #4 - Multiplayer Game of Life Example with Websockets", "text": "Full Example #4 - Multiplayer Game of Life Example with Websockets\nLet’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, 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:\ngrid = [[0 for _ in range(20)] for _ in range(20)]\ndef update_grid(grid: list[list[int]]) -> list[list[int]]:\n new_grid = [[0 for _ in range(20)] for _ in range(20)]\n def count_neighbors(x, y):\n directions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]\n count = 0\n for dx, dy in directions:\n nx, ny = x + dx, y + dy\n if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]): count += grid[nx][ny]\n return count\n for i in range(len(grid)):\n for j in range(len(grid[0])):\n neighbors = count_neighbors(i, j)\n if grid[i][j] == 1:\n if neighbors < 2 or neighbors > 3: new_grid[i][j] = 0\n else: new_grid[i][j] = 1\n elif neighbors == 3: new_grid[i][j] = 1\n return new_grid\nThis 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!\ndef Grid():\n cells = []\n for y, row in enumerate(game_state['grid']):\n for x, cell in enumerate(row):\n cell_class = 'alive' if cell else 'dead'\n 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')\n cells.append(cell)\n return Div(*cells, id='grid')\n\n@rt('/update')\nasync def put(x: int, y: int):\n grid[y][x] = 1 if grid[y][x] == 0 else 0\nAbove 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!\nWebsockets 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:\n...\napp = FastHTML(hdrs=(picolink, gridlink, css, htmx_ws), exts='ws')\n\nplayer_queue = []\nasync def update_players():\n for i, player in enumerate(player_queue):\n try: await player(Grid())\n except: player_queue.pop(i)\nasync def on_connect(send): player_queue.append(send)\nasync def on_disconnect(send): await update_players()\n\n@app.ws('/gol', conn=on_connect, disconn=on_disconnect)\nasync def ws(msg:str, send): pass\n\ndef Home(): return Title('Game of Life'), Main(gol, Div(Grid(), id='gol', cls='row center-xs'), hx_ext=\"ws\", ws_connect=\"/gol\")\n\n@rt('/update')\nasync def put(x: int, y: int):\n grid[y][x] = 1 if grid[y][x] == 0 else 0\n await update_players()\n...\nHere 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. You can find a full fledge hosted example of this app here.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#ft-objects-and-html", "href": "tutorials/by_example.html#ft-objects-and-html", "title": "FastHTML By Example", "section": "FT objects and HTML", "text": "FT objects and HTML\nThese 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.\nFor example, here’s one way we could make a custom class that can be rendered into HTML:\n\nclass Person:\n def __init__(self, name, age):\n self.name = name\n self.age = age\n\n def __ft__(self):\n return ['div', [f'{self.name} is {self.age} years old.'], {}]\n\np = Person('Jonathan', 28)\nprint(to_xml(Div(p, \"more text\", cls=\"container\")))\n\n<div class=\"container\">\n <div>Jonathan is 28 years old.</div>\nmore text\n</div>\n\n\n\nIn 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:\n\nfrom fastcore.all import patch\n\n@patch\ndef __ft__(self:Person):\n return Div(\"Person info:\", Ul(Li(\"Name:\",self.name), Li(\"Age:\", self.age)))\n\nshow(p)\n\n\nPerson info:\n \n \nName:\nJonathan\n \n \nAge:\n28\n \n \n\n\n\nSome 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.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#custom-scripts-and-styling", "href": "tutorials/by_example.html#custom-scripts-and-styling", "title": "FastHTML By Example", "section": "Custom Scripts and Styling", "text": "Custom Scripts and Styling\nThere are many popular JavaScript and CSS libraries that can be used via a simple Script or Style tag. But in some cases you will need to write more custom code. FastHTML’s js.py contains a few examples that may be useful as reference.\nFor example, to use the marked.js library to render markdown in a div, including in components added after the page has loaded via htmx, we do something like this:\nimport { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\nproc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));\nproc_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:\nexport function proc_htmx(sel, func) {\n htmx.onLoad(elt => {\n const elements = htmx.findAll(elt, sel);\n if (elt.matches(sel)) elements.unshift(elt)\n elements.forEach(func);\n });\n}\nThe AI Pictionary example 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.\nAdding styling with custom CSS and libraries such as tailwind is done the same way we add custom JavaScript. The doodle example uses Doodle.CSS to style the page in a quirky way.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#deploying-your-app", "href": "tutorials/by_example.html#deploying-your-app", "title": "FastHTML By Example", "section": "Deploying Your App", "text": "Deploying Your App\nWe can deploy FastHTML almost anywhere you can deploy python apps. We’ve tested Railway, Replit, HuggingFace, and PythonAnywhere.\n\nRailway\n\nInstall the Railway CLI and sign up for an account.\nSet up a folder with our app as main.py\nIn the folder, run railway login.\nUse the fh_railway_deploy script to deploy our project:\n\nfh_railway_deploy MY_APP_NAME\nWhat the script does for us:\n\nDo we have an existing railway project?\n\nYes: Link the project folder to our existing Railway project.\nNo: Create a new Railway project.\n\nDeploy the project. We’ll see the logs as the service is built and run!\nFetches and displays the URL of our app.\nBy 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.\n\nA final note about Railway: We can add secrets like API keys that can be accessed as environment variables from our apps via ‘Variables’. For example, for the image generation app, we can add a REPLICATE_API_KEY variable, and then in main.py we can access it as os.environ['REPLICATE_API_KEY'].\n\n\nReplit\nFork this repl 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.\nYou can store secrets like API keys via the ‘Secrets’ tab in the Replit project settings.\n\n\nHuggingFace\nFollow the instructions in this repository to deploy to HuggingFace spaces.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] }, { "objectID": "tutorials/by_example.html#where-next", "href": "tutorials/by_example.html#where-next", "title": "FastHTML By Example", "section": "Where Next?", "text": "Where Next?\nWe’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 for more ideas, and keep an eye on Jeremy’s YouTube channel where we’ll be releasing a number of “dev chats” related to FastHTML in the near future.", "crumbs": [ "Home", "Tutorials", "FastHTML By Example" ] } ]