hypie 0.1.1.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,816 @@
1
+ Metadata-Version: 2.4
2
+ Name: hypie
3
+ Version: 0.1.1.dev2
4
+ Summary: Add your description here
5
+ Author: Yasser Ahmed
6
+ Author-email: Yasser Ahmed <yasserfawzi1029@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: htpy>=26.5.1
10
+ Requires-Dist: watchfiles>=1.2.0
11
+ Requires-Python: >=3.12
12
+ Project-URL: Repository, https://github.com/Infrared1029/hypie
13
+ Description-Content-Type: text/markdown
14
+
15
+ <p align="center">
16
+ <img src="https://raw.githubusercontent.com/Infrared1029/hypie/main/assets/hypie_logo.jpg" alt="Logo" width="400">
17
+ </p>
18
+
19
+ <!-- # Hypie -->
20
+ ## Intro
21
+ Hypie, pronounced "high-pie", is a python library for building web frontends, built on top of [_hyperscript](https://github.com/bigskysoftware/_hyperscript), a scripting language for the web made by the [htmx](https://github.com/bigskysoftware/htmx) folks, and [htpy](https://github.com/pelme/htpy), a library for generating HTML in python.
22
+
23
+ Right now Hypie is a personal passion side project that is still very experimental. Expect breaking changes.
24
+
25
+ There is no official docs website for hypie (yet), so this README is the closest thing to a documentation for now, if you are interested, I would advise reading through the whole thing, hopefully its not too long.
26
+
27
+ ## Installation
28
+ ```sh
29
+ uv add hypie
30
+ ```
31
+ Or if you are from ancient times
32
+ ```sh
33
+ pip install hypie
34
+ ```
35
+
36
+ ## Motivation
37
+ I like coding in python, I hate coding in javascript (although sometimes it is inevitable), and I find the current frontend ecosystem extremely complex and messy, more so when using the so called "metaframeworks". Like many other people who share the same frustration, this motivated a bunch of attempts to create simpler alternatives that revive more traditional (and simpler) ideas with a modern look, like [_hyperscript](https://github.com/bigskysoftware/_hyperscript) and [htmx](https://github.com/bigskysoftware/htmx), and even within the python ecosystem, like [FastHTML](https://github.com/answerdotai/fasthtml). Hypie is one of those attempts, built on top of _hyperscript and htpy.
38
+
39
+ With hypie the goal is to be able to write powerful frontends, using python, that is backend agnostic (you can use whatever python backend framework you like), using relatively little and hopefully clear abstractions, powered by _hyperscript's [commands and features](https://hyperscript.org/docs/language/).
40
+
41
+ The way Hypie pushes you to think about frontend system is as follows:
42
+ - Server generates HTML "Components" and returns it as a response
43
+ - "Components" should be, for the most part, purely presentational
44
+ - "Components" should emit semantically meaningful events, they should not contain logic that does side effects or is aware of other parts of the system, "Behavioural" parts of the system can handle emitted events, cause all kinds of side effects and is aware of the different components and parts of the system.
45
+ - If you need to generate HTML from the client that does not need a server trip (i.e. a visual component that does not rely on any kind of data from the server), like a Modal, you can define a "Template" that you can render anywhere, client side, no server roundtrips.
46
+
47
+ Before diving into hypie, it is useful to take a very quick look at the two main technologies hypie relies on, htpy and _hyperscript, all examples will assume a fastapi backend, but it should work with **any** python backend framework.
48
+ ## Htpy and Hyperscript primers
49
+ ### Htpy
50
+ [htpy](https://github.com/pelme/htpy) is a python library for writing html, it is essentially a pythonic answer to templating languages.
51
+ ```python
52
+ import htpy
53
+
54
+ my_element = htpy.div(id="my-div", class_="some-class")[
55
+ htpy.h1["Some Title"],
56
+ htpy.p["some text"],
57
+ htpy.div[
58
+ htpy.a(href="/some/site")["click me!"]
59
+ ]
60
+ ]
61
+ ```
62
+ This is equivalent to:
63
+ ```html
64
+ <div id="my-div" class="some-class">
65
+ <h1>Some Title</h1>
66
+ <p>some text</p>
67
+ <div>
68
+ <a href="/some/site">click me!</a>
69
+ </div>
70
+ </div>
71
+ ```
72
+ Htpy makes a distinction between parenthesis and square brackets, you put element attributes in parenthesis as keyword arguments, and you put the actual children/content in square brackets, at first this might look a bit weird, but it is actually a very nice way to represent html in python and makes scanning the code easier once you get used to it.
73
+ You can then return htpy elements as HTML responses in your favourite backend framework.
74
+ ```python
75
+ import htpy
76
+ from fastapi import FastAPI
77
+ from fastapi.responses import HTMLResponse
78
+
79
+ app = FastAPI()
80
+
81
+ @app.get("/")
82
+ def home():
83
+ return HTMLResponse(
84
+ htpy.h1["Hello World!"]
85
+ )
86
+ ```
87
+
88
+ ### _Hyperscript
89
+ [_hyperscript](https://github.com/bigskysoftware/_hyperscript) is a scripting language that can be embedded directly in HTML, here is an example:
90
+ ```html
91
+ <button _="
92
+ on click
93
+ toggle .red on me
94
+ end
95
+ ">
96
+ Click Me
97
+ </button>
98
+ ```
99
+ This is a button, when you click it, you add (or remove) the .red class, you "toggle" the .red class, you probably inferred this already, what I just explained did not add much to what was written already.
100
+ Hyperscript is meant to be very readable, someone can easily take a look at your html and understand what is going on, even if they are not familiar with the language, because it reads like English (more on this later, because I can see the crossing eyebrows), let's quickly dissect _hypersrcipt syntax.
101
+ On the element you want to add behaviour to, you basically set the underscore attribute ("_") to the _hyperscript you want.
102
+
103
+ _Hyperscript syntax is made up of three main components:
104
+ - Features (like the `on [EVENT]` features you saw, and other likes `init`, `live` and more)
105
+ - Commands (like `toggle`, `log`, `add`, `remove`, `put` and more)
106
+ - Expressions (like literals `"hello"`, `1`, `5+3`, dom literals like `#my-div`, `.my-class` and more)
107
+
108
+ So, in general, _hyperscript code looks like this:
109
+ ```html
110
+ <element _="
111
+ FEATURE 1
112
+ COMMAND 1
113
+ COMMAND 2
114
+ ...
115
+ COMMAND N
116
+ end
117
+
118
+ FEATURE 2
119
+ COMMAND 1
120
+ COMMAND 2
121
+ ...
122
+ COMMAND N
123
+ end
124
+
125
+ ...
126
+
127
+ FEATURE N
128
+ ...
129
+ end
130
+ ">
131
+ </element>
132
+ ```
133
+ You can technically inline stuff and remove the `end` keyword in many scenarios, but I tend to keep things that way to keep everything readable, going back to the original example, `on` is a feature, that takes a required `event` argument (technically an `event-spec`, not just the name, but we kept everything to its default values, you can control lots of stuff around the event, like its count, debouncing and [tons of other stuff](https://hyperscript.org/features/on/)), `toggle` is a command that takes "two" arguments, `toggle <expr> on <expr>`, and `.red` is a class literal, and `me` is a special literal that refers to the current element (the one that has _hyperscript defined on).
134
+ A full treatment of _hyperscript capabilities is beyond this readme/docs, so you really should check the docs and learn more about the different features, commands and expressions, because hypie is just a layer on top of _hyperscript.
135
+
136
+ ### Htpy + _Hyperscript
137
+ Now, with htpy and _hyperscript, you can combine both and do something like this:
138
+ ```python
139
+ import htpy
140
+
141
+ htpy.button(_="""
142
+ on click
143
+ toggle .red on me
144
+ end
145
+ """)[
146
+ "Click Me"
147
+ ]
148
+ ```
149
+ This is already pretty useful, and I actually developed using these kind of htpy+_hyperscript elements for a while, but as the project started to get more complex, things started to get a bit messy for the following reasons:
150
+ - _Hyperscript as pure strings in python is really fragile, no syntax highlighting, and dynamically adding or removing commands from features is really ugly
151
+ - _Hyperscript's English like syntax is sometimes a bit hard to work with, as some commands have special keywords and variations that you need to remember, i.e hyperscript is easy to read but sometimes a bit difficult to write, it reads like English, but not everything that can be written as English works (even it makes sense, English wise)
152
+
153
+ For those reasons, I decided to create a python DSL on top of _hyperscript, this allows me to do the following:
154
+ ```python
155
+ import htpy
156
+
157
+ # instead of
158
+ htpy.button(_="""
159
+ on click
160
+ log "I was clicked!"
161
+ toggle .red on me
162
+ end
163
+ """)[
164
+ "Click Me"
165
+ ]
166
+
167
+ # you do
168
+ from hypie.literals import var, cls, me
169
+ from hypie.commands import log, toggle
170
+ from hypie.features import On
171
+
172
+ htpy.button(_=On("click")[
173
+ log("I was clicked!"),
174
+ toggle(cls("red"), on=me)
175
+ ])[
176
+ "Click Me"
177
+ ]
178
+ ```
179
+ That way you get IDE support for the arguments the different commands support, you do not deal with messy strings anymore, and the English issue is gone. Again, this is already helpful, but we can take it a step further, taking inspiration from how components look in frameworks like [Vue](https://github.com/vuejs/core) and [Svelte](https://github.com/sveltejs/svelte), a hypie component is as follows:
180
+
181
+ ### Hypie
182
+ ```python
183
+ from hypie.literals import var, cls
184
+ from hypie.commands import log, toggle
185
+ from hypie.featuers import On
186
+ from hypie.experimental.components import Component
187
+
188
+ class MyButton(Component):
189
+ def template(self):
190
+ return htpy.button(_=On("click")[
191
+ log("I was clicked!"),
192
+ toggle(cls("red"), on=var("me"))
193
+ ])["Click Me"]
194
+
195
+ @staticmethod
196
+ def style():
197
+ return {
198
+ ".red": {
199
+ "background": "red"
200
+ }
201
+ }
202
+ ```
203
+
204
+ Hypie Components work with htpy seamlessly, you can do:
205
+ ```python
206
+ htpy.div[
207
+ MyButton # or MyButton() both work,
208
+ ]
209
+ ```
210
+ and it will work as expected.
211
+
212
+ Styles are bound to the component that defined it only, using css scopes, obviously the style won't take effect unless you inline it or generate a css file to link, to generate the styles you need to run the following command:
213
+
214
+ ```sh
215
+ hypie build -i <path/to/components/dir> -o <path/to/dist_or_static/dir>
216
+ ```
217
+
218
+
219
+ This command will go through all python files in the dir and extract the components, and generate a single css file with all the scoped css of the components, you would usually make the `-o` dir be the static dir or dist dir, sadly escaping a build step is difficult if you want to make the library backend agnostic, without inlining the styles. As you will see later, the build step is not only used for generating css files, but can be used to also generate _hyperscript files when moving behvaioural logic out of components, more on that later! Note that `style` is a static method (class method works too), because its expected to run at build time, without instantiating the class.
220
+ The output css file looks like this:
221
+ ```css
222
+ /* ./app/static/_hypie_styles.css */
223
+
224
+ @scope (button[data-hypie-component='my-button']) {
225
+ .red {
226
+ background-color: red;
227
+ }
228
+
229
+ }
230
+ ```
231
+ Behind the scenes, hypie adds a `data-hypie-component=<kebab-case-component_name>` attrbitue to the top-level element, the generated css uses that to scope the styles.
232
+
233
+ Let's move on to other examples and introduce more capabilities and abstractions as we go.
234
+
235
+ ## Simple Counter Example using Hypie
236
+ Let's create a very basic Counter Component using hypie.
237
+ ```python
238
+ # ui/components.py
239
+ class Counter(Component):
240
+ initial_count: int = 0
241
+
242
+ def template(self):
243
+ count_var = var(":count")
244
+
245
+ return htpy.div[
246
+ htpy.button(
247
+ _=hs(
248
+ set_(count_var, to=self.initial_count),
249
+ On("click")[
250
+ set_(count_var, to=count_var + 1),
251
+ set_(var("me").textContent, to=f"Count is: {count_var}"),
252
+ ],
253
+ )
254
+ )[f"Count is: {self.initial_count}"]
255
+ ]
256
+
257
+ @staticmethod
258
+ def style():
259
+ return {
260
+ "button": {
261
+ "background-color": "#4A90D9",
262
+ "color": "#ffffff",
263
+ "border": "none",
264
+ "border-radius": "6px",
265
+ "padding": "10px 20px",
266
+ "font-size": "16px",
267
+ "font-weight": "600",
268
+ "cursor": "pointer",
269
+ "transition": "background-color 0.2s ease",
270
+ },
271
+ "button:hover": {"background-color": "#3A7BC8"},
272
+ }
273
+ ```
274
+ - There are two new additions here, first, there is the `initial_count` class/instance variable, like dataclasses, you can pass different values to `initial_count` when instantiating the class, you can think of these as `props` in other UI frameworks.
275
+ - the `hs` function collects several features together (in this case, `On` and `set_`)
276
+
277
+ So how does this Counter component works? When it first runs, it sets the a variable called `:count` to initial_count (default is 0, or whatever you have passed when you created Counter), and when a click happens, it increments `:count` and puts the text `f"Count is: {count_var}"` into its textContent, that's it.
278
+ The `:` prefix before the variable signals _hyperscript that this is an `element scoped` variable, in hyperscript you can scope variables to be `local (count)`, `element (:count)`, `dom (^count)`, or `global ($count)`, because we needed access to this element across two different features, set_ and On, we made it element scoped, [more here](https://hyperscript.org/docs/language/#scoping).
279
+
280
+ Note: set_ can be used as a feature or command.
281
+ ```python
282
+ On(click)[
283
+ # used as a command inside a feature
284
+ set_(var("test"), to="something")
285
+ ]
286
+
287
+ hs(
288
+ # used as a feature
289
+ set_(var(":test"), to="something"),
290
+ On("click")[
291
+ ...
292
+ ]
293
+ )
294
+ ```
295
+
296
+ It is also worth noting this part `f"Count is: {count_var}"`, we are using f-strings with `count_var`, which is a _hyperscript expression, that only makes sense in the client, hypie allows you to use f-strings as if you are working with normal python objects, and it will do the right thing behind the scenes, for example, `f"Count is: {count_var}"` is rendered as ``` `Count is: ${:count}` ```, this might look magical but it makes interpolating _hyperscript expressions much easier, and hypie will deal with it behind the scenes.
297
+
298
+ Now you might be tempted to create an endpoint and return the Counter, but this won't work yet, hypie is super minimal, you are just returning HTML at the end of the day, we haven't yet created a Layout component that adds _hyperscript and our css (once we build our component to generate that), so lets do this:
299
+
300
+ First, let's build our css
301
+ `hypie build -i ./app/ui -o ./app/static`
302
+ And then, let's define a Layout component that encapsulates everything.
303
+ ```python
304
+ # /ui/components.py
305
+ class Layout(Component):
306
+ def template(self):
307
+ return htpy.html[
308
+ htpy.head[
309
+ htpy.title["Counter"],
310
+ htpy.link(rel="stylesheet", href="/static/_hypie_styles.css"),
311
+ # _hyperscript
312
+ htpy.script(src="https://cdn.jsdelivr.net/npm/hyperscript.org@0.9.93"),
313
+ ],
314
+ htpy.body[self.children],
315
+ ]
316
+ ```
317
+ Something you might have noticed is `self.children`, this allows us to do stuff like this:
318
+ ```python
319
+ Layout[
320
+ # this goes into self.children
321
+ div[
322
+ h1["Title"],
323
+ p["Random text"],
324
+ Counter(1)
325
+ ]
326
+ ]
327
+ ```
328
+ Okay, now we can create our endpoints
329
+ ```python
330
+ # main.py
331
+ import pathlib
332
+ from fastapi import FastAPI
333
+ from fastapi.staticfiles import StaticFiles
334
+ from fastapi.responses import HTMLResponse
335
+ from .ui.components import Layout, Counter
336
+
337
+ app = FastAPI()
338
+
339
+ app.mount(
340
+ "/static", StaticFiles(directory=pathlib.Path(__file__).parent.resolve() / "static")
341
+ )
342
+
343
+ @app.get("/")
344
+ def counter_example():
345
+ return HTMLResponse(
346
+ Layout[
347
+ Counter(5)
348
+ ]
349
+ )
350
+ ```
351
+
352
+ You can now run the app and see the blue counter button.
353
+ Reminder: We are using FastAPI here but this works with any backend framework! just adjust the code based on your framework's abstractions.
354
+ ## Todo app Example using Hypie
355
+ Let's move on to a more interesting example, a classic Todo app, inspired by the Todo app done in this [FastHTML tutorial](https://www.youtube.com/watch?v=Auqrm7WFc0I).
356
+
357
+ Our end result should look like this:
358
+
359
+ ![todo app screenshot](https://raw.githubusercontent.com/Infrared1029/hypie/main/assets/todos_example.png)
360
+ We are going to do two passes in this example, the first pass will give us a working app, the second pass will introduce new abstractions that allows us to refactor the code to reduce dependencies between components, both apps will be identical functionality wise, just one will be much easier to maintain and extend, let's start with the first pass.
361
+
362
+ ### Pass One: Working App
363
+ We can see 5 major UI components:
364
+ - Layout
365
+ - Header
366
+ - Add Todo Form
367
+ - Todo Item
368
+
369
+ Let's start with the simplest components, Layout and Header, as both are purely presentational
370
+ ```python
371
+ # /ui/components.py
372
+ class Layout(Component):
373
+ def template(self):
374
+ return htpy.fragment[
375
+ htpy.link(rel="stylesheet", href="/static/tw_styles.css"),
376
+ htpy.title["Todos"],
377
+ # _hyperscript
378
+ htpy.script(src="https://cdn.jsdelivr.net/npm/hyperscript.org@0.9.93"),
379
+ htpy.body(class_="flex flex-col gap-2 p-2")[self.children],
380
+ ]
381
+
382
+ class TodosPageHeader(Component):
383
+ def template(self):
384
+ return htpy.h1(class_="text-4xl")["Todos"]
385
+ ```
386
+ Nothing too interesting or new beside the fact that we are using tailwind here, we obviously have to compile this tailwind into a css file before returning this html from our endpoints, if you have `tailwindcss` installed you can do the following:
387
+ create a file named `input.css` in the same dir as your components, with the following contents:
388
+ ```css
389
+ @import "tailwindcss";
390
+ @source ".";
391
+ ```
392
+ and then run:
393
+ `tailwindcss -i ./app/ui/input.css -o ./app/static/tw_styles.css`
394
+ This will scan your files in `@source` and any tailwind class found will be registered, the output then goes to `-o` dir, you can also add a `--watch` argument to avoid re-running the command everytime you make a change.
395
+
396
+ Let's move to the next component, TodoItem.
397
+ A TodoItem can be toggled or deleted, let's first focus on toggling and then discuss deleting after that.
398
+
399
+ ```python
400
+ import hypie.literals as hp_literals
401
+ from hypie.commands import fetch, morph
402
+
403
+ class TodoItem(Component):
404
+ id: int
405
+ title: str
406
+ done: bool
407
+
408
+ def template(self):
409
+ return htpy.li(id=f"todo-{self.id}", class_="flex gap-2")[
410
+ htpy.a(
411
+ class_="text-sky-600 cursor-pointer select-none",
412
+ _=On("click")[fetch(
413
+ f"/toggle/{self.id}",
414
+ with_={"method": "PATCH"},
415
+ as_="HTML",
416
+ ),
417
+ morph(
418
+ hp_literals.id(f"todo-{self.id}"),
419
+ to=hp_literals.result,
420
+ ),],
421
+ )["Toggle"],
422
+ htpy.a(
423
+ class_="text-red-500 cursor-pointer select-none"
424
+ )["Delete"],
425
+ htpy.span[self.title + (" ✅" if self.done else "")],
426
+ ]
427
+ ```
428
+ This implementation only handles toggling, the Delete button does not have any _hypserscript attached to it (hence, it does nothing when clicked), when you click the "Toggle" button, the `fetch` command runs, it sends a request to `/toggle/{self.id}`, sets the request method to `PATCH` and casts the response to `HTML` using `as_=HTML`.
429
+ After that, a second command runs, `morph`, it morphs the DOM node with id `todo-{self.id}`, into the returned HTML (`hp_literals.result`), `result` is a magic variable (like `me`), which references the output of the last run command, in this case, the `fetch` command. For this to work we actually need to implement the endpoints, so let's do that.
430
+ ```python
431
+ # imports omitted for brevity
432
+
433
+ app = FastAPI()
434
+
435
+ db = {
436
+ "todos": [
437
+ {"id": 1, "title": "Do Laundry", "done": True},
438
+ {"id": 2, "title": "Study without using AI", "done": False},
439
+ ]
440
+ }
441
+
442
+
443
+ app.mount(
444
+ "/static",
445
+ StaticFiles(directory=pathlib.Path(__file__).parent / "static"),
446
+ name="static",
447
+ )
448
+
449
+
450
+ @app.get("/")
451
+ def get():
452
+ return HTMLResponse(
453
+ Layout[
454
+ TodosPageHeader,
455
+ htpy.ul(id="todo-items-container")[
456
+ # list is auto-unpacked no need to use *[...]
457
+ [TodoItem(**t) for t in db["todos"]]
458
+ ],
459
+ ]
460
+ )
461
+
462
+
463
+ @app.patch("/toggle/{tid}")
464
+ def toggle_todo(tid: int):
465
+ for todo in db["todos"]:
466
+ if todo["id"] == tid:
467
+ todo["done"] = not todo["done"]
468
+ return HTMLResponse(TodoItem(**todo))
469
+ ```
470
+ Our database is a simple dict to keep things simple, and we seed it with some dummy data, we have two endpoints so far, one to return the actual page, and another to return a toggled todo, everything should work so far so let's move on to deleting.
471
+
472
+ With deleting, we can simply do the exact same as we did with toggling, i.e. send a DELETE request to an endpoint and just remove the DOM node, but its a good opportunity to introduce another abstraction, `Template`.
473
+
474
+ Instead of removing the todo directly, it might be useful to first render a Modal that asks the user if they want to actually remove the TodoItem? we can implement this by creating an endpoint that returns the Modal html and render it, but this feels overkill, it is useful if we can create pure client-side rendered elements, like Modals, without having to talk to the server at all.
475
+ So let's re-iterate, we want, when we click on the delete button, to render a modal, client-side, no server round trips, and through that modal, we perform the actual deletion, let's do that, first we create the Modal Template
476
+ ```python
477
+ from typing import Annotated
478
+ from hypie.literals import Expr
479
+
480
+ class TodoDeleteModalTemplate(Template):
481
+ todo_id: Annotated[int, Expr]
482
+ title: Annotated[str, Expr]
483
+
484
+ def template(self):
485
+ return htpy.div(
486
+ _=On("click")[
487
+ remove(hp_literals.q("[data-delete-todo-modal]"))
488
+ ],
489
+ data_delete_todo_modal=True,
490
+ class_="flex flex-col justify-center items-center w-screen h-screen fixed left-0 top-0 bg-black/80",
491
+ )[
492
+ htpy.div(
493
+ # halt click events originating from the inner div
494
+ _=On("click")[halt_event(halt_bubbling=True)],
495
+ class_="text-black bg-white rounded-sm p-1 flex flex-col justify-center items-center gap-2",
496
+ )[
497
+ htpy.div[f"Are you sure you would like to delete todo: {self.title}"],
498
+ htpy.div(class_="flex gap-2")[
499
+ # when clicking the 'Yes' button, send a DELETE request with provided todo_id, then remove any todo items with that id
500
+ htpy.button(
501
+ _=On("click")[
502
+ fetch(f"/{self.todo_id}", as_="HTML", with_={"method": "DELETE"}),
503
+ remove(hp_literals.id(f"#todo-{self.todo_id}")),
504
+ remove(hp_literals.q("[data-delete-todo-modal]")),
505
+ ],
506
+ class_="w-3xs cursor-pointer bg-red-500 p-1 rounded-sm text-white",
507
+ )["Yes"],
508
+ # When clicking on the 'Cancel' button, remove any [data-delete-todo-modal] from the dom
509
+ htpy.button(
510
+ _=On("click")[
511
+ log("deleting"), remove(hp_literals.q("[data-delete-todo-modal]"))
512
+ ],
513
+ class_="w-3xs cursor-pointer bg-gray-500 p-1 rounded-sm",
514
+ )["Cancel"],
515
+ ],
516
+ ]
517
+ ]
518
+ ```
519
+ This creates a modal that looks like this (when rendered):
520
+ ![todo delete modal](https://raw.githubusercontent.com/Infrared1029/hypie/main/assets//todo_delete_modal.png)
521
+
522
+ Templates are basically client-side parameterized html fragments, that you can render with concrete data on demand, one of the main differences between `Component` and `Template` is that `templates` expect client side expressions as their parameters, not python objects (basic python types like int, float, str, list and dict are coerced to the proper _hyperscript type), another difference, a `Template` needs to register its HTML template before you can render it, this is done using `Template.register_template()`, after registering the template, you can render it and then put it anywhere using the `render` and `put` commands.
523
+ Let's register the template in a stable place:
524
+ ```python
525
+ @app.get("/")
526
+ def get():
527
+ return HTMLResponse(
528
+ Layout[
529
+ # register template
530
+ TodoDeleteModalTemplate.register_template(),
531
+ TodosPageHeader,
532
+ htpy.ul(id="todo-items-container")[
533
+ # list is auto-unpacked no need to use *[...]
534
+ [TodoItem(**t) for t in db["todos"]]
535
+ ],
536
+ ]
537
+ )
538
+ ```
539
+ And now let's render it when click the "delete" button in a TodoItem:
540
+ ```python
541
+ class TodoItem(Component):
542
+ id: int
543
+ title: str
544
+ done: bool
545
+
546
+ def template(self):
547
+ return htpy.li(id=f"todo-{self.id}", class_="flex gap-2")[
548
+ htpy.a(
549
+ class_="text-sky-600 cursor-pointer select-none",
550
+ _=On("click")[fetch(
551
+ f"/toggle/{self.id}",
552
+ as_="HTML",
553
+ with_={"method": "PATCH"},
554
+ ),
555
+ morph(
556
+ hp_literals.id(f"#todo-{self.id}"),
557
+ to=hp_literals.result,
558
+ ),],
559
+ )["Toggle"],
560
+ htpy.a(
561
+ class_="text-red-500 cursor-pointer select-none",
562
+
563
+ # render a TodoDeleteModal and put it at the end of document's body
564
+ _=On("click")[
565
+ render(
566
+ TodoDeleteModalTemplate(
567
+ self.id, self.title
568
+ )
569
+ ),
570
+ # result here is the "rendered" template from the previous command
571
+ put(hp_literals.result, "at end of", hp_literals.body),
572
+ ],
573
+ )["Delete"],
574
+ htpy.span[self.title + (" ✅" if self.done else "")],
575
+ ]
576
+ ```
577
+ So, to conclude this part, if you need to render client-side html, use Templates (templates are basically [_hyperscript templates](https://hyperscript.org/docs/templates/)).
578
+
579
+ Let's move on to the final UI Component, AddTodoForm:
580
+ ```python
581
+ class AddTodoForm(Component):
582
+ def template(self):
583
+ return htpy.div(class_="flex gap-1")[
584
+ htpy.input(
585
+ id="add-todo-input",
586
+ class_="p-1 border-1 border-gray-500 rounded-sm",
587
+ placeholder="Add New Todo",
588
+ ),
589
+ htpy.button(
590
+ class_="bg-cyan-700 text-white p-1 rounded-sm cursor-pointer",
591
+
592
+ # when this button is clicked
593
+ # make a POST request to "/"
594
+ # then put the retruned HTML at the end of #todo-items-container
595
+ # then clear #add-todo-input value
596
+ _=On("click")[
597
+ fetch(
598
+ "/",
599
+ with_={
600
+ "method": "POST",
601
+ "body": hp_literals.as_type(
602
+ {"title": hp_literals.id("add-todo-input").value}, "JSONString"
603
+ ),
604
+ "headers": {
605
+ "Content-Type": "application/json"
606
+ }
607
+ },
608
+ as_="HTML",
609
+ ),
610
+ put(hp_literals.result, "at end of", hp_literals.id("todo-items-container")),
611
+ set_(hp_literals.id("add-todo-input").value, to=""),
612
+
613
+ ],
614
+ )["Save"],
615
+ ]
616
+ ```
617
+ and the associated endpoint:
618
+ ```python
619
+ @app.post("/")
620
+ def create_todo_(title: Annotated[str, Body(embed=True)]):
621
+ id = random.randint(0, 1_000_000)
622
+ db["todos"].append({"id": id, "title": title, "done": False})
623
+ return HTMLResponse(TodoItem(id=id, title=title, done=False))
624
+ ```
625
+ That's it, now we have a very basic todo app!
626
+
627
+ ### Pass Two: Refactor Towards Purer Components and an Event Driven System
628
+ Even though our app works, there is an issue, our components/template are not... **pure**. What does that mean, you might ask? It means, our components are dependent on things outside of their scope, for example, they are performing fetch requests, this depends on what the server returns, whether the network fails, and other variables, that are outside the scope of the component, other components like `AddTodoForm`, beside doing fetch requests as well, is also modifying other parts of the DOM, this creates a dependency between `AddTodoForm` and the parts its trying to modify, if those bits change, we will have to update `AddTodoForm`, not good.
629
+
630
+ Now a valid objection might be: "We still need to make those fetch requests and DOM modifications, the app would not work otherwise!"
631
+ And in a typical LLM fashion the answer would be: "You are absolutely right!", the answer is not to remove those bits, but to structure our app such that our components do not perform those side effects (as best as we can), and parts of the code that actually perform those side effects are in well known locations that we can easily modify and change.
632
+
633
+ So how can we do this? one answer I really like (and _hyperscript likes): Events
634
+
635
+ Let's think about our UI components/templates as purely visual HTML fragments that emit semantically meaningful events, for example, if you have a button that adds something to a cart, instead of implementing the logic that does server fetch or manipulates the DOM (beyond the component's DOM), have it emit an event, something meaningful and unique like, AddItemToCart(item_id: *some_id*), we can then handle that event (and other events) in a more suitable place, so back to our TodoApp, let's think about the different events our app emits.
636
+
637
+ There are around 4 main events:
638
+ - Toggle Todo Item (when you toggle a todo item)
639
+ - Request Removal of Todo Item (when you click delete on a todo item to open up the modal)
640
+ - Remove Todo Item (when you click Yes on the removal modal)
641
+ - Canceling the removal process (when clicking cancel on the modal, this does not have to be its own event because it is really contained within the Modal itself but let's keep it)
642
+ - Adding a Todo Item (when you click on Add, in AddTodoForm)
643
+
644
+ Hypie has an Event abstraction that allows you to define your events and emit them, lets define our events:
645
+ ```python
646
+ from hypie.events import Event
647
+
648
+ class ToggleTodoEvent(Event):
649
+ id: int
650
+
651
+
652
+ class RequestRemoveTodoEvent(Event):
653
+ id: int
654
+ title: str
655
+
656
+
657
+ class RemoveTodoEvent(Event):
658
+ id: int
659
+
660
+
661
+ class AddTodoEvent(Event):
662
+ title: str
663
+
664
+
665
+ class CancelTodoDeletionEvent(Event):
666
+ pass
667
+
668
+ ```
669
+
670
+ events that expect arguments have their arguments defined as class variables (similiar to python dataclasses).
671
+
672
+ We can then re-write our components to emit events instead of doing any kind of side-effects.
673
+ ```python
674
+ class TodoDeleteModalTemplate(Template):
675
+ todo_id: Annotated[int, Expr]
676
+ title: Annotated[str, Expr]
677
+
678
+ def template(self):
679
+ return htpy.div(
680
+ _=On("click")[trigger(CancelTodoDeletionEvent)],
681
+ data_delete_todo_modal=True,
682
+ class_="flex flex-col justify-center items-center w-screen h-screen fixed left-0 top-0 bg-black/80",
683
+ )[
684
+ htpy.div(
685
+ _=On("click")[halt_event(halt_bubbling=True)],
686
+ class_="text-black bg-white rounded-sm p-1 flex flex-col justify-center items-center gap-2 z-99",
687
+ )[
688
+ htpy.div[f"Are you sure you would like to delete todo: {self.title}"],
689
+ htpy.div(class_="flex gap-2")[
690
+ htpy.button(
691
+ _=On("click")[trigger(RemoveTodoEvent(self.todo_id))],
692
+ class_="w-3xs cursor-pointer bg-red-500 p-1 rounded-sm text-white",
693
+ )["Yes"],
694
+ htpy.button(
695
+ _=On("click")[
696
+ trigger(CancelTodoDeletionEvent())
697
+ ],
698
+ class_="w-3xs cursor-pointer bg-gray-500 p-1 rounded-sm",
699
+ )["Cancel"],
700
+ ],
701
+ ]
702
+ ]
703
+
704
+
705
+ class TodoItem(Component):
706
+ id: int
707
+ title: str
708
+ done: bool
709
+
710
+ def template(self):
711
+ return htpy.li(id=f"todo-{self.id}", class_="flex gap-2")[
712
+ htpy.a(
713
+ class_="text-sky-600 cursor-pointer select-none",
714
+ _=On("click")[trigger(ToggleTodoEvent(id=self.id))],
715
+ )["Toggle"],
716
+ htpy.a(
717
+ class_="text-red-500 cursor-pointer select-none",
718
+ _=On("click")[
719
+ trigger(RequestRemoveTodoEvent(id=self.id, title=self.title))
720
+ ],
721
+ )["Delete"],
722
+ htpy.span[self.title + (" ✅" if self.done else "")],
723
+ ]
724
+
725
+
726
+ class AddTodoForm(Component):
727
+ def template(self):
728
+ return htpy.div(class_="flex gap-1")[
729
+ htpy.input(
730
+ id="add-todo-input",
731
+ class_="p-1 border-1 border-gray-500 rounded-sm",
732
+ placeholder="Add New Todo",
733
+ ),
734
+ htpy.button(
735
+ class_="bg-cyan-700 text-white p-1 rounded-sm cursor-pointer",
736
+ _=On("click")[
737
+ trigger(AddTodoEvent(title=hp_literals.id("add-todo-input").value))
738
+ ],
739
+ )["Save"],
740
+ ]
741
+ ```
742
+ using the `trigger` command, we are triggering our semantically meaningful events instead of doing any work inside the components, now for the last part, we need to handle these events and put back the logic we removed there.
743
+ One way to do this is by putting a dummy element, with display: hidden; in a way such that all events that bubble go through it, this would work, but I prefer to have this logic in a place that does not clutter my DOM, that introduces as to the final abstraction, the `HyperScript` class.
744
+
745
+ You can define a class that implements a `script` static/class method, that inherits the `HyperScript` class, the script method must return _hyperscript, after that, running `hypie build -i <input-dir-path> -o <output-dir-path>` will compile this class into a _hyperscript file (`_hypie_hyperscript._hs` file), all features defined in this _hs file are attached to the document, so it can listen to **all** events emitted by your app, without cluttering your DOM, let's see how this looks in our todo app:
746
+ ```python
747
+ class TodoEventHandler(HyperScript):
748
+ @classmethod
749
+ def script(cls):
750
+ return hs(
751
+ On(CancelTodoDeletionEvent)[
752
+ remove(hp_literals.q("[data-delete-todo-modal]"))
753
+ ],
754
+ On(RequestRemoveTodoEvent)[
755
+ render(
756
+ TodoDeleteModalTemplate(
757
+ RequestRemoveTodoEvent.id, RequestRemoveTodoEvent.title
758
+ )
759
+ ),
760
+ put(hp_literals.result, "at end of", hp_literals.var("body")),
761
+ ],
762
+ On(ToggleTodoEvent)[
763
+ fetch(
764
+ f"/toggle/{ToggleTodoEvent.id}",
765
+ as_="HTML",
766
+ with_={"method": "PATCH"},
767
+ ),
768
+ morph(
769
+ hp_literals.id(f"#todo-{ToggleTodoEvent.id}"),
770
+ to=hp_literals.result,
771
+ ),
772
+ ],
773
+ On(RemoveTodoEvent)[
774
+ fetch(f"/{RemoveTodoEvent.id}", as_="HTML", with_={"method": "DELETE"}),
775
+ remove(hp_literals.id(f"#todo-{RemoveTodoEvent.id}")),
776
+ remove(hp_literals.q("[data-delete-todo-modal]")),
777
+ ],
778
+ On(AddTodoEvent)[
779
+ fetch(
780
+ "/",
781
+ with_={
782
+ "method": "POST",
783
+ "body": hp_literals.as_type(
784
+ {"title": AddTodoEvent.title}, "JSONString"
785
+ ),
786
+ "headers": {"Content-Type": "application/json"},
787
+ },
788
+ as_="HTML",
789
+ ),
790
+ put(
791
+ hp_literals.result,
792
+ "at end of",
793
+ hp_literals.id("todo-items-container"),
794
+ ),
795
+ set_(hp_literals.id("add-todo-input").value, to=""),
796
+ ],
797
+ )
798
+ ```
799
+ Note: `hypie build` is used to find all css and hyperscript in the input dir specified, its the same command for both.
800
+
801
+ Now we run `hypie build -i <input-path> -o <output-path> --watch` (the `--watch` argument will detect if changes happen to the dir and re-run the command, instead of re-running it on every change), and now we reference the output file in our `Layout` Component:
802
+ ```python
803
+ class Layout(Component):
804
+ def template(self):
805
+ return htpy.fragment[
806
+ htpy.link(rel="stylesheet", href="/static/tw_styles.css"),
807
+ htpy.title["Todos"],
808
+ # _hyperscript
809
+ htpy.script(type="text/hyperscript", src="/static/_hypie_hyperscript._hs"),
810
+ htpy.script(src="https://cdn.jsdelivr.net/npm/hyperscript.org@0.9.93"),
811
+ htpy.body(class_="flex flex-col gap-2 p-2")[self.children],
812
+ ]
813
+ ```
814
+ And that's it, refactor done! the application works the same, but the code is easier to work with.
815
+
816
+ This marks the end of this somewhat lengthy README, Hypie is still experimental, and there is a lot to document and iron out, hopefully this project, feels like fresh take on frontend development.