Escriba componentes HTML directamente en Python y tendrá una mezcla hermosa pero controvertida.
Sí, controvertido .
Si no le gusta, ignórelo (pero puede usar esto sin la parte HTML-in-Python, ver a continuación;))))
Basado en pyxl. Python 3.6+ solamente, y use tipificación para la validación de datos.
Una vez que tenga su HTML, puede hacer lo que quiera con él. Piense en ello como un reemplazo para su motor de plantilla clásica.
Código fuente : https://github.com/twidi/mixt/
Documentación : https://twidi.github.io/mixt/
Pypi : https://pypi.org/project/mixt/
CI (Circleci): https://circleci.com/gh/twidi/workflows/mixt/
Creemos un archivo example.py
# coding: mixt
from mixt import html , Element , Required
class Hello ( Element ):
class PropTypes :
name : Required [ str ]
def render ( self , context ):
return < div > Hello , { self . name } < / div >
print ( < Hello name = "World" / > )Y ejecutarlo:
$ python example.py
< div > Hello, World < /div >Si no le gusta escribir HTML en Python, aún puede usarlo:
from mixt import html , Element , Required
class Hello ( Element ):
class PropTypes :
name : Required [ str ]
def render ( self , context ):
return html . Div ()( "Hello, " , self . name )
print ( Hello ( name = "World" ))Sí, está inspirado en React (de hecho, principalmente JSX) y tomamos prestado parte del concepto:
Y agregamos:
Ejecute estos dos comandos. El segundo le dirá a Python cómo comprender los archivos con HTML en el interior.
pip install mixt
mixt-post-installPara comprobar que todo está listo, ejecute:
python -m mixt.examples.simpleDeberías tener esta salida:
< div title =" Greeting " > Hello, World </ div > Si no desea usar las cosas HTML-in-Python, no ejecute mixt-post-install . Y luego prueba con (para tener la misma salida):
python -m mixt.examples.simple_pure_pythonClon the Git Project entonces:
make devPara comprobar que todo está listo, ejecute:
python -m mixt.examples.simpleDeberías tener esta salida:
< div title =" Greeting " > Hello, World </ div >Después de haber hecho algún código:
make testsmake lint Si toca las cosas en el directorio codec , tendrá que ejecutar make dev (o al menos make full-clean ) para purgar los archivos pyc Python.
Tenga en cuenta que nuestro CI verificará que cada confirmación pase la make lint , make tests y make check-doc . Así que no olvides ejecutarlos para cada compromiso.
Una forma de hacerlo antes de empujar es:
git rebase develop --exec ' git log -n 1; make checks ' Nota: puede encontrar el código final de esta guía del usuario en src/mixt/examples/user_guide (encontrará mixt.py y pure_python.py ).
Ejecutarlo con:
python -m mixt.examples.user_guide¡Creemos una ... TODO LIST, sí!
Pero antes, recuerda. Esto no es React, no está en el navegador y no hay JavaScript involucrado aquí. Solo hablamos de representar un poco de HTML.
Pero puedes hacer lo que quieras con él. Agregar manejadores de JavaScript, formas simples ...
Hablando de formularios ...
En una lista de TODO, queremos poder agregar un TODO. Es una simple entrada de texto.
Así que creemos nuestro primer componente, el TodoForm . Queremos un formulario con un texto de entrada y un botón.
Un componente es una subclase de la clase Element , con un método render que debe escribir.
# coding: mixt
from mixt import Element , html # html is mandatory to resolve html tags
class TodoForm ( Element ):
def render ( self , context ): # Ignore the ``context`` argument for now.
return # The ```` is only for a better indentation below
< form method = "post" action = "???" >
< label > New Todo : < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form >Tenga en cuenta que esto podría haberse escrito como una función simple:
# coding: mixt
from mixt import Element , html
def TodoForm ():
return
< form method = "post" action = "???" >
< label > New Todo : < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form >Cuando imprima el componente, estos dos darán el mismo resultado:
print ( < TodoForm / > ) < form method =" post " action =" ??? " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Observe cómo está formateado: no hay espacio entre las etiquetas. De hecho, es como en JSX:
JSX elimina el espacio en blanco al principio y el final de una línea. También elimina las líneas en blanco. Se eliminan nuevas líneas adyacentes a las etiquetas; Las nuevas líneas que ocurren en el medio de los literales de cuerdas se condensan en un solo espacio
Para agregar un espacio o una nueva línea, puede pasar un poco de Python. Vamos a, como ejemplo, agregar una nueva línea antes de la etiqueta:
#...
< form method = "post" action = "???" >
{ ' n ' } < label > New Todo : < / label > < itext name = "todo" / >
#...Ahora tenemos esta salida:
< form method =" post " action =" /todo/add " >
< label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Ahora vamos más lejos.
Observe el atributo action del formulario. Necesitamos pasar algo. Pero la codificación dura no suena bien. WWE necesita pasarlo al componente.
Mixt tiene, como React , el concepto de propiedades, también conocido como "accesorios".
En Mixt , los definimos con un tipo, en una clase dentro de nuestro componente, llamado PropTypes :
class TodoForm ( Element ):
class PropTypes :
add_url : str
def render ( self , context ):
return
< form method = "post" action = { self . add_url } >
< label > New Todo : < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form > Aquí definimos un accesorio llamado add_url que debe ser una cadena ( str ). Esto usa la sintaxis de tipificación de Python.
Y observe cómo cambiamos el atributo action de la etiqueta form . Ahora es {self.add_url} en lugar de "???" .
Cuando se pasan los atributos entre los aparatos ortopédicos rizados, se interpretan como Python puro en el tiempo de ejecución. De hecho, como el analizador mixt convertirá todo el archivo a Python puro antes de dejar que el intérprete de Python lo ejecute, permanecerá igual, solo se convertirá el HTML alrededor. Entonces no hay penalización por hacer esto.
Mira cómo se vería esto si nuestro componente estuviera escrito en Pure Python:
from mixt import Element , html
class TodoForm ( Element ):
class PropTypes :
add_url : str
def render ( self , context ):
return html . Form ( method = 'post' , action = self . add_url )(
html . Label ()(
html . Raw ( "New Todo: " )
),
html . InputText ( name = 'todo' ),
html . Button ( type = 'submit' )(
html . Raw ( "Add" ) # or html.Rawhtml(text="Add")
),
) Observe cómo se pasan los accesorios a un componente como argumentos nombrados y cómo se pasa action : action=self.add_url .
Este componente Pure-Python también le muestra cómo funciona: los accesorios se pasan como argumento nombrado a la clase de componentes, entonces se llama este componente, pasando los componentes de los niños como argumentos posicionales a la llamada:
ComponentClass ( prop1 = "val1" , prop2 = "val2" )(
Children1 (),
Children2 (),
)¿Qué son los niños? Los niños son etiquetas dentro de otras etiquetas.
En <div id="divid"><span /><p>foo</p></div> , tenemos:
html.Div , con una id proporción y dos hijos:html.Span , sin hijoshtml.P con un niño:html.RawHtml con el texto "foo"Tenga en cuenta que puede jugar con accesorios y niños. Primero la versión en Pure-Python para mostrar cómo funciona:
def render ( self , context ):
props = { "prop1" : "val1" , "prop2" : "val2" }
children = [ Children1 (), Children2 ()]
return ComponentClass ( ** props )( * children )
# You can pass a list of children to to the call, so this would produce the same result:
# ComponentClass(**props)(children) Entonces la versión mixt :
def render ( self , context ):
props = { "prop1" : "val1" , "prop2" : "val2" }
children = [ < Children1 / > , < Children2 / > ]
return < ComponentClass { ** props } > { * children } < / ComponentClass >
# or, the same, passing the children as a list:
# return <ComponentClass {**props}>{children}</ComponentClass> Ahora volvamos a nuestros accesorios add_url .
¿Cómo pasarlo al componente?
De la misma manera que pasamos los atributos a las etiquetas HTML: de hecho, están definidos en los compomentes HTML (definidos en mixt.html ). Apoyamos todas las etiquetas HTML que, en el momento de la escritura, son válidas (no desaprobadas) en HTML5, con sus atributos (excluyendo los desapercibidos).
Así que hagamos esto:
print ( < TodoForm add_url = "/todo/add" / > ) < form method =" post " action =" /todo/add " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Ok, tenemos nuestro accesorio presente en el HTML renderizado.
¿Qué pasa si no pasamos una cadena? Dijimos en PropTypes que queríamos una cadena ...
Vamos a intentarlo:
print ( < TodoForm add_url = 1 / > ) < form method =" post " action =" 1 " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >¡Funciona! Pero ... ¡no es una cuerda! De hecho, hay un caso especial para los números, puede pasarlos como números en lugar de cadenas y se convierten si es necesario ...
Así que intentemos algo más.
print ( < TodoForm add_url = True / > ) mixt . exceptions . InvalidPropValueError :
< TodoForm > . add_url : `True` is not a valid value for this prop ( type : < class 'bool' > , expected : < class 'str' > ) Y es lo mismo si pasamos True en Python
print ( < TodoForm add_url = { True } / > ) mixt . exceptions . InvalidPropValueError :
< TodoForm > . add_url : `True` is not a valid value for this prop ( type : < class 'bool' > , expected : < class 'str' > ) Ok, engañemos el sistema y pasemos "True" , como una cadena.
print ( < TodoForm add_url = "True" / > ) mixt . exceptions . InvalidPropValueError :
< TodoForm > . add_url : `True` is not a valid value for this prop ( type : < class 'bool' > , expected : < class 'str' > )Sigue siendo lo mismo, ¡pero aquí pasamos una cuerda! Sí, pero hay 4 valores que siempre se evalúan a lo que parecen ser:
None )La única forma de pasar uno de estos valores como una cadena es pasarlos a través de Python, como una cadena:
print ( < TodoForm add_url = { "True" } / > ) < form method =" post " action =" True " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form > Excepto estos 4 valores y números, cada valor que se pasa a un atributo se considera una cadena. Incluso si no hay citas, como en HTML en HTML5, donde las citas no son obligatorias para las cuerdas sin algunos caracteres (no hay espacios, no / ...).
Para pasar algo más, debe rodear el valor en aparatos ortopédicos rizados (y en estos casos no hay necesidad de citas alrededor de los aparatos ortopédicos rizados.
Ok, ahora estamos seguros de que solo aceptamos una cadena ... pero ¿y si no paso nada? Y ... ¿qué no es "nada"?
Comencemos con una cadena vacía en Python:
print ( < TodoForm add_url = { "" } / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Ok, funciona, queríamos una cadena, tenemos una cadena.
Ahora pasamos esta cadena vacía directamente:
print ( < TodoForm add_url = "" / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Todavía funciona, porque todavía es una cadena. Eliminemos las citas, para ver.
print ( < TodoForm add_url = / > ) mixt . exceptions . GeneralParserError : < mixt parser > Unclosed Tags : < TodoForm > Hum, sí, esto no es HTML válido. Así que eliminemos el = :
print ( < TodoForm add_url / > ) mixt . exceptions . InvalidPropValueError :
< TodoForm > . add_url : `True` is not a valid value for this prop ( type : < class 'bool' > , expected : < class 'str' > ) ¿QUÉ? Sí, piense en los atributos HTML5 como required , checked ... Solo necesitan estar presentes como un atributo, sin valor, para considerarse True . Entonces, cuando un atributo no tiene ningún valor, es un booleano y es True .
Además de no aprobar un valor, esas otras dos formas son válidas en HTML5 para un booleano por True :
required=""required="required"Para su conveniencia, agregamos otra forma:
True (el caso no importa), como python o como una cadena: required=True , required={True} , required="true" Y su contraparte, para pasar False :
False (el caso no importa), como python o como una cadena: required=False , required={False} , required="false" Ok para los atributos booleanos. No es nuestro caso. Lo último que podemos hacer es no establecer el atributo en absoluto:
print ( < TodoForm / > )
# this is the same: ``print(<TodoForm add_url=NotProvided />)```
# (``NotProvided`` must be imported from ``mixt``) mixt . exceptions . UnsetPropError : < TodoForm > . add_url : prop is not setEs comprensible: tratamos de acceder a un accesorio que no está establecido, por supuesto que no podemos usarlo.
Pero, ¿y si no accedemos a él? Si no imprimimos el componente, no se representará:
< TodoForm / > < TodoForm at 0x7fbd18ea5630 >Para que podamos crear una instancia, pero fallará en el momento de renderizado. Pero hay una manera de prevenir eso.
Por defecto, todas las propiedades son opcionales. Y no tiene que usar el tipo Optional del módulo typing de Python para eso, sería engorroso hacerlo para cada accesorio.
En su lugar, mixt proporciona un tipo llamado Required que use de la misma manera que Optionnal .
from mixt import Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
def render ( self , context ):
# ...Así que acabamos de decir que queríamos una cuerda, y que es necesaria.
Intentemos de nuevo crearlo sin el accesorio:
< TodoForm / > mixt . exceptions . RequiredPropError : < TodoForm > . add_url : is a required prop but is not setAhora tenemos la excepción planteada anteriormente en nuestro programa.
Para ver otras posibilidades en los accesorios, agregemos uno nuevo para cambiar la etiqueta de texto. Pero no queremos hacer que sea necesario y, en cambio, tenemos un valor predeterminado.
Para esto, es tan fácil como agregar un valor al accesorio en la clase PropTypes :
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
label : str = 'New Todo'
def render ( self , context ):
return
< form method = "post" action = { self . add_url } >
< label > { self . label }: < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form >Ahora intentemos sin pasar el accesorio:
print ( < TodoForm add_url = "/todo/add" / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Y si pasamos uno:
print ( < TodoForm add_url = "/todo/add" label = "Thing to do" / > ) < form method =" post " action =" /todo/add " > < label > Thing to do: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Funciona como se esperaba.
Tenga en cuenta que no puede dar un valor predeterminado mientras se Required el accesorio. No tiene sentido, por lo que se verifica lo antes posible, mientras se construye la class :
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
label : Required [ str ] = 'New Todo' mixt . exceptions . PropTypeRequiredError : < TodoForm > . label : a prop with a default value cannot be required¡Y, por supuesto, el valor predeterminado debe coincidir con el tipo!
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
label : str = { 'label' : 'foo' } mixt . exceptions . InvalidPropValueError :
< TodoForm > . label : `{'label': 'foo'}` is not a valid value for this prop ( type : < class 'dict' > , expected : < class 'str' > ) Otra cosa que queremos hacer en nuestro componente es dejar que construya la etiqueta, pasándola un "tipo" de TODO, pero limitando las opciones. Para esto podemos usar el tipo Choices :
from mixt import Choices , Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
type : Choices = [ 'todo' , 'thing' ]
def render ( self , context ):
return
< form method = "post" action = { self . add_url } >
< label > New { self . type }: < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form >Vamos a intentarlo:
print ( < TodoForm add_url = "/todo/add" type = "todo" / > )
print ( < TodoForm add_url = "/todo/add" type = "thing" / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >
< form method =" post " action =" /todo/add " > < label > New thing: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >¿Y qué pasa si intentamos pasar algo más que las opciones disponibles? Falla, como se esperaba:
print ( < TodoForm add_url = "/todo/add" type = "stuff" / > ) mixt . exceptions . InvalidPropChoiceError : < TodoForm > . type : `stuff` is not a valid choice for this prop ( must be in [ 'todo' , 'thing' ])Pero tal vez no queremos pasarlo y usar un valor predeterminado. ¿Cuál sería el resultado?
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . UnsetPropError : < TodoForm > . type : prop is not set Por lo tanto, tenemos que marcar el accesorio type según sea necesario:
class PropTypes :
add_url : Required [ str ]
type : Required [ Choices ] = [ 'todo' , 'thing' ]Entonces, si no lo pasamos, falla antes:
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . RequiredPropError : < TodoForm > . type : is a required prop but is not setPero no es lo que queremos, queremos un valor predeterminado.
De hecho, usted notó que para tipos distintos a Choices , establecer un valor en PropTypes nos da un valor predeterminado. Pero para Choices es diferente, ya que el valor es la lista de opciones.
Para esto, tenemos DefaultChoices : funciona lo mismo que Choices , pero use la primera entrada en la lista como el valor predeterminado. Y, por supuesto, como con otros tipos que tienen un valor predeterminado, no se puede Required .
Vamos a intentarlo:
from mixt import DefaultChoices , Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
type : DefaultChoices = [ 'todo' , 'thing' ] print ( < TodoForm add_url = "/todo/add" / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Funciona como se esperaba.
Hasta entonces, usamos tipos simples, pero puede usar otros más complicados.
Entonces, por ejemplo, haremos el accesorio add_url para aceptar una función que calcule la URL para nosotros en función del type de accesorio. Pero también queremos permitir cadenas y con un valor predeterminado.
Podemos hacer eso, con la escritura. Nuestra función tomará una cadena, el type y devolverá una cadena, la URL.
Entonces, la sintaxis se Callable[[str], str] para el llamable, y usamos Union para aceptar cosas de tipo Callable o str :
from typing import Union , Callable
from mixt import DefaultChoices , Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Union [ Callable [[ str ], str ], str ] = "/todo/add"
type : DefaultChoices = [ 'todo' , 'thing' ]
def render ( self , context ):
if callable ( self . add_url ):
add_url = self . add_url ( self . type )
else :
add_url = self . add_url
return
< form method = "post" action = { add_url } >
< label > New { self . type }: < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form > Primero, intentémoslo sin el accesorio add_url , ya que tenemos un valor predeterminado:
print ( < TodoForm / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >También debería funcionar si pasamos una cadena:
print ( < TodoForm add_url = "/todolist/add" / > ) < form method =" post " action =" /todolist/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Y ahora podemos pasar una función:
def make_url ( type ):
return f"/ { type } /add"
print ( < TodoForm add_url = { make_url } / > ) mixt . exceptions . InvalidPropValueError : < TodoForm > . add_url :
`<function make_url at 0x7fe2ae87be18>` is not a valid value for this prop ( type : < class 'function' > , expected : Union [ Callable [[ str ], str ], str ])¿Oh? ¿Por qué? Pasé una función aceptando una cadena como argumento y devolviendo una cadena. Sí, ¡pero no olvides que los tipos están revisados! Entonces debemos agregar tipos a nuestra función:
def make_url ( type : str ) -> str :
return f"/ { type } /add"
print ( < TodoForm add_url = { make_url } / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Y si pasamos otro tipo, la URL debería cambiar en consecuencia:
print ( < TodoForm add_url = { make_url } type = "thing" / > ) < form method =" post " action =" /thing/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Incluso podemos hacer de esta función el valor predeterminado para nuestro accesorio:
from typing import Union , Callable
from mixt import DefaultChoices , Element , Required , html
def make_url ( type : str ) -> str :
return f"/ { type } /add"
class TodoForm ( Element ):
class PropTypes :
add_url : Union [ Callable [[ str ], str ], str ] = make_url
type : DefaultChoices = [ 'todo' , 'thing' ]
def render ( self , context ):
if callable ( self . add_url ):
add_url = self . add_url ( self . type )
else :
add_url = self . add_url
return
< form method = "post" action = { add_url } >
< label > New { self . type }: < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form > print ( < TodoForm / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Ahora puede comenzar a preguntarse ... la escritura de pitón es engorroso y la validación puede quitarle parte de nuestro precioso tiempo.
Vamos a responder eso:
Por defecto, mixt se ejecuta en "Dev-Mode". Y en el modo Dev, los accesorios se validan cuando se pasan a un componente. Cuando no está en "Dev-Mode", se omite la validación. Entonces, en la producción, puede desactivar el modo de desarrollo (veremos cómo en un minuto) y pasaremos los accesorios muy rápido:
Choices está, de hecho, en la lista de opcionesrender . Pero puede decir que está en producción que la validación es importante. En efecto. Pero, por supuesto, su código está completamente cubierto por las pruebas, que se ejecuta en modificación de Dev, y por lo tanto, en producción, ¡no necesita esta validación! Y tenga en cuenta que es cómo funciona React, por cierto, con NODE_ENV=production .
¿Cómo cambiar el modo dev? No aplicamos ninguna variable de entorno, pero proponemos algunas funciones. Depende de usted llamarlos:
from mixt import set_dev_mode , unset_dev_mode , override_dev_mode , in_dev_mode
# by default, dev-mode is active
assert in_dev_mode ()
# you can unset the dev-mode
unset_dev_mode ()
assert not in_dev_mode ()
# and set it back
set_dev_mode ()
assert in_dev_mode ()
# set_dev_mode can take a boolean
set_dev_mode ( False )
assert not in_dev_mode ()
set_dev_mode ( True )
assert in_dev_mode ()
# and we have a context manager to override for a block
with override_dev_mode ( False ):
assert not in_dev_mode ()
with override_dev_mode ( True ):
assert in_dev_mode ()
assert not in_dev_mode ()
assert in_dev_mode () Así que intentemos esto con el type accesorio. Recuerda, parece:
type : DefaultChoices = [ 'todo' , 'thing' ]Intentamos pasar otra opción, primero en el modo de desarrollo:
with override_dev_mode ( True ):
print ( < TodoForm type = "stuff" / > ) mixt . exceptions . InvalidPropChoiceError : < TodoForm > . type : `stuff` is not a valid choice for this prop ( must be in [ 'todo' , 'thing' ])Falla como se esperaba.
Y ahora desactivando el mode de desarrollo:
with override_dev_mode ( False ):
print ( < TodoForm type = "stuff" / > ) < form method =" post " action =" /stuff/add " > < label > New stuff: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form > Funciona, tenemos un tipo de TODO que no estaba en nuestras elecciones que se usan, y también está en la action . Es el trabajo de sus pruebas asegurarse de que nunca pase los accesorios no válidos, por lo que puede confiar en la producción y desactivar el modo de desarrollo.
Ahora tenemos nuestra forma. ¿Qué otros componentes necesitamos para nuestra aplicación TODO List?
Por supuesto, necesitamos una forma de mostrar una entrada de TODO.
Pero, ¿qué es una entrada de TODO? Creemos un TodoObject básico:
class TodoObject :
def __init__ ( self , text ):
self . text = textEs una clase muy simple, pero puedes usar lo que quieres, por supuesto. Podría ser modelos Django, etc ...
Para que podamos crear nuestro componente Todo , haciéndolo aceptar un TodoObject requerido como Prop:
class Todo ( Element ):
class PropTypes :
todo : Required [ TodoObject ]
def render ( self , context ):
return < li > { self . todo . text } < / li >Y podemos usarlo:
todo = TodoObject ( "foo" )
print ( < Todo todo = { todo } / > ) < li > foo </ li > Ahora queremos tener una lista de TODOS. Creemos un componente TodoList que aceptará como accesorios una lista de TodoObject .
Pero lo que es diferente a nuestros otros dos componentes, que solo usan etiquetas HTML en su método render , es que ahora encapsularemos un componente en otro. Veamos cómo.
class TodoList ( Element ):
class PropTypes :
todos : Required [ List [ TodoObject ]]
def render ( self , context ):
return < ul > {[ < Todo todo = { todo } / > for todo in self . todos ]} < / ul > Sí, es tan simple como eso: usas <Todo...> para el componente Todo como usaría una etiqueta HTML. La única diferencia es que para las etiquetas HTML, no necesita importarlas directamente (Simple Import html de mixt ), y por convención las escribimos en caso inferior. Para los componentes normales, debe importarlos (aún puede hacerlo from mylib import components y <components.MyComponent ...> ) y usar el caso exacto.
Observe cómo requerimos una lista y la pasamos a la <ul> a través de una comprensión de la lista en frenos rizados.
Puedes hacer las cosas de manera diferente si quieres.
Como separar la comprensión de la lista del HTML:
def render ( self , context ):
todos = [
< Todo todo = { todo } / >
for todo
in self . todos
]
return < ul > { todos } < / ul >O en un método dedicado (que sería útil para las pruebas):
def render_todos ( self , todos ):
return [
< Todo todo = { todo } / >
for todo
in todos
]
def render ( self , context ):
return < ul > { self . render_todos ( self . todos )} < / ul >Depende de ti: al final es solo Python.
Veamos lo que este componente representa:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print ( < TodoList todos = { todos } / > ) < ul > < li > foo </ li > < li > bar </ li > < li > baz </ li > </ ul > Y finalmente tenemos nuestro componente TodoApp que encapsula el formulario y la lista:
class TodoApp ( Element ):
class PropTypes :
todos : Required [ List [ TodoObject ]]
type : DefaultChoices = [ 'todo' , 'thing' ]
def render ( self , context ):
return
< div >
< h1 > The "{self.type}" list < / h1 >
< TodoForm type = { self . type } / >
< TodoList todos = { self . todos } / >
< / div > todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print ( < TodoList todos = { todos } type = "thing" / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > < li > bar </ li > < li > baz </ li > </ ul > </ div >Pasemos este HTML a una belleza HTML:
< div >
< h1 > The "thing" list </ h1 >
< form method =" post " action =" /thing/add " >
< label > New thing: </ label >
< input type =" text " name =" todo " />
< button type =" submit " > Add </ button >
</ form >
< ul >
< li > foo </ li >
< li > bar </ li >
< li > baz </ li >
</ ul >
</ div > Y eso es todo, ¡tenemos nuestra aplicación de lista de tareas! Para usarlo en una página, simplemente cree un componente que rinde el marcado base HTML e integrará el componente TodoApp en él. Ni siquiera necesitas un componente:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print (
< html >
< body >
< TodoApp todos = { todos } type = "thing" / >
< / body >
< / html >
)La salida embellecida sería:
< html >
< body >
< div >
< h1 > The "thing" list </ h1 >
< form method =" post " action =" /thing/add " >
< label > New thing: </ label >
< input type =" text " name =" todo " />
< button type =" submit " > Add </ button >
</ form >
< ul >
< li > foo </ li >
< li > bar </ li >
< li > baz </ li >
</ ul >
</ div >
</ body >
</ html >Tenemos una lista de tareas genéricas, pero siguiendo los tipos disponibles de TODO, es posible que deseemos tener una "lista de tareas" y una "lista de cosas".
Ya tenemos la lista de TODO porque nuestra TodoApp tiene un tipo de todo de forma predeterminada.
Así que creemos una ThingApp .
La primera forma de hacer esto es heredar desde nuestra TodoApp . Pero al heredar no podemos eliminar los accesorios del padre (no es realmente cierto, veremos esto más tarde), por lo que todavía tenemos el type de accesorio de tipo. Pero no queremos aceptar nada más que "cosa". Entonces podemos redefinir el type accesorio como este:
class ThingApp ( TodoApp ):
class PropTypes :
type : DefaultChoices = [ 'thing' ]Usemos este componente:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form method =" post " action =" /thing/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form > < ul > < li > foo </ li > </ ul > </ div > Si intentamos pasar "TODO" para los accesorios type , no funcionará:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "todo" / > ) mixt . exceptions . InvalidPropChoiceError :
< ThingApp > . type : `todo` is not a valid choice for this prop ( must be in [ 'thing' ])Pero aún así, es extraño poder pasar un tipo.
Probemos de otra manera: un componente principal. Un componente que no hace nada más que hacer cosas con sus hijos y devolverlo. Lo que queremos aquí es un componente que devolverá a un TodoApp con el type accesorio forzado a "cosa".
Hagamos esto
class ThingApp ( Element ):
class PropTypes :
todos : Required [ List [ TodoObject ]]
def render ( self , context ):
return < TodoApp todos = { self . todos } type = "thing" / > print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form method =" post " action =" /thing/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form > < ul > < li > foo </ li > </ ul > </ div > Funciona, y esta vez, no podemos pasar el type accesorio:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) mixt . exceptions . InvalidPropNameError : < ThingApp > . type : is not an allowed prop Observe cómo tuvimos que definir el tipo para los accesorios todos . Tanto en TodoApp como TodoThing .
Hay muchas maneras de manejar eso.
El primero sería ignorar el tipo en ThingApp porque se verificará en TodoApp . Entonces usaremos el tipo Any :
from typing import Any
#...
class ThingApp ( Element ):
class PropTypes :
todos : Any
#...Probemos con una lista válida de TODOS:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Pero, ¿y si pasamos algo más?
print ( < ThingApp todos = "foo, bar" / > ) mixt . exceptions . InvalidPropValueError :
< TodoApp > . todos : `foo, bar` is not a valid value for this prop ( type : < class 'str' > , expected : List [ TodoObject ]) Funciona como se esperaba, pero el error se informa a nivel de TodoApp , que es perfectamente normal.
Otra forma sería definir el tipo en un nivel superior:
TodoObjects = Required [ List [ TodoObject ]]
class TodoApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...
class ThingApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...Ahora, si pasamos algo más, tenemos el error informado en el nivel correcto:
print ( < ThingApp todos = "foo, bar" / > ) mixt . exceptions . InvalidPropValueError :
< TodoThing > . todos : `foo, bar` is not a valid value for this prop ( type : < class 'str' > , expected : List [ TodoObject ]) Pero si no puede o no quiere hacer eso, puede mantener el tipo definido en TodoApp et Use el método de clase prop_type de un componente para obtener el tipo de un accesorio:
class ThingApp ( Element ):
class PropTypes :
todos : TodoApp . prop_type ( "todos" )
# ... Pero, ¿realmente importa tener el error planteado para ThingApp o TodoApp ? Porque al final, es realmente TodoApp que tiene que hacer el cheque.
Así que esta debería ser una forma de hacer esto de una manera más genérica.
Vimos anteriormente que un componente puede ser una sola función para representar un componente. Solo tiene que devolver un componente, una etiqueta HTML. Una diferencia con los componentes de clase es que no hay PropTypes , por lo que no hay validación. Pero ... es exactamente lo que necesitamos.
Queremos que nuestro ThingApp acepte algunos accesorios (el todos PROP) y devuelva un TodoApp con un tipo de accesorio type específico.
Entonces podríamos hacer:
def ThingApp ( todos ):
return < TodoApp type = "thing" todos = { todos } / > Aquí podemos ver que no podemos pasar type a ThingsApp , no es un argumento válido.
Vamos a intentarlo:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div > Aquí solo tenemos un accesorio para pasar, por lo que es fácil. Pero imagina si tenemos muchos. Podemos usar la sintaxis {**props} :
def ThingApp ( ** props ):
return < TodoApp type = "thing" { ** props } / >Y puedes hacer con menos caracteres (si cuenta):
ThingApp = lambda ** props : < TodoApp type = "thing" { ** props } / >Estas dos foncciones se comportan exactamente igual.
Y no puede pasar un accesorio type porque sería un error de Python, ya que se pasaría dos veces a TodoApp :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) TypeError : BaseMetaclass object got multiple values for keyword argument 'type' (Sí, habla de BaseMetaclass , que es la metaclase que crea nuestras clases de componentes)
Y cualquier otro accesorio incorrecto sería validado por TodoApp :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} foo = "bar" / > ) mixt . exceptions . InvalidPropNameError : < TodoApp > . foo : is not an allowed prop Con esto en mente, podríamos haber creado una fonción genérica que forzue el tipo de cualquier componente que acepte un type accesorio:
Thingify = lambda component , ** props : < component type = "thing" { ** props } / > print ( < Thingify component = { TodoApp } todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div > El componente renderizado es TodoApp , el type accesorio es "cosa" y los otros accesorios (aquí solo todos ) se pasan correctamente.
Ahora extienda este concepto a un caso más genérico: "componentes de orden superior". En React, un "componente de alto orden", es "una función que toma un componente y devuelve un nuevo componente".
La idea es:
EnhancedComponent = higherOrderComponent ( WrappedComponent )Una forma clásica de hacerlo es devolver una nueva clase de componentes:
def higherOrderComponent ( WrappedComponent ):
class HOC ( Element ):
__display_name__ = f"higherOrderComponent( { WrappedComponent . __display_name__ } )"
class PropTypes ( WrappedComponent . PropTypes ):
pass
def render ( self , context ):
return < WrappedComponent { ** self . props } > { self . childre ()} < / WrappedComponent >
return HOC Observe cómo establecemos la clase PropTypes para heredar del componente envuelto y cómo pasamos todos los accesorios al componente envuelto, junto con los niños. Con el componente devuelto aceptará los mismos accesorios, con los mismos tipos, que el envuelto.
Y también observe el __display_name__ . Se utilizará en excepciones para permitirle ahora el componente que lo elevó. Aquí, sin forzarlo, se habría establecido en HOC , lo que no es útil. En cambio, indicamos que es una versión transformada del componente aprobado.
Aquí es una función que no hace nada útil.
En nuestro ejemplo podríamos haber hecho esto:
def thingify ( WrappedComponent ):
class HOC ( Element ):
__display_name__ = f"thingify( { WrappedComponent . __display_name__ } )"
class PropTypes ( WrappedComponent . PropTypes ):
__exclude__ = { 'type' }
def render ( self , context ):
return < WrappedComponent type = "thing" { ** self . props } > { self . children ()} < / WrappedComponent >
return HOCDos cosas importantes aquí:
__exclude__ = {'type'} para eliminar el type de accesorio de los que heredamos de WrappedComponent.PropTypes . Por lo tanto, el componente devuelto esperará exactamente los mismos accesorios que el envuelto, excepto el type .{self.children()} en el componente envuelto renderizado, porque incluso si realmente sabemos que el componente que envolveremos, TodoApp , no lleva a los niños (no podría, pero no hace nada con ellos), no podemos decir de antemano que siempre será el caso, y también que este componente de orden superior no se usará para envolver otro componente que TodoApp . Así que es mejor hacer esto siempre. Y ahora podemos crear nuestro ThingApp :
ThingApp = thingify ( TodoApp )Y úsalo:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Si intentamos pasar el tipo:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) mixt . exceptions . InvalidPropNameError : < thingify ( TodoApp ) > . type : is not an allowed prop Entonces, según lo planeado, no podemos pasar el tipo. Y observe cómo se usa __display_name__ .
Pensemos en lo poderoso que es esto.
Digamos que queremos que nuestra TodoApp tome una lista de TodoObject . Pero queremos obtenerlos de una "fuente".
Incluso podemos escribirlo directamente este nuevo componente de orden superior de manera genérica:
def from_data_source ( WrappedComponent , prop_name , get_source ):
class HOC ( Element ):
__display_name__ = f"from_data_source( { WrappedComponent . __display_name__ } )"
class PropTypes ( WrappedComponent . PropTypes ):
__exclude__ = { prop_name }
def render ( self , context ):
props = self . props . copy ()
props [ prop_name ] = get_source ( props , context )
return < WrappedComponent { ** props } > { self . children ()} < / WrappedComponent >
return HOC Esta vez, la función from_data_source toma dos argumentos además del WrappedComponent :
prop_name : es el nombre del accesorio del componente envuelto para llenar con algunos datosget_source : es una función que se llamará para obtener los datos Mira cómo heredamos los PropTypes del componente envuelto y cómo excluimos prop_name . Por lo tanto, no tenemos (y no) pasar los datos a nuestro nuevo componente.
Y luego, en render , establecemos un accesorio para pasar al WrappedComponent con el resultado de una llamada a get_source .
Entonces, escribamos una función muy simple (esta podría ser complicada con almacenamiento en caché, filtrado ...) que toman los accesorios y el contexto, y devuelve algunos datos:
def get_todos ( props , context ):
# here it could be a call to a database
return [
TodoObject ( "fooooo" ),
TodoObject ( "baaaar" ),
]Y podemos componer nuestro componente:
SourcedTodoApp = from_data_source ( TodoApp , 'todos' , get_todos )
ThingApp = thingify ( SourcedTodoApp )Y ejecutarlo:
print ( < ThingApp / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > fooooo </ li > < li > baaaar </ li > </ ul > </ div >Funciona como se esperaba, y los datos se obtienen solo cuando el componente necesita ser representado.
Entonces, tenemos una lista de tareas, que pueden obtener datos de una fuente externa. Pero es posible que deseemos que los datos sean diferentes según el usuario.
Lo que podemos hacer, está en el nivel principal, obtener nuestro usuario y pasarlo en cada componente para asegurarse de que cada componente pueda registrar el usuario actual.
¿No sería engorroso?
Resolver este caso de uso es el propósito exacto del concepto Context proporcionado por mixt . Por supuesto, está inspirado en el concepto de contexto en React.
Y como dijeron:
El contexto está diseñado para compartir datos que pueden considerarse "globales" para un árbol de componentes React, como el usuario, tema o lenguaje preferido autenticado actual.
Crear un contexto es tan simple como crear un componente, excepto que heredará de BaseContext y no necesita un método render (hará sus hijos).
Y se necesita una clase PropTypes , que defina los tipos de datos que el contexto aceptará y pasará por el árbol.
Entonces, creemos nuestro contexto que mantendrá la identificación del usuario autenticado.
from mixt import BaseContext
class UserContext ( BaseContext ):
class PropTypes :
authenticated_user_id : Required [ int ] Ahora, queremos actualizar nuestro método get_todos para tener en cuenta el authenticated_user_id .
Recuerde, lo pasamos los accesorios y el contexto. El contexto será útil aquí:
def get_todos ( props , context ):
return {
1 :[
TodoObject ( "1-1" ),
TodoObject ( "1-2" ),
],
2 : [
TodoObject ( "2-1" ),
TodoObject ( "2-2" ),
]
}[ context . authenticated_user_id ]Y ahora podemos representar nuestra aplicación con el contexto:
print (
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / UserContext >
) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < li > 1 - 1 < / li > < li > 1 - 2 < / li > < / ul > < / div >Podemos ver las entradas TODO para el usuario 1.
Probemos con el usuario 2:
print (
< UserContext authenticated_user_id = 2 >
< ThingApp / >
< / UserContext >
) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < li > 2 - 1 < / li > < li > 2 - 2 < / li > < / ul > < / div >Podemos ver las entradas TODO para el usuario 2.
En este caso, por supuesto, podríamos haber aprobado la identificación del usuario como accesorio. Pero imagine que la aplicación TODO está profundamente en el árbol de componentes, es mucho más fácil pasarla de esta manera.
Pero como se dijo en la documentación React:
No use el contexto solo para evitar pasar los accesorios de unos pocos niveles. Se mantenga en casos en los que se debe acceder a los mismos datos en muchos componentes en múltiples niveles.
Cuando no hay contexto, el argumento context del método render se establece en EmptyContext y no en None . Por lo tanto, puede usar directamente el método has_prop para verificar si hay un accesorio disponible a través del contexto.
Actualicemos las funciones get_todos para devolver una lista vacía de objetos TODO si no hay usuario autenticado.
def get_todos ( props , context ):
if not context . has_prop ( 'authenticated_user_id' ) or not context . authenticated_user_id :
return []
return {
1 :[
TodoObject ( "1-1" ),
TodoObject ( "1-2" ),
],
2 : [
TodoObject ( "2-1" ),
TodoObject ( "2-2" ),
]
}[ context . authenticated_user_id ]Probemos esto:
print ( < ThingApp / > ) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < / ul > < / div >Y todavía funciona con un usuario en el contexto:
print (
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / UserContext >
) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < li > 1 - 1 < / li > < li > 1 - 2 < / li > < / ul > < / div >Nota importante sobre los contextos : ¡puede tener muchos contextos! Pero definir el mismo accesorio en muchos contextos puede conducir a un comportamiento indefinido.
A todos les encanta un diseño hermoso, y tal vez algo de interacción.
Es fácilmente factible: generamos HTML y HTML CAN contiene algunos CSS y JS.
Agreguemos primero una interacción: al agregar un elemento en el TodoForm , agregamos a la lista.
Primero agregamos en nuestro componente TodoForm un método render_javascript que alojará nuestro (malo, podríamos hacerlo mejor, pero no es el punto) JavaScript:
class TodoForm ( Element ):
# ...
def render_javascript ( self , context ):
return html . Raw ( """
function on_todo_add_submit(form) {
var text = form.todo.value;
alert(text);
}
""" )Para comenzar, solo mostramos el nuevo texto de TODO.
Ahora actualice nuestro método render para devolver este JavaScript (tenga en cuenta que el uso de un método render_javascript es solo para separar las preocupaciones, podría haber estado directamente en el método render .
class TodoForm ( Element ):
# ...
def render ( self , context ):
# ...
return
< Fragment >
< script > { self . render_javascript ( context )} < / script >
< form method = "post" action = { add_url } onsubmit = "return on_todo_add_submit(this);" >
< label > New { self . type }: < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form >
< / Fragment > Observe la etiqueta Fragment . Es una forma de encapsular muchos elementos para ser devueltos, como en React. Podría haber sido una lista simple pero con comas al final:
return [
< script > ... < / script > ,
< form >
...
< / form >
] Ahora queremos agregar un elemento a la lista. No es el papel de la TodoForm hacer esto, sino a la lista. Así que agregaremos algunos JS en el componente TodoList : una función que tome algún texto y cree una nueva entrada.
En cuanto a TodoForm , agregamos un método render_javascript con (aún malo) JavaScript:
class TodoList ( Element ):
# ...
def render_javascript ( self , context ):
todo_placeholder = < Todo todo = { TodoObject ( text = 'placeholder' )} / >
return html . Raw ( """
TODO_TEMPLATE = "%s";
function add_todo(text) {
var html = TODO_TEMPLATE.replace("placeholder", text);
var ul = document.querySelector('#todo-items');
ul.innerHTML = html + ul.innerHTML;
}
""" % ( todo_placeholder )) Y actualizamos nuestro método render para agregar la etiqueta <script> y una id a la etiqueta ul , utilizada en el JavaScript:
class TodoList ( Element ):
# ...
def render ( self , context ):
return
< Fragment >
< script > { self . render_javascript ( context )} < / script >
< ul id = "todo-items" > {[ < Todo todo = { todo } / > for todo in self . todos ]} < / ul >
< / Fragment > Y ahora podemos actualizar el método render_javascript del componente TodoForm para usar nuestra nueva función JavaScript add_toto :
class TodoForm ( Element ):
# ...
def render_javascript ( self , context ):
return html . Raw ( """
function on_todo_add_submit(form) {
var text = form.todo.value;
add_todo(text);
}
""" )Y eso es todo. Nada especial, de hecho.
Pero echemos un vistazo a la salida de OU TodoApp :
print (
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
)La salida embellecida es:
< div >
< h1 > The "thing" list </ h1 >
< script >
function on_todo_add_submit ( form ) {
var text = form . todo . value ;
add_todo ( text ) ;
}
</ script >
< form method =" post " action =" /thing/add " onsubmit =" return on_todo_add_submit(this); " >
< label > New thing: </ label >
< input type =" text " name =" todo " />
< button type =" submit " > Add </ button >
</ form >
< script >
TODO_TEMPLATE = "<li>placeholder</li>" ;
function add_todo ( text ) {
var html = TODO_TEMPLATE . replace ( "placeholder" , text ) ;
var ul = document . querySelector ( '#todo-items' ) ;
ul . innerHTML = html + ul . innerHTML ;
}
</ script >
< ul id =" todo-items " >
< li > 1-1 </ li >
< li > 1-2 </ li >
</ ul >
</ div > Entonces tenemos muchas etiquetas script . Podría ser genial tener solo uno.
mixt viene con una forma de "recoger" partes de lo que se representa para ponerlos en otro lugar. Tenemos a nuestra disposición dos coleccionistas simples, para ser utilizados como componentes: JSCollector y CSSCollector .
Estos componentes recolectan partes de sus hijos.
La primera forma es usar la etiqueta Collect colección.
Primero cambiemos nuestra llamada principal:
from mixt import JSCollector
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
) Esto recopilará el contenido de toda la etiqueta JSCollector.Collect .
Actualicemos nuestro TodoForm y reemplacemos nuestra etiqueta script por una etiqueta JSCollector.Collect :
class TodoForm ( Element ):
# ...
def render ( self , context ):
if callable ( self . add_url ):
add_url = self . add_url ( self . type )
else :
add_url = self . add_url
return
< JSCollector . Collect > { self . render_javascript ( context )} < / JSCollector . Collect >
< form method = "post" action = { add_url } onsubmit = "return on_todo_add_submit(this);" >
< label > New { self . type }: < / label > < itext name = "todo" / >
< button type = "submit" > Add < / button >
< / form >
< / Fragment > Podemos hacer lo mismo con el TodoList :
class TodoList ( Element ):
# ...
def render ( self , context ):
return
< Fragment >
< JSCollector . Collect > { self . render_javascript ( context )} < / JSCollector . Collect >
< ul id = "todo-items" > {[ < Todo todo = { todo } / > for todo in self . todos ]} < / ul >
< / Fragment >Ahora ejecutemos nuestro código actualizado:
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
)La salida embellecida es:
< div >
< h1 > The "thing" list </ h1 >
< form method =" post " action =" /thing/add " onsubmit =" return on_todo_add_submit(this); " >
< label > New thing: </ label >
< input type =" text " name =" todo " />
< button type =" submit " > Add </ button >
</ form >
< ul id =" todo-items " >
< li > 1-1 </ li >
< li > 1-2 </ li >
</ ul >
</ div >
< script type =" text/javascript " >
function on_todo_add_submit ( form ) {
var text = form . todo . value ;
add_todo ( text ) ;
}
TODO_TEMPLATE = "<li>placeholder</li>" ;
function add_todo ( text ) {
var html = TODO_TEMPLATE . replace ( "placeholder" , text ) ;
var ul = document . querySelector ( '#todo-items' ) ;
ul . innerHTML = html + ul . innerHTML ;
}
</ script > Como puede ver, todos los scripts están en una sola etiqueta script , al final. Más precisamente, al final de donde estaba la etiqueta JSCollector , porque usamos render_position="after" . Otra posibilidad es render_position="before" para poner esto donde comenzó la etiqueta JSCollector .
Todo esto funciona exactamente de la misma manera para la etiqueta CSSCollector , donde el contenido se coloca en una etiqueta <style type="text/css> .
Como usar JS/CSS es bastante común en el mundo HTML, agregamos algo de azúcar para que todo esto sea aún más fácil de hacer.
Si tiene un método render_js , JSCollector recopilará automáticamente el resultado de este método. Lo mismo para CSSSelector y el método render_css .
Con esto, no hay necesidad de una etiqueta JSCollector.Collect .
Para hacer que esto funcione en nuestro ejemplo, en TodoForm y TodoList :
JSCollector.CollectFragment ahora innecesariasrender_javascript a render_js .html.Raw en render_js ya que no es necesario cuando el coleccionista llama a render_js : si la salida es una cadena, se considera un "crudo"De esta manera tenemos exactamente el mismo resultado.
Funciona ahora porque solo tenemos una instancia de un niño con un método render_js .
Pero si tenemos muchos hijos, este método será llamado para cada niño. De hecho, solo debe contener un código que sea muy específico para esta instancia.
Para servir JS/CSS solo una vez para una clase de componentes, tenemos que usar render_js_global o render_css_global (se espera que sea classmethod )
Se recopilará la primera vez, y solo la primera vez, se encuentra una instancia, antes de recolectar el método render_js .
Entonces, aquí podemos cambiar nuestro render_js a render_js_global , decorarlos con @classmethod y aún funcionará igual.
Ahora somos capaces de reagrupar JavaScript o estilo. Pero, ¿qué pasa si queremos ponerlo en otro lugar, como en la etiqueta head o al final de la etiqueta body ?
Es posible con referencias, también conocidos como "Refs". Es el mismo contexto que en React, sin la parte DOM, por supuesto.
Creas un Ref, lo pasa a un componente y puedes usarlo en cualquier lugar.
Actualicemos nuestro código principal para hacer esto.
Primero creamos una ref.
from mixt import Ref
js_ref = Ref () Esto creará un nuevo objeto que mantendrá una referencia a un componente. En un componente, no necesita importar Ref y puede usar js_ref = self.add_ref() , pero no estamos en un componente aquí.
Para salvar un árbitro, simplemente lo pasamos a la ref :
< JSCollector ref = { js_ref } > ... < / JSCollector > Tenga en cuenta que eliminamos el accesorio render_position , porque ahora no queremos que se pongan el JS antes o después de la etiqueta, sino en otro lugar.
Para acceder al componente referenciado por una REF, use el atributo current :
js_collector = js_ref . currentPor supuesto, esto solo se puede hacer después de la representación.
¿Cómo podemos usar esto para agregar una etiqueta script en nuestra head ?
Primero actualice nuestro HTML para incluir las etiquetas clásicas html , head y body :
return str (
< html >
< head >
< / head >
< body >
< JSCollector ref = { js_ref } >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / UserContext >
< / JSCollector >
< / body >
< / html >
) En este punto no tenemos ninguna etiqueta script en la salida:
< html >
< head > </ head >
< body >
< div >
< h1 > The "thing" list </ h1 >
< form method =" post " action =" /thing/add " onsubmit =" return on_todo_add_submit(this); " >
< label > New thing: </ label >
< input type =" text " name =" todo " />
< button type =" submit " > Add </ button >
</ form >
< ul id =" todo-items " >
< li > 1-1 </ li >
< li > 1-2 </ li >
</ ul >
</ div >
</ body >
</ html > Lo primero que debe saber: un coleccionista es capaz de hacer que todas las cosas que recolectó llamando a su método render_collected .
Y recordando que ya incluye la etiqueta script , es posible que deseemos hacer esto:
# ...
< head >
{ js_ref . current . render_collected ()}
< / head >
# ...Pero esto no funciona:
AttributeError : 'NoneType' object has no attribute 'render_collected'Es porque intentamos acceder al valor actual en el momento del renderizado. Debe hacerse después.
Para esto, podemos usar una característica de mixt : si algo agregado al árbol es un llamado, se llamará después de la representación, al convertirlo en cadena.
Entonces podemos usar, por ejemplo, una lambda:
# ...
< head >
{ lambda : js_ref . current . render_collected ()}
< / head >
# ...Y ahora funciona:
< html >
< head >
< script type =" text/javascript " >
function on_todo_add_submit ( form ) {
var text = form . todo . value ;
add_todo ( text ) ;
}
TODO_TEMPLATE = "<li>placeholder</li>" ;
function add_todo ( text ) {
var html = TODO_TEMPLATE . replace ( "placeholder" , text ) ;
var ul = document . querySelector ( '#todo-items' ) ;
ul . innerHTML = html + ul . innerHTML ;
}
</ script >
</ head >
< body >
< div >
< h1 > The "thing" list </ h1 >
< form method =" post " action =" /thing/add " onsubmit =" return on_todo_add_submit(this); " >
< label > New thing: </ label >
< input type =" text " name =" todo " />
< button type =" submit " > Add </ button >
</ form >
< ul id =" todo-items " >
< li > 1-1 </ li >
< li > 1-2 </ li >
</ ul >
</ div >
</ body >
</ html > ¡Hurra lo hicimos! Se explican todas las características principales de mixt . Ahora puede usar mixt en sus propios proyectos.
Como siguiente paso, es posible que desee leer la documentación de la API.