back to index

FastHTML first look - Answer.AI dev chat #4


Whisper Transcript | Transcript Only Page

00:00:00.000 | Hello again, another Dev Chat with Jono and Alexis.
00:00:07.280 | And a lot of stuff to share today.
00:00:17.040 | Actually, maybe a good place to start
00:00:18.920 | would be some updates from yesterday, which
00:00:23.320 | I haven't shown anybody yet.
00:00:25.160 | I kind of felt like it was going slowly,
00:00:29.480 | but then when I was done, I was like, oh, I actually
00:00:31.140 | got quite a lot done.
00:00:32.080 | So that's often the way, isn't it?
00:00:35.660 | You just kind of keep slowly battling away.
00:00:38.360 | So in Fastlight-- tell me if you guys--
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:02.440 | And so what that does is it creates a type.
00:01:10.320 | That type is a data class.
00:01:12.960 | You can make stuff with it.
00:01:18.520 | So the reason why is, for a few reasons.
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:43.280 | and stuff like that, particularly,
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:53.600 | So basically, given that this is something
00:01:55.640 | that you pass keyword arguments into,
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:09.640 | or none.
00:02:12.280 | Things like SQL model, Sebastian's thing,
00:02:15.920 | are much more careful about actually knowing
00:02:18.120 | if it's nullable or not, and if it's not nullable,
00:02:20.640 | it doesn't say so, blah, blah, blah.
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:29.480 | It's created by the database for you.
00:02:33.120 | And so in SQL model, you have to have
00:02:34.960 | different types for the update version and the create version.
00:02:38.080 | And in this case, just like, you know what?
00:02:39.840 | I'm not going to be strict about this.
00:02:45.440 | So anyway, that way, I've got a dictionary here
00:02:53.760 | for AC/DC albums.
00:02:57.240 | There are a lot more.
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:02.920 | good ones.
00:03:04.720 | So you can pass those in for a particular AC/DC album
00:03:08.520 | as keyword arguments.
00:03:11.760 | And you get back an object.
00:03:14.540 | OK, so that's basically how you can
00:03:21.680 | convert the result of a call for your model,
00:03:27.140 | for querying something in the model.
00:03:28.600 | Now you've got back an object.
00:03:31.520 | AUDIENCE: Could I interrupt with a basic question here?
00:03:33.760 | MARTIN SPLITT: Any time.
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:03:55.040 | to recreate the same data class.
00:03:57.040 | AUDIENCE: OK, so it is.
00:03:59.640 | So in that case--
00:04:01.200 | MARTIN SPLITT: There's no magic here.
00:04:03.880 | There's actually-- the data classes
00:04:06.600 | thing in the standard library has something
00:04:08.400 | called make_data_class in it.
00:04:10.200 | AUDIENCE: OK.
00:04:12.800 | In that case, would you say it's more idiomatic
00:04:15.480 | if album_dc was capitalized because it's
00:04:18.320 | a class definition in Constructor, or not really?
00:04:22.800 | MARTIN SPLITT: Yeah, maybe.
00:04:25.120 | I'm not really going to use it for much, as you'll see.
00:04:27.420 | So don't worry too much about that.
00:04:28.880 | AUDIENCE: OK, just wanted to double-check my understanding.
00:04:31.320 | MARTIN SPLITT: Yeah, yeah, it's good.
00:04:32.960 | So I couldn't find something that spits out the source
00:04:38.160 | code for a data class.
00:04:40.440 | So I just wrote a little one.
00:04:44.440 | It doesn't do everything, but it does enough
00:04:46.520 | to cover what this particular one does, at least.
00:04:49.600 | So this would actually recreate that.
00:04:52.800 | So the reason that's potentially interesting
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:26.400 | create a module.
00:05:29.600 | And so create_mod is actually going
00:05:31.240 | to do every single table.
00:05:40.640 | And if you want to, also every single view.
00:05:42.640 | It's going to print out the data class source for all of them.
00:05:46.600 | So here, if I go create_mod_db, then this
00:05:51.320 | will create a file that contains all of them.
00:05:59.600 | Now, in Jupyter, that's entirely useless.
00:06:03.000 | But if you use VS Code, it's very useful,
00:06:08.840 | because you can now do that, or import star, or whatever.
00:06:14.920 | And I've now got--
00:06:20.920 | even in VS Code, you now have autocomplete.
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:32.320 | and then it builds the database from that.
00:06:34.080 | This is like, oh, you have the database.
00:06:35.960 | We'll build the data classes from that.
00:06:37.760 | Yeah, exactly, exactly.
00:06:41.680 | And I don't love CodeGen.
00:06:44.200 | But in the end, if you want VS Code support,
00:06:46.400 | you have to do one of them.
00:06:47.800 | And the reason I prefer this one, for me,
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:02.760 | or using a migration system, or whatever.
00:07:05.200 | It doesn't matter.
00:07:06.160 | And at any point, I could just reflect that into my module
00:07:08.680 | with a single line of code.
00:07:10.400 | So I think that's good.
00:07:14.820 | AUDIENCE: The $5 word for that benefit
00:07:16.440 | would just be to say composability, right?
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:32.120 | AUDIENCE: Well, it is in the sense
00:07:33.480 | that what you've created now composes
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:40.440 | JOHN MUELLER: OK.
00:07:41.160 | I guess I can see that.
00:07:43.480 | Now, we already had this thing where you could--
00:07:46.040 | I think, or is this new?
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:07:55.480 | WHERE ORDER BY LIMIT OFFSET.
00:07:59.720 | But the thing that's now been added
00:08:01.240 | is that when you call dot data class,
00:08:14.480 | it optionally, and by default, also
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:30.560 | it automatically casts it.
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:38.280 | They're actually data classes.
00:08:41.000 | So this is slightly opaque that, oh,
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:52.920 | when you call it.
00:08:55.560 | Yep, exactly.
00:08:57.000 | And it's not meant to be opaque.
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:12.520 | Oh, here we go.
00:09:17.360 | It's at the end.
00:09:18.840 | If you want to have all tables, just call all DCs.
00:09:23.520 | And probably you would actually just not
00:09:27.320 | bother with the return type.
00:09:30.200 | This is how you turn it on, is you just run that.
00:09:33.960 | So you probably always 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:09:58.480 | particularly to UPDATE, INSERT, and UPSERT.
00:10:02.800 | So if you haven't come across UPSERT,
00:10:06.080 | it's not a SQL keyword.
00:10:09.760 | It's a concept, which is that a lot of databases,
00:10:12.800 | including SQLite--
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:20.560 | And if the thing is already there,
00:10:22.000 | then update the existing thing where already there
00:10:24.560 | is referring to the primary key.
00:10:27.400 | So let's create a new table called CATS.
00:10:35.560 | So one of the nice things about SQLite utils
00:10:38.520 | is the fact that CATS is not there doesn't matter.
00:10:41.160 | OK, we've still got a table CATS.
00:10:46.160 | It just doesn't exist.
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:10:53.120 | by seeing if CAT is in dt or is in dt.
00:10:57.160 | Oh, well, I've just added that feature, yes.
00:11:00.560 | So I can say CATS in db.
00:11:09.320 | Oh, is it dt?
00:11:11.000 | Yeah.
00:11:11.760 | So I added a DUNDA contain support to this yesterday.
00:11:15.600 | Oh, nice.
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:23.000 | Yep, OK.
00:11:24.520 | So then I can create it.
00:11:27.200 | And then I can look at the schema.
00:11:28.560 | I added this thing.
00:11:29.600 | I think it's-- I've added it to Fastcore.
00:11:32.080 | Highlight-- it's really badly named.
00:11:34.720 | Well, that's right.
00:11:35.560 | Highlight as a markdown output.
00:11:37.080 | This is actually a markdown output.
00:11:38.160 | You can say what language.
00:11:39.280 | This is just a minor convenience.
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:49.040 | Or the Stringify version is now true.
00:11:53.680 | So we've created a CATS table.
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:12.640 | Or that it takes keyword arguments.
00:12:16.840 | Right.
00:12:57.580 | Maybe we should just mention this curious feature
00:13:00.260 | about SQLite utils.
00:13:26.740 | So then something else I added is now
00:13:28.460 | when you insert or upsert, you get back the inserted row.
00:13:33.940 | Now, we haven't called .dataclass yet.
00:13:45.860 | I mean, we called it earlier, but that was before we--
00:13:50.820 | all VCs.
00:13:52.020 | We haven't-- but the CATS didn't exist yet at that point.
00:13:55.140 | So therefore, we got back the dictionary.
00:13:57.460 | So here's an example of using upsert.
00:14:03.140 | But yeah, so now if we do the dataclass thing--
00:14:06.420 | and let's just show you.
00:14:07.420 | There's no point storing that.
00:14:10.940 | It's just enabling it.
00:14:12.100 | So now if I go cat equals CATS.get1.
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:37.460 | in is a--
00:14:39.380 | OK, that type annotation is now wrong.
00:14:43.260 | Let's fix that, shall we?
00:14:44.700 | BIM_FASTLIGHT/keyword_arguments DEF-- well, there it is.
00:14:54.020 | DEF_UPSERT.
00:14:54.580 | OK, so that's a dictionary of string to any or a data class
00:15:02.060 | which isn't of any particular type.
00:15:03.540 | So I think you just have to call it any.
00:15:05.200 | Cool.
00:15:10.220 | So you can see there wasn't much code for me
00:15:13.740 | to write to add this behavior.
00:15:15.900 | So this is actually something quite nice
00:15:20.020 | I added to Fastcore yesterday.
00:15:22.180 | When you patch something--
00:15:26.060 | so in this case, I'm patching table.
00:15:28.420 | If the something already exists, it
00:15:30.900 | stores the original version of that function
00:15:34.260 | now as _orig_ with that function name.
00:15:37.620 | This is a really nice way for me to now patch new behavior.
00:15:41.620 | So it's just calling the original version,
00:15:43.980 | but it's doing the data class thing.
00:15:49.860 | And at the end--
00:15:50.540 | As long as you don't patch twice.
00:15:52.860 | No, it's fine to patch twice.
00:15:54.460 | It checks if there's already an _orig__upsert there.
00:15:57.100 | So you can patch--
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:05.220 | Or is the default behavior to advise,
00:16:08.080 | meaning that it executes both of them?
00:16:11.060 | So if you repatch, then _orig__upsert will still
00:16:14.940 | be the _orig___orig___upsert.
00:16:18.980 | So it clobbers the old patch.
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:33.420 | If you repatch the patched method,
00:16:37.260 | then the original patch--
00:16:38.860 | Oh, OK, I see.
00:16:40.620 | OK, got it.
00:16:43.180 | OK, so-- oh, we can drop the table.
00:16:47.420 | So that-- yeah, that was basically
00:16:52.580 | stuff that I added as I started to implement the next thing
00:16:55.660 | I'll show you.
00:16:57.460 | Nice.
00:16:57.960 | All right, so this is where things get fun,
00:17:04.620 | is we have a to-do list application.
00:17:11.100 | This to-do list application is like--
00:17:19.620 | it's got nice resizing behavior.
00:17:21.660 | It all feels very modern.
00:17:23.260 | When I click on things, there's no full-screen refresh.
00:17:26.780 | I can add new to-dos to people.fast.html.
00:17:33.140 | People.support.add just appears.
00:17:35.740 | Click on that.
00:17:38.100 | Change it.
00:17:39.620 | Enter.
00:17:40.660 | And it's kind of like it feels like a very--
00:17:43.220 | you can click on anything at any time,
00:17:48.100 | and I haven't found a way to break it.
00:17:49.700 | Do you know what I mean?
00:17:50.660 | As you can see, it's fast.
00:17:51.820 | And obviously, I haven't spent much time styling this.
00:17:57.740 | I haven't spent any time styling this.
00:17:59.660 | But it looks and feels like a reasonably modern kind
00:18:06.940 | of web application.
00:18:09.660 | The entire thing is written in 69 lines of code, many of which
00:18:19.140 | are blank.
00:18:19.640 | And it's all Python.
00:18:24.260 | And there's no templates.
00:18:25.820 | There's no JavaScript.
00:18:27.140 | And there's no CSS.
00:18:28.860 | It's literally a single file of Python.
00:18:30.660 | So this is thanks to a thing I want
00:18:35.660 | to show you guys, which I know both of you have seen bits of,
00:18:39.380 | called fast.html.
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:03.060 | and stuff, which are really great systems.
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:32.260 | complicated framework that's harder
00:19:34.060 | than it would have been to just use it in the first place.
00:19:36.940 | So the idea of this is to let you
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:50.820 | Now you're going to have to use TypeScript,
00:19:52.580 | or now you're done.
00:19:53.380 | You're going to have to learn to create your own IPyWidget
00:19:57.340 | plumbing, or like Solara, like FastUI.
00:20:00.700 | And it's actually loosely based on the framework
00:20:06.820 | I created--
00:20:08.500 | jeez, it's 25 years ago--
00:20:10.620 | for FastMail, which supported one of the busiest
00:20:14.820 | sites on the internet.
00:20:16.500 | It's this approach scales out in any way you like.
00:20:22.260 | So that's the goal.
00:20:23.820 | It's basically reuse your Python knowledge.
00:20:28.180 | You can use any CSS framework.
00:20:30.020 | You can use any JavaScript library.
00:20:31.620 | You can use Web Components.
00:20:32.980 | You can use all that stuff.
00:20:36.420 | And if you create something with your Python
00:20:39.300 | that you think other people might 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:46.900 | Component library, or whatever.
00:20:49.780 | And ditto for other peoples.
00:20:52.140 | So the particular one that, at the moment,
00:20:54.340 | I think it'll probably ship with is--
00:20:56.700 | the CSS is called Pico, super simple, lightweight thing.
00:21:01.300 | But I'm pretty sure, quite soon, we'll
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:20.660 | There's no build step.
00:21:22.180 | There's no code gen.
00:21:24.180 | So in fact, let's take a look.
00:21:26.660 | So if I change this here to to-do list demo,
00:21:36.860 | and I'll pop up the terminal.
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:45.740 | And back over here, there it is.
00:21:49.900 | It's-- yeah, because there's no build step or anything to do.
00:21:53.540 | It just keeps going.
00:21:55.140 | So all right.
00:21:58.460 | So let me show you how the app is built.
00:22:03.620 | And--
00:22:04.740 | ANDREW FITZ GIBBON: Can I interrupt, again,
00:22:06.060 | one or two quick questions?
00:22:07.180 | CHRIS BROADFOOT: Any time.
00:22:08.180 | You don't need permission to interrupt.
00:22:09.500 | You can--
00:22:09.700 | ANDREW FITZ GIBBON: All right.
00:22:11.000 | Then I'll interrupt a little more liberally.
00:22:12.980 | So I'm aware of what Gradio does.
00:22:15.900 | And I think I've used it once or twice.
00:22:17.540 | But I have never explored it in detail
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:27.420 | to a full application.
00:22:29.340 | CHRIS BROADFOOT: Sure.
00:22:30.580 | ANDREW FITZ GIBBON: So I was wondering--
00:22:31.900 | hard to summarize, but if you could give a sense of,
00:22:34.740 | what is it about the way it works that--
00:22:37.140 | or Streamlit works that you feel makes it--
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:22:46.820 | CHRIS BROADFOOT: Yeah, absolutely.
00:22:49.060 | [AUDIO OUT]
00:22:52.420 | [AUDIO OUT]
00:22:55.900 | [AUDIO OUT]
00:22:59.380 | [AUDIO OUT]
00:23:02.860 | [AUDIO OUT]
00:23:05.860 | [AUDIO OUT]
00:23:08.860 | [AUDIO OUT]
00:23:11.860 | [AUDIO OUT]
00:23:14.860 | [AUDIO OUT]
00:23:17.860 | [AUDIO OUT]
00:23:20.860 | [AUDIO OUT]
00:23:23.860 | [AUDIO OUT]
00:23:26.860 | [AUDIO OUT]
00:23:28.860 | That's not how it works.
00:23:30.860 | [AUDIO OUT]
00:23:33.860 | [AUDIO OUT]
00:23:36.860 | So while you're finding that, Alexis,
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:55.380 | but it gets--
00:23:56.580 | it's very, very nice for-- like, this kind of interface here
00:23:59.300 | is so easy in Gradio.
00:24:02.180 | But trying to move to something where, I don't know,
00:24:05.260 | you're storing stuff in the user's cookies,
00:24:07.620 | or you're doing state, or you're keeping
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:16.180 | Yeah, so here's an example.
00:24:17.780 | Right, let's have a look at an example.
00:24:19.540 | So here's an example of something
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:30.580 | model on it.
00:24:32.060 | It's indeed a doggie.
00:24:34.780 | This is a classic kind of Gradio interface.
00:24:38.820 | And if you look at how it's implemented,
00:24:45.380 | you create an image, you create a label,
00:24:49.980 | and then you create an interface that will call this function.
00:24:53.980 | These are your inputs, this is the output,
00:24:56.820 | and these are examples.
00:24:58.380 | It's a very specific kind of application it can create.
00:25:04.620 | And for that particular kind of application,
00:25:06.820 | it creates very quickly and easily.
00:25:09.820 | It's not a general-- like you couldn't create Instagram
00:25:14.820 | in this.
00:25:15.320 | Right.
00:25:15.820 | If you wanted to create Instagram, well,
00:25:21.500 | Meta, at the time before it was Meta,
00:25:24.100 | I guess originally it was created by Instagram,
00:25:26.060 | but now you use Django.
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:39.200 | So fast HTML is like Django.
00:25:43.940 | It lets you build anything.
00:25:46.060 | And Streamlet's similar to Gradio.
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:55.100 | to call everything.
00:25:55.980 | That's it.
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:10.140 | but they don't fundamentally map on
00:26:11.900 | to the underlying abstractions of how a website is built.
00:26:15.500 | Like--
00:26:16.000 | Precisely.
00:26:17.060 | They have an HTML and everything.
00:26:18.700 | And so you hit that--
00:26:19.580 | Let's take a look at this example.
00:26:20.980 | --impedance mismatch.
00:26:22.180 | So let's take a look at this example
00:26:23.780 | of how-- compare it to this.
00:26:26.580 | Let's instead see how was this screen created.
00:26:37.860 | So this screen has got a header.
00:26:41.860 | It's got a thing here with a button and an input.
00:26:45.980 | It's got a list of to-dos.
00:26:47.500 | And optionally, got some details about the to-do underneath.
00:26:52.300 | So this one here--
00:26:54.540 | and it's on the root of our website.
00:26:59.260 | So here is the implementation of that.
00:27:06.420 | And it contains-- if you look at the HTML, it contains a main.
00:27:20.780 | The main contains an H1.
00:27:22.780 | The H1 contains an article.
00:27:24.620 | The article contains a header, an unordered list,
00:27:28.220 | and a footer.
00:27:30.980 | So the thing that you write here is actually
00:27:33.140 | the same as the HTML.
00:27:34.060 | So OK, give me a main with an H1.
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:27:51.620 | with an input and a button.
00:27:55.140 | And we'll give the whole thing a title,
00:27:57.780 | which you can see up here.
00:28:02.020 | So yeah, in this case, the thing that we're actually working
00:28:07.380 | with is HTML.
00:28:09.820 | And also, we're working with things like posts.
00:28:16.900 | And IDs.
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:29.900 | or whatever it is.
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:37.500 | are not totally different.
00:28:40.620 | They're tiny wrappers over the basic foundations
00:28:45.220 | of the internet.
00:28:47.700 | - OK.
00:28:48.540 | And those things that look like function applications
00:28:51.380 | are creating data structures that you then
00:28:59.020 | convert into HTML later.
00:29:00.860 | - Yes, which we will see.
00:29:02.100 | - OK.
00:29:02.420 | - Yes.
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:07.700 | So I have a second question.
00:29:08.860 | - What language is that in?
00:29:10.580 | - It's in Clojure, C-L-O-J-U-R-E.
00:29:16.540 | And--
00:29:17.660 | - Oh, yes, it was J-U-R-E.
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:36.420 | So when you want to build a big HTML page,
00:29:39.060 | you kind of build it just by essentially doing
00:29:42.020 | things that look like nested function
00:29:43.540 | calls that are actually just returning
00:29:46.620 | the basic data structures of vector and dictionary
00:29:50.740 | and a list that mirrors the same thing.
00:29:53.140 | - Yeah.
00:29:53.500 | - And this is very different to--
00:29:54.780 | some libraries will have a form object, right,
00:29:57.700 | that's a Python object or a Pydantic model
00:30:00.380 | or something like that.
00:30:01.900 | And then it also has, on the front end,
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:10.700 | object and the actual front end.
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:19.220 | the HTML form.
00:30:20.740 | So there's a lot of machinery in the middle
00:30:23.420 | there, which is sometimes very nice.
00:30:25.260 | But yeah, in this case, it's like, oh, you just
00:30:27.260 | write the thing that produces that HTML.
00:30:30.980 | - Well, one of the kind of aesthetics that--
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:42.340 | structures are, again, $5 word, composable
00:30:45.620 | with generic manipulators that you already
00:30:47.620 | have for basic data structures.
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:00.140 | just with a comprehension.
00:31:01.340 | - So in fact, every one of those HTML functions
00:31:05.940 | is returning a three-element Python list.
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:19.820 | and a dictionary of the attributes.
00:31:22.220 | And yeah, it's interesting you mentioned the Clojure one.
00:31:24.500 | I haven't seen that before.
00:31:25.580 | But basically, almost every functional language
00:31:27.540 | has a version of this.
00:31:28.540 | So Haskell has one.
00:31:30.540 | OCaml has one.
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:31:44.700 | or those OCaml and Haskell ones are.
00:31:47.420 | It looks like the Clojure one.
00:31:50.580 | And it's how I wrote FastML.
00:31:53.860 | That was written in Perl.
00:31:55.620 | But to me, apart from everything else,
00:32:00.220 | I don't like switching between files.
00:32:02.500 | So templates were created back in the day
00:32:08.340 | because web design was very difficult because you
00:32:13.020 | had to write lots of HTML in order
00:32:14.940 | to deal with the quirks of each browser.
00:32:17.060 | And a lot of the visual representation
00:32:20.740 | was contained in the HTML.
00:32:22.580 | That before CSS, all of it was.
00:32:25.380 | So because we had web designers, that
00:32:27.340 | was a very specific kind of job which
00:32:31.260 | involved, to a large degree, knowing about quirks
00:32:33.780 | of browsers and stuff.
00:32:35.900 | Web designers needed to be able to write HTML and look
00:32:40.580 | at the HTML.
00:32:41.660 | And they were not generally coders.
00:32:43.940 | So we came up with this idea as a community of being like,
00:32:46.420 | oh, let's give them templates.
00:32:47.700 | And so the place where the name will go
00:32:50.900 | will be like curly bracket name.
00:32:52.300 | And the place for the email will be curly bracket email.
00:32:54.340 | And they don't have to worry about that.
00:32:55.700 | They can design the whole thing, give it back to the coder,
00:32:59.260 | and then that's now a template they run.
00:33:02.460 | This doesn't seem useful anymore because we just
00:33:05.900 | do semantic HTML anyway.
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:15.020 | is HTML, the second 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:27.700 | And yeah, this functional approach,
00:33:29.540 | it was actually Austin who--
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:01.140 | But--
00:34:01.620 | I got one more question.
00:34:02.580 | Sorry.
00:34:03.100 | You said I had a license to ask questions.
00:34:04.900 | So here's question number two.
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:25.020 | of the browser so you can work with them,
00:34:26.700 | rather than hit the impedance mismatch problem.
00:34:29.020 | But another part of it is using components
00:34:30.780 | that other people have made.
00:34:33.660 | Like this one, for example, which I made.
00:34:36.380 | I was going to ask about calendar pickers and React
00:34:38.740 | components and stuff like that.
00:34:39.980 | This one, which I made.
00:34:41.260 | So we will see that in a moment.
00:34:42.820 | And they are written in Python and distributed
00:34:45.660 | as pip-installable things.
00:34:47.780 | And so, yeah, we'll see that.
00:34:50.020 | So let's look more at how to implement
00:34:59.980 | this particular form.
00:35:03.020 | So I'm going to assume you know HTML.
00:35:06.900 | So you know there's a thing called a form.
00:35:09.180 | You know there's a thing called a button.
00:35:11.500 | And as you see, we've got like, you know,
00:35:19.980 | it's all autocompletes all here.
00:35:22.900 | So if I start typing, it's all here, ID, inert, input mode.
00:35:30.500 | So I've got all of the attributes there
00:35:33.660 | as autocompletable things.
00:35:34.860 | But they're all standard stuff.
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:35:59.740 | is a main.
00:36:01.100 | So the body contains a main.
00:36:05.540 | The other things that are there have
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:33.300 | but you don't have to.
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:36:45.300 | All right, so the child of a--
00:36:51.740 | so when you put in a tag name, you'll
00:36:57.820 | see that the first thing is star C.
00:37:00.460 | This is where you put children.
00:37:01.820 | So you can write as many children as you like.
00:37:03.740 | So title, hello, there, whatever you like.
00:37:11.060 | Put a link there.
00:37:15.740 | So that's fine.
00:37:22.660 | And then, yeah, any attributes are just
00:37:29.420 | attributes to the tag.
00:37:35.860 | Class is special.
00:37:37.420 | You can't write class equals, because class
00:37:40.420 | is a keyword in Python.
00:37:42.420 | So you have to write C or S equals.
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:09.460 | You have to have it respond to this.
00:38:13.220 | And so FastHTML uses a pretty standard approach
00:38:16.500 | of having a decorator.
00:38:18.300 | So you start out by creating a FastHTML app.
00:38:21.740 | And then the decorator, this can be any HTTP verb.
00:38:26.380 | So get, we'll do a get.
00:38:31.820 | And this is what it's going to listen on.
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:43.020 | or quite strongly based on FastAPI.
00:38:44.580 | I went through the FastAPI tutorial
00:38:46.700 | and tried to make each section of the tutorial
00:38:49.100 | work with the same syntax.
00:38:50.340 | So when you create your FastHTML app,
00:39:00.100 | since we optionally make it, you don't
00:39:02.220 | have to manually create your HTML header.
00:39:04.460 | You have to have some way to say what
00:39:06.060 | headers are going to be there.
00:39:07.300 | So this is how you do that.
00:39:09.220 | And in this case, it's going to be--
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:17.340 | It's just these things predefined.
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:07.300 | What's going on there?
00:40:09.140 | Well, todos, you hopefully won't be too surprised to learn,
00:40:13.140 | comes from Fastlight.
00:40:15.500 | So I create a database using SQLite utils.
00:40:20.420 | And then I ask for the todos table.
00:40:25.260 | And you'll be pleased to see, Alexis,
00:40:27.460 | I used capital T for my todos data class.
00:40:32.660 | So as we've seen, if you type todos parentheses,
00:40:36.820 | it calls a select statement.
00:40:38.300 | And by default, there'll be no limit and nowhere.
00:40:40.980 | And therefore, this is all of your todos.
00:40:45.700 | They get passed as children to a ul.
00:40:48.300 | So how on earth does an unordered list
00:40:50.900 | render a data class?
00:40:53.020 | And the answer is that if your class contains a special dunder
00:40:58.940 | xt, these things are structures.
00:41:01.620 | I call them xt structures, standing for XML tags.
00:41:05.300 | If you have this special thing, this
00:41:07.740 | is what's used to render it in HTML or actually as an xt.
00:41:12.420 | So I patch this into my todo.
00:41:15.340 | There's lots of ways you could have done this, right?
00:41:17.500 | Like, if you don't like this, you
00:41:19.700 | could instead simply have a function called xt.
00:41:23.580 | You could get rid of patch.
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:36.500 | Either one's fine.
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:48.780 | So-- - Yep, exactly.
00:41:50.460 | So there's a few ways you can handle this, right?
00:41:55.620 | So in my case, I just have a little notebook
00:42:19.620 | where I've been fiddling around with things,
00:42:21.460 | and I created it there.
00:42:23.820 | And I added a few todos to it.
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:36.060 | deleted and recreated it.
00:42:37.300 | So if I now go back to here--
00:42:45.820 | wait, is that using a different database, todos.tb?
00:43:02.580 | That's strange.
00:43:15.540 | Hm, now why did refreshing--
00:43:19.060 | something was caching that.
00:43:23.060 | Interesting.
00:43:24.540 | Yeah, that's what I think about.
00:43:26.460 | Yeah, so if I change the--
00:43:29.180 | rerun the database, I get back the different set of rows.
00:43:32.060 | So yeah, it's like--
00:43:33.820 | Simon Willison, in his examples, tends
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:48.660 | there, kind of neat.
00:43:50.820 | But I prefer to use something called migrations, which
00:43:54.060 | we probably won't talk about today,
00:43:56.420 | to do more stuff in SQL or outside
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:15.500 | equals main at the bottom.
00:44:17.460 | And that way, you can run Python db app.py,
00:44:20.980 | and it would create things.
00:44:23.740 | Yeah, you could have something at the top that's like, oh,
00:44:26.140 | if it doesn't exist, then create it.
00:44:28.300 | So yeah, there's lots of ways you can do that.
00:44:31.060 | But it's a good point.
00:44:36.860 | Now, the each todo has two links.
00:44:44.340 | And so here are those two links.
00:44:46.660 | Now, rather than using an a tag, I'm
00:44:48.980 | using an ax tag that sometimes I add an extended version
00:44:52.900 | of a tag.
00:44:53.540 | And when I extend a tag, I add an x to the end of it.
00:44:57.020 | And it's not very extended.
00:44:58.900 | It's just like-- partly, it's like some of the defaults
00:45:03.260 | are a bit different.
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:10.380 | - So a doesn't mean anchor.
00:45:12.020 | a is a general purpose constructor
00:45:14.060 | for a tag-like structure.
00:45:15.780 | - No, a means anchor.
00:45:17.020 | This is an anchor tag.
00:45:18.540 | Yeah, these are all-- you see their links?
00:45:20.300 | They're a links.
00:45:21.060 | OK, yeah.
00:45:22.340 | - So ax is an anchor tag that also has other--
00:45:24.380 | - An extended version of an anchor tag,
00:45:25.980 | or an extended version of a hyperlink, yeah.
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:33.340 | It's just an anchor.
00:45:34.140 | Got it.
00:45:36.620 | - Otherwise, it wouldn't know what tag to use.
00:45:39.220 | I didn't see.
00:45:41.580 | Yeah, so each to-do item is a list
00:45:46.100 | that contains that first link.
00:45:48.860 | And maybe it's done.
00:45:51.940 | And then that second link.
00:45:54.100 | And also, it's got an ID.
00:45:55.940 | So it knows which to-do it is.
00:45:58.500 | So it's got this little tiny function here,
00:46:00.340 | which just returns to-do dash ID.
00:46:03.700 | So if I look at one of these list items,
00:46:11.460 | you can see here, li to-do one.
00:46:13.620 | I don't know where all this data default font size is coming
00:46:20.700 | from.
00:46:21.200 | Curious, probably some Pico thing, or I don't know.
00:46:29.180 | OK, yeah, so when I click Delete,
00:46:37.260 | that sends a delete HTTP method to that endpoint.
00:46:46.420 | And I can just call todos.delete,
00:46:49.020 | because that's how SQLite utils slash Fastlight works.
00:46:58.780 | So the next thing to talk about is
00:47:00.620 | how come we have what appears to be a SPA?
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:25.820 | And HTMX basically adds what I think
00:47:30.060 | of as the obvious missing features
00:47:35.980 | to your browser and HTML.
00:47:40.420 | And so it adds four features, basically.
00:47:44.380 | And once you've got HTMX, which is a JavaScript library that's
00:47:49.860 | auto-installed if you use FastHTML,
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:01.540 | a web 1.0 browser, could do two things.
00:48:04.780 | It could provide a hyperlink, which, when clicked on,
00:48:07.700 | sent a GET request.
00:48:09.700 | Or it could have a form, which, when submitted,
00:48:11.980 | sends normally a POST request.
00:48:15.060 | There are lots of other HTTP methods.
00:48:18.260 | So HTMX adds all of those other methods
00:48:22.700 | as things that a browser link button, et cetera, can do.
00:48:26.580 | The second issue with web 1.0 HTML
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:38.420 | and the entire page replaced with a result.
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:31.180 | Damn it, what's the fourth one?
00:49:36.820 | So we've got any HTTP method.
00:49:39.460 | You've got what you change.
00:49:41.420 | You've got what can trigger it.
00:49:44.780 | I'm sure the fourth one will come up in a moment.
00:49:47.580 | So the way this works is that you
00:49:53.740 | add additional attributes that's--
00:50:01.180 | let's right click on this and choose Inspect--
00:50:03.140 | that have an HX at the start.
00:50:08.940 | So HX-get means this will cause a get method
00:50:14.100 | to be sent to this, which obviously
00:50:18.380 | is the same as what an ATAG does anyway.
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:30.380 | replace the entire page.
00:50:31.700 | How do you decide what it does do?
00:50:35.260 | Whatever target is is what's replaced.
00:50:38.220 | So there's a thing called currentTodo,
00:50:40.620 | which is actually a empty div down here.
00:50:50.020 | So when we click on this, it'll call todo/1,
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:50:58.300 | to /todos/1.
00:51:02.980 | And you can see, there it is.
00:51:04.220 | There's that little snippet.
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:20.020 | So you get this little HTML partial,
00:51:25.140 | and it will be inserted into this div.
00:51:33.740 | So let's try it.
00:51:34.380 | If I click, and you can now see, yep, there it is.
00:51:38.340 | It's been inserted into this div.
00:51:42.340 | That make sense?
00:51:44.780 | So that's what HTMX is.
00:51:46.340 | And Alexis was talking earlier about walking
00:51:55.700 | into the flexible foundations of the web
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:13.340 | dramatically more flexible and powerful
00:52:16.540 | by just taking what's already there
00:52:18.700 | and doing an almost, in hindsight, the obvious thing,
00:52:23.780 | which is just to remove the constraints.
00:52:27.300 | And maybe just to clarify something,
00:52:29.060 | you don't have to use HTMX with FastHTML.
00:52:31.700 | These are two orthogonal things.
00:52:33.100 | I've built basically the same app.
00:52:35.260 | But any time you want to do something,
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:52.660 | But yeah, absolutely.
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:03.660 | for HTMX in the header.
00:53:05.780 | And in the autocomplete, all of the HX things
00:53:12.140 | are there for you.
00:53:13.260 | And it's just minor little things.
00:53:15.180 | But yes, it's not--
00:53:17.500 | you certainly don't have to use HTMX.
00:53:22.460 | So here's a question about HTMX.
00:53:25.540 | You say that it allows you to make any element interactable.
00:53:31.380 | That makes sense.
00:53:32.700 | Thinking about native development,
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:40.540 | and then will allow gesture recognition
00:53:43.100 | to be associated with it, taps or swipes, other things.
00:53:47.980 | And then you say that it lets you
00:53:51.860 | choose which kind of HTTP method is going to be executed.
00:53:57.780 | So that's good, because beyond GET and POST,
00:53:59.700 | there's things like PATCH and whatnot
00:54:01.980 | that enable partial updates that aren't as well known
00:54:04.780 | but have relevant semantics.
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:11.860 | and PUT to update a TODO.
00:54:14.660 | But so my question is, while those HTTP methods have
00:54:21.700 | a lot of relevant semantics, there's
00:54:23.160 | all sorts of actions one wants that
00:54:24.700 | aren't expressed by an HTTP method, that aren't just--
00:54:28.700 | yeah.
00:54:29.200 | So in that case, then you would use PATH.
00:54:33.060 | So for example, SHOW and EDIT FORM,
00:54:36.740 | there's a different PATH for that.
00:54:38.580 | So you can have a--
00:54:40.140 | so like, and in fact, you could use only POST,
00:54:43.740 | or you could use only GET.
00:54:45.100 | Like, there's no particular reason
00:54:46.560 | you have to use any other HTTP verb.
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:56.940 | That's a GET.
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:04.380 | It could mean, in a simple way, hey,
00:55:06.260 | update this component that's in the DOM.
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:20.180 | to the server.
00:55:21.580 | You choose what request method it is,
00:55:25.180 | and you choose what PATH it goes to,
00:55:28.460 | and you choose what data is in it,
00:55:30.380 | and you choose what headers are there.
00:55:31.980 | So it's like, it's the normal things
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:39.620 | You then decide what happens.
00:55:41.060 | What happens when this PATH gets this HTTP method?
00:55:46.340 | Oh, it runs this function.
00:55:49.220 | - OK, so it's single page and very dynamic,
00:55:52.860 | because we don't see a whole page reload.
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:55:59.860 | - Exactly.
00:56:01.540 | So for example, one of the things
00:56:04.140 | you are likely to want at some point
00:56:05.780 | is something where maybe it's create new user.
00:56:10.220 | And as you start typing the username,
00:56:12.740 | a little thing pops up saying username is available.
00:56:15.540 | So that's an example.
00:56:16.900 | So you can absolutely attach hx-keyup or whatever.
00:56:24.220 | Sorry, like you can attach--
00:56:25.900 | I think it'd be hx-trigger=keyup.
00:56:28.420 | So now we're attaching to the--
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:56:45.820 | or whatever.
00:56:46.660 | So it's surprisingly OK.
00:56:54.140 | Like, at least even from Australia,
00:56:57.540 | we're at 70 milliseconds each way to the US.
00:57:00.180 | And most servers are in the US.
00:57:03.420 | It doesn't feel at all slow.
00:57:07.940 | Like, you wouldn't write a computer game in this
00:57:11.340 | because every time you click a key,
00:57:14.380 | it has to do a full round trip.
00:57:18.780 | But anything that can happen in about 150 milliseconds
00:57:22.980 | is going to be fine.
00:57:24.980 | - OK.
00:57:25.460 | - And for stuff you do want, you can still
00:57:27.100 | write JavaScript, of course, if you did want stuff that happens
00:57:29.860 | in less than 150 milliseconds.
00:57:33.060 | - So I noticed that the HTMX magic is managed
00:57:38.580 | through these hx attributes.
00:57:41.660 | And part of interoperating with the web
00:57:44.500 | is not just being able to use HTML and JS,
00:57:46.260 | but using the enormous suite of components
00:57:49.020 | that other people have made.
00:57:51.460 | So I know that in terms of heavyweight reusable components,
00:57:54.980 | like if I want a complicated--
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:07.740 | and then confirm it.
00:58:09.100 | That's a very plausible thing I would want.
00:58:10.860 | - HTMX is great for that.
00:58:11.740 | - OK.
00:58:12.260 | Because I don't want to make that myself,
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:21.420 | So what's the integration story?
00:58:22.820 | - So look, somebody else, hopefully,
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:32.820 | will create the pip installable thing.
00:58:35.900 | So for example, there's a sortable JavaScript library.
00:58:39.780 | - OK.
00:58:40.780 | - And here's an HTMX demo of using the sortable JavaScript
00:58:46.820 | library.
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:58:55.860 | And you can see underneath, these
00:59:01.660 | are the requests that were coming in.
00:59:04.100 | So this is actually using HTMX that now,
00:59:08.340 | because sortables has been attached to HTMX,
00:59:13.260 | these are triggering HTMX HTTP events.
00:59:18.460 | And you can see the response coming back
00:59:21.060 | is, again, a little HTML partial.
00:59:23.420 | So the first person to make HTMX and sortables work together
00:59:28.020 | is going to have rights in JavaScript.
00:59:30.100 | And so here's the JavaScript they wrote.
00:59:33.660 | But once somebody's written that once,
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:42.660 | or something in their form, whatever.
00:59:45.940 | And it's not a lot of--
00:59:48.460 | A, it's not a lot of JavaScript.
00:59:49.780 | And B, the JavaScript's not complicated
00:59:51.980 | because HTMX is using very standard JavaScript stuff.
00:59:58.700 | So I don't know much about React,
01:00:01.540 | but I know that it tends to be integrated.
01:00:03.700 | It provides a reusable component model,
01:00:05.980 | which is why it's been successful.
01:00:07.420 | But I also know that component model
01:00:09.340 | is integrated with a certain data flow model.
01:00:13.700 | Do we know if anyone has used HTMX to--
01:00:15.860 | This is not for React.
01:00:18.220 | So this is for stuff more like--
01:00:21.180 | What about web components?
01:00:22.460 | Sure, web components, for example.
01:00:24.220 | Yeah, web components.
01:00:26.140 | So this is the way the web's going, right?
01:00:28.980 | Well, let us hope, yeah.
01:00:30.940 | Is these kinds of--
01:00:34.220 | these components can go in React or Vue or Angular.
01:00:37.780 | Hopefully, at some point, somebody's
01:00:39.260 | going to add the first HTML version in here.
01:00:43.140 | And React, I'm not familiar with that.
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:00:57.180 | component models anyway.
01:00:58.180 | So maybe some of those component models
01:01:00.140 | are easier to HTMX-ify.
01:01:02.060 | And then the kind of promised land
01:01:03.540 | that you might get to is that there are PIP-installable
01:01:05.500 | components once you're there.
01:01:07.140 | Yeah, that's not some distant promised land.
01:01:08.620 | I mean, hopefully, within a few weeks,
01:01:10.220 | there'll be quite a few.
01:01:14.260 | And as I say, it comes with some already super basic stuff
01:01:19.300 | like group and card.
01:01:25.580 | So yeah, great.
01:01:29.940 | So let's have a look behind the scenes a little bit.
01:01:33.780 | And then we can maybe come back to here
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:01:51.780 | is fastcore.xml.
01:01:59.660 | And originally, it was just a list.
01:02:03.100 | XT was just a list.
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:16.060 | That's all they are.
01:02:17.180 | And it also has an init that you can only pass in exactly three
01:02:21.940 | things.
01:02:22.780 | But it's just a list.
01:02:26.180 | And so that's what an XT object is.
01:02:30.780 | It's a list.
01:02:31.380 | And you can just use a list.
01:02:33.140 | There's no need to use an XT object.
01:02:34.780 | This is just meant for your convenience.
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:44.260 | And so XT got my fancy tag.
01:02:53.420 | And then that contains a child and another child
01:03:03.900 | and an attribute.
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.140 | I wrote this.
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:33.060 | So it's just history.
01:03:35.460 | Anyway, it's not much code, so it's fine.
01:03:39.260 | AUDIENCE: I'm just thinking about the analogy
01:03:41.260 | with the anchor type, where you had anchor,
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:57.980 | OK, all right.
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:09.460 | and added them all to the module.
01:04:12.100 | So now, as a result, if you call HTML, head, title, body, div,
01:04:17.860 | p, input, image, instead of using class,
01:04:22.500 | you can use HTML class or class or class.
01:04:26.740 | These are all kind of standard things.
01:04:28.500 | Oh, actually, I've also seen some people use underscore
01:04:30.820 | class, as you said, as well.
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:04:56.260 | list.
01:04:57.700 | Or these are three element lists.
01:05:00.940 | So that's it.
01:05:04.060 | And it doesn't have to be--
01:05:06.700 | like, you can put text there, as well.
01:05:09.580 | Maybe that would be better to demonstrate.
01:05:11.660 | So OK, so as I mentioned, they've got property names.
01:05:17.660 | What else is there to show you?
01:05:24.180 | OK, so then the thing that converts that structure
01:05:28.260 | into XML/HTML is this.
01:05:31.860 | So you can see here, I've--
01:05:34.780 | here it is.
01:05:35.420 | OK, so that's the basic foundation.
01:05:38.540 | So there's hardly any code, as usual.
01:05:41.580 | I don't like writing code.
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:11.260 | Starlet is a ASGI--
01:06:16.260 | I think it's a ASGI framework.
01:06:19.500 | This ASGI thing is a standard way
01:06:24.860 | to create Python web applications.
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:42.140 | I don't know what it is.
01:06:43.140 | I just use other people's things.
01:06:45.020 | So now that I do know what it is, at some point,
01:06:47.300 | I'll do a little course in that.
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:09.420 | to functions, and have responses,
01:07:14.100 | and have things in requests, or whatever.
01:07:18.460 | I think it's not necessarily designed
01:07:19.980 | to be something that most people write things indirectly.
01:07:22.380 | I suspect it's mainly designed for people
01:07:24.100 | to write stuff like fastHTML in.
01:07:26.300 | Or indeed, fastAPI, which is a super popular library,
01:07:30.500 | is written on top of Starlet as well.
01:07:37.860 | So fastAPI is designed to create APIs.
01:07:41.820 | So all of the--
01:07:43.140 | so you go to the tutorial.
01:07:53.900 | Wait, how am I supposed to use the tutorial?
01:07:56.020 | This is not good.
01:08:03.620 | Oh, I see.
01:08:04.620 | That's interesting.
01:08:05.420 | You zoom in, and it makes everything disappear.
01:08:11.020 | Yeah.
01:08:11.500 | So basically, in the tutorial, it's a really good tutorial.
01:08:14.460 | And a lot of the things you learn here
01:08:16.180 | will also apply to fastHTML.
01:08:18.500 | It always returns JSON.
01:08:20.140 | It returns dictionaries which become JSON.
01:08:22.220 | So at the end of this tutorial, you don't actually
01:08:24.980 | end up with an application at all.
01:08:26.740 | You end up with an API that you could
01:08:28.260 | write an application on top of.
01:08:29.580 | And that's going to require JavaScript.
01:08:32.860 | So fastHTML is designed to be something
01:08:35.780 | where the thing that gets returned here
01:08:38.220 | are HTMX partials or HTMX pages.
01:08:43.660 | So that's basically what this is.
01:08:48.380 | So here's a list of all of the HTMX headers, for example.
01:08:56.260 | So we haven't even talked about this.
01:08:57.800 | But when HTMX calls your HTTP verb endpoint,
01:09:07.080 | it always includes, for example, an hx-request header.
01:09:10.120 | So you can always tell if something
01:09:12.080 | is sent by HTMX or not.
01:09:13.960 | What I might do is jump to the tests
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:33.160 | And then this is quite neat.
01:09:35.720 | Starlet has a thing called testClient,
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:01.200 | And I can say, OK, what URL?
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:14.480 | defined a listener there.
01:10:17.680 | And you can see that's returned a response.
01:10:20.480 | [AUDIO OUT]
01:10:23.360 | Which has various things in it, including text.
01:10:28.720 | OK, so--
01:10:29.800 | It's somewhat helpful here to know a little bit
01:10:32.240 | about the starlet requests, and responses,
01:10:34.520 | and things like that.
01:10:36.480 | But maybe that's a good separate cutting point for--
01:10:40.040 | I mean, you kind of hopefully don't, right?
01:10:42.840 | Because here, I just returned text.
01:10:46.080 | So I'm kind of trying to make it so you don't have
01:10:48.080 | to know about any of those things.
01:10:49.640 | But you can use them if you want to.
01:10:51.920 | But in fact, in our tests, none of them
01:10:55.520 | use a request or a response, except for the very first one
01:10:58.640 | just to prove to you that you can.
01:11:02.200 | Yeah.
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:08.000 | I know what I'm doing with that, then that's
01:11:09.800 | great for the people who have that.
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:14.320 | just use starlet, in a sense, maybe.
01:11:16.280 | You know, so yeah.
01:11:19.320 | So basically-- so I mean, one thing
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:33.880 | you can whack a thing in curly brackets.
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:02.800 | And if you add a parameter to your function,
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:15.640 | be passed this special request object.
01:12:19.440 | Oh, you know what?
01:12:24.920 | It would be better to look at index,
01:12:26.600 | because that actually shows you, I think.
01:12:29.360 | Yeah, here we go.
01:12:35.000 | So this one here--
01:12:37.480 | well, there's a lot of stuff in request.
01:12:39.160 | I mean, we could just print it out.
01:12:40.620 | So that's-- actually, no, it probably won't work.
01:12:45.620 | Never mind.
01:12:47.060 | So yeah, the request object is something
01:12:48.980 | that you could go to starlets and go to requests.
01:12:54.740 | And you can see here, request.
01:12:56.900 | And there's various things in a request object.
01:12:59.780 | The idea is, though, with FastHTML
01:13:01.420 | is that you never have to worry about any of that.
01:13:04.020 | But it is there if you want to.
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:14.540 | So authentication is often the domain
01:13:17.380 | where one starts to care about HTTP headers,
01:13:19.660 | because cookies and things like that
01:13:23.380 | are managed through headers.
01:13:24.700 | Well, you're not going to have to worry about that either.
01:13:27.140 | It's all going to be automatic.
01:13:28.580 | But let me get there so you can see.
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:39.340 | that's going to get passed to your function
01:13:42.260 | if your function has a parameter with the same name.
01:13:44.300 | And give it a type, right?
01:13:50.220 | So it can-- there you go, OK?
01:14:03.740 | So this is a bit of magic, although it's
01:14:06.580 | pretty standard magic for these kinds of libraries
01:14:10.140 | is to work this way.
01:14:14.180 | And it will endeavor to cast things appropriately.
01:14:21.100 | So in this case, obviously, there's
01:14:41.180 | no such thing as ints or whatever in paths.
01:14:44.580 | But we've asked it to become an int,
01:14:46.420 | and it will turn it into one, like so.
01:15:01.380 | OK, which actually, that's exactly what this one already
01:15:10.380 | does, so it's not very interesting.
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:38.260 | because that's what this gets.
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:15:52.340 | So here, I've registered something
01:15:53.820 | called image extensions as a regex.
01:15:56.900 | And then if you stick it in here,
01:15:59.100 | it will match anything that matches
01:16:01.100 | this regular expression.
01:16:02.180 | The type can be other more interesting things.
01:16:08.620 | In this case, model name is an enum.
01:16:14.180 | So /models/alexnet returns alexnet.
01:16:18.900 | AUDIENCE: Oh, you overwrote /name.
01:16:35.900 | I broke it.
01:16:37.620 | That's what I did.
01:16:38.380 | Come back to that.
01:16:50.620 | So if I look for /models/gpt5--
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:13.220 | we're recording this video.
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:34.620 | You can also add stuff as headers.
01:17:38.460 | They'll be lowercased and dashes turned into underscores.
01:17:45.700 | If there are HTMX special headers,
01:17:55.020 | and you have a HTMX headers type,
01:17:57.780 | they will be passed to there.
01:17:59.260 | Or you can just use the special name, HTMX.
01:18:01.620 | AUDIENCE: So in those examples, is
01:18:06.620 | what we would see if we hit those endpoints,
01:18:08.900 | that those values are echoed back?
01:18:11.220 | Like, it would read the user agent from the request
01:18:13.340 | and then return that back in the response?
01:18:15.100 | Yeah, so it's returning a string.
01:18:21.460 | So the string is the number 1.
01:18:24.180 | So it just returns the number 1.
01:18:27.980 | You can see here, this is the expected value.
01:18:30.140 | So it's returning the number 1 as a string.
01:18:34.260 | Just like up here, we returned the string.
01:18:38.980 | Hi there.
01:18:40.540 | This time, we're returning--
01:18:41.740 | ANDREW BROGDON: Yeah, I understand the return
01:18:43.740 | mechanism.
01:18:44.260 | But what I'm looking at is the parameters in the--
01:18:48.540 | or the-- let me see the def of UA, right.
01:18:53.220 | OK, so UA user agent colon.
01:18:55.900 | User agent's a special keyword that
01:18:57.540 | means I'm going to take my input parameter from the HTTP
01:18:59.980 | header, is that right?
01:19:00.900 | No, no, no.
01:19:02.700 | Nothing special at all.
01:19:04.940 | There's only two special ones, which is HTMX and request.
01:19:09.740 | Everything else is not special.
01:19:11.660 | Everything else looks in order to see
01:19:15.300 | if it's a path parameter.
01:19:18.020 | If not, is it a query string?
01:19:22.820 | If not, is it a header?
01:19:25.820 | [AUDIO OUT]
01:19:28.300 | So it just keeps looking to try and-- it does everything it can
01:19:31.020 | to find something.
01:19:31.820 | And so it found a header for user agent.
01:19:36.660 | Yeah, got it.
01:19:39.060 | And if not, is it a cookie?
01:19:44.380 | So here, we set a cookie.
01:19:47.100 | So this is where we do, for now at least, require--
01:19:50.020 | have to create a starlet response object.
01:19:53.860 | And we set the cookie.
01:19:56.060 | And so here, that cookie was called now.
01:19:58.900 | So here, we put now.
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:07.160 | So it gets the cookie.
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:20.060 | is this dictionary.
01:20:23.140 | And so if it's--
01:20:25.860 | this is quite fun, I think.
01:20:27.460 | If you have data in a POST request,
01:20:33.300 | and you have a data class, it will automatically
01:20:37.220 | put it into that data class.
01:20:40.860 | So here, data is now a bode object,
01:20:45.340 | because you asked it to be.
01:20:47.220 | And if we try to put in something
01:20:55.500 | that you can't fill into a bode object,
01:20:57.180 | then you will, of course, get an exception.
01:20:58.980 | It's like, oh, you can't put a C in there.
01:21:00.700 | You don't have to use a data class.
01:21:06.500 | You can also use a dictionary.
01:21:08.780 | Now, if you use a dictionary, then it
01:21:13.820 | won't be a and the number one anymore.
01:21:17.260 | It'll be a and the string one, because it has no idea what
01:21:20.420 | data type you want it to be.
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:29.380 | support.
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:38.700 | Oh, it has to be a string, and also it
01:21:40.340 | needs to be a string that starts with these letters.
01:21:42.220 | And-- - You can do all that.
01:21:44.340 | I don't love it myself.
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:21:57.380 | because you can put it in the function,
01:21:58.980 | and then you can respond to those validation
01:22:01.260 | issues in a more nuanced, handler-specific way.
01:22:05.620 | But yeah, you could.
01:22:07.940 | You can also create a named tuple.
01:22:10.140 | So that works fine.
01:22:11.780 | Again, named tuples are untyped, so it
01:22:14.660 | won't know what that is.
01:22:17.300 | Or you can just create any old class,
01:22:22.300 | and as long as it has annotations,
01:22:26.100 | it will know what types things have to be.
01:22:27.900 | So Bodhi2 is a number, because it's looked inside here
01:22:34.460 | and being like, oh, it's a number or none,
01:22:37.060 | so we'll try and make it a number.
01:22:40.700 | Yeah, so it tries-- basically, FastHTML does everything
01:22:43.580 | it can to give you the parameters
01:22:47.340 | that you've requested, is basically
01:22:49.180 | the way to think of it.
01:22:52.580 | So here are two things that occurred to me.
01:22:54.580 | Well, three.
01:22:55.180 | One is I'm having all these flashbacks
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:03.900 | or it's just a coincidence.
01:23:04.980 | Yeah, it's an attempt to be like-- no,
01:23:06.580 | it's an attempt to be as normal as possible.
01:23:09.380 | OK, well, that explains it.
01:23:10.580 | I guess I wasn't doing it in a weird way.
01:23:12.300 | If we go to the FastAPI tutorial,
01:23:17.540 | I literally went through every line of code,
01:23:20.380 | copied and pasted it, and tried to make
01:23:22.100 | it work exactly the same way, except the return is not
01:23:25.940 | JavaScript anymore.
01:23:27.260 | So yeah, if you're a FastAPI user,
01:23:29.620 | it should be extremely normal.
01:23:33.460 | So the second thing that occurs to me
01:23:35.020 | is that the behavior of search with fall through when
01:23:41.100 | you get a key, essentially.
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:50.660 | OK, is it in the header?
01:23:51.660 | And here it is, by the way.
01:23:52.820 | And when I sent this code to John O, he was happy.
01:23:54.940 | He was like, oh, OK, it's nice to see.
01:23:57.140 | Well, and I had to see that, right?
01:23:59.340 | Before I saw that, I didn't trust it.
01:24:01.140 | And what I was writing was, just always give me the request.
01:24:04.300 | Thank you very much.
01:24:05.140 | And I'll check if in request.cookies,
01:24:08.260 | or request.useragent, or whatever,
01:24:10.060 | I'll go get the thing myself.
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:16.980 | OK, now I'm happy to trust.
01:24:18.220 | It's slightly magic, and I'm not saying that's bad.
01:24:21.860 | I can see it's dead handy.
01:24:23.340 | It also triggers my magic Spidey sense.
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:40.900 | So the order of these was carefully chosen.
01:24:43.860 | But if you have a query parameter and a path parameter
01:24:46.540 | that are the same, or if you have
01:24:48.020 | a query parameter that's the same as a cookie,
01:24:49.900 | you won't get your cookie anymore.
01:24:51.740 | Well, also fortunately, the header field namespace
01:24:55.460 | is not polluted with like 8 million things
01:24:57.900 | that are out of our control.
01:24:59.060 | It's a relatively orderly part of the universe.
01:25:01.020 | So it's not a problem.
01:25:03.820 | The last observation I have--
01:25:05.700 | and this may be just that I'm not
01:25:07.060 | used to doing a ton of web development--
01:25:09.100 | is when I look at how the code looks
01:25:11.300 | and how the logic of it looks, there's
01:25:13.340 | these two namespaces that are side by side,
01:25:16.540 | because we're trying to understand their interaction.
01:25:19.020 | But they're still different.
01:25:20.180 | And that's the namespace of the URL path.
01:25:24.300 | We have to choose the string that represents the path.
01:25:26.580 | And then separate from that is the namespace
01:25:30.460 | of the function names and the kind of parameters
01:25:32.900 | within the functions.
01:25:34.420 | So one is necessarily--
01:25:38.780 | The name of the function, you could call them all underscore,
01:25:44.420 | I think.
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:05.660 | you have the path.
01:26:07.300 | You have this effectively a mini-language,
01:26:10.260 | which is what's expressed in the URL path.
01:26:12.660 | Yes, that's--
01:26:13.180 | And then another language, which expresses the action that's
01:26:17.340 | actually performed, the symbol that
01:26:19.340 | represents the action that's performed in the server
01:26:22.420 | program.
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:27.540 | Am I changing the underlines?
01:26:28.740 | Is this exposing too much?
01:26:29.820 | Is this exposing too little?
01:26:31.220 | It all feels a bit jerry-rigged together.
01:26:34.660 | But I guess that's the web for you.
01:26:36.540 | I quite like it, especially because you
01:26:38.860 | can have multiple methods.
01:26:40.140 | So you can have--
01:26:41.060 | the get on slash is generate home page,
01:26:45.860 | or home might be my function, because it's
01:26:48.460 | going to respond back with a full web page full of all
01:26:50.700 | the content that I want.
01:26:52.580 | But the post on that might just be
01:26:54.460 | like you're submitting the form to sign up
01:26:57.300 | for my email newsletter.
01:26:58.660 | And so then I can have a separate function that's
01:27:00.700 | listening for the post option.
01:27:03.100 | I just renamed everything to underscore.
01:27:05.220 | And it still works.
01:27:06.580 | Nice.
01:27:07.580 | Maybe that's not a bad thing.
01:27:08.820 | Because yeah, why are you spending
01:27:10.340 | time naming this function that never gets used?
01:27:16.620 | How would you call it with testing, then?
01:27:18.340 | You can't use the function name anymore, right?
01:27:20.460 | It's fine.
01:27:20.940 | I'm just-- client.get.
01:27:24.420 | So I do have this.
01:27:25.500 | All my tests work that way.
01:27:28.500 | I kind of like that.
01:27:29.420 | But I don't spend a lot of time doing web development.
01:27:32.220 | So I don't know if my tastes are informed
01:27:34.580 | by my deep judgment and experience,
01:27:36.140 | or informed by my vast ignorance of the domain.
01:27:38.340 | I guess a little of each.
01:27:39.540 | I'm not sure which of those I should listen to.
01:27:41.540 | Anyway.
01:27:42.540 | I've got to go.
01:27:44.380 | This was very cool.
01:27:45.300 | This was very cool.
01:27:46.660 | Thanks very much.
01:27:48.700 | I had a feeling that was going to take a while.
01:27:50.620 | We've got a lot more.
01:27:51.580 | Well, not a lot more than before.
01:27:53.020 | But anyway.
01:27:53.520 | A bit more.
01:27:54.780 | But this is something that we'll be showing people
01:27:56.900 | a lot more from, I suspect.
01:27:58.060 | Different ways to write the same apps.
01:27:59.660 | Different extensions of it.
01:28:00.900 | How to make it play nicely with the database stuff
01:28:03.980 | that you showed, which we didn't really
01:28:05.620 | get a chance to go into.
01:28:06.820 | Like, why those data classes play so nicely with it.
01:28:09.200 | Yeah.
01:28:09.700 | Yeah.
01:28:10.300 | So there's a few cool parts, too.
01:28:12.020 | All right.
01:28:12.520 | See you.