I'm trying to implement a dynamic form in Elm 0.19 using hecrj/composable-form.
I receive a json with the fields, their descriptions, etc, so I don't know beforehand how many fields it will have.
So the traditional way of defining a form:
Form.succeed OutputValues
|> Form.append field1
|> Form.append field2
doesn't work because I don't know the OutputValues structure beforehand.
I've seen there is a function Form.list which looks like a promising path, though it seems to expect all fields equal, which is not my case, I may have a text field and a select field for example.
Is there any straight forward way of doing this with this library?
Thank you.
The form library doesn't explicitly support what you're trying to do, but we can make it work!
tldr;
Here's my example of how you can take JSON and create a form: https://ellie-app.com/bJqNh29qnsva1
How to get there
Form.list is definitely the promising path. You're also exactly right that Form.list requires all of the fields to be of the same type. So let's start there! We can make one data structure that can hold them by making a custom type. In my example, I called it DynamicFormFieldValue. We'll make a variant for each kind of field. I created ones for text, integer, and select list. Each one will need to hold the value of the field and all of the extras (like title and default value) to make it show up nicely. This will be what we decode the JSON into, what the form value is, and what the form output will be. The resulting types looks like this:
type alias TextFieldRequirements =
{ name : String
, default : Maybe String
}
type alias IntFieldRequirements =
{ name : String
, default : Maybe Int
}
type alias SelectFieldRequirements =
{ name : String
, default : Maybe String
, options : List ( String, String )
}
type DynamicFormFieldValue
= TextField String TextFieldRequirements
| IntField Int IntFieldRequirements
| SelectField String SelectFieldRequirements
To display the form, you just need a function that can take the form value and display the appropriate form widget. The form library provides Form.meta to change the form based on the value. So, we will pattern match on the custom type and return Form.textField, Form.numberField, or Form.selectField. Something like this:
dynamicFormField : Int -> Form DynamicFormFieldValue DynamicFormFieldValue
dynamicFormField fieldPosition =
Form.meta
(\field ->
case field of
TextField textValue ({ name } as requirements) ->
Form.textField
{ parser = \_ -> Ok field
, value = \_ -> textValue
, update = \value oldValue -> TextField value requirements
, error = always Nothing
, attributes =
{ label = name
, placeholder = ""
}
}
IntField intValue ({ name } as requirements) ->
Form.numberField
{ parser = \_ -> Ok field
, value = \_ -> String.fromInt intValue
, update = \value oldValue -> IntField (Maybe.withDefault intValue (String.toInt value)) requirements
, error = always Nothing
, attributes =
{ label = name
, placeholder = ""
, step = Nothing
, min = Nothing
, max = Nothing
}
}
SelectField selectValue ({ name, options } as requirements) ->
Form.selectField
{ parser = \_ -> Ok field
, value = \_ -> selectValue
, update = \value oldValue -> SelectField value requirements
, error = always Nothing
, attributes =
{ label = name
, placeholder = ""
, options = options
}
}
)
Hooking this display function up is a bit awkward with the library. Form.list wasn't designed with use-case in mind. We want the list to stay the same length and just be iterated over. To achieve this, we will remove the "add" and "delete" buttons and be forced to provide a dummy default value (which will never get used).
dynamicForm : Form (List DynamicFormFieldValue) (List DynamicFormFieldValue)
dynamicForm =
Form.list
{ default =
-- This will never get used
TextField "" { name = "", default = Nothing }
, value = \value -> value
, update = \value oldValue -> value
, attributes =
{ label = "Dynamic Field Example"
, add = Nothing
, delete = Nothing
}
}
dynamicFormField
Hopefully the ellie example demonstrates the rest and you can adapt it to your needs!
Related
I am playing a little bit with Elm these days, but I stuck with a simple case where I want to update a record field. My code is like this:
-- MODEL
initialModel : Model
initialModel =
{ selectedLanguage = "german"
, allCards = Card.cards
}
type alias Msg =
{ description : String
, data : String
, id : String
}
The update function
update : Msg -> Model -> Model
update msg model =
case List.head (model.allCards) of
Just card ->
{ card | fliped = True }
Nothing -> model
but I see this:
Something is off with the 1st branch of this `case` expression:
50| { card | fliped = True }
^^^^^^^^^^^^^^^^^^^^^^^^
The 1st branch is a record of type:
{ back : String, fliped : Bool, front : String, id : String }
But the type annotation on `update` says it should be:
Model
Hint: Seems like a record field typo. Maybe back should be allCards?
Hint: Can more type annotations be added? Type annotations always help me give
more specific messages, and I think they could help a lot in this case!
Detected errors in 1 module.
I think I should always return a model from update function like my type says, but cannot figure out how. Any advice here?
You'll have update the allCards field of model too. You can nest the card update inside the model update if the former returns a list instead of just a single card:
update : Msg -> Model -> Model
update msg model =
{ model
| allCards =
case model.allCards of
card :: rest ->
{ card | fliped = True } :: rest
[] ->
[]
}
Or you can bind the new allCards to a name if you prefer:
update : Msg -> Model -> Model
update msg model =
let
newAllCards =
case model.allCards of
card :: rest ->
{ card | fliped = True } :: rest
[] ->
[]
in
{ model | allCards = newAllCards }
I pattern match directly on the list here instead of using List.head, as that also gives me the remainder of the list and I don't have to deal with an intermediary Maybe value (or two actually, since List.tail returns a Maybe as well). The card::rest branch hits if allCards contains at least one card, so the only remaining case is therefore [], which is easy enough to handle.
Also, flipped is spelled with two ps ;)
I am trying to model a type as a union where each member of that union has properties in common with all other members.
I am currently achieving this like so:
type alias File = {
name : String
}
type CommonFileState extra = CommonFileState {
id : String
, file : File
} extra
type alias ValidFileState = CommonFileState {
validatedAt : Int
}
type alias InvalidFileState = CommonFileState {
reason : String
}
type alias LoadingFileState = CommonFileState {}
type FileState = Valid ValidFileState | Invalid InvalidFileState | Loading LoadingFileState
Now if I want to read one of those common properties on any given FileState, I must match against each member of the union:
getId : FileState -> String
getId fileState = case fileState of
Valid (CommonFileState {id} extra) -> id
Invalid (CommonFileState {id} extra) -> id
Loading (CommonFileState {id} extra) -> id
This feels wrong to me, because I have to duplicate the property access for each member. If I needed to manipulate this property somehow (e.g. concatenating something onto the string), I would also have to duplicate this.
I want to be able to easily access common properties of my union, and operate on those common properties.
When I started searching for other ways to do this, I found one alternative was to nest the union inside a record, which also holds the common properties:
type alias ValidCurrentFileState = {
validatedAt : Int
}
type alias InvalidCurrentFileState = {
reason : String
}
type alias LoadingCurrentFileState = {}
type CurrentFileState = Valid ValidCurrentFileState | Invalid InvalidCurrentFileState| Loading LoadingCurrentFileState
type alias File = {
name : String
}
type alias FileState = {
id : String
, file : File
, currentState : CurrentFileState
}
getId : FileState -> String
getId {id} = id
However this is awkward because I have to name the nested union, which adds a level of unnecessary indirection: "file state" and "current file state" are conceptually the same.
Are there any other ways of doing this which don't have the problems I mentioned?
I think you are thinking about this the wrong way around.
The purpose of modelling (in Elm) is capture the possible states of your data, and to exclude - in your model - 'impossible' states, so that the compiler can statically prevent the code every creating such states.
Once you're happy with your model, you write the helpers you need to make your core logic easy to express and to maintain.
I suspect I would normally go with your second approach, but I don't know all the issues you need to account for.
I want to add a certain value to a list. Both are inside my ELM-model:
type alias Model =
{ syllables : List Syllable
, words : List Word
, newSyllable : String
, newWord : String
}
I want to add the newSyllable value to the list of syllables, when I click the button.
I placed this attribute inside my view:
onClick TransferSyllable
Everything works right, but I wonder how I can transfer one value of my model into the list of values!?
Thanks.
EDIT:
This is my definition of "Syllable":
type alias Syllable =
{ content : String
, start : Bool
, mid : Bool
, end : Bool
}
I want to insert the value to the end of the list.
Since Syllable is a record type with four fields, and newSyllable is just a string, you'll need a function that turns a String into a Syllable. I'll assume that function has the signature:
makeSyllable : String -> Syllable
Adding the syllable onto the end of the list can be done using List.append. Since append takes a List a, you'll need to add brackets around newSyllable when passing it to append:
{ model | syllables = List.append model.syllables [ makeSyllable model.newSyllable ] }
The canonical example for getting the value from an input is:
view : Address String -> String -> Html
view address string =
div []
[ input
[ placeholder "Text to reverse"
, value string
, on "input" targetValue (Signal.message address)
, myStyle
]
[]
, div [ myStyle ] [ text (String.reverse string) ]
]
I get this. But I want my address to be of type Address String Action (where Action is some other type I define). To my understanding, this would mean the address expects a String followed by a Action type as it's "arguments" (I think of Address as a function, but that might not be correct).
Is it possible to use an address type of Address String Action, and then use it with an input in a similar way? Or am I allowed to do Address String Action in the first place?
The example you link to is probably a bit too simplistic in that both the Action and Model are a string. You will seldom run into that.
I've tweaked the example with something that is more canonical to elm in its current form:
main =
StartApp.start { model = { text = "" }, view = view, update = update }
type Action
= SetText String
type alias Model =
{ text : String }
update : Action -> Model -> Model
update action model =
case action of
SetText text ->
{ model | text = text }
view : Address Action -> Model -> Html
view address model =
div []
[ input
[ placeholder "Text to reverse"
, value model.text
, on "input" targetValue (Signal.message address << SetText)
, myStyle
]
[]
, div [ myStyle ] [ text (String.reverse model.text) ]
]
Notice how the Action type is a union type listing all the different ways you can interact with the page. In this example, the only thing you can do is to set the text.
The signature of view is now more explicit. The first argument is the address of a mailbox that deals in type Action, and the second argument contains the current state of the model.
view : Address Action -> Model -> Html
There is no need to go down a path of trying something like Address String Action since now Action encapsulates the setting of the text.
"first part" &&&& fun _ ->
let ident
"second part" &&&& fun _ ->
ident ....
I need to use variable "ident".
I just need to pass value of variable from first part of test to second one...
I want to ask you if there is any easy way how to define and use global variable or even if you have better (and easy) idea of doing that
Keep in mind, please, that I am a beginner, so I would prefer easier ones.
Global variables will often make your code difficult to work with - particularly if they are mutable.
Instead, consider returning the values you need to keep track of as composite values. An easy data type to start with would be a tuple:
let ``first part`` id =
let someOtherValue = "Foo"
someOtherValue, id + 1
This function takes an int (the current ID) as input, and returns string * int (a tuple where the first element is a string, and the second element and int) as output.
You can call it like this:
> let other, newId = ``first part`` 42;;
val other : string = "Foo"
val newId : int = 43
Notice that you can use pattern matching to immediately destructure the values into two named symbols: other and newId.
Your second function could also take an ID as input:
let ``second part`` id otherArgument =
// use id here, if you need it
"Bar"
You can call it like this, with the newId value from above:
> let result = ``second part`` newId "Baz";;
val result : string = "Bar"
If you find yourself doing this a lot, you can define a record for the purpose:
type Identifiable<'a> = { Id : int; Value : 'a }
Now you can begin to define higher-order functions to deal with such a type, such as e.g. a map function:
module Identifiable =
let map f x = { Id = x.Id; Value = f x.Value }
// Other functions go here...
This is a function that maps the Value of an Identifiable from one value to another, but preserves the identity.
Here's a simple example of using it:
> let original = { Id = 42; Value = "1337" };;
val original : Identifiable<string> = {Id = 42;
Value = "1337";}
> let result' = original |> Identifiable.map System.Int32.Parse;;
val result' : Identifiable<int> = {Id = 42;
Value = 1337;}
As you can see, it preserves the value 42, but changes the Value from a string to an int.
You can still change the ID explicitly, if you want to do that:
> let result'' = { result' with Id = 7 };;
val result'' : Identifiable<int> = {Id = 7;
Value = 1337;}
Since this was getting out of hand for comments this is how I would do it for an example
let mutable t = 0
let first =
t <- 1 + 1
//other stuff
let second =
//can use t here and it will have a value of 2
In some cases you have to use a ref:
let t = ref 0
let first =
t := 1 + 1
//other stuff
let second =
//can use t here and it will have a value of 2 -
// you use "!t" to get the value
If you define ident at the top of your file like this :
let ident = "foo"
// rest of your code using ident
ident are global and you can use in the next part of your file.
EDIT :
If ident wil change in the next part of your code, use this :
let ident = ref "foo"