Spaces:
Sleeping
Sleeping
| # JS App Walkthrough | |
| <!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! --> | |
| ## Installation | |
| You’ll need the following software to complete the tutorial, read on for | |
| specific installation instructions: | |
| 1. Python | |
| 2. A Python package manager such as pip (which normally comes with | |
| Python) or uv | |
| 3. FastHTML | |
| 4. Web browser | |
| 5. Railway.app account | |
| If you haven’t worked with Python before, we recommend getting started | |
| with [Miniconda](https://docs.anaconda.com/miniconda/). | |
| Note 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. | |
| ### Install FastHTML | |
| For Mac, Windows and Linux, enter: | |
| ``` sh | |
| pip install python-fasthtml | |
| ``` | |
| ## First steps | |
| By the end of this section you’ll have your own FastHTML website with | |
| tests deployed to railway.app. | |
| ### Create a hello world | |
| Create 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: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| from fasthtml.common import * | |
| app = FastHTML() | |
| rt = app.route | |
| @rt('/') | |
| def get(): | |
| return 'Hello, world!' | |
| serve() | |
| ``` | |
| </div> | |
| Finally, run `python main.py` in your terminal and open your browser to | |
| the ‘Link’ that appears. | |
| ### QuickDraw: A FastHTML Adventure 🎨✨ | |
| The 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: | |
| <figure> | |
| <img src="imgs/quickdraw.png" alt="QuickDraw" /> | |
| <figcaption aria-hidden="true">QuickDraw</figcaption> | |
| </figure> | |
| #### Drawing Rooms | |
| Drawing 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: | |
| 1. Room Creation and Storage | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| db = database('data/drawapp.db') | |
| rooms = db.t.rooms | |
| if rooms not in db.t: | |
| rooms.create(id=int, name=str, created_at=str, pk='id') | |
| Room = rooms.dataclass() | |
| @patch | |
| def __ft__(self:Room): | |
| return Li(A(self.name, href=f"/rooms/{self.id}")) | |
| ``` | |
| </div> | |
| Or you can use our `fast_app` function to create a FastHTML app with a | |
| SQLite database and dataclass in one line: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| def render(room): | |
| return Li(A(room.name, href=f"/rooms/{room.id}")) | |
| app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id') | |
| ``` | |
| </div> | |
| We 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. | |
| - We’re using a SQLite database (via FastLite) to store our rooms. | |
| - Each room has an id (integer), a name (string), and a created_at | |
| timestamp (string). | |
| - The Room dataclass is automatically generated based on this structure. | |
| 2. Creating a room | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| @rt("/") | |
| def get(): | |
| # The 'Input' id defaults to the same as the name, so you can omit it if you wish | |
| create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), | |
| Button("Create Room"), | |
| hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") | |
| rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') | |
| return Titled("DrawCollab", | |
| H1("DrawCollab"), | |
| create_room, rooms_list) | |
| @rt("/rooms") | |
| async def post(room:Room): | |
| room.created_at = datetime.now().isoformat() | |
| return rooms.insert(room) | |
| ``` | |
| </div> | |
| - When a user submits the “Create Room” form, this route is called. | |
| - It creates a new Room object, sets the creation time, and inserts it | |
| into the database. | |
| - It 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. | |
| 3. Let’s give our rooms shape | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| @rt("/rooms/{id}") | |
| async def get(id:int): | |
| room = rooms[id] | |
| return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/")) | |
| ``` | |
| </div> | |
| - This route renders the interface for a specific room. | |
| - It fetches the room from the database and renders a title, heading, | |
| and paragraph. | |
| Here is the full code so far: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| from fasthtml.common import * | |
| from datetime import datetime | |
| def render(room): | |
| return Li(A(room.name, href=f"/rooms/{room.id}")) | |
| app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id') | |
| @rt("/") | |
| def get(): | |
| create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), | |
| Button("Create Room"), | |
| hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") | |
| rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') | |
| return Titled("DrawCollab", create_room, rooms_list) | |
| @rt("/rooms") | |
| async def post(room:Room): | |
| room.created_at = datetime.now().isoformat() | |
| return rooms.insert(room) | |
| @rt("/rooms/{id}") | |
| async def get(id:int): | |
| room = rooms[id] | |
| return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/")) | |
| serve() | |
| ``` | |
| </div> | |
| Now 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. | |
| #### The Canvas - Let’s Get Drawing! 🖌️ | |
| Time to add the actual drawing functionality. We’ll use Fabric.js for | |
| this: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| # ... (keep the previous imports and database setup) | |
| @rt("/rooms/{id}") | |
| async def get(id:int): | |
| room = rooms[id] | |
| canvas = Canvas(id="canvas", width="800", height="600") | |
| color_picker = Input(type="color", id="color-picker", value="#3CDD8C") | |
| brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") | |
| js = """ | |
| var canvas = new fabric.Canvas('canvas'); | |
| canvas.isDrawingMode = true; | |
| canvas.freeDrawingBrush.color = '#3CDD8C'; | |
| canvas.freeDrawingBrush.width = 10; | |
| document.getElementById('color-picker').onchange = function() { | |
| canvas.freeDrawingBrush.color = this.value; | |
| }; | |
| document.getElementById('brush-size').oninput = function() { | |
| canvas.freeDrawingBrush.width = parseInt(this.value, 10); | |
| }; | |
| """ | |
| return Titled(f"Room: {room.name}", | |
| A(Button("Leave Room"), href="/"), | |
| canvas, | |
| Div(color_picker, brush_size), | |
| Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"), | |
| Script(js)) | |
| # ... (keep the serve() part) | |
| ``` | |
| </div> | |
| Now we’ve got a drawing canvas! FastHTML makes it easy to include | |
| external libraries and add custom JavaScript. | |
| #### Saving and Loading Canvases 💾 | |
| Now 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: | |
| 1. Modify the database schema: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id') | |
| ``` | |
| </div> | |
| 2. Add a save button that grabs the canvas’ state and sends it to the | |
| server: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| @rt("/rooms/{id}") | |
| async def get(id:int): | |
| room = rooms[id] | |
| canvas = Canvas(id="canvas", width="800", height="600") | |
| color_picker = Input(type="color", id="color-picker", value="#3CDD8C") | |
| brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") | |
| save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}") | |
| # ... (rest of the function remains the same) | |
| ``` | |
| </div> | |
| 3. Add routes for saving and loading canvas data: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| @rt("/rooms/{id}/save") | |
| async def post(id:int, canvas_data:str): | |
| rooms.update({'canvas_data': canvas_data}, id) | |
| return "Canvas saved successfully" | |
| @rt("/rooms/{id}/load") | |
| async def get(id:int): | |
| room = rooms[id] | |
| return room.canvas_data if room.canvas_data else "{}" | |
| ``` | |
| </div> | |
| 4. Update the JavaScript to load existing canvas data: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` javascript | |
| js = f""" | |
| var canvas = new fabric.Canvas('canvas'); | |
| canvas.isDrawingMode = true; | |
| canvas.freeDrawingBrush.color = '#3CDD8C'; | |
| canvas.freeDrawingBrush.width = 10; | |
| // Load existing canvas data | |
| fetch(`/rooms/{id}/load`) | |
| .then(response => response.json()) | |
| .then(data => {{ | |
| if (data && Object.keys(data).length > 0) {{ | |
| canvas.loadFromJSON(data, canvas.renderAll.bind(canvas)); | |
| }} | |
| }}); | |
| // ... (rest of the JavaScript remains the same) | |
| """ | |
| ``` | |
| </div> | |
| With 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. | |
| Here is the completed code: | |
| <div class="code-with-filename"> | |
| **main.py** | |
| ``` python | |
| from fasthtml.common import * | |
| from datetime import datetime | |
| def render(room): | |
| return Li(A(room.name, href=f"/rooms/{room.id}")) | |
| app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id') | |
| @rt("/") | |
| def get(): | |
| create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), | |
| Button("Create Room"), | |
| hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") | |
| rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') | |
| return Titled("QuickDraw", | |
| create_room, rooms_list) | |
| @rt("/rooms") | |
| async def post(room:Room): | |
| room.created_at = datetime.now().isoformat() | |
| return rooms.insert(room) | |
| @rt("/rooms/{id}") | |
| async def get(id:int): | |
| room = rooms[id] | |
| canvas = Canvas(id="canvas", width="800", height="600") | |
| color_picker = Input(type="color", id="color-picker", value="#000000") | |
| brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") | |
| save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}") | |
| js = f""" | |
| var canvas = new fabric.Canvas('canvas'); | |
| canvas.isDrawingMode = true; | |
| canvas.freeDrawingBrush.color = '#000000'; | |
| canvas.freeDrawingBrush.width = 10; | |
| // Load existing canvas data | |
| fetch(`/rooms/{id}/load`) | |
| .then(response => response.json()) | |
| .then(data => {{ | |
| if (data && Object.keys(data).length > 0) {{ | |
| canvas.loadFromJSON(data, canvas.renderAll.bind(canvas)); | |
| }} | |
| }}); | |
| document.getElementById('color-picker').onchange = function() {{ | |
| canvas.freeDrawingBrush.color = this.value; | |
| }}; | |
| document.getElementById('brush-size').oninput = function() {{ | |
| canvas.freeDrawingBrush.width = parseInt(this.value, 10); | |
| }}; | |
| """ | |
| return Titled(f"Room: {room.name}", | |
| A(Button("Leave Room"), href="/"), | |
| canvas, | |
| Div(color_picker, brush_size, save_button), | |
| Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"), | |
| Script(js)) | |
| @rt("/rooms/{id}/save") | |
| async def post(id:int, canvas_data:str): | |
| rooms.update({'canvas_data': canvas_data}, id) | |
| return "Canvas saved successfully" | |
| @rt("/rooms/{id}/load") | |
| async def get(id:int): | |
| room = rooms[id] | |
| return room.canvas_data if room.canvas_data else "{}" | |
| serve() | |
| ``` | |
| </div> | |
| ### Deploying to Railway | |
| You 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](https://railway.app/) and install the [Railway | |
| CLI](https://docs.railway.app/guides/cli). Once installed, make sure to | |
| run `railway login` to log in to your account. | |
| To 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: | |
| ``` sh | |
| fh_railway_deploy quickdraw | |
| ``` | |
| <div> | |
| > **Note** | |
| > | |
| > Your app must be located in a `main.py` file for this to work. | |
| </div> | |
| ### Conclusion: You’re a FastHTML Artist Now! 🎨🚀 | |
| Congratulations! You’ve just built a sleek, interactive web application | |
| using FastHTML. Let’s recap what we’ve learned: | |
| 1. FastHTML allows you to create dynamic web apps with minimal code. | |
| 2. We used FastHTML’s routing system to handle different pages and | |
| actions. | |
| 3. We integrated with a SQLite database to store room information and | |
| canvas data. | |
| 4. We utilized Fabric.js to create an interactive drawing canvas. | |
| 5. We implemented features like color picking, brush size adjustment, | |
| and canvas saving. | |
| 6. We used HTMX for seamless, partial page updates without full | |
| reloads. | |
| 7. We learned how to deploy our FastHTML application to Railway for | |
| easy hosting. | |
| You’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: | |
| - Implementing different drawing tools (e.g., shapes, text) | |
| - Adding user authentication | |
| - Creating a gallery of saved drawings | |
| - Implementing real-time collaborative drawing using WebSockets | |
| Whatever you choose to build next, FastHTML has got your back. Now go | |
| forth and create something awesome! Happy coding! 🖼️🚀 | |