Напишите HTML -компоненты непосредственно в Python, и у вас есть красивая, но противоречивая смесь.
Да, противоречиво .
Если вам это не нравится, игнорируйте это (но вы можете использовать это без части HTML-In-Python, см. Ниже;))
На основе Pyxl. Только Python 3.6+ и используйте типирование для проверки данных.
Как только у вас есть HTML, вы можете делать с ним все, что хотите. Думайте об этом как о замене для вашего классического шаблонного двигателя.
Исходный код : https://github.com/twidi/mixt/
Документация : https://twidi.github.io/mixt/
Pypi : https://pypi.org/project/mixt/
Ci (circleci): https://circleci.com/gh/twidi/workflows/mixt/
Давайте создадим файл 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" / > )И выполнить это:
$ python example.py
< div > Hello, World < /div >Если вам не нравится писать HTML в Python, вы все равно можете его использовать:
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" ))Да, это вдохновлено React (на самом деле, в основном JSX), и мы заимствуем часть концепции:
И мы добавили:
Запустите эти две команды. Второй расскажет Python, как понимать файлы с HTML внутри.
pip install mixt
mixt-post-installЧтобы проверить, что все готово, беги:
python -m mixt.examples.simpleУ вас должен быть этот вывод:
< div title =" Greeting " > Hello, World </ div > Если вы не хотите использовать материал HTML-In-Python, не запускайте mixt-post-install . А затем протестируйте с (чтобы иметь одинаковый выход):
python -m mixt.examples.simple_pure_pythonТогда клонировать проект GIT:
make devЧтобы проверить, что все готово, беги:
python -m mixt.examples.simpleУ вас должен быть этот вывод:
< div title =" Greeting " > Hello, World </ div >После того, как он сделал какой -то код:
make testsmake lint Если вы касаетесь вещей в каталоге codec , вам придется запустить make dev (или, по крайней мере, make full-clean ) для очистки файлов pyc Python.
Обратите внимание, что наш CI будет проверять, что каждый коммит проходит make lint , make tests и make check-doc . Так что не забудьте запустить их для каждого коммита.
Один из способов сделать это до того, как натолкнуться - это:
git rebase develop --exec ' git log -n 1; make checks ' Примечание. Вы можете найти окончательный код этого руководства пользователя в src/mixt/examples/user_guide (вы найдете mixt.py и pure_python.py ).
Запустить это с:
python -m mixt.examples.user_guideДавайте создадим ... Todo List, да!
Но раньше, помните. Это не реагирует, это не в браузере, и здесь нет JavaScript. Мы говорим только о рендеринге HTML.
Но вы можете сделать с этим то, что хотите. Добавьте обработчики JavaScript, простые формы ...
Говоря о формах ...
В списке Todo мы хотим иметь возможность добавить Todo. Это простой текстовый ввод.
Итак, давайте создадим наш первый компонент, TodoForm . Мы хотим форму с входным текстом и кнопкой.
Компонент - это подкласс класса Element с методом render , который вы должны написать.
# 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 >Обратите внимание, что это могло быть написано как простая функция:
# 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 >При печати компонент эти два дадут тот же результат:
print ( < TodoForm / > ) < form method =" post " action =" ??? " > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Обратите внимание, как это отформатировано: нет места между метками. На самом деле, это как в JSX:
JSX удаляет пробелы в начале и заканчивая линию. Это также удаляет пустые линии. Новые линии, примыкающие к тегам, удалены; Новые линии, которые встречаются в середине струнных литералов, сгущены в одно пространство
Чтобы добавить пространство или новую линию, вы можете пройти немного Python. Давайте, например, добавим новую линию перед этикеткой:
#...
< form method = "post" action = "???" >
{ ' n ' } < label > New Todo : < / label > < itext name = "todo" / >
#...Теперь у нас есть этот вывод:
< form method =" post " action =" /todo/add " >
< label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Теперь пойдем дальше.
Обратите внимание на атрибут action формы. Нам нужно что -то пройти. Но твердое кодирование это не звучит правильно. WWE нужно передать его компоненту.
Mixt имеет, как React , концепция свойств, он же «реквизит».
В Mixt мы определяем их с помощью типа в классе внутри нашего компонента, названного 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 > Здесь мы определили опору с именем add_url , которая должна быть строкой ( str ). Это использует синтаксис типирования Python.
И обратите внимание, как мы изменили атрибут action тега form . Теперь это {self.add_url} вместо "???" Полем
Когда атрибуты передаются между вьющимися скобками, они интерпретируются как чистый питон во время выполнения. Фактически, поскольку анализатор mixt преобразует весь файл в Pure Python, прежде чем позволить интерпретатору Python запускать его, он останется прежним, будет преобразован только HTML вокруг. Так что нет никакого штрафа, чтобы сделать это.
Посмотрите, как бы это выглядело, если бы наш компонент был написан в Pure Python:
from mixt import Element , html
class TodoForm ( Element ):
class PropTypes :
add_url : str
def render ( self , context ):
return html . Form ( method = 'post' , action = self . add_url )(
html . Label ()(
html . Raw ( "New Todo: " )
),
html . InputText ( name = 'todo' ),
html . Button ( type = 'submit' )(
html . Raw ( "Add" ) # or html.Rawhtml(text="Add")
),
) Обратите внимание, как реквизиты передаются компоненту как называемые аргументы и как передается action : action=self.add_url .
Этот чистый компонент питона также показывает вам, как он работает: реквизиты передаются в качестве аргументации в классе компонентов, затем называется этот компонент, передавая детские компоненты в качестве позиционных аргументов к вызову:
ComponentClass ( prop1 = "val1" , prop2 = "val2" )(
Children1 (),
Children2 (),
)Что такое дети? Дети - это теги внутри других тегов.
В <div id="divid"><span /><p>foo</p></div> , мы имеем:
html.Div , с id опоры и двумя детьми:html.Span , без детейhtml.P с одним ребенком:html.RawHtml компонент с текстом "foo"Обратите внимание, что вы можете играть с реквизитом и детьми. Сначала версия в Pure Python, чтобы показать, как она работает:
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) Затем версия 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> Теперь давайте вернемся к нашему реквизиту add_url .
Как передать его компоненту?
Точно так же, как мы передали атрибуты тегам HTML: на самом деле это реквизиты, определенные в компонентах HTML (определены в mixt.html ). Мы поддерживаем все теги HTML, которые во время написания являются действительными (не устаревшими) в HTML5, с их атрибутами (за исключением устаревших).
Итак, давайте сделаем это:
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 >ОК, у нас есть наша опора в визуализированной HTML.
Что если мы не пройдем строку? В PropTypes мы сказали, что нужна струна ...
Попробуем:
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 >Оно работает! Но ... это не строка !! На самом деле, есть особый случай для чисел, вы можете передать их в виде чисел вместо струн, и они преобразуются при необходимости ...
Итак, давайте попробуем что -нибудь еще.
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' > ) И то же самое, если мы передаем True в 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' > ) ОК, давайте обманом рассмотрим систему и передадим "True" как строку.
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' > )Все еще то же самое, но здесь мы прошли строку! Да, но есть 4 значения, которые всегда оцениваются на то, что они, кажется,:
None )Единственный способ передать одно из этих значений в виде строки, - это пропустить их через Python в качестве строки:
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 > За исключением этих 4 значений и чисел, каждое значение, которое передается атрибуту, считается строкой. Даже если в HTML5 нет цитат, как в HTML, где цитаты не являются обязательными для строк без некоторых символов (без пробелов, нет / ...).
Чтобы пройти что -то еще, вы должны окружить ценность в кудрявых скобках (и в этом случае нет необходимости в цитатах вокруг кудрявых скоб.
Хорошо, теперь мы уверены, что принимаем только строку .... но что, если я ничего не передам? И ... что "ничего" ничего "?
Начнем с пустой строки в Python:
print ( < TodoForm add_url = { "" } / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Хорошо, это работает, мы хотели строку, у нас есть строка.
А теперь давайте прямо передать эту пустую строку:
print ( < TodoForm add_url = "" / > ) < form method =" post " action ="" > < label > New Todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Это все еще работает, потому что это все еще строка. Давайте удалим цитаты, чтобы увидеть.
print ( < TodoForm add_url = / > ) mixt . exceptions . GeneralParserError : < mixt parser > Unclosed Tags : < TodoForm > Гум да, это недопустимо, html. Итак, давайте удалим = :
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' > ) ЧТО? Да, подумайте об атрибутах HTML5, таких как required , checked ... они должны присутствовать только в качестве атрибута без значения, чтобы считаться True . Поэтому, когда атрибут не имеет никакой ценности, это логическое, и это True .
В дополнение к тому, чтобы не пройти значение, эти два других способа действительны в HTML5 для логического к True :
required=""required="required"Для вашего удобства мы добавили другой способ:
True (случай не имеет значения), как Python или в виде строки: required=True , required={True} , required="true" И его аналог, чтобы пройти False :
False (случай не имеет значения), как Python или в виде строки: required=False , required={False} , required="false" ОК для логических атрибутов. Это не наш случай. Последнее, что мы можем сделать, это вообще не устанавливать атрибут:
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 setЭто понятно: мы стараемся получить доступ к опоре, которая не установлена, конечно, мы не можем ее использовать.
Но что, если мы не получаем к нему доступ? Если мы не печатаем компонент, он не будет отображаться:
< TodoForm / > < TodoForm at 0x7fbd18ea5630 >Таким образом, мы можем создать экземпляр, но он потерпит неудачу во время рендеринга. Но есть способ предотвратить это.
По умолчанию все свойства являются необязательными. И вам не нужно использовать Optional тип из модуля typing Python для этого, было бы громоздко сделать это для каждой опоры.
Вместо этого mixt предоставляет тип с именем, который Required , чтобы вы использовали так же, как Optionnal .
from mixt import Element , Required , html
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
def render ( self , context ):
# ...Итак, мы только что сказали, что хотим строку, и что это требуется.
Давайте попробуем еще раз создать его без опоры:
< TodoForm / > mixt . exceptions . RequiredPropError : < TodoForm > . add_url : is a required prop but is not setТеперь у нас есть исключение, поднятое ранее в нашей программе.
Чтобы увидеть другие возможности в реквизите, давайте добавим новый, чтобы изменить текстовую метку. Но мы не хотим делать это необходимым и вместо этого иметь значение по умолчанию.
Для этого это так же просто, как добавление значения к опоре в классе 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 >А теперь давайте попробуем, не пройдя опору:
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 >И если мы пройдем один:
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 >Это работает, как и ожидалось.
Обратите внимание, что вы не можете дать значение по умолчанию при наличии Required опоры. Это не имеет смысла, поэтому он проверяется как можно скорее, пока class построен:
class TodoForm ( Element ):
class PropTypes :
add_url : Required [ str ]
label : Required [ str ] = 'New Todo' mixt . exceptions . PropTypeRequiredError : < TodoForm > . label : a prop with a default value cannot be requiredИ, конечно, значение по умолчанию должно соответствовать типу!
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' > ) Еще одна вещь, которую мы хотим сделать в нашем компоненте, - это позволить ему построить этикетку, передавая его «тип» TODO, но ограничивая выбор. Для этого мы можем использовать тип 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 >Попробуем:
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 >А что, если мы попытаемся передать что -то другое, кроме доступных вариантов? Это терпит неудачу, как и ожидалось:
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' ])Но, возможно, мы не хотим передать его и использовать значение по умолчанию. Каким будет результат?
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . UnsetPropError : < TodoForm > . type : prop is not set Таким образом, мы должны отметить опору type по мере необходимости:
class PropTypes :
add_url : Required [ str ]
type : Required [ Choices ] = [ 'todo' , 'thing' ]Так что, если мы не передаем это, это терпит неудачу раньше:
print ( < TodoForm add_url = "/todo/add" / > ) mixt . exceptions . RequiredPropError : < TodoForm > . type : is a required prop but is not setНо это не то, что мы хотим, мы хотим значение по умолчанию.
На самом деле, вы заметили, что для типов, отличных от Choices , настройка значения в PropTypes дает нам значение по умолчанию. Но для Choices это отличается, так как значение - список вариантов.
Для этого у нас есть DefaultChoices : он работает так же, как Choices , но используйте первую запись в списке в качестве значения по умолчанию. И, конечно же, как и в случае с другими типами, имеющими дефолт, это не может Required .
Попробуем:
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 >Это работает, как и ожидалось.
До тех пор мы использовали простые типы, но вы можете использовать более сложные.
Так, например, мы сделаем опору add_url , чтобы принять функцию, которая будет вычислять URL для нас на основе опоры type . Но мы также хотим разрешить строки и со значением по умолчанию.
Мы можем сделать это, с напечатанием. Наша функция возьмет строку, type и вернет строку, URL.
Таким образом, синтаксис является Callable[[str], str] для вызова, и мы используем Union , чтобы принять вещи типа Callable или 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 > Во -первых, давайте попробуем без поддержки add_url , так как у нас есть по умолчанию:
print ( < TodoForm / > ) < form method =" post " action =" /todo/add " > < label > New todo: </ label > < input type =" text " name =" todo " /> < button type =" submit " > Add </ button > </ form >Это должно работать тоже, если мы передаем строку:
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 >И теперь мы можем передать функцию:
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 ])Ой? Почему? Я передал функцию, принимающую строку в качестве аргумента и возвращая строку. Да, но не забывайте, что типы проверены! Поэтому мы должны добавить типы в нашу функцию:
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 >И если мы передаем другой тип, URL должен измениться соответственно:
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 >Мы можем даже сделать эту функцию значением по умолчанию для нашей опоры:
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 >Теперь вы можете задумываться ... Типинг Python является громоздким, а проверка может убрать часть нашего драгоценного времени.
Давайте ответим на это:
По умолчанию mixt работает в «Dev-Mode». И в Dev-режиме реквизиты подтверждаются при передаче в компонент. Когда вы не находитесь в «Dev-Mode», проверка пропускается. Таким образом, в производстве вы можете деактивировать Dev-режим (посмотрим, как через минуту) и очень быстро пройти реквизит:
Choices в списке вариантовrender . Но вы можете сказать, что в производстве важна проверка. Действительно. Но, конечно, ваш код полностью покрывается тестами, которые вы запускаете в режиме Dev, и поэтому в производстве вам не нужна эта проверка! И обратите внимание, что, кстати, это так, как работает React, с NODE_ENV=production .
Как изменить Dev-Mode? Мы не применяем какую -либо переменную среды, но мы предлагаем некоторые функции. Вы должны их позвонить:
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 () Итак, давайте попробуем это с опорой type . Помните, это похоже на:
type : DefaultChoices = [ 'todo' , 'thing' ]Мы стараемся передать еще один выбор, сначала в 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' ])Это терпит неудачу, как и ожидалось.
А теперь деактивируя 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 > Это работает, у нас есть тип Todo, который не был в нашем выборе, который используется, и также находится в action . Это работа ваших тестов, чтобы убедиться, что вы никогда не проходите недействительные реквизиты, поэтому вы можете быть уверены в производстве и деактивировали Dev-режим.
Теперь у нас есть наша форма. Какие еще компоненты нам нужны для нашего приложения Todo List?
Конечно, нам нужен способ отобразить запись Todo.
Но что такое запись Todo? Давайте создадим базовый TodoObject :
class TodoObject :
def __init__ ( self , text ):
self . text = textЭто очень простой класс, но вы можете использовать то, что хотите, конечно. Это могут быть модели Django и т. Д.
Таким образом, мы можем создать наш компонент Todo , заставив его принять необходимый TodoObject в качестве Prop:
class Todo ( Element ):
class PropTypes :
todo : Required [ TodoObject ]
def render ( self , context ):
return < li > { self . todo . text } < / li >И мы можем использовать это:
todo = TodoObject ( "foo" )
print ( < Todo todo = { todo } / > ) < li > foo </ li > Теперь мы хотим иметь список Todos. Давайте создадим компонент TodoList , который примет в качестве реквизита список TodoObject .
Но то, что отличается от двух наших других компонентов, которые используют только HTML -теги в своем методе render , так это то, что теперь мы включим компонент в другой. Посмотрим, как.
class TodoList ( Element ):
class PropTypes :
todos : Required [ List [ TodoObject ]]
def render ( self , context ):
return < ul > {[ < Todo todo = { todo } / > for todo in self . todos ]} < / ul > Да, это так просто: вы используете <Todo...> для компонента Todo , как вы бы использовали тег HTML. Единственное отличие состоит в том, что для тегов HTML вам не нужно импортировать их напрямую (простой импорт html из mixt ), и согласно соглашению мы пишем их в более низком уровне. Для обычных компонентов вы должны импортировать их (вы все еще можете сделать from mylib import components и <components.MyComponent ...> ) и использовать точный случай.
Обратите внимание, как нам потребовался список, и передали его в <ul> через понимание списка в кудрявых красах.
Вы можете делать что -то по -другому, если хотите.
Как отделение понимания списка от HTML:
def render ( self , context ):
todos = [
< Todo todo = { todo } / >
for todo
in self . todos
]
return < ul > { todos } < / ul >Или в специальном методе (который будет полезен для тестирования):
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 >Это зависит от вас: в конце концов это просто питон.
Посмотрим, что представлено этим компонентом:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print ( < TodoList todos = { todos } / > ) < ul > < li > foo </ li > < li > bar </ li > < li > baz </ li > </ ul > И, наконец, у нас есть наш компонент TodoApp , который инкапсулирует форму и список:
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 >Давайте передадим этот HTML к HTML Beautifier:
< 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 > И вот и все, у нас есть приложение Todo-List! Чтобы использовать его на странице, просто создайте компонент, который сделает наценку HTML и интегрирует в него компонент TodoApp . Вам даже не нужен компонент:
todos = [ TodoObject ( "foo" ), TodoObject ( "bar" ), TodoObject ( "baz" )]
print (
< html >
< body >
< TodoApp todos = { todos } type = "thing" / >
< / body >
< / html >
)Прекрасный выход был:
< 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 >У нас есть общий список TODO, но после доступных типов TODO мы можем захотеть иметь «List Todo-List» и «List».
У нас уже есть список TODO, потому что у нашего TodoApp есть тип todo по умолчанию.
Итак, давайте создадим ThingApp .
Первый способ сделать это - унаследовать от нашего TodoApp . Но, наследуя, мы не можем удалить реквизиты от родителя (это не совсем так, мы увидим это позже), поэтому у нас все еще есть type по умолчанию. Но мы не хотим принимать ничего, кроме «вещь». Таким образом, мы можем переопределить type опоры, как это:
class ThingApp ( TodoApp ):
class PropTypes :
type : DefaultChoices = [ '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 > Если мы попытаемся передать «Todo» для реквизита type , это не сработает:
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' ])Но все же странно иметь возможность пройти тип.
Давайте попробуем другой способ: родительский компонент. Компонент, который не делает ничего другого, что делает что -то со своими детьми и возвращает его. Что мы хотим здесь, это компонент, который вернет TodoApp с опорой type , вынужденной «вещь».
Давайте сделаем это
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 > Это работает, и на этот раз мы не можем передать опору type :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) mixt . exceptions . InvalidPropNameError : < ThingApp > . type : is not an allowed prop Обратите внимание, как мы должны были определить тип для реквизита todos . И в TodoApp , и TodoThing .
Есть много способов справиться с этим.
Первым было бы игнорировать тип в ThingApp потому что он будет зарегистрирован в TodoApp . Итак, мы будем использовать тип Any :
from typing import Any
#...
class ThingApp ( Element ):
class PropTypes :
todos : Any
#...Попробуем с действительным списком Todos:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Но что, если мы пройдем что -то еще?
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 ]) Он работает, как и ожидалось, но ошибка сообщается на уровне TodoApp , что совершенно нормально.
Другим способом было бы определить тип на более высоком уровне:
TodoObjects = Required [ List [ TodoObject ]]
class TodoApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...
class ThingApp ( Element ):
class PropTypes :
todos : TodoObjects
# ...Теперь, если мы передаем что -то еще, у нас появится ошибка на правильном уровне:
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 ]) Но если вы не можете или не хотите этого делать, вы можете сохранить тип, определенный в TodoApp et, использовать метод класса prop_type компонента, чтобы получить тип опоры:
class ThingApp ( Element ):
class PropTypes :
todos : TodoApp . prop_type ( "todos" )
# ... Но действительно ли важно, чтобы ошибка была вызвана для ThingApp или TodoApp ? Потому что в конце концов, это действительно TodoApp , который должен сделать чек.
Так что это должен быть способ сделать это более общим способом ..
Ранее мы видели, что компонент может быть единственной функцией для визуализации компонента. Это просто нужно вернуть компонент, HTML -тег. Одно отличие с компонентами класса заключается в том, что не существует PropTypes , поэтому нет валидации. Но ... это именно то, что нам нужно.
Мы хотим, чтобы наша ThingApp приняла некоторые реквизиты (опора todos ) и возвращал TodoApp с определенной type .
Итак, мы могли бы сделать:
def ThingApp ( todos ):
return < TodoApp type = "thing" todos = { todos } / > Здесь мы видим, что мы не можем передать type ThingsApp , это не является достоверным аргументом.
Попробуем:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div > Здесь у нас есть только одна опора, так что это легко. Но представьте, если у нас есть много. Мы можем использовать синтаксис {**props} :
def ThingApp ( ** props ):
return < TodoApp type = "thing" { ** props } / >И вы можете сделать с еще меньшим количеством символов (если это имеет значение):
ThingApp = lambda ** props : < TodoApp type = "thing" { ** props } / >Эти два фонации ведут себя точно так же.
И вы не можете передать опору type , потому что это будет ошибка Python, так как она будет передана дважды TodoApp :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) TypeError : BaseMetaclass object got multiple values for keyword argument 'type' (Да, он говорит о BaseMetaclass , который является Metaclass, который создает наши классы компонентов)
И любые другие неправильные реквизиты будут подтверждены TodoApp :
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} foo = "bar" / > ) mixt . exceptions . InvalidPropNameError : < TodoApp > . foo : is not an allowed prop Имея это в виду, мы могли бы создать общий фонрак, который заставляет тип любого компонента, принимающего 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 > Визуализированный компонент - это TodoApp , опора type - это «вещь», а другой реквизит (здесь только todos ) правильно переданы.
Теперь расширяйте эту концепцию до более общего случая: «компоненты высшего порядка». В React «компонент высокого порядка» - это «функция, которая принимает компонент и возвращает новый компонент».
Идея:
EnhancedComponent = higherOrderComponent ( WrappedComponent )Классический способ сделать это - вернуть новый класс компонентов:
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 Обратите внимание на то, как мы установили класс PropTypes , чтобы унаследовать от одного из обернутых компонентов, и как мы передаем все реквизиты обернутому компоненту вместе с детьми. С возвращенным компонентом примет те же реквизиты с теми же типами, что и обернутый.
А также обратите внимание на __display_name__ . Он будет использоваться в исключениях, чтобы позволить вам теперь компонент, который его поднял. Здесь, не заставляя его, он был бы настроен на HOC , что не полезно. Вместо этого мы указываем, что это преобразованная версия пропущенного компонента.
Вот функция, которая не делает ничего полезного.
В нашем примере мы могли бы сделать это:
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 HOCДве важные вещи здесь:
__exclude__ = {'type'} чтобы удалить опору type из тех, которые мы наследуем от WrappedComponent.PropTypes . Таким образом, возвращенный компонент ожидает того же реквизита, что и обернутый, за исключением type .{self.children()} в обернутый компонент, потому что даже если мы на самом деле знаем, что компонент, который мы TodoApp , не забирает детей (это может, но это не делает с ними ничего), мы не можем заранее сказать, что это всегда будет иметь место, а также, что этот компонент высшего порядка не будет использован для обмотана другого компонента, чем TodoApp . Так что лучше это делать. А теперь мы можем создать нашу ThingApp :
ThingApp = thingify ( TodoApp )И используйте это:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > foo </ li > </ ul > </ div >Если мы попытаемся передать тип:
print ( < ThingApp todos = {[ TodoObject ( "foo" )]} type = "thing" / > ) mixt . exceptions . InvalidPropNameError : < thingify ( TodoApp ) > . type : is not an allowed prop Таким образом, как и планировалось, мы не можем передать тип. И обратите внимание, как используется __display_name__ .
Давайте подумаем о том, насколько это мощно.
Давайте скажем, что мы хотим, чтобы наш TodoApp взял список TodoObject . Но мы хотим получить их из «источника».
Мы даже можем напрямую написать это новым компонентом высшего порядка общим способом:
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 На этот раз функция from_data_source принимает два аргумента в дополнение к WrappedComponent :
prop_name : это имя опоры обернутого компонента, чтобы заполнить некоторые данныеget_source : Это функция, которая будет вызвана для получения данных Посмотрите, как мы унаследовали PropTypes от обернутого компонента и как мы исключили prop_name . Таким образом, мы не можем (и не можем) передавать данные нашему новому компоненту.
А затем в render мы установили опору для перехода к WrappedComponent , с результатом вызова get_source .
Итак, давайте напишем очень простую функцию (это может быть сложной с кэшированием, фильтрацией ...), которая принимает реквизит и контекст, и возвращает некоторые данные:
def get_todos ( props , context ):
# here it could be a call to a database
return [
TodoObject ( "fooooo" ),
TodoObject ( "baaaar" ),
]И мы можем составить наш компонент:
SourcedTodoApp = from_data_source ( TodoApp , 'todos' , get_todos )
ThingApp = thingify ( SourcedTodoApp )И запустить это:
print ( < ThingApp / > ) < div > < h1 > The "thing" list </ h1 > < form > ... </ form > < ul > < li > fooooo </ li > < li > baaaar </ li > </ ul > </ div >Он работает, как и ожидалось, и данные извлекаются только тогда, когда компонент должен быть отображен.
Итак, у нас есть список TODO, который может извлекать данные из внешнего источника. Но мы можем хотеть, чтобы данные были разными в зависимости от пользователя.
То, что мы можем сделать, это на основном уровне, получить нашего пользователя и передавать его на каждом компоненте, чтобы быть уверенным, что каждый компонент может получить текущий зарегистрированный пользователя.
Разве это не было бы громоздко?
Решение этого варианта использования является точной целью Context концепции, предоставленной mixt . Это, конечно, вдохновлен концепцией контекста в React.
И как они сказали:
Контекст предназначен для обмена данными, которые можно считать «глобальными» для дерева компонентов React, таких как текущий аутентифицированный пользователь, тема или предпочтительный язык.
Создание контекста так же просто, как создание компонента, за исключением того, что он будет наследовать от BaseContext и не нуждается в методе render (он приведет своих детей).
И он занимает класс PropTypes , который определяет типы данных, которые контекст примет и передаст дерево.
Итак, давайте создадим наш контекст, который будет удерживать идентификатор аутентифицированного пользователя.
from mixt import BaseContext
class UserContext ( BaseContext ):
class PropTypes :
authenticated_user_id : Required [ int ] Теперь мы хотим обновить наш метод get_todos , чтобы принять во внимание authenticated_user_id .
Помните, мы передали это реквизит и контекст. Контекст будет полезен здесь:
def get_todos ( props , context ):
return {
1 :[
TodoObject ( "1-1" ),
TodoObject ( "1-2" ),
],
2 : [
TodoObject ( "2-1" ),
TodoObject ( "2-2" ),
]
}[ context . authenticated_user_id ]И теперь мы можем представить наше приложение в контексте:
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 >Мы можем увидеть записи TODO для пользователя 1.
Попробуем с пользователем 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 >Мы можем увидеть записи TODO для пользователя 2.
В этом случае, конечно, мы могли бы пройти идентификатор пользователя в качестве опоры. Но представьте, что приложение Todo глубоко находится в дереве компонентов, гораздо проще передать его таким образом.
Но, как сказано в документации React:
Не используйте контекст, чтобы избежать прохождения реквизита на несколько уровней. Придерживайтесь случаев, когда одни и те же данные необходимо получить во многих компонентах на нескольких уровнях.
Когда нет контекста, context -аргумент метода render устанавливается на EmptyContext , а не к None . Таким образом, вы можете напрямую использовать метод has_prop , чтобы проверить, доступна ли предложение через контекст.
Давайте обновим функции get_todos , чтобы вернуть пустой список объектов TODO, если пользователь не существует.
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 ]Попробуем это:
print ( < ThingApp / > ) < div > < h1 > The "thing" list < / h1 > < form > ... < / form > < ul > < / ul > < / div >И это все еще работает с пользователем в контексте:
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 >Важное примечание о контекстах : у вас может быть много контекстов! Но определение той же опоры во многих контекстах может привести к неопределенному поведению.
Все любят красивый дизайн и, возможно, какое -то взаимодействие.
Это легко выполнить: мы генерируем HTML и HTML, содержащий некоторые CSS и JS.
Давайте сначала добавим некоторое взаимодействие: при добавлении элемента в TodoForm давайте добавим его в список.
Сначала мы добавим в наш компонент TodoForm метод render_javascript , который будет размещать наш (плохо, мы могли бы добиться большего, но это не значит) 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);
}
""" )Чтобы начать, мы отображаем только новый текст Todo.
Теперь обновите наш метод render , чтобы вернуть этот JavaScript (обратите внимание, что использование метода render_javascript заключается только для разделения проблем, он мог быть непосредственно в методе 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 > Обратите внимание на тег Fragment . Это способ инкапсулировать многие элементы, которые будут возвращены, как в React. Это мог быть простой список, но с комами в конце:
return [
< script > ... < / script > ,
< form >
...
< / form >
] Теперь мы хотим добавить элемент в список. Это не роль TodoForm сделать это, а в списке. Итак, мы добавим немного JS в компонент TodoList : функция, которая занимает немного текста и создает новую запись.
Что касается TodoForm , мы добавляем метод render_javascript с (все еще плохим) 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 )) И мы обновляем наш метод render , чтобы добавить тег <script> и id в тег ul , используемый в 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 > И теперь мы можем обновить метод render_javascript компонента TodoForm , чтобы использовать нашу новую функцию add_toto JavaScript:
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);
}
""" )И это все. На самом деле ничего особенного.
Но давайте посмотрим на вывод OU TodoApp :
print (
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
)Прекрасный выход:
< 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 > Итак, у нас есть много тега script . Было бы здорово иметь только один.
mixt поставляется с способом «собрать» части того, что приведет, чтобы поместить их в другое место. У нас есть в нашем распоряжении два простых коллекционера, которые будут использоваться в качестве компонентов: JSCollector и CSSCollector .
Эти компоненты собирают части их детского дерева.
Первый путь - это использование тега Collector Collect .
Сначала давайте изменим наш главный звонок:
from mixt import JSCollector
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
) Это будет собирать содержание всех тегов JSCollector.Collect .
Давайте обновим наш TodoForm и заменим нашу тег script на тег 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 > Мы можем сделать то же самое с 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 >Теперь давайте запустим наш обновленный код:
print (
< JSCollector render_position = "after" >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / User >
< / JSCollector >
)Прекрасный выход:
< 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 > Как вы можете видеть, все сценарии находятся в одном теге script , в конце. Точнее, в конце того, где был тег JSCollector , потому что мы использовали render_position="after" . Другая возможность - render_position="before" чтобы поместить это туда, где начался тег JSCollector .
Все это работает точно так же для тега CSSCollector , где контент помещается в тег <style type="text/css> .
Поскольку использование JS/CSS довольно распространено в мире HTML, мы добавили немного сахара, чтобы сделать все это еще проще.
Если у вас есть метод render_js , JSCollector автоматически собирает результат этого метода. То же самое для CSSSelector и метода render_css .
При этом нет необходимости в JSCollector.Collect Tag.
Чтобы сделать эту работу в нашем примере, в TodoForm и TodoList :
JSCollector.Collect TagsFragmentrender_javascript для render_js .html.Raw в render_js , так как он не требуется, когда коллекционер вызывает сами render_js : если вывод является строкой, он считается «необработанным».Таким образом, у нас есть точно такой же результат.
Это работает сейчас, потому что у нас есть только один экземпляр ребенка с методом render_js .
Но если у нас много детей, этот метод будет вызван для каждого ребенка. Если факт, он должен содержать только код, который очень специфичен для этого экземпляра.
Чтобы обслуживать JS/CSS только один раз для класса компонентов, мы должны использовать render_js_global или render_css_global (ожидается, что он будет classmethod )
Он будет собран в первый раз, и только в первый раз, когда будет найден экземпляр, прежде чем собирать метод render_js .
Итак, здесь мы можем изменить наш render_js на render_js_global , украсить их @classmethod , и он все равно будет работать так же.
Теперь мы можем перегруппировать JavaScript или стиль. Но что, если мы хотим поместить его в другом месте, как в head или в конце тега body ?
Это возможно со ссылками, так как «Refs». Это тот же контекст, что и в React, без части DOM, конечно.
Вы создаете рефери, передаете его компоненту, и вы можете использовать его в любом месте.
Давайте обновим наш основной код для этого.
Сначала мы создаем рефери.
from mixt import Ref
js_ref = Ref () Это создаст новый объект, который содержит ссылку на компонент. В компоненте вам не нужно импортировать Ref и вы можете использовать js_ref = self.add_ref() , но мы здесь не в компоненте.
Чтобы сохранить рефери, мы просто передаем его ref -опоре:
< JSCollector ref = { js_ref } > ... < / JSCollector > Обратите внимание, что мы удалили опору render_position , потому что теперь мы не хотим, чтобы JS был размещен до или после тега, но в других местах.
Чтобы получить доступ к компоненту, на который ссылается рефери, используйте current атрибут:
js_collector = js_ref . currentКонечно, это можно сделать только после рендеринга.
Как мы можем использовать это, чтобы добавить тег script в нашу head .
Сначала обновите наш HTML, чтобы включить классические теги html , head и body :
return str (
< html >
< head >
< / head >
< body >
< JSCollector ref = { js_ref } >
< UserContext authenticated_user_id = 1 >
< ThingApp / >
< / UserContext >
< / JSCollector >
< / body >
< / html >
) На этом этапе у нас нет тега script в выводе:
< 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 > Первое, что нужно знать: коллекционер способен отображать все вещи, которые он собирал, призвав его метод render_collected .
И помнить, что он уже включает в себя тег script , мы можем сделать это:
# ...
< head >
{ js_ref . current . render_collected ()}
< / head >
# ...Но это не работает:
AttributeError : 'NoneType' object has no attribute 'render_collected'Это потому, что мы пытаемся получить доступ к текущему значению во время рендеринга. Это должно быть сделано после.
Для этого мы можем использовать функцию mixt : если что -то добавленное в дерево является вызовом, она будет вызвана после рендеринга при преобразовании в строку.
Итак, мы можем использовать, например, лямбда:
# ...
< head >
{ lambda : js_ref . current . render_collected ()}
< / head >
# ...А теперь это работает:
< 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 > Ура, мы сделали это! Все основные особенности mixt объяснены. Теперь вы можете использовать mixt в своих собственных проектах.
В качестве следующего шага вы можете прочитать документацию API.