Schreiben Sie HTML -Komponenten direkt in Python und Sie haben eine schöne, aber kontroverse Mischung.
Ja, umstritten .
Wenn es Ihnen nicht gefällt, ignorieren Sie es (aber Sie können dies ohne den HTML-in-Python-Teil verwenden, siehe unten;)))
Basierend auf Pyxl. Nur Python 3.6+ und verwenden Sie die Datenvalidierung.
Sobald Sie Ihre HTML haben, können Sie damit tun, was Sie wollen. Betrachten Sie es als Ersatz für Ihre klassische Vorlagenmotor.
Quellcode : https://github.com/twidi/mixt/
Dokumentation : https://twidi.github.io/mixt/
Pypi : https://pypi.org/project/mixt/
CI (Circleci): https://circleci.com/gh/twidi/workflows/mixt/
Erstellen wir ein 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" / > )Und führen es aus:
$ python example.py
< div > Hello, World < /div >Wenn Sie nicht gerne HTML in Python schreiben möchten, können Sie es trotzdem verwenden:
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" ))Ja, es ist von React (tatsächlich hauptsächlich JSX) inspiriert, und wir leihen uns einen Teil des Konzepts aus:
Und wir haben hinzugefügt:
Führen Sie diese beiden Befehle aus. Der zweite wird Python sagen, wie man Dateien mit HTML im Inneren versteht.
pip install mixt
mixt-post-installUm zu überprüfen, ob alles fertig ist, rennen Sie:
python -m mixt.examples.simpleSie sollten diese Ausgabe haben:
< div title =" Greeting " > Hello, World </ div > Wenn Sie das HTML-in-Python-Zeug nicht verwenden möchten, führen Sie mixt-post-install nicht aus. Und dann testen mit (um den gleichen Ausgang zu haben):
python -m mixt.examples.simple_pure_pythonKlonen Sie dann das Git -Projekt:
make devUm zu überprüfen, ob alles fertig ist, rennen Sie:
python -m mixt.examples.simpleSie sollten diese Ausgabe haben:
< div title =" Greeting " > Hello, World </ div >Nach einem Code:
make testsmake lint Wenn Sie Dinge im codec Verzeichnis berühren, müssen Sie ausführen, make dev (oder zumindest make full-clean ), um die pyc Python-Dateien zu reinigen.
Beachten Sie, dass unser CI prüft, ob jedes Commit den make lint durchführt, make tests und make check-doc . Vergessen Sie also nicht, diese für jeden Commit zu führen.
Eine Möglichkeit, es vor dem Schieben zu tun, ist:
git rebase develop --exec ' git log -n 1; make checks ' HINWEIS: Sie finden den endgültigen Code dieser Benutzerhandbuch in src/mixt/examples/user_guide (Sie finden mixt.py und pure_python.py ).
Führen Sie es mit:
python -m mixt.examples.user_guideLassen Sie uns eine ... Todo -Liste erstellen, ja!
Aber früher, denken Sie daran. Dies ist nicht reagiert, es ist nicht auf dem Browser und es gibt hier kein JavaScript. Wir sprechen nur über das Rendern von HTML.
Aber Sie können damit tun, was Sie wollen. Fügen Sie JavaScript -Handler hinzu, einfache Formen ...
Über Formen sprechen ...
In einer Todo -Liste möchten wir in der Lage sein, ein Todo hinzuzufügen. Es ist eine einfache Texteingabe.
Erstellen wir also unsere erste Komponente, das TodoForm . Wir möchten ein Formular mit einem Eingabetxt und einer Schaltfläche.
Eine Komponente ist eine Unterklasse der Element mit einer render -Methode, die Sie schreiben müssen.
# 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 >Beachten Sie, dass dies als einfache Funktion geschrieben werden könnte:
# 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 >Beim Druck der Komponente liefern diese beiden das gleiche Ergebnis:
print ( < TodoForm / > ) < form method =" post " action =" ??? " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Beachten Sie, wie es formatiert ist: kein Platz zwischen Tags. Tatsächlich ist es wie in JSX:
JSX entfernt Weißespace am Anfang und das Ende einer Linie. Es entfernt auch leere Zeilen. Neue Linien neben Tags werden entfernt. Neue Linien, die in der Mitte von Streichliteralen auftreten
Um einen Raum oder eine neue Linie hinzuzufügen, können Sie Python übergeben. Fügen wir beispielsweise eine neue Zeile vor dem Etikett hinzu:
#...
< form method = "post" action = "???" >
{ ' n ' } < label > New Todo : < / label > < itext name = "todo" / >
#...Jetzt haben wir diese Ausgabe:
< form method =" post " action =" /todo/add " >
< label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Jetzt gehen wir weiter.
Beachten Sie das action des Formulars. Wir müssen etwas passieren. Aber hartcodieren es nicht richtig. WWE muss es an die Komponente weitergeben.
Mixt hat wie React das Konzept der Eigenschaften, auch bekannt als "Requisiten".
In Mixt definieren wir sie mit einem Typ in einer Klasse in unserer Komponente mit dem Namen 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 > Hier definierten wir eine Requisite mit dem Namen add_url , die eine Zeichenfolge ( str ) sein muss. Dies verwendet die Python -Typing -Syntax.
Und beachten Sie, wie wir das action des form -Tags geändert haben. Es ist jetzt {self.add_url} anstelle von "???" .
Wenn Attribute zwischen lockigen Klammern weitergegeben werden, werden sie zur Laufzeit als reines Python interpretiert. Wenn der mixt -Parser die gesamte Datei in Python umwandelt, bevor der Python -Dolmetscher sie ausführt, bleibt er gleich, nur das HTML wird umgewandelt. Es gibt also keine Strafe, dies zu tun.
Schauen Sie, wie dies aussehen würde, wenn unsere Komponente in reinem Python geschrieben würde:
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")
),
) Beachten Sie, wie die Requisiten an eine Komponente als benannte Argumente übergeben werden und wie action übergeben wird: action=self.add_url .
Diese Pure-Python-Komponente zeigt Ihnen auch, wie sie funktioniert: Requisis werden als genanntes Argument an die Komponentenklasse übergeben. Dann wird diese Komponente aufgerufen, die Kinderkomponenten als Positionsargumente an den Anruf übergeben:
ComponentClass ( prop1 = "val1" , prop2 = "val2" )(
Children1 (),
Children2 (),
)Was sind Kinder? Kinder sind Tags in anderen Tags.
In <div id="divid"><span /><p>foo</p></div> haben wir:
html.Div -Komponente mit einer Prop id und zwei Kindern:html.Span -Komponente ohne Kinderhtml.P -Komponente mit einem Kind:html.RawHtml -Komponente mit dem Text "Foo"Beachten Sie, dass Sie mit Requisiten und Kindern spielen können. Zuerst die Version in Pure-Python, um zu zeigen, wie es funktioniert:
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) Dann die mixt -Version:
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> Kehren wir nun zu unseren Requisiten add_url zurück.
Wie übergeben Sie es an die Komponente?
Genau wie wir haben Attribute an HTML -Tags übergeben: Sie sind tatsächlich Requisiten in den HTML -Kompomenten (definiert in mixt.html ). Wir unterstützen alle HTML -Tags, die zum Zeitpunkt des Schreibens in HTML5 mit ihren Attributen (mit Ausnahme der veralteten) gültig sind (nicht veraltet).
Also lass uns das machen:
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, wir haben unsere Requisite in der gerenderten HTML.
Was ist, wenn wir keine Schnur übergeben? Wir sagten in PropTypes , dass wir eine Zeichenfolge wollten ...
Versuchen wir es:
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 >Es funktioniert! Aber ... es ist keine Zeichenfolge !! Tatsächlich gibt es einen Sonderfall für Zahlen, Sie können sie als Zahlen anstelle von Zeichenfolgen übergeben und sie werden bei Bedarf konvertiert ...
Probieren wir also etwas anderes aus.
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' > ) Und es ist genauso, wenn wir in Python True bestehen
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, lasst uns das System ausstreuen und "True" als Zeichenfolge übergeben.
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' > )Immer noch das gleiche, aber hier haben wir eine Schnur passiert! Ja, aber es gibt 4 Werte, die immer bewertet werden, was sie zu sein scheinen:
None )Die einzige Möglichkeit, einen dieser Werte als Zeichenfolge zu übergeben, besteht darin, sie über Python als Zeichenfolge zu übergeben:
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 > Mit Ausnahme dieser 4 Werte und Zahlen wird jeder Wert, der an ein Attribut übergeben wird, als Zeichenfolge angesehen. Auch wenn es keine Zitate gibt, wie in HTML in HTML5, wo Zitate für Zeichenfolgen ohne einige Zeichen nicht obligatorisch sind (keine Leerzeichen, no / ...).
Um etwas anderes zu bestehen, müssen Sie den Wert in lockigen Klammern umgeben (und in diesem Fall sind keine Zitate um die lockigen Klammern erforderlich.
Ok, jetzt sind wir sicher, dass wir nur Zeichenfolge akzeptieren ... aber was ist, wenn ich nichts bestehe? Und ... was ist "nichts"?
Beginnen wir mit einer leeren Saite in 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, es funktioniert, wir wollten eine Zeichenfolge, wir haben eine Zeichenfolge.
Lassen Sie uns nun diese leere Zeichenfolge direkt übergeben:
print ( < TodoForm add_url = "" / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Es funktioniert immer noch, weil es immer noch eine Zeichenfolge ist. Lassen Sie uns die Zitate entfernen, um zu sehen.
print ( < TodoForm add_url = / > ) mixt . exceptions . GeneralParserError : < mixt parser > Unclosed Tags : < TodoForm > Hum ja, das ist kein gültiges HTML. Entfernen wir also das = :
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' > ) WAS? Ja, denken Sie an HTML5 -Attribute wie required , checked ... Sie müssen nur als Attribut ohne Wert vorhanden sein, um True betrachtet zu werden. Wenn ein Attribut keinen Wert hat, ist es ein Boolescher und es ist True .
Zusätzlich zu einem Wert sind diese beiden anderen Möglichkeiten in HTML5 für einen Booleschen von True gültig:
required=""required="required"Für Ihre Bequemlichkeit haben wir einen anderen Weg hinzugefügt:
True (Fall spielt keine Rolle), als Python oder als Zeichenfolge: required=True , required={True} , required="true" Und sein Gegenstück, um False zu bestehen:
False (Fall spielt keine Rolle), als Python oder als Zeichenfolge: required=False , required={False} , required="false" OK für die Booleschen Attribute. Es ist nicht unser Fall. Das Letzte, was wir tun können, ist, das Attribut überhaupt nicht festzulegen:
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 ist verständlich: Wir versuchen, auf eine Requisite zuzugreifen, die nicht festgelegt ist. Natürlich können wir sie nicht verwenden.
Aber was ist, wenn wir nicht darauf zugreifen? Wenn wir die Komponente nicht drucken, wird sie nicht gerendert:
< TodoForm / > < TodoForm at 0x7fbd18ea5630 >So können wir eine Instanz erstellen, aber es wird zur Renderzeit scheitern. Aber es gibt eine Möglichkeit, das zu verhindern.
Standardmäßig sind alle Eigenschaften optional. Und Sie müssen den Optional Typ nicht aus dem Python typing dafür verwenden. Es wäre umständlich, es für jede Requisite zu tun.
Stattdessen stellt mixt einen Typ mit dem Namen Required , den Sie genauso verwenden wie Optionnal .
from mixt import Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
def render ( self , context ):
# ...Also haben wir nur gesagt, wir wollten eine Schnur und das ist erforderlich.
Versuchen wir es erneut, es ohne die Requisite zu erstellen:
< TodoForm / > mixt . exceptions . RequiredPropError : < TodoForm > . add_url : is a required prop but is not setJetzt haben wir die Ausnahme früher in unserem Programm aufgelegt.
Um andere Möglichkeiten in Requisiten zu sehen, fügen wir eine neue hinzu, um das Textetikett zu ändern. Wir wollen es jedoch nicht erforderlich machen und haben stattdessen einen Standardwert.
Dafür ist es so einfach wie ein Wert in der PropTypes -Klasse einen Wert hinzuzufügen:
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 >Versuchen wir es nun, ohne die Stütze zu bestehen:
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 >Und wenn wir einen bestehen:
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 >Es funktioniert wie erwartet.
Beachten Sie, dass Sie keinen Standardwert geben können, während die Requisite Required ist. Es macht keinen Sinn, es wird also so schnell wie möglich überprüft, während die class konstruiert ist:
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 requiredUnd natürlich muss der Standardwert mit dem Typ übereinstimmen!
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' > ) Eine andere Sache, die wir in unserer Komponente tun möchten, ist es, das Etikett konstruieren zu lassen, ihm einen "Typ" von Todo zu übergeben, aber die Auswahl zu begrenzen. Dazu können wir den Choices verwenden:
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 >Versuchen wir es:
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 >Und was ist, wenn wir versuchen, etwas anderes als die verfügbaren Entscheidungen zu übergeben? Es scheitert wie erwartet:
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' ])Aber vielleicht wollen wir es nicht übergeben und einen Standardwert verwenden. Was wäre das Ergebnis?
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . UnsetPropError : < TodoForm > . type : prop is not set Daher müssen wir die type -Stütze nach Bedarf markieren:
class PropTypes :
add_url : Required [ str ]
type : Required [ Choices ] = [ 'todo' , 'thing' ]Wenn wir es also nicht bestehen, scheitert es früher:
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . RequiredPropError : < TodoForm > . type : is a required prop but is not setAber es ist nicht das, was wir wollen, wir wollen einen Standardwert.
Tatsächlich haben Sie festgestellt, dass für andere Typen als Choices uns ein Wert in PropTypes einen Standardwert ergibt. Aber für Choices ist es anders, da der Wert die Liste der Auswahlmöglichkeiten ist.
Dafür haben wir DefaultChoices : Es funktioniert genauso wie Choices , verwenden jedoch den ersten Eintrag in der Liste als Standardwert. Und natürlich kann es wie bei anderen Typen nicht Required sein.
Versuchen wir es:
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 >Es funktioniert wie erwartet.
Bis dahin haben wir einfache Typen verwendet, aber Sie können kompliziertere verwenden.
So machen wir beispielsweise die add_url -Prop, eine Funktion zu akzeptieren, die die URL für uns basierend auf der type -Requisite berechnet. Wir möchten aber auch Zeichenfolgen und mit einem Standardwert zulassen.
Wir können das mit Tippen tun. Unsere Funktion nimmt eine Zeichenfolge, den type und gibt eine Zeichenfolge zurück, die URL.
Die Syntax ist also Callable[[str], str] für den Callable, und wir verwenden Union , um Dinge vom Typ Callable oder str zu akzeptieren:
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 > Versuchen wir es zunächst ohne add_url -Requisite, da wir einen Standard haben:
print ( < TodoForm / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Es sollte auch funktionieren, wenn wir eine Zeichenfolge übergeben:
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 >Und jetzt können wir eine Funktion übergeben:
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? Warum? Ich habe eine Funktion bestanden, die eine Zeichenfolge als Argument annahm und eine Zeichenfolge zurückgab. Ja, aber vergessen Sie nicht, dass diese Typen überprüft werden! Deshalb müssen wir unserer Funktion Typen hinzufügen:
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 >Und wenn wir einen anderen Typ bestehen, sollte sich die URL entsprechend ändern:
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 >Wir können diese Funktion sogar zum Standardwert für unsere Requisite machen:
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 >Jetzt können Sie sich fragen ... Python -Typing ist umständlich und validieren können einen Teil unserer kostbaren Zeit wegnehmen.
Lassen Sie mich das beantworten:
Standardmäßig läuft mixt in "Dev-Mode". Und im Dev-Modus werden Requisiten validiert, wenn sie an eine Komponente übergeben werden. Wenn Sie nicht in "Dev-Mode" sind, wird die Validierung übersprungen. In der Produktion können Sie also den Dev-Mode deaktivieren (wir werden in einer Minute sehen) und Requisiten sehr schnell passieren:
Choices in der Liste der Auswahlmöglichkeiten stehtrender -Methode. Sie können jedoch sagen, dass es in der Produktion ist, dass die Validierung wichtig ist. In der Tat. Aber natürlich wird Ihr Code vollständig durch Tests abgedeckt, die Sie im Dev-Modus ausführen. In der Produktion benötigen Sie diese Validierung nicht! Und beachten Sie, dass React übrigens mit NODE_ENV=production funktioniert.
Wie ändere ich Dev-Mode? Wir erzwingen keine Umgebungsvariablen, schlagen jedoch einige Funktionen vor. Es liegt an Ihnen, sie anzurufen:
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 () Versuchen wir dies also mit der type -Requisite. Denken Sie daran, es sieht aus wie:
type : DefaultChoices = [ 'todo' , 'thing' ]Wir versuchen, eine andere Wahl zu bestehen, zuerst in Dev-Mode:
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' ])Es scheitert wie erwartet.
Und jetzt durch Deaktivieren von Dev-Mode:
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 > Es funktioniert, wir haben einen TODO -Typ, der nicht in unseren Entscheidungen verwendet wird, und ist auch in der action . Es ist die Arbeit Ihrer Tests, um sicherzustellen, dass Sie niemals ungültige Requisiten bestehen, sodass Sie sich in der Produktion und der Deaktivierung von Dev-Mode sicher machen können.
Jetzt haben wir unsere Form. Welche anderen Komponenten benötigen wir für unsere TODO -List -App?
Natürlich brauchen wir eine Möglichkeit, einen Todo -Eintrag anzuzeigen.
Aber was ist ein Todoeintrag? Erstellen wir ein grundlegendes TodoObject :
class TodoObject :
def __init__ ( self , text ):
self . text = textEs ist eine sehr einfache Klasse, aber Sie können natürlich das verwenden, was Sie wollen. Es könnten Django -Modelle usw. sein ...
So können wir unsere Todo -Komponente erstellen und so ein erforderliches TodoObject als Prop akzeptieren:
class Todo ( Element ):
class PropTypes :
todo : Required [ TodoObject ]
def render ( self , context ):
return < li > { self . todo . text } < / li >Und wir können es verwenden:
todo = TodoObject ( "foo" )
print ( < Todo todo = { todo } / > ) < li > foo </ li > Jetzt wollen wir eine Liste von Todos haben. Lassen Sie uns eine TodoList -Komponente erstellen, die als Requisiten eine Liste von TodoObject akzeptiert.
Aber was sich von unseren beiden anderen Komponenten unterscheidet, die nur HTML -Tags in ihrer render -Methode verwenden, werden wir jetzt eine Komponente in einen anderen in einen anderen zusammenfassen. Mal sehen, wie.
class TodoList ( Element ):
class PropTypes :
todos : Required [ List [ TodoObject ]]
def render ( self , context ):
return < ul > {[ < Todo todo = { todo } / > for todo in self . todos ]} < / ul > Ja, es ist so einfach das: Sie verwenden <Todo...> für die Todo -Komponente, da Sie ein HTML -Tag verwenden würden. Der einzige Unterschied besteht darin, dass Sie für HTML-Tags sie nicht direkt importieren müssen (einfacher mixt -Import html ) und durch Konvention sie in niedrigerer Fall schreiben. Für normale Komponenten müssen Sie sie importieren (Sie können immer noch from mylib import components und <components.MyComponent ...> ) und den genauen Fall verwenden.
Beachten Sie, wie wir eine Liste benötigten, und über ein Listenverständnis in Curly-Braces in die <ul> weitergegeben.
Sie können die Dinge anders machen, wenn Sie möchten.
Wie das Listenverständnis von der HTML zu trennen:
def render ( self , context ):
todos = [
< Todo todo = { todo } / >
for todo
in self . todos
]
return < ul > { todos } < / ul >Oder in einer dedizierten Methode (das wäre zum Testen nützlich):
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 >Es liegt an dir: Am Ende ist es nur Python.
Mal sehen, was durch diese Komponente gerendert wird:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print ( < TodoList todos = { todos } / > ) < ul > < li > foo </ li > < li > bar </ li > < li > baz </ li > </ ul > Und schließlich haben wir unsere TodoApp -Komponente, die das Formular und die Liste zusammenfassen:
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 >Lassen Sie uns diesen HTML an einen HTML -Verschieber übergeben:
< 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 > Und das war's, wir haben unsere Todo-List-App! Um es in einer Seite zu verwenden, erstellen Sie einfach eine Komponente, die das HTML -Basismarkup rendert und die TodoApp -Komponente darin integriert. Sie brauchen nicht einmal eine Komponente:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print (
< html >
< body >
< TodoApp todos = { todos } type = "thing" / >
< / body >
< / html >
)Die verschönere Ausgabe wäre:
< 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 >Wir haben eine generische Todo-Liste, aber nach den verfügbaren Arten von Todo möchten wir vielleicht eine "Todo-Liste" und eine "Ding-Liste" haben.
Wir haben bereits die Todo -Liste, weil unser TodoApp standardmäßig über eine Art von todo verfügt.
Lassen Sie uns also ThingApp erstellen.
Die erste Art, dies zu tun, besteht darin, von unserem TodoApp zu erben. Aber durch Erben werden wir keine Requisiten vom Elternteil entfernen (es ist nicht wirklich wahr, wir werden dies später sehen), also haben wir standardmäßig immer noch die type -Prop. Aber wir wollen nichts anderes als "Ding" akzeptieren. So können wir die type -Stütze wie folgt neu definieren:
class ThingApp ( TodoApp ):
class PropTypes :
type : DefaultChoices = [ 'thing' ]Verwenden wir diese Komponente:
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 > Wenn wir versuchen, "todo" für die type -Requisiten zu bestehen, funktioniert es nicht:
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' ])Trotzdem ist es seltsam, einen Typ passieren zu können.
Versuchen wir einen anderen Weg: eine übergeordnete Komponente. Eine Komponente, die nichts anderes tut, um Dinge mit seinen Kindern zu tun und sie zurückzugeben. Was wir hier wollen, ist eine Komponente, die einen TodoApp mit dem type -Requisite zurückgibt, der zu "Ding" gezwungen ist.
Lass uns das machen
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 > Es funktioniert und diesmal können wir die type -Requisite nicht übergeben:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) mixt . exceptions . InvalidPropNameError : < ThingApp > . type : is not an allowed prop Beachten Sie, wie wir den Typ für die todos -Requisiten definieren mussten. Sowohl in TodoApp als auch TodoThing .
Es gibt viele Möglichkeiten, damit umzugehen.
Der erste wäre, den Typ in ThingApp zu ignorieren, da er in TodoApp eingecheckt wird. Wir werden Any den Typ verwenden:
from typing import Any
#...
class ThingApp ( Element ):
class PropTypes :
todos : Any
#...Versuchen wir es mit einer gültigen Liste von Todos:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Aber was ist, wenn wir etwas anderes bestehen?
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 ]) Es funktioniert wie erwartet, aber der Fehler wird auf der Ebene TodoApp gemeldet, was völlig normal ist.
Eine andere Möglichkeit wäre, den Typ auf einer höheren Ebene zu definieren:
TodoObjects = Required [ List [ TodoObject ]]
class TodoApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...
class ThingApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...Wenn wir nun etwas anderes übergeben, haben wir den Fehler auf der richtigen Ebene gemeldet:
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 ]) Wenn Sie dies jedoch nicht können oder nicht wollen, können Sie den Typ in TodoApp et definiert halten. Verwenden Sie die Methode der prop_type -Klasse einer Komponente, um den Typ eines Requestes zu erhalten:
class ThingApp ( Element ):
class PropTypes :
todos : TodoApp . prop_type ( "todos" )
# ... Aber ist es wirklich von Bedeutung, den Fehler für ThingApp oder TodoApp aufzuheben? Denn am Ende ist es wirklich TodoApp , der den Scheck durchführen muss.
Dies sollte also eine Möglichkeit sein, dies generischer zu tun.
Wir haben früher gesehen, dass eine Komponente eine einzige Funktion sein kann, um eine Komponente zu rendern. Es muss nur eine Komponente zurückgeben, ein HTML -Tag. Ein Unterschied zu Klassenkomponenten besteht darin, dass es keine PropTypes gibt, also keine Validierung. Aber ... es ist genau das, was wir brauchen.
Wir möchten, dass unser ThingApp einige Requisiten (die todos -Prop) akzeptiert und einen TodoApp mit einer bestimmten type -Requisite zurückgeben.
So konnten wir:
def ThingApp ( todos ):
return < TodoApp type = "thing" todos = { todos } / > Hier können wir sehen, dass wir type nicht an ThingsApp übergeben können, es ist kein gültiges Argument.
Versuchen wir es:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div > Hier haben wir nur einen Requisiten, um es zu verabschieden, also ist es einfach. Aber stellen Sie sich vor, wir haben viele. Wir können die {**props} -Syntax verwenden:
def ThingApp ( ** props ):
return < TodoApp type = "thing" { ** props } / >Und Sie können noch weniger Charaktere tun (wenn es zählt):
ThingApp = lambda ** props : < TodoApp type = "thing" { ** props } / >Diese beiden Fonktionen verhalten sich genau gleich.
Und Sie können keine type -Requisite übergeben, da es sich um einen Python -Fehler handelt, da er zweimal an TodoApp übergeben würde:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) TypeError : BaseMetaclass object got multiple values for keyword argument 'type' (Ja, es geht um BaseMetaclass , das ist die Metaklasse, die unsere Komponentenklassen erstellt.)
Und alle anderen falschen Requisiten würden von TodoApp validiert:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} foo = "bar" / > ) mixt . exceptions . InvalidPropNameError : < TodoApp > . foo : is not an allowed prop In diesem Sinne hätten wir eine generische Folge erstellen können, die den Typ jeder Komponente erzwingt, die eine type -Requisite akzeptiert:
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 > Die gerenderte Komponente ist TodoApp , die type -Requisite ist "Sache" und die anderen Requisiten (hier nur todos ) werden korrekt übergeben.
Erweitern Sie nun dieses Konzept auf einen generischen Fall: "Komponenten höherer Ordnung". In React eine "Komponente mit hoher Ordnung" ist "eine Funktion, die eine Komponente übernimmt und eine neue Komponente zurückgibt".
Die Idee ist:
EnhancedComponent = higherOrderComponent ( WrappedComponent )Eine klassische Möglichkeit, dies zu tun, besteht darin, eine neue Komponentenklasse zurückzugeben:
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 Beachten Sie, wie wir die PropTypes -Klasse so festlegen, dass sie von der eingewickelten Komponente von der verpackten Komponente erben und wie wir alle Requisiten zusammen mit den Kindern an die verpackte Komponente übergeben. Mit der zurückgegebenen Komponente akzeptiert die gleichen Requisiten mit den gleichen Typen wie die eingewickelt.
Und beachten Sie auch die __display_name__ . Es wird in Ausnahmen verwendet, um Sie jetzt die Komponente zu ermöglichen, die sie angehoben hat. Ohne es zu erzwingen wäre es auf HOC eingestellt worden, was nicht hilfreich ist. Stattdessen geben wir an, dass es sich um eine transformierte Version der bestandenen Komponente handelt.
Hier ist es eine Funktion, die nichts Nützliches tut.
In unserem Beispiel hätten wir das tun können:
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 HOCZwei wichtige Dinge hier:
__exclude__ = {'type'} verwenden, um die type -Prop von den von WrappedComponent.PropTypes zu entfernen. Die zurückgegebene Komponente erwartet also genau die gleichen Requisiten wie die eingewickelte, mit Ausnahme des type .{self.children()} in der verwickelten Komponente hinzugefügt, denn selbst wenn wir tatsächlich wissen, dass die Komponente, die wir wickeln werden, ohne Kinder TodoApp , keine Kinder nimmt (sie könnte aber nichts mit ihnen tun), können wir nicht im Voraus sagen, dass es immer der Fall ist und dass diese Komponente mit höherer Ordnung nicht verwendet wird, um eine andere Komponente zu wickeln als TodoApp . Es ist also besser, dies immer zu tun. Und jetzt können wir unser ThingApp erstellen:
ThingApp = thingify ( TodoApp )Und benutze es:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Wenn wir versuchen, den Typ zu bestehen:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) mixt . exceptions . InvalidPropNameError : < thingify ( TodoApp ) > . type : is not an allowed prop Wie geplant können wir den Typ nicht bestehen. Und beachten Sie, wie die __display_name__ verwendet wird.
Denken wir darüber nach, wie mächtig das ist.
Nehmen wir an, wir möchten, dass unser TodoApp eine Liste von TodoObject einnimmt. Aber wir wollen sie von einer "Quelle" holen.
Wir können es sogar direkt diesen neuen Komponenten höherer Ordnung auf generische Weise schreiben:
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 Diesmal nimmt die Funktion from_data_source zusätzlich zum WrappedComponent zwei Argumente vor:
prop_name : Es ist der Name der Requisite der verpackten Komponente, um einige Daten zu füllenget_source : Es ist eine Funktion, die aufgerufen wird, um die Daten zu erhalten Schauen Sie, wie wir die PropTypes aus der verpackten Komponente erbten und wie wir prop_name ausgeschlossen haben. Wir haben die Daten also nicht an unsere neue Komponente übergeben (und können) also nicht übergeben.
Und dann haben wir in render eine Requisite eingestellt, um mit dem Ergebnis eines Anrufs nach get_source an WrappedComponent zu gelangen.
Schreiben wir also eine sehr einfache Funktion (dies könnte ein kompliziertes mit Caching, Filterung sein), das die Requisiten und den Kontext annimmt und einige Daten zurückgibt:
def get_todos ( props , context ):
# here it could be a call to a database
return [
TodoObject ( "fooooo" ),
TodoObject ( "baaaar" ),
]Und wir können unsere Komponente komponieren:
SourcedTodoApp = from_data_source ( TodoApp , 'todos' , get_todos )
ThingApp = thingify ( SourcedTodoApp )Und leiten Sie es:
print ( < ThingApp / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > fooooo </ li > < li > baaaar </ li > </ ul > </ div >Es funktioniert wie erwartet und die Daten werden nur abgerufen, wenn die Komponente gerendert werden muss.
Wir haben also eine Todo -Liste, die Daten von einer externen Quelle abrufen kann. Möglicherweise möchten wir, dass die Daten je nach Benutzer unterschiedlich sind.
Was wir tun können, es ist auf der Hauptebene, bringen Sie unseren Benutzer und geben Sie ihn an jeder Komponente weiter, um sicherzustellen, dass jede Komponente den aktuellen angemeldeten Benutzer erhalten kann.
Wäre es nicht umständlich?
Das Lösen dieses Anwendungsfalls ist der genaue Zweck des von mixt bereitgestellten Context . Es ist natürlich vom Konzept des Kontextes in React inspiriert.
Und wie sie sagten:
Der Kontext soll Daten austauschen, die für einen Baum von React -Komponenten als „global“ angesehen werden können, z. B. für den aktuellen authentifizierten Benutzer, das Thema oder die bevorzugte Sprache.
Das Erstellen eines Kontexts ist so einfach wie das Erstellen einer Komponente, außer dass er von BaseContext ererbt und keine render -Methode benötigt (es wird seine Kinder rendern).
Und es dauert eine PropTypes -Klasse, die die Datenarten definiert, die der Kontext den Baum akzeptiert und weitergibt.
Lassen Sie uns also unseren Kontext erstellen, der die ID des authentifizierten Benutzers enthält.
from mixt import BaseContext
class UserContext ( BaseContext ):
class PropTypes :
authenticated_user_id : Required [ int ] Jetzt möchten wir unsere get_todos -Methode aktualisieren, um die authenticated_user_id zu berücksichtigen.
Denken Sie daran, wir haben ihm die Requisiten und den Kontext übergeben. Der Kontext wird hier nützlich sein:
def get_todos ( props , context ):
return {
1 :[
TodoObject ( "1-1" ),
TodoObject ( "1-2" ),
],
2 : [
TodoObject ( "2-1" ),
TodoObject ( "2-2" ),
]
}[ context . authenticated_user_id ]Und jetzt können wir unsere App mit dem Kontext rendern:
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 >Wir können die Todo -Einträge für den Benutzer 1 sehen.
Versuchen wir es mit dem Benutzer 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 >Wir können die Todo -Einträge für den Benutzer 2 sehen.
In diesem Fall hätten wir die Benutzer -ID natürlich als Requisite übergeben können. Stellen Sie sich jedoch vor, die Todo -App ist tief im Komponentenbaum, es ist viel einfacher, sie so zu übergeben.
Aber wie in der React -Dokumentation gesagt:
Verwenden Sie keinen Kontext, um zu vermeiden, dass Sie ein paar Level abgeben. Halten Sie sich an Fälle, in denen in vielen Komponenten auf mehreren Ebenen auf die gleichen Daten zugegriffen werden müssen.
Wenn es keinen Kontext gibt, wird das context der render -Methode auf EmptyContext und nicht auf None festgelegt. Sie können also die Methode has_prop direkt verwenden, um zu überprüfen, ob eine Requisite über den Kontext verfügbar ist.
Lassen Sie uns die Funktionen get_todos aktualisieren, um eine leere Liste von Todo -Objekten zurückzugeben, wenn kein authentifizierter Benutzer vorliegt.
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 ]Versuchen wir das:
print ( < ThingApp / > ) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < / ul > < / div >Und es funktioniert immer noch mit einem Benutzer im Kontext:
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 >Wichtiger Hinweis zu Kontexten : Sie können viele Kontexte haben! Das Definieren der gleichen Requisiten in vielen Kontexten kann jedoch zu undefiniertem Verhalten führen.
Jeder liebt ein schönes Design und vielleicht etwas Interaktion.
Es ist leicht machbar: Wir erzeugen HTML und HTML CAN einige CSS und JS.
Fügen wir zuerst einige Interaktion hinzu: Wenn Sie ein Element in das TodoForm hinzufügen, fügen wir es der Liste hinzu.
Zuerst fügen wir in unserer TodoForm -Komponente eine render_javascript -Methode hinzu, die unsere (schlechte, wir könnten es besser machen, aber es ist nicht der Punkt) 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);
}
""" )Zu Beginn zeigen wir nur den neuen Todo -Text an.
Aktualisieren Sie nun unsere render -Methode, um dieses JavaScript zurückzugeben (beachten Sie, dass die Verwendung einer render_javascript -Methode nur die trennen Bedenken besteht, sondern möglicherweise direkt in der render -Methode gewesen sein.
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 > Beachten Sie das Fragment -Tag. Es ist eine Möglichkeit, viele Elemente zu verkörpern, die zurückgegeben werden sollen, wie bei React. Es hätte eine einfache Liste sein können, aber mit Komas am Ende:
return [
< script > ... < / script > ,
< form >
...
< / form >
] Jetzt möchten wir der Liste einen Artikel hinzufügen. Es ist nicht die Rolle des TodoForm , dies zu tun, sondern für die Liste. Daher werden wir einige JS in der TodoList -Komponente hinzufügen: eine Funktion, die einen neuen Eintrag erstellt und einen neuen Eintrag erstellt.
Für TodoForm fügen wir eine render_javascript -Methode mit (noch schlechter) JavaScript hinzu:
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 )) Und wir aktualisieren unsere render -Methode, um das im JavaScript verwendete ul -Tag und eine id zu dem uldieren <script> und einer ID hinzuzufügen:
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 > Und jetzt können wir die render_javascript -Methode der TodoForm -Komponente aktualisieren, um unsere neue Funktion add_toto JavaScript zu verwenden:
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);
}
""" )Und das ist alles. Tatsächlich nichts Besonderes.
Aber werfen wir einen Blick auf die Ausgabe von OU TodoApp :
print (
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
)Die verschönerte Ausgabe ist:
< 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 > Wir haben also viele script -Tag. Es könnte großartig sein, nur einen zu haben.
mixt bietet eine Möglichkeit, Teile dessen zu "sammeln", was gerendert wird, um sie woanders zu bringen. Wir haben zwei einfache Sammler zur Verfügung, die als Komponenten verwendet werden sollen: JSCollector und CSSCollector .
Diese Komponenten sammeln Teile des Kinderbaums.
Der erste Weg ist die Verwendung des Collect -Tags.
Lassen Sie uns zunächst unseren Hauptanruf ändern:
from mixt import JSCollector
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
) Dadurch wird der Inhalt des gesamten JSCollector.Collect -Tags erfasst.
Aktualisieren wir unser TodoForm und ersetzen Sie unser script -Tag durch einen JSCollector.Collect -Tag:
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 > Wir können dasselbe mit dem TodoList tun:
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 >Lassen Sie uns nun unseren aktualisierten Code ausführen:
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
)Die verschönerte Ausgabe ist:
< 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 > Wie Sie sehen können, befinden sich alle Skripte am Ende in einem einzigen script -Tag. Genauer gesagt, am Ende des JSCollector -Tags, da wir render_position="after" verwendeten. Eine andere Möglichkeit ist render_position="before" um dies zu setzen, an dem das JSCollector -Tag gestartet wurde.
All dies funktioniert genauso für das CSSCollector -Tag, wobei der Inhalt in ein <style type="text/css> Tag eingesetzt wird.
Da die Verwendung von JS/CSS in der HTML -Welt weit verbreitet ist, haben wir etwas Zucker hinzugefügt, um all dies noch einfacher zu machen.
Wenn Sie eine render_js -Methode haben, sammelt der JSCollector automatisch das Ergebnis dieser Methode. Gleiches gilt für CSSSelector und die render_css -Methode.
Damit ist kein JSCollector.Collect -Tag erforderlich.
Um diese Arbeit in unserem Beispiel in TodoForm und TodoList zu machen:
JSCollector.Collect -TagsFragment -Tagsrender_javascript -Methoden in render_js um.html.Raw in render_js , da er nicht benötigt wird, wenn der Sammler render_js selbst aufruft: Wenn die Ausgabe eine Zeichenfolge ist, wird er als "rohe" als "roh" angesehenAuf diese Weise haben wir genau das gleiche Ergebnis.
Es funktioniert jetzt, weil wir nur eine Instanz eines Kindes mit einer render_js -Methode haben.
Aber wenn wir viele Kinder haben, wird diese Methode für jedes Kind aufgerufen. Tatsache sollte es nur Code enthalten, der für diese Instanz sehr spezifisch ist.
Um JS/CSS nur einmal für eine Komponentenklasse zu servieren, müssen wir render_js_global oder render_css_global verwenden (erwartet, dass er classmethod ist)
Es wird beim ersten Mal und erst beim ersten Mal eine Instanz gefunden, bevor die Methode render_js gesammelt wird.
Hier können wir unsere render_js in render_js_global ändern, sie mit @classmethod dekorieren und es wird immer noch gleich funktionieren.
Wir sind jetzt in der Lage, JavaScript oder Stil neu zu gruppieren. Aber was ist, wenn wir es woanders platzieren wollen, wie im head -Tag oder am Ende des body -Tags?
Es ist möglich mit Referenzen, auch bekannt als "Refs". Es ist der gleiche Kontext wie in React, ohne den DOM -Teil natürlich.
Sie erstellen einen Schiedsrichter, geben ihn an eine Komponente weiter und können ihn überall verwenden.
Lassen Sie uns dazu unseren Hauptcode aktualisieren.
Zuerst erstellen wir einen Schiedsrichter.
from mixt import Ref
js_ref = Ref () Dadurch wird ein neues Objekt erstellt, das einen Verweis auf eine Komponente enthält. In einer Komponente müssen Sie Ref nicht importieren und können js_ref = self.add_ref() verwenden, aber wir sind hier nicht in einer Komponente.
Um einen Schiedsrichter zu retten, geben wir ihn einfach an die ref Prop:
< JSCollector ref = { js_ref } > ... < / JSCollector > Beachten Sie, dass wir die render_position -Requisite entfernt haben, da wir jetzt nicht möchten, dass die JS vor oder nach dem Tag, sondern anderswo vorgestellt werden.
Verwenden Sie das current Attribut, um auf die von einem Ref verwiesene Komponente zuzugreifen:
js_collector = js_ref . currentDies kann natürlich erst nach dem Rendering erfolgen.
Wie können wir dies verwenden, um ein script -Tag in unseren head hinzuzufügen?
Aktualisieren Sie zuerst unsere HTML, um die klassischen html , head und body -Tags einzuschließen:
return str (
< html >
< head >
< / head >
< body >
< JSCollector ref = { js_ref } >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / UserContext >
< / JSCollector >
< / body >
< / html >
) Zu diesem Zeitpunkt haben wir kein script -Tag in der Ausgabe:
< 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 > Erstes zu wissen: Ein Sammler kann alle Dinge, die er gesammelt hat, durch den Kicker seiner Methode render_collected rendern.
Und wenn wir uns daran erinnern, dass es bereits das script -Tag enthält, möchten wir dies vielleicht:
# ...
< head >
{ js_ref . current . render_collected ()}
< / head >
# ...Aber das funktioniert nicht:
AttributeError : 'NoneType' object has no attribute 'render_collected'Es liegt daran, dass wir versuchen, zur Renderszeit auf den aktuellen Wert zuzugreifen. Es muss danach geschehen.
Dazu können wir eine Funktion von mixt verwenden: Wenn etwas dem Baum hinzugefügt wird, wird es nach dem Rendering aufgerufen, wenn sie in die String konvertiert werden.
So können wir zum Beispiel eine Lambda verwenden:
# ...
< head >
{ lambda : js_ref . current . render_collected ()}
< / head >
# ...Und jetzt funktioniert es:
< 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 wir haben es geschafft! Alle Hauptmerkmale von mixt erklärten. Sie können jetzt mixt in Ihren eigenen Projekten verwenden.
In einem nächsten Schritt möchten Sie möglicherweise die API -Dokumentation lesen.