Как обрабатывать сообщения из рекурсивного HTML-интерфейса в Elm? - PullRequest
1 голос
/ 27 октября 2019

Я пытаюсь создать пользовательский интерфейс, который позволяет пользователю манипулировать рекурсивной структурой данных. Например, представьте себе визуальный редактор схем или редактор таблиц базы данных, в котором у вас есть простые старые типы (строки и целые числа) и составные типы, состоящие из этих простых типов (массивы, структуры). В приведенном ниже примере Struct_ похож на объект JavaScript, где ключи - это строки, а значения - любого типа, включая вложенные Array_ s и Struct_ s.

-- underscores appended to prevent confusion about native Elm types. These are custom to my application.
type ValueType
    = String_
    | Int_
    | Float_
    | Array_ ValueType
    | Struct_ (List (String, ValueType))

type alias Field =
    { id : Int
    , label : String
    , hint : String
    , hidden : Bool
    , valueType : ValueType
    }

type alias Schema = List Field

ТеперьДля создания пользовательского интерфейса я могу сделать простую рекурсивную функцию:

viewField : Field -> Html Msg
viewField field =
    div []
    [ input [ type_ "text", value field.label ] []
    , viewValueType field.valueType
    ]

viewValueType : ValueType -> Html Msg
viewValueType valueType =
    let
        structField : (String, ValueType) -> Html Msg
        structField (key, subtype) =
            div []
                [ input [type_ "text", placeholder "Key", value key, onInput EditStructSubfieldKey] []
                , viewValueType subtype
                ]

        options : List(Html Msg)
        options = case valueType of
            String_ -> -- string ui
            Int_ -> -- int ui
            Float_ -> -- float ui
            Array_ subtype ->
                [ label [] [ text "subtype" ]
                , viewValueType subtype
                ]
            Struct_ fields ->
                [ label [] [ text "subfields" ]
                , List.map structField fields
                , button [ onClick AddStructSubfield ] [ text "Add subfield" ]
                ]
    in
    div [] options

Моя проблема возникает при попытке манипулировать моим состоянием с помощью этой рекурсивной структуры. Какая структура данных в Msg s будет приспосабливать пользовательские правки к этой структуре, добавлять новые поля, подполя и редактировать их свойства? Как мне правильно декодировать это в моем цикле update?

Например ...

type alias Model =
    { fields : List Field }

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        AddStructSubfield _???_ ->
            ({model | fields = ???}, Cmd.none)
        EditStructSubfieldKey _???_ ->
            ({model | fields = ???}, Cmd.none)

Какие данные вы бы прикрепили к этому сообщению AddStructSubfield или EditStructSubfieldKey(это передается с помощью обработчика onClick в button выше) для правильного обновления вашего состояния, особенно когда, скажем, Struct_ вложен в другой Struct_, вложенный в Array_? EditStructSubfieldKey, например, будет содержать только новую строку, введенную пользователем, но недостаточно информации для адресации глубоко вложенного элемента.

Ответы [ 2 ]

2 голосов
/ 28 октября 2019

Мы делаем именно это в нашей кодовой базе, но не открыли «библиотеку», которая это поддерживала. Но ответ на ваш вопрос заключается в том, что вам нужно добавить понятие Path к вашему коду и сообщениям.

type Path 
    = Field: String 
    | Index: Int 

Тогда ваш взгляд должен постоянно обновлять путь при спуске [Field "f1", Index 3, ...],и ваша функция обновления должна поддерживаться с помощью вставки, удаления, ..., которые принимают путь и существующую структуру и возвращают вам новую.

0 голосов
/ 29 октября 2019

Я решил эту проблему, передав функцию обновления по рекурсивной цепочке. Я максимально упростил этот пример, показывая рекурсивный характер обновления. Это позволяет обновлять бесконечно вложенные структуры и списки, не беспокоясь о кодировании / декодировании пути. Недостатком, я считаю, является то, что мое единственное обновление Msg всегда заменит всю модель. Я не уверен в семантике того, как это повлияет на проверку равенства в Elm, и если это вызовет проблемы с производительностью в определенных приложениях.

Этот пример можно скопировать / вставить в https://elm -lang.org / try как есть, чтобы увидеть его в действии.

import Html exposing (Html, div, input, ul, li, text, select, button, option)
import Html.Attributes exposing (value, type_, selected)
import Html.Events exposing (onInput, onClick)
import Browser

type ValueType
    = String_
    | Int_
    | Array_ ValueType
    | Struct_ (List Field)

type alias Field =
    { label : String
    , valueType : ValueType
    }

type alias Model = Field

main = Browser.sandbox { init = init, update = update, view = view }

init : Model
init =
    { label = "Root Field", valueType = String_ }

type Msg
    = UpdateField Field

update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateField field ->
            field

view : Model -> Html Msg
view model =
    let
        updater : Field -> Msg
        updater field =
            UpdateField field
    in
    div [] [ viewField updater model ]

viewField : (Field -> Msg) -> Field -> Html Msg
viewField updater field =
    let
        updateLabel : String -> Msg
        updateLabel newLabel =
            updater {field | label = newLabel}

        updateValueType : ValueType -> Msg
        updateValueType newValueType =
            updater {field | valueType = newValueType}
    in
    li []
    [ input [ type_ "text", value field.label, onInput updateLabel ] [ ]
    , viewTypeOptions updateValueType field.valueType
    ]

viewTypeOptions : (ValueType -> Msg) -> ValueType -> Html Msg
viewTypeOptions updater valueType =
    let
        typeOptions = case valueType of
            String_ ->
                div [] []
            Int_ ->
                div [] []
            Array_ subtype ->
                let
                    subUpdater : ValueType -> Msg
                    subUpdater newType =
                        updater <| Array_ newType
                in
                div [] [ div [] [ text "Subtype" ], viewTypeOptions subUpdater subtype ]
            Struct_ fields ->
                let
                    fieldAdder : Msg
                    fieldAdder =
                        updater <| Struct_ ({label = "", valueType = String_} :: fields)

                    fieldUpdater : Int -> Field -> Msg
                    fieldUpdater index newField =
                         updater <| Struct_ <| replaceInList index newField fields
                in
                div []
                  [ ul [] (List.indexedMap (\i -> (viewField <| fieldUpdater i)) fields)
                  , button [ onClick fieldAdder ] [ text "+ Add Field" ]
                  ]

        isArray t = case t of
            Array_ _ -> True
            _ -> False

        isStruct t = case t of
            Struct_ _ -> True
            _ -> False

        stringToType str = case str of
            "string" -> String_
            "int" -> Int_
            "array" -> Array_ String_
            "struct" -> Struct_ []
            _ -> String_

        changeType str =
            updater <| stringToType str

    in
    div []
    [ select [ onInput changeType ]
        [ option [ value "string", selected <| valueType == String_ ] [ text "String" ]
        , option [ value "int", selected <| valueType == Int_ ] [ text "Integer" ]
        , option [ value "array", selected <| isArray valueType ] [ text "Array" ]
        , option [ value "struct", selected <| isStruct valueType ] [ text "Struct" ]
        ]
    , typeOptions
    ]

replaceInList : Int -> a -> List a -> List a
replaceInList index item list =
    let
        head = List.take index list
        tail = List.drop (index+1) list
    in
    head ++ [ item ] ++ tail
...