Écrivez des composants HTML directement dans Python et vous avez un mélange beau mais controversé.
Oui, controversé .
Si vous ne l'aimez pas, ignorez-le (mais vous pouvez l'utiliser sans la partie HTML-in-python, voir ci-dessous;))
Basé sur pyxl. Python 3.6+ seulement et utilisez la saisie pour la validation des données.
Une fois que vous avez votre HTML, vous pouvez faire ce que vous voulez. Considérez-le comme un remplacement de votre moteur de modèle classique.
Code source : https://github.com/twidi/mixt/
Documentation : https://twidi.github.io/mixt/
PYPI : https://pypi.org/project/mixt/
CI (circleci): https://circleci.com/gh/twidi/workflows/mixt/
Créons un fichier 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" / > )Et l'exécutez:
$ python example.py
< div > Hello, World < /div >Si vous n'aimez pas écrire HTML dans Python, vous pouvez toujours l'utiliser:
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" ))Oui, il est inspiré par React (en fait, principalement JSX) et nous empruntions une partie du concept:
Et nous avons ajouté:
Exécutez ces deux commandes. Le second indique à Python comment comprendre les fichiers avec HTML à l'intérieur.
pip install mixt
mixt-post-installPour vérifier que tout est prêt, courez:
python -m mixt.examples.simpleVous devriez avoir cette sortie:
< div title =" Greeting " > Hello, World </ div > Si vous ne voulez pas utiliser les trucs HTML-in-python, n'exécutez pas mixt-post-install . Puis tester avec (pour avoir la même sortie):
python -m mixt.examples.simple_pure_pythonClone le projet GIT alors:
make devPour vérifier que tout est prêt, courez:
python -m mixt.examples.simpleVous devriez avoir cette sortie:
< div title =" Greeting " > Hello, World </ div >Après avoir fait du code:
make testsmake lint Si vous touchez les choses dans le répertoire codec , vous devrez exécuter make dev (ou du moins make full-clean ) pour purger les fichiers pyc Python.
Notez que notre CI vérifiera que chaque engagement passe la make lint , make tests et make check-doc . Alors n'oubliez pas de les exécuter pour chaque engagement.
Une façon de le faire avant de pousser est:
git rebase develop --exec ' git log -n 1; make checks ' Remarque: Vous pouvez trouver le code final de ce guide de l'utilisateur dans src/mixt/examples/user_guide (vous trouverez mixt.py et pure_python.py ).
Exécutez-le avec:
python -m mixt.examples.user_guideCréons une liste ... TODO, ouais!
Mais avant, rappelez-vous. Ce n'est pas réagi, ce n'est pas sur le navigateur et il n'y a pas de javascript impliqué ici. Nous ne parlons que de rendu du HTML.
Mais vous pouvez faire ce que vous voulez. Ajouter des gestionnaires JavaScript, des formulaires simples ...
Parler de formulaires ...
Dans une liste de TODO, nous voulons pouvoir ajouter un TODO. C'est une entrée de texte simple.
Créons donc notre premier composant, le TodoForm . Nous voulons un formulaire avec un texte d'entrée et un bouton.
Un composant est une sous-classe de la classe Element , avec une méthode render que vous devez écrire.
# 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 >Notez que cela aurait pu être écrit comme une fonction 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 >Lorsque vous imprimez le composant, ces deux donnent le même résultat:
print ( < TodoForm / > ) < form method =" post " action =" ??? " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Remarquez comment il est formaté: pas d'espace entre les balises. En fait, c'est comme dans JSX:
JSX supprime les espaces au début et la fin d'une ligne. Il supprime également les lignes vierges. Les nouvelles lignes adjacentes aux étiquettes sont supprimées; Les nouvelles lignes qui se produisent au milieu des littéraux à cordes sont condensées en un seul espace
Pour ajouter un espace ou une nouvelle ligne, vous pouvez passer du python. Ajoutons, par exemple, ajoutons une nouvelle ligne avant l'étiquette:
#...
< form method = "post" action = "???" >
{ ' n ' } < label > New Todo : < / label > < itext name = "todo" / >
#...Nous avons maintenant cette sortie:
< form method =" post " action =" /todo/add " >
< label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Allons maintenant plus loin.
Remarquez l'attribut action du formulaire. Nous devons passer quelque chose. Mais le codage dur, cela ne semble pas bien. La WWE doit la transmettre au composant.
Mixt a, comme React , le concept de propriétés, alias "accessoires".
Dans Mixt , nous les définissons avec un type, dans une classe à l'intérieur de notre composant, nommé 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 > Ici, nous avons défini un accessoire nommé add_url qui doit être une chaîne ( str ). Cela utilise la syntaxe de typage Python.
Et remarquez comment nous avons changé l'attribut action de la balise form . C'est maintenant {self.add_url} au lieu de "???" .
Lorsque les attributs sont passés entre les accolades bouclées, ils sont interprétés comme un pur python au moment de l'exécution. En fait, comme l'analyseur mixt convertira le fichier entier en pur python avant de laisser l'interpréteur Python l'exécuter, il restera le même, seul le HTML autour sera converti. Il n'y a donc pas de pénalité pour le faire.
Regardez à quoi cela ressemblerait si notre composant était écrit en pur 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")
),
) Remarquez comment les accessoires sont passés à un composant comme les arguments nommés et comment action est passée: action=self.add_url .
Ce composant pur-python vous montre également comment il fonctionne: les accessoires sont passés comme argument nommé à la classe de composants, alors ce composant est appelé, passant les composants des enfants comme arguments de position à l'appel:
ComponentClass ( prop1 = "val1" , prop2 = "val2" )(
Children1 (),
Children2 (),
)Que sont les enfants? Les enfants sont des balises à l'intérieur d'autres balises.
Dans <div id="divid"><span /><p>foo</p></div> , nous avons:
html.Div , avec un id d'hélice et deux enfants:html.Span , sans enfantshtml.P avec un enfant:html.RawHtml avec le texte "FOO"Notez que vous pouvez jouer avec les accessoires et les enfants. D'abord la version en pur-python pour montrer comment cela fonctionne:
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) Ensuite, la version 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> Revenons maintenant à nos accessoires add_url .
Comment le transmettre au composant?
La même manière que nous avons passé des attributs aux balises HTML: ce sont en fait des accessoires définis dans les compoments HTML (définis dans mixt.html ). Nous prenons en charge toutes les balises HTML qui, au moment de l'écriture, sont valides (non obsolètes) dans HTML5, avec leurs attributs (à l'exclusion des dépréciés).
Alors faisons ceci:
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, nous avons notre accessoire présent dans le HTML rendu.
Et si nous ne passons pas de chaîne? Nous avons dit dans PropTypes que nous voulions une ficelle ...
Essayons-le:
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 >Ça marche! Mais ... ce n'est pas une chaîne !! En fait, il existe un cas spécial pour les chiffres, vous pouvez les transmettre en tant que numéros au lieu de chaînes et ils sont convertis si nécessaire ...
Alors essayons autre chose.
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' > ) Et c'est la même chose si nous passons True dans 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, trompons le système et passons "True" , comme une chaîne.
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' > )Toujours le même, mais ici, nous avons passé une chaîne! Oui, mais il y a 4 valeurs qui sont toujours évaluées à ce qu'elles semblent être:
None )La seule façon de passer l'une de ces valeurs en tant que chaîne est de les passer via Python, comme une chaîne:
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 > À l'exception de ces 4 valeurs et nombres, chaque valeur transmise à un attribut est considérée comme une chaîne. Même s'il n'y a pas de citations, comme dans HTML dans HTML5, où les citations ne sont pas obligatoires pour les chaînes sans certains caractères (pas d'espaces, non / ...).
Pour passer autre chose, vous devez entourer la valeur des accolades bouclées (et dans ce cas, il n'y a pas besoin de citations autour des accolades bouclées.
Ok, maintenant nous sommes sûrs que nous n'acceptions que String ... mais que se passe-t-il si je ne passe rien? Et ... qu'est-ce que "rien"?
Commençons par une chaîne vide dans 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 ça marche, nous voulions une chaîne, nous avons une chaîne.
Passons maintenant directement cette chaîne vide:
print ( < TodoForm add_url = "" / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Cela fonctionne toujours, car c'est toujours une chaîne. Supprimons les citations, pour voir.
print ( < TodoForm add_url = / > ) mixt . exceptions . GeneralParserError : < mixt parser > Unclosed Tags : < TodoForm > Hum Ouais, ce n'est pas un HTML valide. Alors supprimons le = :
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' > ) QUOI? Oui, pensez aux attributs HTML5 comme required , checked ... ils n'ont qu'à être présent en tant qu'attribut, sans valeur, pour être considéré True . Ainsi, lorsqu'un attribut n'a pas de valeur, c'est un booléen, et c'est True .
En plus de ne pas passer une valeur, ces deux autres façons sont valides dans HTML5 pour un booléen par True :
required=""required="required"Pour votre commodité, nous avons ajouté une autre façon:
True (le cas n'a pas d'importance), comme python ou comme une chaîne: required=True , required={True} , required="true" Et son homologue, pour passer False :
False (le cas n'a pas d'importance), comme python ou comme une chaîne: required=False , required={False} , required="false" OK pour les attributs booléens. Ce n'est pas notre cas. La dernière chose que nous pouvons faire est de ne pas définir l'attribut du tout:
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 setC'est compréhensible: nous essayons d'accéder à un accessoire qui n'est pas défini, bien sûr, nous ne pouvons pas l'utiliser.
Mais que se passe-t-il si nous n'y accédons pas? Si nous n'imprimons pas le composant, il ne sera pas rendu:
< TodoForm / > < TodoForm at 0x7fbd18ea5630 >Nous pouvons donc créer une instance, mais il échouera au moment du rendu. Mais il existe un moyen d'empêcher cela.
Par défaut, toutes les propriétés sont facultatives. Et vous n'avez pas à utiliser le type Optional à partir du module typing Python pour cela, il serait lourd de le faire pour chaque accessoire.
Au lieu de cela, mixt fournit un type nommé Required que vous utilisez de la même manière que Optionnal .
from mixt import Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
def render ( self , context ):
# ...Nous avons donc dit que nous voulions une ficelle et que cela soit nécessaire.
Essayons à nouveau de le créer sans l'hélice:
< TodoForm / > mixt . exceptions . RequiredPropError : < TodoForm > . add_url : is a required prop but is not setNous avons maintenant l'exception soulevée plus tôt dans notre programme.
Pour voir d'autres possibilités dans les accessoires, ajoutons-en un nouveau pour modifier l'étiquette de texte. Mais nous ne voulons pas le faire requis et avoir une valeur par défaut.
Pour cela, c'est aussi simple que d'ajouter une valeur à l'hélice dans la classe 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 >Essayons maintenant sans passer l'hélice:
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 >Et si nous passons un:
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 >Cela fonctionne comme prévu.
Notez que vous ne pouvez pas donner de valeur par défaut tout en ayant l'hélice Required . Cela n'a aucun sens, il est donc vérifié dès que possible, tandis que la class est construite:
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 requiredEt bien sûr, la valeur par défaut doit correspondre au type!
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' > ) Une autre chose que nous voulons faire dans notre composant est de la laisser construire l'étiquette, de la passer un "type" de TODO, mais de limiter les choix. Pour cela, nous pouvons utiliser le type 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 >Essayons-le:
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 >Et si nous essayons de passer autre chose que les choix disponibles? Il échoue, comme prévu:
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' ])Mais peut-être que nous ne voulons pas le passer et utiliser une valeur par défaut. Quel serait le résultat?
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . UnsetPropError : < TodoForm > . type : prop is not set Nous devons donc marquer le type accessoire au besoin:
class PropTypes :
add_url : Required [ str ]
type : Required [ Choices ] = [ 'todo' , 'thing' ]Donc, si nous ne le dépassons pas, il échoue plus tôt:
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . RequiredPropError : < TodoForm > . type : is a required prop but is not setMais ce n'est pas ce que nous voulons, nous voulons une valeur par défaut.
En fait, vous avez remarqué que pour les types autres que Choices , la définition d'une valeur dans PropTypes nous donne une valeur par défaut. Mais pour Choices c'est différent, car la valeur est la liste des choix.
Pour cela, nous avons DefaultChoices : il fonctionne de la même manière que Choices , mais utilisez la première entrée de la liste comme valeur par défaut. Et bien sûr, comme pour les autres types ayant une valeur par défaut, cela ne peut pas être Required .
Essayons-le:
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 >Cela fonctionne comme prévu.
Jusque-là, nous avons utilisé des types simples, mais vous pouvez en utiliser des types plus compliqués.
Ainsi, par exemple, nous ferons en sorte que l'hélice add_url accepte une fonction qui calculera l'URL pour nous en fonction de l'hélice type . Mais nous voulons également autoriser les chaînes et avec une valeur par défaut.
Nous pouvons le faire, avec la saisie. Notre fonction prendra une chaîne, le type et renverra une chaîne, l'URL.
Ainsi, la syntaxe est Callable[[str], str] pour le callable, et nous utilisons Union pour accepter des choses de type Callable ou 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 > Tout d'abord, essayons-le sans l'hélice add_url , car nous avons une valeur par défaut:
print ( < TodoForm / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Cela devrait aussi fonctionner si nous passons une chaîne:
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 >Et maintenant, nous pouvons passer une fonction:
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? Pourquoi? J'ai passé une fonction acceptant une chaîne comme argument et renvoyant une chaîne. Oui, mais n'oubliez pas que les types sont vérifiés! Nous devons donc ajouter des types à notre fonction:
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 >Et si nous passons un autre type, l'URL doit changer en conséquence:
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 >Nous pouvons même faire de cette fonction la valeur par défaut de notre accessoire:
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 >Maintenant, vous pouvez commencer à vous demander ... Le typage Python est lourd et valider peut emporter une partie de notre temps précieux.
Je répondons à cela:
Par défaut, mixt s'exécute dans "Dev-mode". Et en mode de développement, les accessoires sont validés lorsqu'ils sont transmis à un composant. Lorsque vous n'êtes pas dans le "Dev-mode", la validation est ignorée. Donc, en production, vous pouvez désactiver le mode de développement (nous verrons comment dans une minute) et passer des accessoires très rapidement:
Choices est, en effet, dans la liste des choixrender . Mais vous pouvez dire que c'est en production que la validation est importante. En effet. Mais bien sûr, votre code est entièrement couvert par les tests, que vous exécutez en mode Dev, et donc en production, vous n'avez pas besoin de cette validation! Et notez que c'est comment fonctionne React, en passant, avec NODE_ENV=production .
Comment changer de mode dev? Nous n'appliquons aucune variable d'environnement mais nous proposons certaines fonctions. C'est à vous de les appeler:
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 () Essayons donc ceci avec le type prop. Rappelez-vous, il ressemble:
type : DefaultChoices = [ 'todo' , 'thing' ]Nous essayons de passer un autre choix, d'abord en mode Dev:
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' ])Il échoue comme prévu.
Et maintenant en désactivant le mode de développement:
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 > Cela fonctionne, nous avons un type TODO qui n'était pas dans nos choix utilisés, et qui est également dans l' action . C'est le travail de vos tests pour vous assurer que vous ne transmettez jamais les accessoires non valides, vous pouvez donc être confiant dans la production et désactiver le mode Dev.
Maintenant, nous avons notre forme. De quels autres composants avons-nous besoin pour notre application ToDo List?
Bien sûr, nous avons besoin d'un moyen d'afficher une entrée TODO.
Mais qu'est-ce qu'une entrée TODO? Créons un TodoObject de base:
class TodoObject :
def __init__ ( self , text ):
self . text = textC'est une classe très simple, mais vous pouvez utiliser ce que vous voulez, bien sûr. Ce pourrait être des modèles Django, etc ...
Ainsi, nous pouvons créer notre composant Todo , ce qui en fait accepter un TodoObject requis comme prop:
class Todo ( Element ):
class PropTypes :
todo : Required [ TodoObject ]
def render ( self , context ):
return < li > { self . todo . text } < / li >Et nous pouvons l'utiliser:
todo = TodoObject ( "foo" )
print ( < Todo todo = { todo } / > ) < li > foo </ li > Maintenant, nous voulons avoir une liste de Todos. Créons un composant TodoList qui acceptera comme accessoires une liste de TodoObject .
Mais ce qui est différent de nos deux autres composants, qui n'utilisent que des balises HTML dans leur méthode render , c'est que nous allons maintenant encapsuler un composant dans un autre. Voyons comment.
class TodoList ( Element ):
class PropTypes :
todos : Required [ List [ TodoObject ]]
def render ( self , context ):
return < ul > {[ < Todo todo = { todo } / > for todo in self . todos ]} < / ul > Oui, c'est aussi simple que cela: vous utilisez <Todo...> pour le composant Todo que vous utiliseriez une balise HTML. La seule différence est que pour les balises HTML, vous n'avez pas besoin de les importer directement (simple importation html à partir de mixt ), et par convention, nous les écrivons dans des cas inférieurs. Pour les composants normaux, vous devez les importer (vous pouvez toujours faire from mylib import components et <components.MyComponent ...> ) et utiliser le cas exact.
Remarquez comment nous avons besoin d'une liste et l'avons transmise dans le <ul> via une composition de liste dans des broyeurs bouclés.
Vous pouvez faire les choses différemment si vous le souhaitez.
Comme séparer la compréhension de la liste du HTML:
def render ( self , context ):
todos = [
< Todo todo = { todo } / >
for todo
in self . todos
]
return < ul > { todos } < / ul >Ou dans une méthode dédiée (qui serait utile pour les tests):
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 >C'est à vous: à la fin, c'est juste Python.
Voyons ce qui est rendu par ce composant:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print ( < TodoList todos = { todos } / > ) < ul > < li > foo </ li > < li > bar </ li > < li > baz </ li > </ ul > Et enfin, nous avons notre composant TodoApp qui résume le formulaire et la liste:
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 >Passons ce HTML à un embellier 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 > Et c'est tout, nous avons notre application de liste de toxicomanie! Pour l'utiliser dans une page, créez simplement un composant qui rendra le balisage de base HTML et intégrez le composant TodoApp . Vous n'avez même pas besoin d'un composant:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print (
< html >
< body >
< TodoApp todos = { todos } type = "thing" / >
< / body >
< / html >
)La sortie embelli serait:
< 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 >Nous avons une liste de TODO générique, mais en suivant les types de TODO disponibles, nous voulons peut-être avoir une "liste de todo" et une "liste de choses".
Nous avons déjà la liste TODO car notre TodoApp a un type de todo par défaut.
Créons donc une ThingApp .
La première façon de le faire est de hériter de notre TodoApp . Mais en héritant, nous ne pouvons pas supprimer les accessoires du parent (ce n'est pas vraiment vrai, nous verrons cela plus tard), donc nous avons toujours le type prop par défaut. Mais nous ne voulons accepter autre chose que "chose". Nous pouvons donc redéfinir le type prop tel: ceci:
class ThingApp ( TodoApp ):
class PropTypes :
type : DefaultChoices = [ 'thing' ]Utilisons ce composant:
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 nous essayons de passer "TODO" pour les accessoires type , cela ne fonctionnera pas:
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' ])Mais quand même, c'est étrange de pouvoir passer un type.
Essayons une autre façon: un composant parent. Un composant qui ne fait rien d'autre qui faire des choses avec ses enfants et le retourner. Ce que nous voulons ici, c'est un composant qui renverra un TodoApp avec le type d'accessoire forcé à "chose".
Faisons ça
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 > Cela fonctionne, et cette fois, nous ne pouvons pas passer le type prop:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) mixt . exceptions . InvalidPropNameError : < ThingApp > . type : is not an allowed prop Remarquez comment nous avons dû définir le type pour les accessoires todos . À la fois dans TodoApp et TodoThing .
Il existe de nombreuses façons de gérer cela.
Le premier serait d'ignorer le type dans ThingApp car il sera enregistré dans TodoApp . Nous allons donc utiliser le type Any
from typing import Any
#...
class ThingApp ( Element ):
class PropTypes :
todos : Any
#...Essayons avec une liste valide de Todos:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Mais que se passe-t-il si nous passons autre chose?
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 ]) Il fonctionne comme prévu, mais l'erreur est signalée au niveau TodoApp , ce qui est parfaitement normal.
Une autre façon serait de définir le type à un niveau supérieur:
TodoObjects = Required [ List [ TodoObject ]]
class TodoApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...
class ThingApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...Maintenant, si nous passons autre chose, nous avons l'erreur signalée au bon niveau:
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 ]) Mais si vous ne pouvez pas ou ne voulez pas le faire, vous pouvez garder le type défini dans TodoApp et utilisez la méthode de classe prop_type d'un composant pour obtenir le type d'un accessoire:
class ThingApp ( Element ):
class PropTypes :
todos : TodoApp . prop_type ( "todos" )
# ... Mais est-il vraiment important de faire en sorte que l'erreur soit augmentée pour ThingApp ou TodoApp ? Parce qu'à la fin, c'est vraiment TodoApp qui doit faire le chèque.
Donc, cela devrait être un moyen de le faire d'une manière plus générique.
Nous avons vu plus tôt qu'un composant peut être une fonction unique pour rendre un composant. Il n'a qu'à retourner un composant, une balise HTML. Une différence avec les composants de la classe est qu'il n'y a pas PropTypes , donc pas de validation. Mais ... c'est exactement ce dont nous avons besoin.
Nous voulons que notre ThingApp accepte certains accessoires (le todos Prop) et renvoie un TodoApp avec un type type spécifique.
Afin que nous puissions faire:
def ThingApp ( todos ):
return < TodoApp type = "thing" todos = { todos } / > Ici, nous pouvons voir que nous ne pouvons pas passer type à ThingsApp , ce n'est pas un argument valide.
Essayons-le:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div > Ici, nous n'avons qu'un seul accessoire à passer, donc c'est facile. Mais imaginez si nous en avons beaucoup. Nous pouvons utiliser la syntaxe {**props} :
def ThingApp ( ** props ):
return < TodoApp type = "thing" { ** props } / >Et vous pouvez faire avec encore moins de caractères (si cela compte):
ThingApp = lambda ** props : < TodoApp type = "thing" { ** props } / >Ces deux fonctions se comportent exactement de la même manière.
Et vous ne pouvez pas passer un type type car ce serait une erreur python, car elle serait transmise deux fois à TodoApp :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) TypeError : BaseMetaclass object got multiple values for keyword argument 'type' (Oui, il parle de BaseMetaclass qui est la métaclasse qui crée nos cours de composants)
Et tout autre mauvais accessoire serait validé par TodoApp :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} foo = "bar" / > ) mixt . exceptions . InvalidPropNameError : < TodoApp > . foo : is not an allowed prop Dans cet esprit, nous aurions pu créer une fonction générique qui force le type de tout composant acceptant un type type :
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 > Le composant rendu est TodoApp , le type prop est "chose" et les autres accessoires (ici seulement todos ) sont correctement passés.
Étendez maintenant ce concept à un cas plus générique: "Composants d'ordre supérieur". Dans React, un "composant d'ordre élevé" est "une fonction qui prend un composant et renvoie un nouveau composant".
L'idée est:
EnhancedComponent = higherOrderComponent ( WrappedComponent )Une façon classique de le faire est de retourner une nouvelle classe de composants:
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 Remarquez comment nous définissons la classe PropTypes pour hériter de celui de la composante enveloppée, et comment nous passons tous les accessoires au composant enveloppé, avec les enfants. Avec le composant retourné acceptera les mêmes accessoires, avec les mêmes types, que celui enveloppé.
Et remarquez également le __display_name__ . Il sera utilisé dans des exceptions pour vous permettre maintenant le composant qui l'a soulevé. Ici, sans le forcer, il aurait été réglé à HOC , ce qui n'est pas utile. Au lieu de cela, nous indiquons qu'il s'agit d'une version transformée du composant passé.
Ici, c'est une fonction qui ne fait rien d'utile.
Dans notre exemple, nous aurions pu faire ceci:
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 HOCDeux choses importantes ici:
__exclude__ = {'type'} pour supprimer l'hélice type de celles dont nous héritons de WrappedComponent.PropTypes . Ainsi, le composant renvoyé s'attendra aux mêmes accessoires exactement que celui enveloppé, sauf pour type .{self.children()} dans le composant enveloppé rendu, car même si nous savons réellement que le composant que nous envelopperons, TodoApp , ne prend pas les enfants (cela pourrait mais cela ne fait rien avec eux), nous ne pouvons pas dire à l'avance que ce sera toujours le cas, et aussi que ce composant d'ordre supérieur ne sera pas utilisé pour envelopper un autre composant que TodoApp . Il vaut donc mieux faire ça. Et maintenant, nous pouvons créer notre ThingApp :
ThingApp = thingify ( TodoApp )Et utilisez-le:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Si nous essayons de passer le type:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) mixt . exceptions . InvalidPropNameError : < thingify ( TodoApp ) > . type : is not an allowed prop Donc, comme prévu, nous ne pouvons pas passer le type. Et remarquez comment le __display_name__ est utilisé.
Réfléchissons à quel point cela est puissant.
Disons que nous voulons garder notre TodoApp à prendre une liste de TodoObject . Mais nous voulons les obtenir d'une "source".
Nous pouvons même l'écrire directement ce nouveau composant d'ordre supérieur d'une manière générique:
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 Cette fois, la fonction from_data_source prend deux arguments en plus WrappedComponent :
prop_name : c'est le nom de l'hélice du composant enveloppé pour remplir avec certaines donnéesget_source : c'est une fonction qui sera appelée pour obtenir les données Regardez comment nous avons hérité des PropTypes du composant enveloppé et comment nous avons exclu prop_name . Nous n'avons donc pas (et ne pouvons pas) transmettre les données à notre nouveau composant.
Et puis dans render , nous avons défini un accessoire pour passer pour WrappedComponent avec le résultat d'un appel à get_source .
Alors écrivons une fonction très simple (cela pourrait être compliqué avec la mise en cache, le filtrage ...) qui prennent les accessoires et le contexte, et renvoie certaines données:
def get_todos ( props , context ):
# here it could be a call to a database
return [
TodoObject ( "fooooo" ),
TodoObject ( "baaaar" ),
]Et nous pouvons composer notre composant:
SourcedTodoApp = from_data_source ( TodoApp , 'todos' , get_todos )
ThingApp = thingify ( SourcedTodoApp )Et exécutez-le:
print ( < ThingApp / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > fooooo </ li > < li > baaaar </ li > </ ul > </ div >Il fonctionne comme prévu et les données sont récupérées uniquement lorsque le composant doit être rendu.
Ainsi, nous avons une liste de TODO, qui peut récupérer les données d'une source externe. Mais nous pouvons vouloir que les données soient différentes en fonction de l'utilisateur.
Ce que nous pouvons faire, c'est au niveau principal, obtenir notre utilisateur et le transmettre sur chaque composant pour être sûr que chaque composant est en mesure d'obtenir l'utilisateur actuel.
Ne serait-il pas lourd?
La résolution de ce cas d'utilisation est l'objectif exact du concept Context fourni par mixt . Il est, bien sûr, inspiré par le concept de contexte dans React.
Et comme ils l'ont dit:
Le contexte est conçu pour partager des données qui peuvent être considérées comme «globales» pour un arbre de composants React, tels que l'utilisateur, le thème ou la langue préférée authentifiés actuels.
La création d'un contexte est aussi simple que la création d'un composant, sauf qu'elle héritera à partir de BaseContext et n'a pas besoin d'une méthode render (elle rendra ses enfants).
Et il faut une classe PropTypes , qui définit les types de données que le contexte acceptera et transmettra l'arbre.
Créons donc notre contexte qui maintiendra l'ID de l'utilisateur authentifié.
from mixt import BaseContext
class UserContext ( BaseContext ):
class PropTypes :
authenticated_user_id : Required [ int ] Maintenant, nous voulons mettre à jour notre méthode get_todos pour prendre en compte le authenticated_user_id .
N'oubliez pas que nous l'avons transmis les accessoires et le contexte. Le contexte sera utile ici:
def get_todos ( props , context ):
return {
1 :[
TodoObject ( "1-1" ),
TodoObject ( "1-2" ),
],
2 : [
TodoObject ( "2-1" ),
TodoObject ( "2-2" ),
]
}[ context . authenticated_user_id ]Et maintenant, nous pouvons rendre notre application avec le contexte:
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 >Nous pouvons voir les entrées TODO pour l'utilisateur 1.
Essayons avec l'utilisateur 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 >Nous pouvons voir les entrées TODO pour l'utilisateur 2.
Dans ce cas, bien sûr, nous aurions pu passer l'ID utilisateur en tant qu'hélice. Mais imaginez que l'application TODO est profondément dans l'arbre des composants, il est beaucoup plus facile de le passer de cette façon.
Mais comme dit dans la documentation React:
N'utilisez pas le contexte juste pour éviter de passer des accessoires à quelques niveaux. Tenez-vous à des cas où les mêmes données doivent être accessibles dans de nombreux composants à plusieurs niveaux.
Lorsqu'il n'y a pas de contexte, l'argument context de la méthode render est défini sur EmptyContext et pas à None . Vous pouvez donc utiliser directement la méthode has_prop pour vérifier si un accessoire est disponible via le contexte.
Mettons à jour les fonctions get_todos pour renvoyer une liste vide d'objets TODO s'il n'y a pas d'utilisateur authentifié.
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 ]Essayons ceci:
print ( < ThingApp / > ) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < / ul > < / div >Et cela fonctionne toujours avec un utilisateur dans le contexte:
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 >Remarque importante sur les contextes : vous pouvez avoir de nombreux contextes! Mais définir le même accessoire dans de nombreux contextes peut conduire à un comportement non défini.
Tout le monde aime un beau design, et peut-être une interaction.
Il est facilement faisable: nous générons du HTML et du HTML peut contenir des CSS et JS.
Ajoutons d'abord une certaine interaction: lors de l'ajout d'un élément dans le TodoForm , ajoutons-le à la liste.
Nous ajoutons d'abord dans notre composant TodoForm une méthode render_javascript qui hébergera notre (mauvais, nous pourrions faire mieux mais ce n'est pas le point) 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);
}
""" )Pour commencer, nous affichons uniquement le nouveau texte TODO.
Maintenant, mettez à jour notre méthode render pour retourner ce JavaScript (notez que l'utilisation d'une méthode render_javascript est uniquement pour séparer les préoccupations, il aurait pu être directement dans la méthode 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 > Remarquez l'étiquette Fragment . C'est un moyen d'encapsuler de nombreux éléments à retourner, comme dans React. Cela aurait pu être une liste simple mais avec des comas à la fin:
return [
< script > ... < / script > ,
< form >
...
< / form >
] Maintenant, nous voulons ajouter un élément à la liste. Ce n'est pas le rôle du TodoForm de le faire, mais de la liste. Nous allons donc ajouter des JS dans le composant TodoList : une fonction qui prend du texte et créent une nouvelle entrée.
Quant au TodoForm , nous ajoutons une méthode render_javascript avec (toujours mauvais) 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 )) Et nous mettons à jour notre méthode render pour ajouter la balise <script> et un id à la balise ul , utilisée dans le 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 > Et maintenant, nous pouvons mettre à jour la méthode render_javascript du composant TodoForm pour utiliser notre nouvelle fonction 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);
}
""" )Et c'est tout. Rien de spécial, en fait.
Mais jetons un coup d'œil à la sortie d'ou TodoApp :
print (
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
)La sortie embellie est:
< 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 > Nous avons donc beaucoup de balises script . Cela pourrait être formidable d'en avoir un.
mixt est livré avec un moyen de "collecter" des parties de ce qui est rendu pour les mettre ailleurs. Nous avons à notre disposition deux collectionneurs simples, à utiliser comme composants: JSCollector et CSSCollector .
Ces composants collectent des parties de l'arbre de leurs enfants.
La première façon consiste à utiliser la balise Collector Collect .
Changeons d'abord notre appel principal:
from mixt import JSCollector
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
) Cela collectera le contenu de toute la balise JSCollector.Collect .
Mettons à jour notre TodoForm et remplaçons notre balise script par une balise 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 > Nous pouvons faire de même avec le 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 >Maintenant, exécutons notre code mis à jour:
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
)La sortie embellie est:
< 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 > Comme vous pouvez le voir, tous les scripts sont dans une seule balise script , à la fin. Plus précisément, à la fin de l'endroit où se trouvait la balise JSCollector , car nous avons utilisé render_position="after" . Une autre possibilité est render_position="before" pour mettre cela là où la balise JSCollector a commencé.
Tout cela fonctionne exactement de la même manière pour la balise CSSCollector , où le contenu est placé dans une balise <style type="text/css> .
Comme l'utilisation de JS / CSS est assez courant dans le monde HTML, nous avons ajouté du sucre pour rendre tout cela encore plus facile à faire.
Si vous avez une méthode render_js , le JSCollector collectera automatiquement le résultat de cette méthode. Idem pour CSSSelector et la méthode render_css .
Avec cela, pas besoin d'une balise JSCollector.Collect .
Pour que cela fonctionne dans notre exemple, dans TodoForm et TodoList :
JSCollector.CollectFragment désormais inutilesrender_javascript à render_js .html.Raw dans render_js car il n'est pas nécessaire lorsque le collecteur appelle render_js lui-même: si la sortie est une chaîne, elle est considérée comme une "brute"De cette façon, nous avons exactement le même résultat.
Cela fonctionne maintenant parce que nous n'avons qu'une seule instance d'un enfant avec une méthode render_js .
Mais si nous avons beaucoup d'enfants, cette méthode sera appelée pour chaque enfant. En fait, il ne doit contenir que du code très spécifique à cette instance.
Pour ne servir JS / CSS qu'une seule fois pour une classe de composants, nous devons utiliser render_js_global ou render_css_global (qui devrait être classmethod )
Il sera collecté la première fois, et la première fois, une instance est trouvée, avant de collecter la méthode render_js .
Donc, ici, nous pouvons changer notre render_js en render_js_global , les décorer avec @classmethod et cela fonctionnera toujours de la même manière.
Nous sommes maintenant capables de regrouper JavaScript ou Style. Mais que se passe-t-il si nous voulons le mettre ailleurs, comme dans l'étiquette head ou à la fin de l'étiquette body ?
C'est possible avec les références, alias "Refs". C'est le même contexte que dans React, sans la partie Dom bien sûr.
Vous créez une référence, passez-la à un composant et vous pouvez l'utiliser n'importe où.
Mettons à jour notre code principal pour ce faire.
Nous créons d'abord une réf.
from mixt import Ref
js_ref = Ref () Cela créera un nouvel objet qui tiendra une référence à un composant. Dans un composant, vous n'avez pas besoin d'importer Ref et pouvez utiliser js_ref = self.add_ref() , mais nous ne sommes pas dans un composant ici.
Pour sauver une référence, nous le transmettons simplement au ref Prop:
< JSCollector ref = { js_ref } > ... < / JSCollector > Remarquez que nous avons supprimé l'hélice render_position , car maintenant nous ne voulons pas que le JS soit mis avant ou après la balise, mais ailleurs.
Pour accéder au composant référencé par une référence, utilisez l'attribut current :
js_collector = js_ref . currentBien sûr, cela ne peut être fait qu'après le rendu.
Comment pouvons-nous l'utiliser pour ajouter une balise script dans notre head .
Mettez d'abord à jour notre HTML pour inclure le html classique, les étiquettes head et body :
return str (
< html >
< head >
< / head >
< body >
< JSCollector ref = { js_ref } >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / UserContext >
< / JSCollector >
< / body >
< / html >
) À ce stade, nous n'avons pas de balise script dans la sortie:
< 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 > Première chose à savoir: un collectionneur est en mesure de rendre toutes les choses qu'elle a collectées en appelant sa méthode render_collected .
Et en se souvenant qu'il inclut déjà la balise script , nous voulons peut-être faire ceci:
# ...
< head >
{ js_ref . current . render_collected ()}
< / head >
# ...Mais cela ne fonctionne pas:
AttributeError : 'NoneType' object has no attribute 'render_collected'C'est parce que nous essayons d'accéder à la valeur actuelle au moment du rendu. Cela doit être fait après.
Pour cela, nous pouvons utiliser une fonctionnalité de mixt : si quelque chose ajouté à l'arborescence est un appelable, il sera appelé après le rendu, lors de la convertissement en chaîne.
Nous pouvons donc utiliser par exemple une lambda:
# ...
< head >
{ lambda : js_ref . current . render_collected ()}
< / head >
# ...Et maintenant cela fonctionne:
< 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 > Hourra, nous l'avons fait! Toutes les principales caractéristiques de mixt expliquées. Vous pouvez désormais utiliser mixt dans vos propres projets.
Dans une prochaine étape, vous voudrez peut-être lire la documentation de l'API.