Как инкапсулировать логику в дочерних элементах, таких как компоненты?
Я пытаюсь понять, как создавать повторно используемые компоненты, используя архитектуру Elmish в F# Bolero от WebSharper (например, ввод повторно проверенных форм). Из всех примеров, которые я видел, родительский уровень верхнего уровня должен обрабатывать все сообщения / обновления и логику, а дочерние - просто для представлений. Мне интересно, есть ли способ обойти это, будь то, когда ребенок обрабатывает свое собственное состояние + сообщения, и распространяет определенные сообщения родителю (что я попытался в коде ниже), или есть другой дизайн, чтобы справиться с этим.
В моем конкретном случае я пытаюсь создать компонент ввода формы для имени пользователя, который проверяет, что ни одно из полей не является пустым. Мне не нравится идея, что родительский дескриптор обновляет отдельные поля FirstName и LastName, он должен заботиться только о получении сообщения Submit. Обработка каждого сообщения, которое выдает ребенок, может привести к потере тонны, если вы используете ребенка более одного раза
Примечание. Код, который я предоставил, не компилируется, так как я пытаюсь понять, как реализовать задуманный дизайн.
open Elmish
open Bolero
open Bolero.Html
module NameInput =
type Model = { FirstName : string; LastName : string }
type Message =
| ChangeFirstName of string
| ChangeLastName of string
| Submit of Model
let update model msg =
match msg with
| ChangeFirstName s ->
{ model with FirstName = s}, Cmd.none
| ChangeLastName s ->
{ model with LastName = s}, Cmd.none
| Submit m ->
m, Cmd.ofMsg (Submit m)
type Component() =
inherit ElmishComponent<Message, Model>()
let invalidField s = s <> ""
override this.View model dispatch =
let fnClass = if (invalidField model.FirstName) then "invalid" else "valid"
let lnClass = if (invalidField model.LastName) then "invalid" else "valid"
div [] [
label [] [ text "First Name: " ]
input [
attr.``class`` fnClass
on.change (fun e -> update model (ChangeFirstName (unbox e.Value)))
]
label [] [ text "Last Name: " ]
input [
attr.``class`` lnClass
on.change (fun e -> update model (ChangeLastName (unbox e.Value)))
]
button [ on.click (fun _ -> update model (Submit model)) ] [ text "Submit" ]
]
type Message =
| NameSubmitted of NameInput.Message.Submit
type Model = { UserName : NameInput.Model }
let initModel = { UserName = { FirstName = ""; LastName = "" } }
let update msg model =
match msg with
| NameSubmitted name ->
// Greet the user
{ model with UserName = name }, Cmd.none
let view model dispatch =
concat [
ecomp<NameInput.Component,_,_>
model.Username dispatch
]
type MyApp() =
inherit ProgramComponent<Model, Message>()
override this.Program =
Program.mkProgram (fun _ -> initModel, Cmd.none) update view
1 ответ
Спасибо @rmunn и @hvester за ссылки, они помогли мне лучше понять Elmish и смогли найти решение. В качестве справки для всех, кто может наткнуться на это, вот решение. InternalMessage не нуждается в приватности, он просто скрывает эти случаи от функции обновления основной программы, чтобы можно было легко увидеть, какие сообщения им нужно обработать. Однако, если он общедоступен, компилятор выдаст ошибку, если вы попытаетесь сопоставить его в случае InternalMessage, не развернув сначала сообщение в InternalMessage (поэтому программист все еще легко знает, какие сообщения являются внутренними).
module NameInput =
type Model = { FirstName : string; LastName : string }
type private InternalMessage =
| ChangeFirstName of string
| ChangeLastName of string
type Message =
| Internal of InternalMessage
| Submit of Model
let update msg model =
match msg with
| ChangeFirstName s ->
{ model with FirstName = s }
| ChangeLastName s ->
{ model with LastName = s }
type Component() =
inherit ElmishComponent<Model, Message>()
let invalidField s = s <> ""
override this.View model dispatch =
let fnClass = if (invalidField model.FirstName) then "invalid" else "valid"
let lnClass = if (invalidField model.LastName) then "invalid" else "valid"
div [] [
label [] [ text "First Name: " ]
input [
attr.``class`` fnClass
on.change (fun e -> dispatch << Internal << ChangeFirstName <| unbox e.Value)
]
label [] [ text "Last Name: " ]
input [
attr.``class`` lnClass
on.change (fun e -> dispatch << Internal << ChangeLastName <| unbox e.Value)
]
button [ on.click (fun _ -> dispatch <| Submit model) ] [ text "Submit" ]
]
type Model = { Name : NameInput.Model }
let initModel = { Name = { FirstName = ""; LastName = "" } }
type Message =
| NameInput of NameInput.Message
let update message model =
match message with
| NameInput ni ->
match ni with
| NameInput.Internal i ->
{ model with Name = model.Name |> NameInput.update i}
| NameInput.Submit n ->
{ model with Name = n }