back to indexFastHTML first look - Answer.AI dev chat #4
00:00:00.000 |
Hello again, another Dev Chat with Jono and Alexis. 00:00:29.480 |
but then when I was done, I was like, oh, I actually 00:00:54.160 |
I don't know if this is obvious and easy or not, 00:00:57.680 |
but basically, I added this .dataclass thing. 00:01:25.120 |
One is to type completion, at least in Jupyter, 00:01:29.680 |
it's nice to be able to see what I'm meant to be typing in. 00:01:35.200 |
And it's nice for Python to know about what types are expected 00:01:46.120 |
as we'll see later, for it's necessary for the web 00:01:50.400 |
application framework stuff I was building, as you'll see. 00:01:59.480 |
and the keyword arguments are the same as the field names. 00:02:05.680 |
And you'll see all of them are defined as the database type 00:02:18.120 |
if it's nullable or not, and if it's not nullable, 00:02:22.640 |
I very intentionally don't do that, because this int, 00:02:27.200 |
actually, you don't have to pass it when you create it. 00:02:34.960 |
different types for the update version and the create version. 00:02:45.440 |
So anyway, that way, I've got a dictionary here 00:02:58.160 |
I don't know why they only have two in this database. 00:03:00.360 |
It's a bit of a disappointment, but at least they're 00:03:04.720 |
So you can pass those in for a particular AC/DC album 00:03:31.520 |
AUDIENCE: Could I interrupt with a basic question here? 00:03:35.000 |
AUDIENCE: So when you define the variable album_dc, 00:03:39.640 |
is that album_dc completely equivalent to what 00:03:43.280 |
I would get if I declared a data class in the usual way 00:03:45.920 |
by saying @data_class and then said class data-- 00:03:49.000 |
MARTIN SPLITT: I've even written a function called data_class 00:03:51.540 |
source that takes the data class and returns the source code 00:04:12.800 |
In that case, would you say it's more idiomatic 00:04:18.320 |
a class definition in Constructor, or not really? 00:04:25.120 |
I'm not really going to use it for much, as you'll see. 00:04:28.880 |
AUDIENCE: OK, just wanted to double-check my understanding. 00:04:32.960 |
So I couldn't find something that spits out the source 00:04:46.520 |
to cover what this particular one does, at least. 00:04:58.600 |
is then that I added this create_module thing, which 00:05:04.600 |
literally just opens a file, prints from data classes, 00:05:08.320 |
import data class, and then prints the source code to it. 00:05:13.200 |
And so that means that you could now, as you see here, 00:05:42.640 |
It's going to print out the data class source for all of them. 00:05:51.320 |
will create a file that contains all of them. 00:06:08.840 |
because you can now do that, or import star, or whatever. 00:06:23.280 |
So this is like the other way around to something 00:06:29.560 |
like SQL model, where you define the data classes, 00:06:51.520 |
is that this way I can create my database schema using a GUI, 00:06:56.120 |
or using SQLite utils, or using SQL statements, 00:07:06.160 |
And at any point, I could just reflect that into my module 00:07:18.560 |
Because by just being able to consume the SQL model 00:07:22.560 |
as it comes in, you can use anything you wanted to create 00:07:24.980 |
JOHN MUELLER: My brain's not working well enough to quite see 00:07:29.480 |
why that's composability, but I will take your word for it. 00:07:35.400 |
with any of a variety of tools that someone else could 00:07:37.960 |
have made for defining the SQL schema to begin with. 00:07:43.480 |
Now, we already had this thing where you could-- 00:07:48.400 |
We already had this thing where I had dundercall. 00:07:50.520 |
And dundercall is basically the same as doing a SELECT 00:08:18.720 |
stores the data class inside the table object. 00:08:24.400 |
And if that's happened, then whenever you call dundercall, 00:08:32.480 |
So now you can see when I call ALBUM_LIMIT=2, 00:08:36.160 |
the things I get back are not dictionaries anymore. 00:08:47.920 |
if you happen to have made a data class from this 00:08:50.600 |
at any time in the past, you now have different behavior 00:08:59.440 |
It's actually like-- another way to think of it 00:09:01.840 |
would be like, oh, ignore the thing that's returned from this. 00:09:05.760 |
And in fact, probably what you'd want to do most of the time 00:09:10.120 |
I'm not sure I documented this, so let's do this now. 00:09:18.840 |
If you want to have all tables, just call all DCs. 00:09:30.200 |
This is how you turn it on, is you just run that. 00:09:38.280 |
And so that applies then not just to DUNDER_CALL, 00:09:41.440 |
also applies to .get, which gets by primary key. 00:09:47.000 |
So those are the two things that let you get data classes. 00:09:52.720 |
So then I've also added a lot of additional behavior, 00:10:09.760 |
It's a concept, which is that a lot of databases, 00:10:14.880 |
and originally, I think it came from Postgres-- 00:10:16.920 |
have some kind of thing that lets you insert things. 00:10:22.000 |
then update the existing thing where already there 00:10:38.520 |
is the fact that CATS is not there doesn't matter. 00:10:48.480 |
Yeah, this took me a second or two to wrap my head around. 00:10:51.080 |
Like, OK, well, I can check if it's there or not 00:11:11.760 |
So I added a DUNDA contain support to this yesterday. 00:11:16.760 |
Oh, well, I was surprised it worked, and I was very happy. 00:11:18.800 |
I didn't realize it was there because you just added it. 00:11:20.200 |
Yes, and I think you can also do it on the table itself. 00:11:41.160 |
So you can see the schema that this has done. 00:11:44.840 |
And so if I now re-run CATS in dt, it's now true. 00:11:57.080 |
So when we-- what did I say this same applies to? 00:12:04.960 |
That it's automatically casting to the data class? 00:12:57.580 |
Maybe we should just mention this curious feature 00:13:28.460 |
when you insert or upsert, you get back the inserted row. 00:13:45.860 |
I mean, we called it earlier, but that was before we-- 00:13:52.020 |
We haven't-- but the CATS didn't exist yet at that point. 00:14:03.140 |
But yeah, so now if we do the dataclass thing-- 00:14:16.180 |
So now I can set my attributes, because this is a data class. 00:14:30.900 |
And I can pass that in, because the first thing to be passed 00:14:44.700 |
BIM_FASTLIGHT/keyword_arguments DEF-- well, there it is. 00:14:54.580 |
OK, so that's a dictionary of string to any or a data class 00:15:37.620 |
This is a really nice way for me to now patch new behavior. 00:15:54.460 |
It checks if there's already an _orig__upsert there. 00:15:58.580 |
So is the default behavior when you do the second patch 00:16:02.300 |
to clobber, but you have access to the old patch? 00:16:11.060 |
So if you repatch, then _orig__upsert will still 00:16:21.940 |
OK, so it clobbers the old one, but the old one's 00:16:24.100 |
there if you want to access it from the new one. 00:16:28.020 |
The grandparent's there, but the old patch is not there. 00:16:52.580 |
stuff that I added as I started to implement the next thing 00:17:23.260 |
When I click on things, there's no full-screen refresh. 00:17:51.820 |
And obviously, I haven't spent much time styling this. 00:17:59.660 |
But it looks and feels like a reasonably modern kind 00:18:09.660 |
The entire thing is written in 69 lines of code, many of which 00:18:35.660 |
to show you guys, which I know both of you have seen bits of, 00:18:41.180 |
And the goal of fast.html and this kind of ecosystem 00:18:45.300 |
is to allow people to create single-file web applications 00:18:54.420 |
without the shortcomings of Gradio and Streamlet 00:19:05.260 |
But the shortcomings of those is that they're 00:19:08.180 |
for creating dashboards and proof of concepts and whatever. 00:19:11.660 |
If you like, you wouldn't create your whole startup 00:19:15.940 |
as a Gradio app, probably, or a Streamlet app. 00:19:18.980 |
And when people get to the bit where it's like, 00:19:20.980 |
OK, I now want to have a different component that 00:19:23.060 |
works in this different way and takes advantage 00:19:25.100 |
of this JavaScript library, they say, oh, OK, that's possible. 00:19:29.580 |
Now you have to learn this entire new, massive, 00:19:34.060 |
than it would have been to just use it in the first place. 00:19:40.100 |
start building something as easily as Streamlet or Gradio 00:19:43.140 |
would, but naturally support growth from there. 00:19:48.460 |
There's no point where it's like, OK, you're done. 00:19:53.380 |
You're going to have to learn to create your own IPyWidget 00:20:00.700 |
And it's actually loosely based on the framework 00:20:10.620 |
for FastMail, which supported one of the busiest 00:20:16.500 |
It's this approach scales out in any way you like. 00:20:41.300 |
you can stick it up on PyPy, and other people 00:20:43.500 |
can pip install your StyleSheet framework, or your Web 00:20:56.700 |
the CSS is called Pico, super simple, lightweight thing. 00:21:03.060 |
have a Daisy UI, pip install, FastHTMLDaisyUI, whatever. 00:21:11.580 |
Or if there are certain JavaScript things you like, 00:21:16.620 |
you can easily wrap them with this, and pip install those. 00:21:38.180 |
You can see this running in the background here. 00:21:40.620 |
And as soon as I hit Save, it's refreshed itself. 00:21:49.900 |
It's-- yeah, because there's no build step or anything to do. 00:22:20.340 |
enough to have an understanding of why it might not 00:22:24.820 |
be the kind of thing that scales up from a quick start 00:22:31.900 |
hard to summarize, but if you could give a sense of, 00:22:41.180 |
doesn't give you that sort of continuous path 00:22:43.700 |
from quick start to a bigger, more complicated thing? 00:23:38.860 |
Gradio is very oriented around getting some inputs 00:23:42.500 |
to a function and then displaying back the outputs. 00:23:46.300 |
Like, it's ideal for I have a model that makes predictions 00:23:48.660 |
or I have something that generates something. 00:23:51.180 |
You can do some state tracking and things like that, 00:23:56.580 |
it's very, very nice for-- like, this kind of interface here 00:24:02.180 |
But trying to move to something where, I don't know, 00:24:09.300 |
track of multiple things, or you want multiple pages 00:24:11.460 |
in your app, it does get very cumbersome very quickly. 00:24:21.660 |
that has an Upload button, or you can click an example, 00:24:27.300 |
and you can click Submit, and it runs a machine learning 00:24:49.980 |
and then you create an interface that will call this function. 00:24:58.380 |
It's a very specific kind of application it can create. 00:25:09.820 |
It's not a general-- like you couldn't create Instagram 00:25:24.100 |
I guess originally it was created by Instagram, 00:25:28.060 |
So Django, as you all know, is a general purpose web framework 00:25:34.500 |
for which you can build anything that your brain can think of. 00:25:47.660 |
It's the same kind of idea of like sketch out 00:25:52.740 |
the basic inputs and outputs and maybe one function 00:25:58.220 |
So one thing I think about when I think about this design 00:26:02.220 |
space is that it sounds like Streamlet and Gradio give you 00:26:07.900 |
abstractions that are very convenient to work with, 00:26:11.900 |
to the underlying abstractions of how a website is built. 00:26:26.580 |
Let's instead see how was this screen created. 00:26:41.860 |
It's got a thing here with a button and an input. 00:26:47.500 |
And optionally, got some details about the to-do underneath. 00:27:06.420 |
And it contains-- if you look at the HTML, it contains a main. 00:27:24.620 |
The article contains a header, an unordered list, 00:27:37.900 |
The body is an unordered list containing some to-dos. 00:27:47.180 |
And above that will be a header containing a form 00:28:02.020 |
So yeah, in this case, the thing that we're actually working 00:28:09.820 |
And also, we're working with things like posts. 00:28:19.060 |
So yeah, we are working with the same elements 00:28:26.140 |
that anybody building something with React, or Django, 00:28:30.940 |
And that's why, with this, you can build anything. 00:28:33.740 |
And the abstractions that you're building with 00:28:40.620 |
They're tiny wrappers over the basic foundations 00:28:48.540 |
And those things that look like function applications 00:29:02.780 |
- Yeah, so this reminds me of a library called Hiccup that 00:29:05.420 |
works in a similar way, not a Python library. 00:29:19.980 |
- And it uses the native data structures of vector 00:29:27.940 |
and dictionaries to represent an element and its attributes. 00:29:39.060 |
you kind of build it just by essentially doing 00:29:46.620 |
the basic data structures of vector and dictionary 00:29:54.780 |
some libraries will have a form object, right, 00:30:03.940 |
a view component and some JavaScript and a WebSocket 00:30:08.500 |
stream to synchronize data between the Python 00:30:12.620 |
And then the view or Svelte or whatever your JavaScript 00:30:16.140 |
framework is does the magic and eventually produces 00:30:25.260 |
But yeah, in this case, it's like, oh, you just 00:30:33.220 |
or design approaches that can work well when it's possible 00:30:36.500 |
is to try to embrace basic data structures as much as possible 00:30:39.620 |
rather than introduce new types, because then the basic data 00:30:48.980 |
Like in Python, that would be like comprehension, right? 00:30:51.260 |
If you're representing your list in your HTML that 00:30:54.420 |
will be eventually turned into HTML as just a Python list, 00:30:57.460 |
then you can go and modify every element in your list 00:31:01.340 |
- So in fact, every one of those HTML functions 00:31:10.060 |
The three-element Python list contains, in order, 00:31:15.220 |
the name of the tag as a string, a list of the children, 00:31:22.220 |
And yeah, it's interesting you mentioned the Clojure one. 00:31:25.580 |
But basically, almost every functional language 00:31:32.380 |
And there are similar things in Python as well, 00:31:39.500 |
although they're not quite as functional as FastHTML 00:32:08.340 |
because web design was very difficult because you 00:32:31.260 |
involved, to a large degree, knowing about quirks 00:32:35.900 |
Web designers needed to be able to write HTML and look 00:32:43.940 |
So we came up with this idea as a community of being like, 00:32:52.300 |
And the place for the email will be curly bracket email. 00:32:55.700 |
They can design the whole thing, give it back to the coder, 00:33:02.460 |
This doesn't seem useful anymore because we just 00:33:08.820 |
So to me, the idea of having a separate file that 00:33:12.180 |
contains two separate languages, the first separate language 00:33:17.540 |
is like Ginger or whatever, I'm not very fond of. 00:33:23.100 |
So I want to be able to do it all in one file. 00:33:34.140 |
and also Daniel, who's one of the authors of one 00:33:40.180 |
of my favorite books, which is called Two Scoops of Django. 00:33:43.340 |
They both told me about some of these functional libraries. 00:33:48.600 |
So yeah, so I'll show you guys more about this in a moment. 00:34:06.660 |
You talked about how this design, one of the goals 00:34:09.980 |
you had in mind was for it to offer a smooth path onto a more 00:34:17.140 |
full-featured website that you would do for a real app. 00:34:20.780 |
And part of that is cleaving to the underlying abstractions 00:34:26.700 |
rather than hit the impedance mismatch problem. 00:34:36.380 |
I was going to ask about calendar pickers and React 00:34:42.820 |
And they are written in Python and distributed 00:35:22.900 |
So if I start typing, it's all here, ID, inert, input mode. 00:35:39.060 |
You'll see I don't return an HTML object with a body. 00:35:48.780 |
But instead, I return a tuple with two things. 00:35:51.500 |
One is what's going to be inside the body, which, as we saw, 00:36:10.100 |
been added by my extensions, so you can ignore those. 00:36:14.540 |
So this is the thing that goes into the body. 00:36:16.380 |
And then this is the thing that goes into the head. 00:36:19.540 |
So that's-- you can return an HTML if you want to. 00:36:22.500 |
Like, I could have gone return HTML head body, 00:36:35.540 |
And we'll see why this has got some benefits later. 00:36:40.660 |
So for now, I'll just say, like, this is both more convenient. 00:36:43.460 |
And as it turned out, it's got some benefits. 00:37:01.820 |
So you can write as many children as you like. 00:37:44.180 |
So you'll see that we have a-- the main has a class. 00:37:55.220 |
OK, so that's the basic idea of constructing HTML in Python. 00:38:13.220 |
And so FastHTML uses a pretty standard approach 00:38:21.740 |
And then the decorator, this can be any HTTP verb. 00:38:33.540 |
So if you've used Flask, this is pretty standard. 00:38:37.740 |
Or FastAPI, FastHTML is loosely based on Fast-- 00:38:46.700 |
and tried to make each section of the tutorial 00:39:10.820 |
so FastHTML comes with a link ready to go for Pico CSS. 00:39:15.060 |
This is, again, something you can give people. 00:39:19.860 |
And I added some additional stuff into a stylesheet. 00:39:33.660 |
So if you look in the FastHTML repo, you'll find examples. 00:39:39.900 |
And yeah, I just changed some of the sizing a little bit. 00:39:44.500 |
I found their defaults a bit too big for my liking. 00:39:46.940 |
But you don't have to use any CSS if you don't want to. 00:39:57.140 |
OK, so how on earth does it go ul star todos? 00:40:09.140 |
Well, todos, you hopefully won't be too surprised to learn, 00:40:32.660 |
So as we've seen, if you type todos parentheses, 00:40:38.300 |
And by default, there'll be no limit and nowhere. 00:40:53.020 |
And the answer is that if your class contains a special dunder 00:41:01.620 |
I call them xt structures, standing for XML tags. 00:41:07.740 |
is what's used to render it in HTML or actually as an xt. 00:41:15.340 |
There's lots of ways you could have done this, right? 00:41:19.700 |
could instead simply have a function called xt. 00:41:26.100 |
And you could have just gone map, like that, right? 00:41:34.300 |
Like, you could certainly do it that way as well. 00:41:37.340 |
- A very minor thing, Jeremy, but the todos table, 00:41:46.060 |
if it doesn't exist, might, I think, cause issues. 00:41:50.460 |
So there's a few ways you can handle this, right? 00:42:25.500 |
So in fact, you can see, if I run these cells, 00:42:33.860 |
I've got recreate equals true, so that's actually 00:42:45.820 |
wait, is that using a different database, todos.tb? 00:43:29.180 |
rerun the database, I get back the different set of rows. 00:43:36.140 |
to have stuff in his insert that has additional things 00:43:45.260 |
to describe how to create the table if it's not already 00:43:50.820 |
But I prefer to use something called migrations, which 00:43:59.060 |
of the main application to get the initial structure in place. 00:44:08.740 |
Yeah, or some people have like a if dunder name 00:44:23.740 |
Yeah, you could have something at the top that's like, oh, 00:44:28.300 |
So yeah, there's lots of ways you can do that. 00:44:48.980 |
using an ax tag that sometimes I add an extended version 00:44:53.540 |
And when I extend a tag, I add an x to the end of it. 00:44:58.900 |
It's just like-- partly, it's like some of the defaults 00:45:04.100 |
So it starts out with the text you want is first. 00:45:07.180 |
And so I don't need any keywords here or whatever. 00:45:22.340 |
- So ax is an anchor tag that also has other-- 00:45:28.460 |
- OK, so it's not like block or some kind of superclass 00:45:31.340 |
that represents all block-like tags or anything. 00:45:36.620 |
- Otherwise, it wouldn't know what tag to use. 00:46:13.620 |
I don't know where all this data default font size is coming 00:46:21.200 |
Curious, probably some Pico thing, or I don't know. 00:46:37.260 |
that sends a delete HTTP method to that endpoint. 00:46:49.020 |
because that's how SQLite utils slash Fastlight works. 00:47:11.180 |
There's no full screen refreshes or anything. 00:47:13.780 |
But it looks like I've written a web 1.0 style app here. 00:47:21.340 |
And the trick to make this work is something called HTMX. 00:47:44.380 |
And once you've got HTMX, which is a JavaScript library that's 00:47:52.300 |
your browser behaves as if the following things are true. 00:47:55.260 |
Normally, a browser, or kind of without JavaScript and stuff, 00:48:04.780 |
It could provide a hyperlink, which, when clicked on, 00:48:09.700 |
Or it could have a form, which, when submitted, 00:48:22.700 |
as things that a browser link button, et cetera, can do. 00:48:29.460 |
was only two things could cause any action to happen. 00:48:34.860 |
Clicking a hyperlink could cause a GET request to be called 00:48:41.060 |
Or it could cause a submit to be done with the form data. 00:48:46.060 |
And again, the page to be replaced with a result. 00:48:52.140 |
HTMX makes it so that any object can cause behavior. 00:48:59.700 |
The third is that with web 1.0, the result from the server 00:49:08.140 |
can only do one thing, which is replace the whole page. 00:49:11.980 |
HTMX makes it so that you can either replace the page 00:49:16.580 |
or replace a single DOM element or delete a DOM element 00:49:20.020 |
or insert after a DOM element or insert before a DOM element. 00:49:24.060 |
So it lets you change where the result from the server goes. 00:49:44.780 |
I'm sure the fourth one will come up in a moment. 00:50:01.180 |
let's right click on this and choose Inspect-- 00:50:21.900 |
It's the sender get method, but this is an HTMX get method. 00:50:26.900 |
OK, so I mentioned that the result doesn't necessarily 00:50:53.620 |
and it'll insert the result into currentTodo. 00:50:56.060 |
So we could see what's going to happen by manually going 00:51:06.020 |
So the HTML and head and body has got added by my browser. 00:51:10.700 |
But actually, I guess if we look at the network tab, 00:51:16.380 |
you can see, actually, this was the response. 00:51:34.380 |
If I click, and you can now see, yep, there it is. 00:52:01.020 |
rather than creating some single-purpose abstraction. 00:52:05.380 |
So HTMX basically endeavors to make that web foundation 00:52:18.700 |
and doing an almost, in hindsight, the obvious thing, 00:52:38.180 |
you can just get the full page, and the full page 00:52:41.340 |
is rebuilt now with an extra to-do item in the list. 00:52:44.740 |
Yeah, I mean, maybe either now or in the future, 00:52:47.980 |
you could show us that if you've got the web 1.0 version of this. 00:52:53.780 |
So the things that FastHTML does to make HTMX more convenient 00:53:00.620 |
is, for example, by default, it includes the JavaScript 00:53:05.780 |
And in the autocomplete, all of the HX things 00:53:25.540 |
You say that it allows you to make any element interactable. 00:53:34.080 |
this is like in Cocoa Touch having a gesture recognizer. 00:53:38.100 |
It's an object that you can attach to any UI element 00:53:43.100 |
to be associated with it, taps or swipes, other things. 00:53:51.860 |
choose which kind of HTTP method is going to be executed. 00:54:01.980 |
that enable partial updates that aren't as well known 00:54:06.340 |
Or here, we've got-- sorry, you can see them here. 00:54:08.420 |
We've got DELETE to delete a TODO, POST to create a TODO, 00:54:14.660 |
But so my question is, while those HTTP methods have 00:54:24.700 |
aren't expressed by an HTTP method, that aren't just-- 00:54:40.140 |
so like, and in fact, you could use only POST, 00:54:48.740 |
You can only use PATHs for everything, and some people do. 00:54:52.860 |
So you could have slash TODO, slash DELETE, slash ID. 00:54:59.380 |
- So I can use HTMX to make an arbitrary element interactable, 00:55:02.140 |
but then I have a choice about what the action means. 00:55:08.860 |
Or it could mean, actually, hey, send a thing to my server, 00:55:13.100 |
a GET request, and that thing is now going to trigger-- 00:55:15.820 |
- Well, I mean, it always means send an HTTP request 00:55:33.480 |
you have to choose when you create any HTTP request. 00:55:36.540 |
And then on the server, this is what you write. 00:55:41.060 |
What happens when this PATH gets this HTTP method? 00:55:54.740 |
But it's still doing a round trip to the server, 00:55:56.740 |
even for incremental local modifications in the page. 00:56:05.780 |
is something where maybe it's create new user. 00:56:12.740 |
a little thing pops up saying username is available. 00:56:16.900 |
So you can absolutely attach hx-keyup or whatever. 00:56:34.100 |
as you type, it's sending stuff to the server. 00:56:36.460 |
And then the server sends back something saying, 00:56:39.740 |
put user is available into the user available MTDIV 00:57:07.940 |
Like, you wouldn't write a computer game in this 00:57:18.780 |
But anything that can happen in about 150 milliseconds 00:57:27.100 |
write JavaScript, of course, if you did want stuff that happens 00:57:33.060 |
- So I noticed that the HTMX magic is managed 00:57:51.460 |
So I know that in terms of heavyweight reusable components, 00:57:57.660 |
like, let's say I want to have a location picker, which 00:57:59.980 |
is going to present a browsable, zoomable little view into a map 00:58:04.660 |
and allow me to put a pin and then show me the address 00:58:13.940 |
but I also don't want to have to become a full-time React 00:58:18.260 |
JS person in order to be able to use it in this thing. 00:58:25.900 |
somebody who's not me or who likes Python more than I do-- 00:58:29.660 |
sorry, who likes JavaScript more than I do, hopefully, 00:58:35.900 |
So for example, there's a sortable JavaScript library. 00:58:40.780 |
- And here's an HTMX demo of using the sortable JavaScript 00:58:47.900 |
You can see these things are being dragged around. 00:58:50.780 |
And I've got to try and get it between the two. 00:59:23.420 |
So the first person to make HTMX and sortables work together 00:59:35.260 |
they can stick that on PyPy, and now everybody 00:59:37.340 |
can use it by just writing sortable equals true 00:59:51.980 |
because HTMX is using very standard JavaScript stuff. 01:00:09.340 |
is integrated with a certain data flow model. 01:00:34.220 |
these components can go in React or Vue or Angular. 01:00:50.940 |
I mean, but it seems like it's the first generation 01:00:53.300 |
of an idea that has been refined with some of these later 01:01:03.540 |
that you might get to is that there are PIP-installable 01:01:14.260 |
And as I say, it comes with some already super basic stuff 01:01:29.940 |
So let's have a look behind the scenes a little bit. 01:01:35.420 |
to look at some of the details, if that's of interest. 01:01:43.740 |
So the kind of basic thing, I guess, this is all built on 01:02:04.620 |
Now it derives from list and has three very handy properties-- 01:02:09.220 |
tag, children, and attributes, which are just 01:02:12.100 |
in case you forget the order of what's 0, 1, and 2. 01:02:17.180 |
And it also has an init that you can only pass in exactly three 01:02:37.220 |
So if you use the XT function, you pass in a tag, 01:02:40.700 |
you pass in the children, and you pass in the keywords. 01:02:53.420 |
And then that contains a child and another child 01:03:09.620 |
AUDIENCE: So why are you doing XT as a function 01:03:11.540 |
rather than patching it in as a method onto the list type? 01:03:15.180 |
Well, I guess because list isn't a type of search, OK? 01:03:19.420 |
You know, I mean, now that I've added this-- so originally, 01:03:23.740 |
Now that I've got this, I could have replaced it 01:03:29.460 |
as having under init could have done these tiny little things. 01:03:39.260 |
AUDIENCE: I'm just thinking about the analogy 01:03:44.340 |
and then you used patch to retroactively conform 01:03:47.780 |
to this de facto protocol of underscore underscore XT. 01:03:51.380 |
Yeah, but you're not doing that here with list. 01:03:55.380 |
And I don't want to change the constructor of all lists. 01:03:58.860 |
I very much doubt you even could, because it's built in. 01:04:03.100 |
OK, so then I just grabbed all of the main HTML tags 01:04:12.100 |
So now, as a result, if you call HTML, head, title, body, div, 01:04:28.500 |
Oh, actually, I've also seen some people use underscore 01:04:33.660 |
Underscore class, which means we should also allow underscore 01:04:43.080 |
OK, so there's various different ways you can spell it. 01:04:48.580 |
So yeah, so as you can see, it's just returned 1, 2, 3 element 01:05:11.660 |
So OK, so as I mentioned, they've got property names. 01:05:24.180 |
OK, so then the thing that converts that structure 01:05:43.940 |
I don't like having to make people read lots of code. 01:05:48.780 |
So then on top of that, there's fasthtml.core. 01:05:55.020 |
Now, fasthtml.core is the thing which does this stuff. 01:06:03.220 |
And this is basically a pretty thin wrapper around Starlet. 01:06:29.260 |
And basically, almost nobody understands it or knows 01:06:36.700 |
what it is, because when I started asking around 01:06:40.580 |
to try to learn about it, everybody was like, 01:06:45.020 |
So now that I do know what it is, at some point, 01:06:51.540 |
For now, I'm just going to say, don't worry about it too much. 01:06:55.260 |
It's a thing which Starlet provides that thing for you. 01:07:01.660 |
And Starlet lets you create routes that send you off 01:07:19.980 |
to be something that most people write things indirectly. 01:07:26.300 |
Or indeed, fastAPI, which is a super popular library, 01:08:05.420 |
You zoom in, and it makes everything disappear. 01:08:11.500 |
So basically, in the tutorial, it's a really good tutorial. 01:08:22.220 |
So at the end of this tutorial, you don't actually 01:08:48.380 |
So here's a list of all of the HTMX headers, for example. 01:09:07.080 |
it always includes, for example, an hx-request header. 01:09:21.280 |
just to give you a sense of what fastHTML.core does. 01:09:26.320 |
So fastHTML is something that I can create an app. 01:09:41.120 |
which I can now call cli.get on, for example. 01:09:50.160 |
So that may be the first one we should do this manually. 01:09:52.560 |
So cli.get-- it handles any HTTP verb that can go here. 01:10:04.640 |
And you can also pass in headers, cookies, et cetera. 01:10:10.680 |
So we're going to go to /hi, because we've just 01:10:23.360 |
Which has various things in it, including text. 01:10:29.800 |
It's somewhat helpful here to know a little bit 01:10:36.480 |
But maybe that's a good separate cutting point for-- 01:10:46.080 |
So I'm kind of trying to make it so you don't have 01:10:55.520 |
use a request or a response, except for the very first one 01:11:02.680 |
So if I didn't know the fast HTML way to do things, 01:11:04.960 |
but I did know, well, if you give me a starlet request, 01:11:10.880 |
But if you don't have that, then you don't need to worry about it. 01:11:13.200 |
But if you do have-- if you're fine with that, 01:11:23.880 |
you do need to know from starlet and from Flask 01:11:26.520 |
and from just about everything is that the thing where you say, 01:11:30.400 |
oh, this is the endpoint I want you to listen on, 01:11:35.760 |
And that means anything that gets put in there will work. 01:11:44.520 |
Yeah, maybe the first few we should do manually. 01:11:46.480 |
So yeah, as you say, starlet has this concept of requests. 01:12:08.320 |
to your route function, that's called request or rec or r 01:12:13.080 |
or any sub scene of request, it will automatically 01:12:40.620 |
So that's-- actually, no, it probably won't work. 01:12:48.980 |
that you could go to starlets and go to requests. 01:12:56.900 |
And there's various things in a request object. 01:13:01.420 |
is that you never have to worry about any of that. 01:13:05.740 |
So one of the things in there is a dictionary of headers. 01:13:09.540 |
So the test client uses this as their header. 01:13:24.700 |
Well, you're not going to have to worry about that either. 01:13:31.740 |
We won't do authentication today, but we will do cookies. 01:13:35.660 |
OK, so if there's a curly bracket thing here, 01:13:42.260 |
if your function has a parameter with the same name. 01:14:06.580 |
pretty standard magic for these kinds of libraries 01:14:14.180 |
And it will endeavor to cast things appropriately. 01:15:01.380 |
OK, which actually, that's exactly what this one already 01:15:13.100 |
If you return an xt, it will be automatically 2xml'd. 01:15:25.340 |
So when we call /html/1, we get back all this HTML, 01:15:40.340 |
So you can-- this is quite a nice, neat little thing. 01:15:47.900 |
You can register regular expression parameter types. 01:16:02.180 |
The type can be other more interesting things. 01:17:02.300 |
sorry, yes, /models/gpt5, then I'll get back a value error. 01:17:09.820 |
gpt5 is not a valid model name, at least not at the point 01:17:14.340 |
OK, so you can also put stuff as query parameters. 01:17:31.220 |
So again, it just comes through exactly the same way. 01:17:38.460 |
They'll be lowercased and dashes turned into underscores. 01:18:11.220 |
Like, it would read the user agent from the request 01:18:27.980 |
You can see here, this is the expected value. 01:18:41.740 |
ANDREW BROGDON: Yeah, I understand the return 01:18:44.260 |
But what I'm looking at is the parameters in the-- 01:18:57.540 |
means I'm going to take my input parameter from the HTTP 01:19:04.940 |
There's only two special ones, which is HTMX and request. 01:19:28.300 |
So it just keeps looking to try and-- it does everything it can 01:19:47.100 |
So this is where we do, for now at least, require-- 01:20:01.100 |
It will grab the cookie, because it's not a header, 01:20:04.660 |
and it's not a query string, and it's not a path object. 01:20:13.620 |
Also, you can pass things into a POST request. 01:20:18.100 |
So in this case, the data we're going to pass in 01:20:33.300 |
and you have a data class, it will automatically 01:21:17.260 |
It'll be a and the string one, because it has no idea what 01:21:22.700 |
So this is a good reason that data classes work nicely 01:21:25.740 |
with FastHTML, and that's why I added Fastlight's data class 01:21:31.500 |
- And you could do, like, if you wanted to be real fancy, 01:21:35.060 |
you could do Pydantic and then have custom validation on that. 01:21:40.340 |
needs to be a string that starts with these letters. 01:21:45.660 |
I would rather do that in the function, the handler. 01:21:51.580 |
To me, that's a better place for that, unless-- 01:21:54.900 |
yeah, I don't see the point of using Pydantic for that, 01:22:01.260 |
issues in a more nuanced, handler-specific way. 01:22:27.900 |
So Bodhi2 is a number, because it's looked inside here 01:22:40.700 |
Yeah, so it tries-- basically, FastHTML does everything 01:22:57.820 |
from the last time I built a significant web app endpoint. 01:23:00.420 |
There's so many of the design patterns are similar. 01:23:02.060 |
I'm not sure if this is because how everyone builds endpoints, 01:23:22.100 |
it work exactly the same way, except the return is not 01:23:35.020 |
is that the behavior of search with fall through when 01:23:43.780 |
Is the key something that was given to me in the path? 01:23:46.540 |
OK, well, is it something that's there in a query parameter? 01:23:52.820 |
And when I sent this code to John O, he was happy. 01:24:01.140 |
And what I was writing was, just always give me the request. 01:24:12.420 |
Because I didn't know what magic you were doing. 01:24:14.500 |
And so I was like, OK, fine, I can see it now. 01:24:18.220 |
It's slightly magic, and I'm not saying that's bad. 01:24:26.340 |
The one possible failure mode I could imagine here 01:24:29.140 |
is if someone naively chose as their name for a parameter, 01:24:32.820 |
a query parameter, a key that's already conventionally used 01:24:37.260 |
as a header name, or a cookie name, or something like that. 01:24:38.900 |
Well, that would be OK, because headers go last. 01:24:43.860 |
But if you have a query parameter and a path parameter 01:24:48.020 |
a query parameter that's the same as a cookie, 01:24:51.740 |
Well, also fortunately, the header field namespace 01:24:59.060 |
It's a relatively orderly part of the universe. 01:25:16.540 |
because we're trying to understand their interaction. 01:25:24.300 |
We have to choose the string that represents the path. 01:25:30.460 |
of the function names and the kind of parameters 01:25:38.780 |
The name of the function, you could call them all underscore, 01:25:45.460 |
And it bothers me a little bit that there's just like-- yeah. 01:25:48.260 |
So the reason I haven't done anything about that 01:25:51.580 |
is because this is how fast API and Flask and everything works. 01:25:56.060 |
Yeah, when we're building the web from scratch, 01:26:01.260 |
one wouldn't want to have this weird world where 01:26:13.180 |
And then another language, which expresses the action that's 01:26:19.340 |
represents the action that's performed in the server 01:26:23.180 |
And then you have a whole mapping thing in between. 01:26:25.860 |
And you have to like, well, is this the right name? 01:26:48.460 |
going to respond back with a full web page full of all 01:26:58.660 |
And so then I can have a separate function that's 01:27:10.340 |
time naming this function that never gets used? 01:27:18.340 |
You can't use the function name anymore, right? 01:27:29.420 |
But I don't spend a lot of time doing web development. 01:27:36.140 |
or informed by my vast ignorance of the domain. 01:27:39.540 |
I'm not sure which of those I should listen to. 01:27:48.700 |
I had a feeling that was going to take a while. 01:27:54.780 |
But this is something that we'll be showing people 01:28:00.900 |
How to make it play nicely with the database stuff 01:28:06.820 |
Like, why those data classes play so nicely with it.