Как инкапсулировать логику в дочерних элементах, таких как фреймворки компонентов?

Я пытаюсь понять, как создавать повторно используемые компоненты, используя архитектуру 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

person Weebs    schedule 06.01.2019    source источник
comment
Я еще недостаточно хорошо понимаю Elmish, чтобы написать полный ответ, но взгляните на github.com/ MangelMaxime/fulma-demo и, в частности, в его родительско-дочерней структуре. Каждый уровень архитектуры знает, какие сообщения принадлежат ему, а какие сообщения принадлежат дочернему уровню, и соответствующим образом передает сообщения дочерним уровням. Таким образом, вам не нужно, чтобы родительский слой обрабатывал все состояние. Я все еще сам во всем этом разбираюсь, так что вам лучше прочитать код, чем заставлять меня болтать об этом.   -  person rmunn    schedule 07.01.2019
comment
Я думаю, вы можете решить эту проблему, используя внешние сообщения (ExternalMsg), как описано в medium.com/@MangelMaxime/   -  person hvester    schedule 07.01.2019


Ответы (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 }
person Weebs    schedule 09.01.2019