I'm trying to build a UI that allows a user to manipulate a recursive data structure. For example, imagine a visual schema editor or database table editor in which you have plain old types (strings and integers) and compound types made up of those plain types (arrays, structs). In the example below, a Struct_ is like a JavaScript object, where the keys are strings and the values are any type, including nested Array_s and 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
Now to go about building a UI for this I can make a simple recursive function:
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
My issue arises when trying to manipulate my state with this recursive structure. What data structure in a Msgs would accommodate user edits to this structure, adding new fields, subfields, and editing their properties? How would I properly decode that in my update loop?
For example...
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)
What kind of data would you attach to that AddStructSubfield or EditStructSubfieldKey message (that's passed with the onClick handler to the button above) to properly update your state, specifically when the Struct_ is say, nested inside of another Struct_, nested inside of an Array_? EditStructSubfieldKey, for example, will only contain the new string that the user has entered, but not enough information to address a deeply-nested item.
We do exactly this in our code base, but haven't open sourced the 'library' that supported this. But the answer to your question is that you need to add the notion of a Path to your code and messages.
type Path
= Field: String
| Index: Int
Then your view has to keep updating the path as you descend [Field "f1", Index 3, ...], and your update function needs to be supported by insert, delete,... that take a Path and the existing structure and return you a new one.
I ended up solving this by passing an updater function down the recursive chain. I've simplified this example as much as possible while showing the recursive nature of the updating. This allows for updating infinitely nested structures and lists without worrying about encoding/decoding a path. The downside, I believe, is that my single update Msg will always replace the entire model. I'm not sure about the semantics of how this will affect Elm's equality checking, and if that will produce performance issues in certain applications.
This example can be copy/pasted into https://elm-lang.org/try as-is to see it in action.
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
Related
I am trying to declare a custom attribute in a record and trying to read it. It seems to be not working. Please advise.
// Custom Attribute for record fields
[<AttributeUsage(AttributeTargets.Field)>]
type Name(x: string) =
inherit Attribute()
member _.value = x
// Record type
type User =
{ [<Name("id")>]
Id: int
[<Name("email")>]
Email: string
[<Name("organization_id")>]
OrganizationId: option<string> }
// Trying to read the attribute. This is not working. I am getting <null> here.
let parse () =
FSharpType.GetRecordFields(typeof<User>)
|> Array.map (fun p -> p.Name, p.GetCustomAttribute(typeof<Name>, false))
|> Array.iter (fun (t, a) -> printfn "%s : %A" t a)
The below code fixes the problem. For more detailed answer, refer this link.
// Changing to property instead of field
[<AttributeUsage(AttributeTargets.Property)>]
type Name(x: string) =
inherit Attribute()
member _.value = x
I am new to Giraffe and having difficulties displaying an integer. My model is
type Person =
{
FirstName : string
LastName : string
Age : int
}
And my view is:
let index2 (model : Person) =
[
partial()
p [] [
encodedText model.FirstName
br []
encodedText model.LastName
br []
int model.Age
]
] |> layout
The error message returned is "All elements of a list must be of the same type. So how do I display the age?
I am not quite familiar with Giraffe (rather Fable), so I do not know what to install and set to reproduce it.
Nevertheless, looking at p [] [ ... ], it is clear that the function requires a list as a second parameter and therefore all the elements there should be of the same type.
Let's look inside. As per GiraffeViewEngine.fs, encodedText returns Text which is a case in XmlNode DU:
type XmlNode =
| ParentNode of XmlElement * XmlNode list
| VoidElement of XmlElement
| Text of string
So this sets the type for the whole list, XmlNode.
br function is defined as let br = voidTag "br" where voidTag function returns VoidElement which is another case in XmlNode DU, so it is fine.
And so I do not know what you wanted to say by int model.Age, and int is actually an operator in F# to convert object to integer. Anyways, it does not return anything of type XmlNode. Instead, try
p [] [
encodedText model.FirstName
br []
encodedText model.LastName
br []
encodedText (string model.Age)
]
Thereby you will ToString the age, it will turn to Text case of XmlNode and the code should compile.
I am a relative beginner in Elm. I learned how to recur over records, and to create extensible records. My target application has tree-shaped data, that is to be rendered in several tree views.
In order for Elm to allow recursive models, one needs
a Union type for the children field,
a Maybe in that type definition, and
the lazy anonymous function construct if Json decoding is needed.
So far so good, but I cannot combine recursion with extension. Here below is a minimized version of how far I got. This program is capable of rendering either one of the two example types (Folder and Note), but not both. (see lines commented with -- NOTE and -- FOLDER).
The problem is with the kids function. Elm doesn't allow it to produce two different output types. I am stuck with either duplicating the code, or do without record extensions. Both seems like show-stoppers.
Is there a way to get this working with both, extension and recursion, and without code duplication?
Run on Ellie
module Main exposing (main)
import Html exposing (..)
import Maybe
-- MAIN
main = Html.beginnerProgram
{ model = init
, update = update
, view = view
}
-- MODEL
type alias Model =
{ folder : Folder
, note : Note
}
type alias Node a =
{ a | name : String
, children : Children a
}
type alias Folder =
{ name : String
, children : ChildFolders
}
type alias Note =
{ name : String
, children : ChildNotes
}
type Children a = Children a (Maybe (List (Node a)))
type ChildFolders = ChildFolders (Maybe (List Folder))
type ChildNotes = ChildNotes (Maybe (List Note))
-- INIT
init : Model
init = Model
(Folder "Parent F" someFolders)
(Note "Parent N" (ChildNotes Nothing))
someFolders : ChildFolders
someFolders = ChildFolders
( Just
( [ Folder "Child F1" (ChildFolders Nothing)
, Folder "Child F2" (ChildFolders Nothing)
, Folder "Child F3" (ChildFolders Nothing)
]
)
)
-- UPDATE
type Msg = NoOp
update : Msg -> Model -> Model
update msg model =
case msg of
NoOp -> model
-- VIEW
view : Model -> Html msg
view model =
div []
[ viewBranch model.folder -- FOLDER
-- , viewBranch model.note -- NOTE
]
-- viewBranch : (?) -> Html msg
viewBranch node =
uli
( text node.name
:: ( node
|> kids
|> List.map viewBranch
)
)
uli : List (Html msg) -> Html msg
uli items = ul [] [ li [] items ]
-- kids : (?) -> (?)
kids { children } =
case children of
(ChildFolders data) -> Maybe.withDefault [] data -- FOLDER
-- (ChildNotes data) -> Maybe.withDefault [] data -- NOTE
In elm, is something like the below possible
foo : Int -> Html
foo inputNum =
addToNumHistory inputNum ;
display inputNum
where the aim of the above is to execute multiple lines of code?
If not, is this because the above is an example of a side effect?
If something like the above syntax is not possible, how would one go about executing two functions/lines of code simultaneously, as in the above, or as a result of a given input (case branch) ?
Edit
The above is a bad example. The following uses the Elm Architecture:
--Model
type alias Model =
{ number : Int
, numberHistory : List Int
}
type Action
= Number Int
--Update
update : Action -> Model
update action =
case action of
Number num->
addToNumHistory num
addToNumHistory : Int -> Model -> Model
addToNumHistory num modelHistory =
{ model
| number = num
, numberHistory = num :: model.numberHistory
}
--View
view : Signal.Address Action -> Model -> Html
view action model =
div []
[ field
"text"
address
Number model.number
"Enter lucky number here pal!"
model.number
]
Given this, am I right in presuming that to 'execute multiple lines' in such a fashion as to alter an underlying model, one would simply use/extend the model - for example, to effect a change analogous to the following:
--Update
update : Action -> Model
update action =
case action of
Number num->
addToNumHistory num;
addToChangeHistory
one would simply extend the model as follows:
--Model
type alias Model =
{ number : Int
, numberHistory : List Int
, changeHistory : List Date
}
--Update
update : Action -> Model
update action =
case action of
Number num->
addToNumHistoryWithChangeHistory num
addToNumHistoryWithChangeHistory : Int -> Model -> Model
addToNumHistory num modelHistory =
{ model
| number = num
, numberHistory = num :: model.numberHistory
, changeHistory = getCurrentDate :: model.changeHistory
}
getCurrentDate : Date
In this specific case, you don't need to have side-effects.
I've had to add two utility functions to create a functioning example.
onInput to handle 'input' event
parseInt to retrieve Int from a String
The rest is a basic Elm Architecture lifecycle as of 0.16
Please consider this minimal example I made for use with StartApp.Simple:
import Html exposing (text, input, div, Html, Attribute)
import Html.Attributes exposing (value)
import Html.Events exposing (on, targetValue)
import String
import Signal exposing (Address)
import StartApp.Simple as StarApp
--Utils
onInput : Address a -> (String -> a) -> Attribute
onInput address f =
on "input" targetValue (\v -> Signal.message address (f v))
parseInt : String -> Int
parseInt string =
case String.toInt string of
Ok value ->
value
Err error ->
0
--Model
type alias Model =
{ number : Int
, numberHistory : List Int
}
initModel : Model
initModel =
{ number = 0
, numberHistory = []
}
--Update
type Action
= UpdateNumber String
update : Action -> Model -> Model
update action model =
case action of
UpdateNumber num ->
addToNumHistory (parseInt num) model
addToNumHistory : Int -> Model -> Model
addToNumHistory num model =
{ model
| number = num
, numberHistory = num :: model.numberHistory
}
--View
view : Signal.Address Action -> Model -> Html
view address model =
div
[]
[ input
{- On every 'input' event,
grab the value of input field and send to UpdateNumber
-}
[ onInput address UpdateNumber, value (toString model.number) ]
[]
, div [] [ text (toString model.number) ]
, div
[]
( model.numberHistory
|> List.reverse
|> List.map (toString)
|> List.map text
)
]
main : Signal Html
main =
StarApp.start
{ view = view
, update = update
, model = initModel
}
The following code compiles and runs correctly:
type FooUnion = MyCase of int * string
FSharp.Reflection.FSharpType.GetUnionCases(typeof<FooUnion>)
|> Array.tryFind(fun a -> a.Name = "MyCase")
|> Option.map(fun d -> FSharp.Reflection.FSharpValue.MakeUnion(d, [| 1; "test" |]))
|> Option.bind(function | :? FooUnion as s -> Some s | _ -> None)
However, if I remove the fully qualified FSharp.Reflection and move it to an open statement, the code no longer compiles: -
open FSharp.Reflection
type FooUnion = MyCase of int * string
FSharpType.GetUnionCases(typeof<FooUnion>)
|> Array.tryFind(fun a -> a.Name = "MyCase")
|> Option.map(fun d -> FSharpValue.MakeUnion(d, [| 1; "test" |]))
|> Option.bind(function | :? FooUnion as s -> Some s | _ -> None)
with errors on the call to MakeUnion: -
No overloads match the method 'MakeUnion' [ within the VS IDE ]
error FS0001: This expression was expected to have type
int
but here has type
string [ within FSI if I execute the code manually ]
The FSharpValue type contains a single MakeUnion method:
static member MakeUnion : unionCase:Reflection.UnionCaseInfo * args:obj [] * ?bindingFlags:System.Reflection.BindingFlags -> obj
But the FSharp.Reflection namespace contains an extension methods with a slightly different signature.
The FSharp compiler only implicitly box the content of the args array when there is no overload, so opening the namespace require to change the code to:
FSharpValue.MakeUnion(d, [| box 1; box "test" |])
even if you prefix with the full namespace.