Cursor rule
.cursor/rules/htmx.mdcThis rule is to help create interactive behavior with HTMX and fasthtml, which is the preferred approach
Cursor rules
Quality
54/100
Scores the file, not the repository.Length
794 words
12 headings · 5 code blocksRepository
69
— · pushed 363 days agoLast changed
3 days ago
First indexed 3 days ago.123456To use HTMX with FastHTML you can pass the attributes to the HTML elements (e.g. `Button("Show", hx_get=my_route, hx_target='#my-target')`). Key things to keep in mind:78In FastHTML routes stringify to the route path so you can be pythonic instead of passing strings. For example if we have the following routes:910```python11@rt12def my_route(): return "Hello world"1314@rt15def my_second_route(my_arg:str): return my_arg16```1718In this example `Button("Show", hx_get=my_route)` would translate to `Button("Show", hx_get='/my_route')`.1920For routes that have query or path parameters we can use the `to` method if it's not passed via a form. For example, `Button("Show", hx_get=my_second_route.to(my_arg="Goodbye"))` would translate to `Button("Show", hx_get='/my_second_route?my_arg=Goodbye')`.2122`@rt` by default exposes routes as both a `GET` and a `POST`, which is almost always what I want.2324## File Upload Example2526```python27from base64 import b64encode28from fasthtml.common import *29from monsterui.all import *3031app, rt = fast_app(hdrs=Theme.blue.headers())3233@rt34def index():35 inp = Card(36 H3("Drag and drop images here"),37 # HTMX for uploading multiple images38 Input(type="file",name="images", multiple=True, required=True,39 # Call the upload route on change40 post=upload, hx_target="#image-list", hx_swap="afterbegin", hx_trigger="change",41 # encoding for multipart42 hx_encoding="multipart/form-data",accept="image/*"))4344 return DivCentered(inp, H3("👇 Uploaded images 👇"), Div(id="image-list"))4546async def ImageCard(image):47 contents = await image.read()48 # Create a base64 string49 img_data = f"data:{image.content_type};base64,{b64encode(contents).decode()}"50 # Create a card with the image51 return Card(H4(image.filename), Img(src=img_data, alt=image.filename))5253@rt54async def upload(images: list[UploadFile]):55 # Create a grid filled with 1 image card per image56 return Grid(*[await ImageCard(image) for image in images])5758serve()59```6061## Cascading DropDown Example6263```python64from fasthtml.common import *65from monsterui.all import *66from fasthtml import ft6768app, rt = fast_app(hdrs=Theme.blue.headers())6970chapters = ['ch1', 'ch2', 'ch3']71lessons = {72 'ch1': ['lesson1', 'lesson2', 'lesson3'],73 'ch2': ['lesson4', 'lesson5', 'lesson6'],74 'ch3': ['lesson7', 'lesson8', 'lesson9']}7576def mk_opts(nm, cs):77 return (78 ft.Option(f'-- select {nm} --', disabled='', selected='', value=''),79 *map(ft.Option, cs))8081@rt82def get_lessons(chapter: str):83 return ft.Select(*mk_opts('lesson', lessons[chapter]), name='lesson')8485@rt86def index():87 chapter_dropdown = ft.Select(88 *mk_opts('chapter', chapters),89 name='chapter',90 hx_get=get_lessons, hx_target='#lessons',91 label='Chapter:')9293 return Container(94 DivLAligned(FormLabel("Chapter:", for_="chapter"),chapter_dropdown),95 DivLAligned(96 FormLabel("Lesson:", for_="lesson"),97 Div(id='lessons')),98 cls='space-y-4')99100serve()101```102103## Infinite Scroll Example104105```python106from fasthtml.common import *107from monsterui.all import *108import uuid109110column_names = ('name', 'email', 'id')111112def generate_contact(id: int) -> Dict[str, str]:113 return {'name': 'Agent Smith',114 'email': f'void{str(id)}@matrix.com',115 'id': str(uuid.uuid4())116 }117118def generate_table_row(row_num: int) -> Tr:119 contact = generate_contact(row_num)120 return Tr(*[Td(contact[key]) for key in column_names])121122def generate_table_part(part_num: int = 1, size: int = 20) -> Tuple[Tr]:123 paginated = [generate_table_row((part_num - 1) * size + i) for i in range(size)]124 paginated[-1].attrs.update({125 'get': f'page?idx={part_num + 1}',126 'hx-trigger': 'revealed',127 'hx-swap': 'afterend'})128 return tuple(paginated)129130app, rt = fast_app(hdrs=Theme.blue.headers())131132@rt133def index():134 return Titled('Infinite Scroll',135 Div(Table(136 Thead(Tr(*[Th(key) for key in column_names])),137 Tbody(generate_table_part(1)))))138139@rt140def page(idx:int|None = 0):141 return generate_table_part(idx)142```143144## Simple Todo App Example145146```python147# Database Model148class Todo:149 title: str150 done: bool151 due: date152 id: int153154# Sqlite Database connection with fastlite155db = database('intermediate_todo.db')156157# Create a connection to the database table todo.158# Creates the table if it doesn't exist with columns id and title making id the primary key by default159todos = db.create(Todo)160161# Create a fasthtml app with the slate theme162app, rt = fast_app(hdrs=Theme.slate.headers())163164def tid(id): return f'todo-{id}'165166# Render all the todos ordered by todo due date167def mk_todo_list(): return Grid(*todos(order_by='due'), cols=1)168169@app.delete170async def delete_todo(id:int):171 "Delete if it exists, if not someone else already deleted it so no action needed"172 try: todos.delete(id)173 except NotFoundError: pass174 # Because there is no return, the todo will be swapped with None and removed from UI175176# patch is a decorator that patches the __ft__ method of the Todo class177# this is used to customize the html representation of the Todo object178@patch179def __ft__(self:Todo):180 # Set color to red if the due date is passed181 dd = datetime.strptime(self.due, '%Y-%m-%d').date()182 due_date = Strong(dd.strftime('%Y-%m-%d'),style= "" if date.today() <= dd else "background-color: red;")183184 # Action Buttons185 _targets = {'hx_target':f'#{tid(self.id)}', 'hx_swap':'outerHTML'}186 done = CheckboxX( hx_get =toggle_done.to(id=self.id).lstrip('/'), **_targets, checked=self.done),187 delete = Button('delete', hx_delete=delete_todo.to(id=self.id).lstrip('/'), **_targets)188 edit = Button('edit', hx_get =edit_todo .to(id=self.id).lstrip('/'), **_targets)189190 # Strike through todo if it is completed191 style = Del if self.done else noop192193 return Card(DivLAligned(done,194 style(Strong(self.title, target_id='current-todo')),195 style(P(due_date,cls=TextPresets.muted_sm)),196 edit,197 delete),198 id=tid(self.id))199200@rt201async def index():202 "Main page of the app"203 return Titled('Todo List',mk_todo_form(),Div(mk_todo_list(),id='todo-list'))204205@rt206async def upsert_todo(todo:Todo):207 # Create/update a todo if there is content208 if todo.title.strip(): todos.insert(todo,replace=True)209 # Reload main page with updated database content210 return mk_todo_list(),mk_todo_form()(hx_swap_oob='true',hx_target='#todo-input',hx_swap='outerHTML')211212@rt213async def toggle_done(id:int):214 "Reverses done boolean in the database and returns the todo (rendered with __ft__)"215 return todos.update(Todo(id=id, done=not todos[id].done))216217218def mk_todo_form(todo=Todo(title=None, done=False, due=date.today(), id=None), btn_text="Add"):219 """Create a form for todo creation/editing with optional pre-filled values"""220 inputs = [Input(id='new-title', name='title',value=todo.title, placeholder='New Todo'),221 Input(id='new-done', name='done', value=todo.done, hidden=True),222 Input(id='new-due', name='due', value=todo.due)]223224 # If there is an ID use it for editing existing row in db225 if todo.id: inputs.append(Input(id='new-id', name='id', value=todo.id, hidden=True))226227 return Form(DivLAligned(228 *inputs,229 Button(btn_text, cls=ButtonT.primary, post=upsert_todo,hx_target='#todo-list', hx_swap='innerHTML')),230 id='todo-input', cls='mb-6')231232@rt233async def edit_todo(id:int): return Card(mk_todo_form(todos.get(id), btn_text="Save"))234235serve()236```237238
Also in ai-evals-course/isaac-fasthtml-workshop
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| ai-evals-course/isaac-fasthtml-workshop.cursor/rules/fasthtml.mdc · 69 | Cursor rules | archtypesdatabaseui+2 | 81/100 | 3 days ago | |
| ai-evals-course/isaac-fasthtml-workshop.cursor/rules/fastapi-react-backend-rules.mdc · 69 | Cursor rules | lint-formatstyledependenciesdo-not | 40/100 | 3 days ago | |
| ai-evals-course/isaac-fasthtml-workshop.cursor/rules/fastapi-react-frontend-rules.mdc · 69 | Cursor rules | lint-formatstyledependenciesdo-not | 54/100 | 3 days ago | |
| ai-evals-course/isaac-fasthtml-workshop.cursor/rules/ui.mdc · 69 | Cursor rules | no sections | 16/100 | 3 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 45 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 3 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 3 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 45 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 3 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 3 days ago | |
| skillrecordings/egghead-next.cursor/rules/project-update-rules.mdc · 1.4k | Cursor rules | buildtestlint-formatstyle+7 | 96/100 | 3 days ago |
