martes, 17 de septiembre de 2024

Fast HTML

 Para que no de problemas, hay que crear un entorno virtual que no comparta con nadie, pues si se aprovecha un entorno virtual existente, algunas librerías pueden dar problemas de compatibilidad.

A partir de quí, se siguen las instrucciones de  https://docs.fastht.ml/tutorials/quickstart_for_web_devs.html

0.Introducción

Crear una aplicación y servirla: Hace falta hacer el import de fasthtml, hacer un get, devolver código html y "servirla"

from fasthtml.common import FastHTML, serve

app = FastHTML()

@app.get("/")
def home():
    return "<h1>Hello, World</h1>"

serve()


Crear código html: FastHtml tiene funciones para crear "div", "p" etc, y además puede utilizar marcas del framework css "pico". Solo se muestra como construir el html

page = Html(
    Head(Title('Some page')),
    Body(Div('Some text, ', A('A link', href='https://example.com'), Img(src="https://placehold.co/200"), cls='myclass')))
print(to_xml(page))
return page


Routing: Se definen de 3 maneras:

  1. Como @app.route('/ruta01', methods=['get','post']) más la función que devuelve el html de esta ruta
  2. Como @app.get('/ruta02') más la función que devuelve el html
  3. Como @rt('/ruta03') mas una función que se llama igual que el método (get(), post(), delete() ...

@app.route("/", methods='get')
def home():
    return H1('Hello, World')

@app.get("/")
def my_function():
    return "Hello World from a GET request"

rt = app.route
@rt("/")
def post():
    return "Hello World from a POST request"

client.post("/").text


Styling: Para que todos los "headers" tengan el mismo estilo, se añade la opción "hdrs" cuando llamamos a FastHTML(). También podemos añadir otras librerías como Flexbox

from fasthtml.common import *
css = Style(':root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}')
flexbotlink = Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css")
app = FastHTML(hdrs=(picolink, css, flexbotlink))


Testing: Usar scarlette.testclient para ver el html generado

from starlette.testclient import TestClient
client = TestClient(app)
r = client.get("/")
print(r.text)


1. htmx

OJO: Sin htmx devolvemos tuplas de html, pero con htmx se devolverán todos el código!!!!

Veamos este ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
app = FastHTML()

count = 0

@app.get("/")
def home():
    return Title("Count Demo"), Main(
        H1("Count Demo"),
        P(f"Count is set to {count}", id="count"),
        Button("Increment", hx_post="/increment", hx_target="#count", hx_swap="innerHTML")
    )

@app.post("/increment")
def increment():
    print("incrementing")
    global count
    count += 1
    return f"Count is set to {count}"

serve()

Línea 10: El botón hace una petición tipo post (hx_post) a "/increment", cuyo resultado se aplica al elemento (hx_target) con id="count" (que en este caso es el elemento "p" de la línea anterior (9). Pero con hx_swap="innerHTML", lo que hacemos es substituir el html interno del elemento por el contenido que se devuelve en la llamada por post

Línea 13 a18: Fúnción de llamada a post que devuelve el contenido a asignar al elemento "p" de la línea 10.


2. Todo app

Nos mostraria esta pantalla:



Veamos el código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#1. Import fasthtml
from fasthtml.common import * 

#2. Define the todo element's render function
def todoRender(todo):
	tid_=f'todo-{todo.id}'
	
	toggle = A(
		Mark('Toggle'), 
		hx_get=f'/toggle/{todo.id}', 
		target_id=tid_)
	
	delete = A(
		Mark('Delete'), 
		hx_delete=f'/{todo.id}', 
		hx_swap="outerHTML", 
		target_id=tid_
	)
	
	return Li(
    	toggle, ' ', delete, ' ',
        ' '+todo.title+ ' '+str(todo.id)+(' ✅' if todo.done else ''),
		id=tid_
	)

#3. Create a FastHTML app
app, rt, todos, Todo = fast_app(
    'todos.db',
    live=True, 
    render=todoRender,
    id= int, 
    title=str, 
    done=bool,
    pk='id'
)   

#4. Define the input element
def mk_input():
	return Input(
		placeholder="What needs to be done?", 
		id='title',
		hx_swap_oob='true',
		)

#5. Main form
@rt("/")
def get():
    frm= Form(
		Group(
			mk_input(),
			Button("Add")
		),
		hx_post='/',
		target_id='todo-list',
		hx_swap='beforeend'
	)
	
    return Titled(
        'Todos',
        Card(
            Ul(*todos(), id='todo-list'),
            header=frm
		)    
	)    

#6. Toggle element 
@rt("/toggle/{tid}")
def get(tid:int):
	
	todo=todos[tid]
	todo.done = not todo.done
	return todos.update(todo)
	
#7. Delete element	
@rt("/{tid}")
def delete(tid:int):
	todos.delete(tid)

#8. Add element
@rt("/")
def post(todo:Todo):
	return todos.insert(todo), mk_input()
		

#9. Run the application on the server
serve()

Línea 27: En vez de llamar a FastHTML() utilizamos fast_app() y devolvemos:

  • app, rt: para routing
  • todo: que és una "matriz" de elementos (no confundir con tupla, dictionary o list)
  • Todo: que es el tipo de elemento. OJO: VS se queja diciendo "Variable not allowed in a type expression", però al executar no dona cap problema. El motiu és que "pylance" no puede averiguar ese tipo pues se genera dinámicamente. 

Los parámetros de llamada son:

  • 'todos.db' que es el nombre de una base de datos sqlite que se guarda en la misma carpeta. Se podría poner 'data/todo.db' para que lo gardara en la subacarpeta data
  • live=True para que recargue el navegador la página cuando hay un cambio. Solo para desarrollo y no para producción
  • render=todoRender que es la función de renderizado de cada elemento "todo"
  • id,title,done,pk: son elementos a tener en cuenta a la hora de renderizar.

Línea 5: Función de renderizado, a la que se indican vínculos a acciones (get para toggle y delete para delete) que se aplican al target_id. Se crea una lista cuyos elementos tienen un "id" que utilian los vínculos para actuar sobre ellos.

Para obtener el símbolo  buscamoe en google "check button emoji"  que nos lleva a https://emojipedia.org/check-mark-button donde podemos copiar el elemento

Línea 68: Cuidado con el manejo de la matriz "todos". No podemos hacer:

todos[tid].done = not todos[tid].done

Linea 38: Cremos una función que la llamamos para construir cada vez el "input" del título de elemento todo, así aseguramos que borramos su contenido cuando hacemos una alta (botón add)

Línea 42hx_swap_oob='true' indica que se cambiara todo el elemento indicado en el id

Línea 55: hx_swap='beforeend' indica que se añade la respuestaal final

Línea 55: hx_swap='outerHTML' indica que cambia todo el elemento con la respuesta


3. Usando Flexbox

Vemos un ejemplo de como se utiliza:


#1. Import flexbox
#2. Create a parent Div with 3 childre:
#   the first has the 12 cells and the next 2 children with 6 columns each 
grid = Html(
    Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css", type="text/css"),
    Div(
        Div(Div("This takes up the full width", cls="box", style="background-color: #800000;"), cls="col-xs-12"),
        Div(Div("This takes up half", cls="box", style="background-color: #008000;"), cls="col-xs-6"),
Div(Div("This takes up half", cls="box", style="background-color: #0000B0;"), cls="col-xs-6"), cls="row", style="color: #fff;" ) ) show(grid)


4. Trabajando con la sesión

El parámetro "session" del enrutamiento es un pequeño diccionario donde guardar datos

# if the session_id does not exist, then let's assign one
@app.get("/")
def get(session):
    if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())
    return H1(f"Session ID: {session['session_id']}")




No hay comentarios :

Publicar un comentario