[Разработка веб-сайтов, Программирование, Haskell, Функциональное программирование] Создаем веб-приложение на Haskell с использованием Reflex. Часть 2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Часть 1.
Всем привет! Продолжаем серию туториалов по разработке веб-приложения на Reflex.
В этой части мы добавим возможность выполнять различные манипуляции со списком задач.
Действия над Todo
Добавим возможность отмечать задания выполненными, а также редактировать и удалять их.
В первую очередь для этого расширим тип Todo, добавив состояние. В случае, если задание не выполнено, то его можно редактировать.
data TodoState
= TodoDone
| TodoActive { stateEdit :: Bool }
deriving (Generic, Eq, Show)
data Todo = Todo
{ todoText :: Text
, todoState :: TodoState }
deriving (Generic, Eq, Show)
newTodo :: Text -> Todo
newTodo todoText = Todo { todoState = TodoActive False, .. }
Далее определим события, происходящие в системе. В рабочих проектах мы использовали два подхода для этого. Первый — это перечислить все возможные события как отдельные конструкторы и реализовать функцию-обработчик, которая будет обновлять состояние в зависимости от произошедшего события.
data TodoEvent
= NewTodo Todo
| ToggleTodo Int
| StartEditTodo Int
| FinishEditTodo (Text,Int)
| DeleteTodo Int
deriving (Generic, Eq, Show)
Плюсами такого подхода является возможность увидеть, какое конкретное событие происходит в системе, и обновление, которое оно несет в себе (все это позволяет сделать функция traceEvent). Но эти преимущества не всегда получается использовать, особенно когда событие несет в себе очень много данных, которые, в итоге, сложно проанализировать. Если все же необходимо увидеть изменение значений, то события, в любом случае, изменяют Dynamic, значение которого также можно отследить при помощи функции traceDyn.
Другой подход — использование функций обновления, которые представляются в виде моноида Endo (грубо говоря, это абстрактное название функции, у которой совпадает тип аргумента и результата). Суть этого подхода в том, что значением, которое несёт событие обновления, является функция, которая и задаёт саму логику обновления. В данном случае, мы теряем возможность вывести значение события (которая, как выяснилось, не всегда полезна), но очевидным плюсом является то, что нам нет необходимости иметь доступ к текущему состоянию, создавать тип со всеми возможными событиями (которых может быть очень много), а также определять отдельный обработчик, который будет обновлять состояние в соответствии с полученным событием.
В данном туториале мы будем использовать второй подход.
Изменим структуру корневого виджета:
rootWidget :: MonadWidget t m => m ()
rootWidget =
divClass "container" $ do
elClass "h2" "text-center mt-3" $ text "Todos"
newTodoEv <- newTodoForm
rec
todosDyn <- foldDyn appEndo mempty $ leftmost [newTodoEv, todoEv]
delimiter
todoEv <- todoListWidget todosDyn
blank
Первое, что мы тут видим — это использование расширения RecursiveDo (его, соответственно надо включить). Это один из распространенных приёмов в разработке reflex приложений, т.к. очень часто возникают ситуации, когда событие, происходящее в нижней части страницы, влияет на элементы на верху страницы. В данном случае событие todoEv используется в определении todosDyn, а todosDyn в свою очередь является аргументом для виджета, из которого приходит событие todoEv.
Далее мы видим обновление параметров функции foldDyn. Здесь есть использование новой функции leftmost. Она принимает список событий и возвращает событие, которое срабатывает в тот момент, когда сработает любое из событий в списке. Если в данный момент срабатывает два события из списка, то будет возвращено то, что стоит левее в списке (отсюда и название). Также список заданий теперь не список, а IntMap (для упрощения будем использовать type Todos = IntMap Todo). В первую очередь это сделано для того, чтобы мы могли напрямую обращаться к какому-либо элементу по идентификатору. Для обновления списка используется appEndo. В случае, если бы мы определили каждое событие как отдельный конструктор, то нам бы пришлось также определить функцию-обработчик, которая выглядела бы примерно так:
updateTodo :: TodoEvent -> Todos -> Todos
updateTodo ev todos = case ev of
NewTodo todo -> nextKey todos =: todo <> todos
ToggleTodo ix -> update (Just . toggleTodo) ix todos
StartEditTodo ix -> update (Just . startEdit) ix todos
FinishEditTodo (v, ix) -> update (Just . finishEdit v) ix todos
DeleteTodo ix -> delete ix todos
Несмотря на отсутствие необходимости определять эту функцию, тут используются несколько других вспомогательных функций, которые все равно понадобятся нам в будущем.
startEdit :: Todo -> Todo
startEdit todo = todo { todoState = TodoActive True }
finishEdit :: Text -> Todo -> Todo
finishEdit val todo = todo
{ todoState = TodoActive False, todoText = val }
toggleTodo :: Todo -> Todo
toggleTodo Todo{..} = Todo {todoState = toggleState todoState,..}
where
toggleState = \case
TodoDone -> TodoActive False
TodoActive _ -> TodoDone
nextKey :: IntMap Todo -> Int
nextKey = maybe 0 (succ . fst . fst) . maxViewWithKey
Изменилась и функция добавления нового элемента, теперь она возвращает событие, а не само задание. Также добавим очистку поля после добавления нового задания.
newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))
newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo
iEl <- inputElement $ def
& initialAttributes .~
( "type" =: "text"
<> "class" =: "form-control"
<> "placeholder" =: "Todo" )
& inputElementConfig_setValue .~ ("" <$ btnEv)
let
addNewTodo = \todo -> Endo $ \todos ->
insert (nextKey todos) (newTodo todo) todos
newTodoDyn = addNewTodo <$> value iEl
btnAttr = "class" =: "btn btn-outline-secondary"
<> "type" =: "button"
(btnEl, _) <- divClass "input-group-append" $
elAttr' "button" btnAttr $ text "Add new entry"
let btnEv = domEvent Click btnEl
pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
Функция todoListWidget теперь возвращает изменение списка, и она так же подверглась небольшим изменениям:
todoListWidget :: MonadWidget t m => Dynamic t Todos -> m (Event t (Endo Todos))
todoListWidget todosDyn = rowWrapper $ do
evs <- listWithKey
(M.fromAscList . IM.toAscList <$> todosDyn) todoWidget
pure $ switchDyn $ leftmost . M.elems <$> evs
Первое, что замечаем, это замена функции simpleList функцией listWithKey. Отличаются они типом первого параметра — первая функция принимает список [], вторая — Map. Список будет выведен в отсортированным по ключу порядке. Здесь важно возвращаемое значение. Каждое задание возвращает событие (удаление, изменение). В конкретно нашем случае функция listWithKey будет иметь следующий тип:
listWithKey
:: MonadWidget t m
=> Dynamic t (Map Int Todo)
-> (Int -> Dynamic t Todo -> m (Event t TodoEvent))
-> m (Dynamic t (Map Int TodoEvent))
Примечание: эта функция является частью пакета reflex и имеет более сложный тип. Здесь уже показан специализированный тип.
Здесь используется знакомая уже функция leftmost для всех значений из Map. Выражение leftmost . elems <$> evs имеет следующий тип: Dynamic t (Event t TodoEvent). Для того чтобы извлечь Event из Dynamic, используется функция switchDyn. Она работает следующим образом: возвращает событие, которое срабатывает тогда, когда срабатывает внутреннее событие. В том случае, если Dynamic и Event срабатывают одновременно, будет возвращен старый Event, до момента обновления события в Dynamic. Существует функция switchPromtlyDyn, которая работает иначе: если одновременно происходит обновление Dynamic, срабатывает событие, которое было до обновления Dynamic, и "стреляет" событие, которые теперь содержит Dynamic, то будет возвращено именно новое событие, которое теперь содержит Dynamic. Если такая ситуация является невозможной, то всегда лучше предпочитать использовать switchDyn, т.к. функция switchPromtlyDyn является более сложной и выполняет дополнительные действий, и, более того, может привести к циклам.
У задания появились разные состояния, поэтому функция отображения одного задания также претерпела изменения:
todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do
todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
TodoDone -> todoDone ix todoText
TodoActive False -> todoActive ix todoText
TodoActive True -> todoEditable ix todoText
switchHold never todoEvEv
Здесь мы используем новую функцию dyn. Она имеет следующий тип:
dyn
:: (Adjustable t m, NotReady t m, PostBuild t m)
=> Dynamic t (m a)
-> m (Event t a)
В качестве входного параметра она получает виджет, завернутый Dynamic. Это означает, что при каждом обновлении Dynamic, будет также обновляться DOM. Выходным значением является событие, которое несет значение, возвращаемое виджетом. Специализированный тип для нашего случая будет выглядеть следующим образом:
dyn
:: MonadWidget t m
=> Dynamic t (m (Event t (Endo Todos)))
-> m (Event t (Event t (Endo Todos)))
Тут мы встречаемся с событием, вложенным в другое событие. Есть две функции из пакета reflex, которые могут работать с таким типом: coincedence и switchHold. Первая функция возвращает событие, которое будет срабатывать только тогда, когда срабатывают одновременно внешнее и внутреннее события. Это не наш случай. Функция switchHold имеет следующий тип:
switchHold :: (Reflex t, MonadHold t m) => Event t a -> Event t (Event t a) -> m (Event t)
Эта функция переключается на новое событие каждый раз, когда срабатывает внешнее событие. До первого срабатывания внешнего события, будет стрелять событие, переданное первым параметром. Именно так мы и используем эту функцию в нашем случае. До какого-либо первого изменения списка, оттуда не может прийти никакое событие, и мы используем событие never. Это специальное событие, которое, в соответствии с названием, не срабатывает никогда.
Функция todoWidget использует разные виджеты для разных состояний.
todoActive :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoActive ix todoText = divClass "d-flex border-bottom" $ do
divClass "p-2 flex-grow-1 my-auto" $
text todoText
divClass "p-2 btn-group" $ do
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Done"
(editEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Edit"
(delEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Drop"
pure $ Endo <$> leftmost
[ update (Just . toggleTodo) ix <$ domEvent Click doneEl
, update (Just . startEdit) ix <$ domEvent Click editEl
, delete ix <$ domEvent Click delEl
]
todoDone :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoDone ix todoText = divClass "d-flex border-bottom" $ do
divClass "p-2 flex-grow-1 my-auto" $
el "del" $ text todoText
divClass "p-2 btn-group" $ do
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Undo"
(delEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Drop"
pure $ Endo <$> leftmost
[ update (Just . toggleTodo) ix <$ domEvent Click doneEl
, delete ix <$ domEvent Click delEl
]
todoEditable :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoEditable ix todoText = divClass "d-flex border-bottom" $ do
updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $
editTodoForm todoText
divClass "p-2 btn-group" $ do
(doneEl, _) <- elAttr' "button"
( "class" =: "btn btn-outline-secondary"
<> "type" =: "button" ) $ text "Finish edit"
let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix
pure $
tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)
editTodoForm :: MonadWidget t m => Text -> m (Dynamic t Text)
editTodoForm todo = do
editIEl <- inputElement $ def
& initialAttributes .~
( "type" =: "text"
<> "class" =: "form-control"
<> "placeholder" =: "Todo")
& inputElementConfig_initialValue .~ todo
pure $ value editIEl
Все использованные тут функции были рассмотрены раньше, поэтому не будем углубляться в объяснение каждой в отдельности.
Оптимизация
Вернемся к функции listWithKey:
listWithKey
:: MonadWidget t m
=> Dynamic t (Map Int Todo)
-> (Int -> Dynamic t Todo -> m (Event t TodoEvent))
-> m (Dynamic t (Map Int TodoEvent))
Функция работает таким образом, что любое обновления переданного Dynamic спровоцирует обновление каждого отдельного элемента. Даже если мы, например, изменили один элемент, это обновление будет передано в каждый элемент, хоть он и не изменит значения. Теперь вернемся к функции todoWidget.
todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do
todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
TodoDone -> todoDone ix todoText
TodoActive False -> todoActive ix todoText
TodoActive True -> todoEditable ix todoText
switchHold never todoEvEv
Если вспомним, как работает функция dyn, то она обновляет DOM каждый раз, когда происходит обновление todoDyn. Учитывая, что при изменении одного элемента из списка, это обновление передается на каждый элемент в отдельности, получаем, что весь участок DOM, который выводит наши задания, будет перестраиваться (проверить это можно при помощи панели разработчика в браузере). Это явно не то, что мы хотим. Тут на помощь приходит функция holdUniqDyn.
todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t TodoEvent)
todoWidget ix todoDyn' = do
todoDyn <- holdUniqDyn todoDyn'
todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
TodoDone -> todoDone ix td
TodoActive False -> todoActive ix td
TodoActive True -> todoEditable ix td
switchHold never todoEvEv
Мы добавили строку todoDyn <- holdUniqDyn todoDyn'. Что тут происходит? Дело в том, что хоть Dynamic и "выстреливает", значение, которое он содержит, не меняется. Функция holdUniqDyn работает именно так, что если переданный ей Dynamic "выстрелил" и не изменил свое значение, то выходной Dynamic не будет "выстреливать", и, соответственно, в нашем случае не будет лишних перестроек DOM.
Полученный результат можно посмотреть в нашем репозитории.
В следующей части рассмотрим другой способ управления событиями и использование библиотеки GHCJS-DOM.
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование] Новые направления развитии ИТ отрасли 2021
- [Ненормальное программирование, Программирование, Софт] Я пользуюсь Excel, чтобы писать код (перевод)
- [Программирование, C++, Git, Qt] QGit, улучшения
- [Программирование, Анализ и проектирование систем, Проектирование и рефакторинг, Микросервисы] Разложение монолита: Декомпозиция БД (часть 2)
- [Программирование, Assembler] Преобразование строки символов в число двойной точности (double)
- [Настройка Linux, Программирование, C, Rust, Разработка под Linux] Линус Торвальдс рассказал о том, где Rust впишется в Linux
- [FPGA, Старое железо, Игры и игровые приставки] Прикоснемся к магии или как я вступил в ряды MISTического общества
- [Open source, Программирование, Системное программирование, Компиляторы, Rust] Rust 1.51.0: const generics MVP, новый распознаватель функциональности Cargo (перевод)
- [Программирование, Робототехника, Интернет вещей] 10 плат для начала разработки IoT в 2021г (перевод)
- [Разработка веб-сайтов, Серверная оптимизация, Сетевые технологии, Серверное администрирование] Где поместить свой сервер, чтобы обеспечить максимальную скорость? Насколько это важно? (перевод)
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_programmirovanie (Программирование), #_haskell, #_funktsionalnoe_programmirovanie (Функциональное программирование), #_haskell, #_frp, #_reflex, #_reflexplatform, #_reflexfrp, #_vebrazrabotka (веб-разработка), #_funktsionalnoe_programmirovanie (функциональное программирование), #_fp (фп), #_frp (фрп), #_fp, #_blog_kompanii_typeable (
Блог компании Typeable
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_programmirovanie (
Программирование
), #_haskell, #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:47
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Часть 1. Всем привет! Продолжаем серию туториалов по разработке веб-приложения на Reflex. В этой части мы добавим возможность выполнять различные манипуляции со списком задач. Действия над Todo Добавим возможность отмечать задания выполненными, а также редактировать и удалять их. В первую очередь для этого расширим тип Todo, добавив состояние. В случае, если задание не выполнено, то его можно редактировать. data TodoState
= TodoDone | TodoActive { stateEdit :: Bool } deriving (Generic, Eq, Show) data Todo = Todo { todoText :: Text , todoState :: TodoState } deriving (Generic, Eq, Show) newTodo :: Text -> Todo newTodo todoText = Todo { todoState = TodoActive False, .. } Далее определим события, происходящие в системе. В рабочих проектах мы использовали два подхода для этого. Первый — это перечислить все возможные события как отдельные конструкторы и реализовать функцию-обработчик, которая будет обновлять состояние в зависимости от произошедшего события. data TodoEvent
= NewTodo Todo | ToggleTodo Int | StartEditTodo Int | FinishEditTodo (Text,Int) | DeleteTodo Int deriving (Generic, Eq, Show) Плюсами такого подхода является возможность увидеть, какое конкретное событие происходит в системе, и обновление, которое оно несет в себе (все это позволяет сделать функция traceEvent). Но эти преимущества не всегда получается использовать, особенно когда событие несет в себе очень много данных, которые, в итоге, сложно проанализировать. Если все же необходимо увидеть изменение значений, то события, в любом случае, изменяют Dynamic, значение которого также можно отследить при помощи функции traceDyn. Другой подход — использование функций обновления, которые представляются в виде моноида Endo (грубо говоря, это абстрактное название функции, у которой совпадает тип аргумента и результата). Суть этого подхода в том, что значением, которое несёт событие обновления, является функция, которая и задаёт саму логику обновления. В данном случае, мы теряем возможность вывести значение события (которая, как выяснилось, не всегда полезна), но очевидным плюсом является то, что нам нет необходимости иметь доступ к текущему состоянию, создавать тип со всеми возможными событиями (которых может быть очень много), а также определять отдельный обработчик, который будет обновлять состояние в соответствии с полученным событием. В данном туториале мы будем использовать второй подход. Изменим структуру корневого виджета: rootWidget :: MonadWidget t m => m ()
rootWidget = divClass "container" $ do elClass "h2" "text-center mt-3" $ text "Todos" newTodoEv <- newTodoForm rec todosDyn <- foldDyn appEndo mempty $ leftmost [newTodoEv, todoEv] delimiter todoEv <- todoListWidget todosDyn blank Первое, что мы тут видим — это использование расширения RecursiveDo (его, соответственно надо включить). Это один из распространенных приёмов в разработке reflex приложений, т.к. очень часто возникают ситуации, когда событие, происходящее в нижней части страницы, влияет на элементы на верху страницы. В данном случае событие todoEv используется в определении todosDyn, а todosDyn в свою очередь является аргументом для виджета, из которого приходит событие todoEv. Далее мы видим обновление параметров функции foldDyn. Здесь есть использование новой функции leftmost. Она принимает список событий и возвращает событие, которое срабатывает в тот момент, когда сработает любое из событий в списке. Если в данный момент срабатывает два события из списка, то будет возвращено то, что стоит левее в списке (отсюда и название). Также список заданий теперь не список, а IntMap (для упрощения будем использовать type Todos = IntMap Todo). В первую очередь это сделано для того, чтобы мы могли напрямую обращаться к какому-либо элементу по идентификатору. Для обновления списка используется appEndo. В случае, если бы мы определили каждое событие как отдельный конструктор, то нам бы пришлось также определить функцию-обработчик, которая выглядела бы примерно так: updateTodo :: TodoEvent -> Todos -> Todos
updateTodo ev todos = case ev of NewTodo todo -> nextKey todos =: todo <> todos ToggleTodo ix -> update (Just . toggleTodo) ix todos StartEditTodo ix -> update (Just . startEdit) ix todos FinishEditTodo (v, ix) -> update (Just . finishEdit v) ix todos DeleteTodo ix -> delete ix todos Несмотря на отсутствие необходимости определять эту функцию, тут используются несколько других вспомогательных функций, которые все равно понадобятся нам в будущем. startEdit :: Todo -> Todo
startEdit todo = todo { todoState = TodoActive True } finishEdit :: Text -> Todo -> Todo finishEdit val todo = todo { todoState = TodoActive False, todoText = val } toggleTodo :: Todo -> Todo toggleTodo Todo{..} = Todo {todoState = toggleState todoState,..} where toggleState = \case TodoDone -> TodoActive False TodoActive _ -> TodoDone nextKey :: IntMap Todo -> Int nextKey = maybe 0 (succ . fst . fst) . maxViewWithKey Изменилась и функция добавления нового элемента, теперь она возвращает событие, а не само задание. Также добавим очистку поля после добавления нового задания. newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))
newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo iEl <- inputElement $ def & initialAttributes .~ ( "type" =: "text" <> "class" =: "form-control" <> "placeholder" =: "Todo" ) & inputElementConfig_setValue .~ ("" <$ btnEv) let addNewTodo = \todo -> Endo $ \todos -> insert (nextKey todos) (newTodo todo) todos newTodoDyn = addNewTodo <$> value iEl btnAttr = "class" =: "btn btn-outline-secondary" <> "type" =: "button" (btnEl, _) <- divClass "input-group-append" $ elAttr' "button" btnAttr $ text "Add new entry" let btnEv = domEvent Click btnEl pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl Функция todoListWidget теперь возвращает изменение списка, и она так же подверглась небольшим изменениям: todoListWidget :: MonadWidget t m => Dynamic t Todos -> m (Event t (Endo Todos))
todoListWidget todosDyn = rowWrapper $ do evs <- listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget pure $ switchDyn $ leftmost . M.elems <$> evs Первое, что замечаем, это замена функции simpleList функцией listWithKey. Отличаются они типом первого параметра — первая функция принимает список [], вторая — Map. Список будет выведен в отсортированным по ключу порядке. Здесь важно возвращаемое значение. Каждое задание возвращает событие (удаление, изменение). В конкретно нашем случае функция listWithKey будет иметь следующий тип: listWithKey
:: MonadWidget t m => Dynamic t (Map Int Todo) -> (Int -> Dynamic t Todo -> m (Event t TodoEvent)) -> m (Dynamic t (Map Int TodoEvent)) Примечание: эта функция является частью пакета reflex и имеет более сложный тип. Здесь уже показан специализированный тип.
У задания появились разные состояния, поэтому функция отображения одного задания также претерпела изменения: todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of TodoDone -> todoDone ix todoText TodoActive False -> todoActive ix todoText TodoActive True -> todoEditable ix todoText switchHold never todoEvEv Здесь мы используем новую функцию dyn. Она имеет следующий тип: dyn
:: (Adjustable t m, NotReady t m, PostBuild t m) => Dynamic t (m a) -> m (Event t a) В качестве входного параметра она получает виджет, завернутый Dynamic. Это означает, что при каждом обновлении Dynamic, будет также обновляться DOM. Выходным значением является событие, которое несет значение, возвращаемое виджетом. Специализированный тип для нашего случая будет выглядеть следующим образом: dyn
:: MonadWidget t m => Dynamic t (m (Event t (Endo Todos))) -> m (Event t (Event t (Endo Todos))) Тут мы встречаемся с событием, вложенным в другое событие. Есть две функции из пакета reflex, которые могут работать с таким типом: coincedence и switchHold. Первая функция возвращает событие, которое будет срабатывать только тогда, когда срабатывают одновременно внешнее и внутреннее события. Это не наш случай. Функция switchHold имеет следующий тип: switchHold :: (Reflex t, MonadHold t m) => Event t a -> Event t (Event t a) -> m (Event t)
Эта функция переключается на новое событие каждый раз, когда срабатывает внешнее событие. До первого срабатывания внешнего события, будет стрелять событие, переданное первым параметром. Именно так мы и используем эту функцию в нашем случае. До какого-либо первого изменения списка, оттуда не может прийти никакое событие, и мы используем событие never. Это специальное событие, которое, в соответствии с названием, не срабатывает никогда. Функция todoWidget использует разные виджеты для разных состояний. todoActive :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoActive ix todoText = divClass "d-flex border-bottom" $ do divClass "p-2 flex-grow-1 my-auto" $ text todoText divClass "p-2 btn-group" $ do (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Done" (editEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Edit" (delEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Drop" pure $ Endo <$> leftmost [ update (Just . toggleTodo) ix <$ domEvent Click doneEl , update (Just . startEdit) ix <$ domEvent Click editEl , delete ix <$ domEvent Click delEl ] todoDone :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos)) todoDone ix todoText = divClass "d-flex border-bottom" $ do divClass "p-2 flex-grow-1 my-auto" $ el "del" $ text todoText divClass "p-2 btn-group" $ do (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Undo" (delEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Drop" pure $ Endo <$> leftmost [ update (Just . toggleTodo) ix <$ domEvent Click doneEl , delete ix <$ domEvent Click delEl ] todoEditable :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos)) todoEditable ix todoText = divClass "d-flex border-bottom" $ do updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $ editTodoForm todoText divClass "p-2 btn-group" $ do (doneEl, _) <- elAttr' "button" ( "class" =: "btn btn-outline-secondary" <> "type" =: "button" ) $ text "Finish edit" let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix pure $ tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl) editTodoForm :: MonadWidget t m => Text -> m (Dynamic t Text) editTodoForm todo = do editIEl <- inputElement $ def & initialAttributes .~ ( "type" =: "text" <> "class" =: "form-control" <> "placeholder" =: "Todo") & inputElementConfig_initialValue .~ todo pure $ value editIEl Все использованные тут функции были рассмотрены раньше, поэтому не будем углубляться в объяснение каждой в отдельности. Оптимизация Вернемся к функции listWithKey: listWithKey
:: MonadWidget t m => Dynamic t (Map Int Todo) -> (Int -> Dynamic t Todo -> m (Event t TodoEvent)) -> m (Dynamic t (Map Int TodoEvent)) Функция работает таким образом, что любое обновления переданного Dynamic спровоцирует обновление каждого отдельного элемента. Даже если мы, например, изменили один элемент, это обновление будет передано в каждый элемент, хоть он и не изменит значения. Теперь вернемся к функции todoWidget. todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of TodoDone -> todoDone ix todoText TodoActive False -> todoActive ix todoText TodoActive True -> todoEditable ix todoText switchHold never todoEvEv Если вспомним, как работает функция dyn, то она обновляет DOM каждый раз, когда происходит обновление todoDyn. Учитывая, что при изменении одного элемента из списка, это обновление передается на каждый элемент в отдельности, получаем, что весь участок DOM, который выводит наши задания, будет перестраиваться (проверить это можно при помощи панели разработчика в браузере). Это явно не то, что мы хотим. Тут на помощь приходит функция holdUniqDyn. todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t TodoEvent)
todoWidget ix todoDyn' = do todoDyn <- holdUniqDyn todoDyn' todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of TodoDone -> todoDone ix td TodoActive False -> todoActive ix td TodoActive True -> todoEditable ix td switchHold never todoEvEv Мы добавили строку todoDyn <- holdUniqDyn todoDyn'. Что тут происходит? Дело в том, что хоть Dynamic и "выстреливает", значение, которое он содержит, не меняется. Функция holdUniqDyn работает именно так, что если переданный ей Dynamic "выстрелил" и не изменил свое значение, то выходной Dynamic не будет "выстреливать", и, соответственно, в нашем случае не будет лишних перестроек DOM. Полученный результат можно посмотреть в нашем репозитории. В следующей части рассмотрим другой способ управления событиями и использование библиотеки GHCJS-DOM. =========== Источник: habr.com =========== Похожие новости:
Блог компании Typeable ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ), #_programmirovanie ( Программирование ), #_haskell, #_funktsionalnoe_programmirovanie ( Функциональное программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:47
Часовой пояс: UTC + 5