How to avoid round trips between custom type and string in elm? - elm

I encountered an issue that there are lots of round trip between a custom type and string . When user clicks/changes an textarea or droplist in HTML, the onInput caputures a string that represents the updated value in that element.
Then the message would send this string to update function to update the model. The update function need to convert that string to my custom type , since it was modeled that way.
type color = green | yellow | red
type alias Model =
{ name :String
,skinColor: color}
// to present `color` in html, it needs to be converted to string
viewFruitColor c =
div [] [
case c of
green -> "green"
yellow -> "yellow"
red -> "red"
,onSelect ChangeColor
]
// some ChangeColor msg here
// in update function, which handles changing the color of fruit, it has to convert a string back to a type
update msg model =
case msg of
ChangeColor s // s -> String here
let newColor = // can we just get `s` as a color type instead of a String ??
case s of
"green"-> green
"yellow" -> yellow
"red" -> red
in
{ fruit | skinColor = newColor }
this is only for one single field in model, what if there are mutilple fields, that would boost up a lots of reduntant codes just taking round trips between type and string (lots of case clauses)

Just use functions, elm was designed to write code like this:
colorToString : color -> String
colorToString c =
case c of
green -> "green"
yellow -> "yellow"
red -> "red"
parseColor : String -> color
parseColor s =
case s of
"green"-> green
"yellow" -> yellow
"red" -> red
_ -> red -- it is a good idea to have a default, or otherwise use the Maybe type
And then your code would work for example like
viewFruitColor c =
div [] [
colorToString c
,onSelect ChangeColor
]
There is no way to bypass it. The user can send you an arbitrary string and you need to handle it. On the other hand, on the app side it is good to work with algebraic types as you do, because it defines the domain more precisely. The overall strategy is:
you receive a string from a user
you turn it to color with parseColor
you do your logic with color
you turn the color to string with colorToString
you send the html back

Related

How to check user name and last name length in one textfield in android jetpack compose?

There is an outlinedtextfield where the user enters his/her name and surname, and this outlinetextfield enters the user name and surname, but if the user's name and surname are less than 3 characters, I want to make the border of the outlinetextfield red, but I couldn't figure out how to do this control because space intervened.
for example :
a(space)b wrong
jac(space)jac correct
tony(space)stark correct
this is my example code:
OutlinedTextField(
value = state.name,
onValueChange = { onChangeName(it) },
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(25.dp),
label = {
Text(
text = "name and lastname",
fontSize = 14.sp,
color = Color.White
)
},
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = if (state.name.length < 7) Color.Red else DefaultDYTColor,
unfocusedBorderColor = Color.Transparent
)
)
When I do it this way, it accepts it as correct even if I type 7 characters without spaces, but it should not be like this.
for example:
tonystark
According to the code I wrote, this is correct because it is greater than 7 characters
How to achieve this issue ? Do you have any suggestion or solve ?
This is just an algorithmic problem, you can solve it like this:
fun isValidName(name: String): Boolean {
// split name on each space and filter out those that are blank (consecutive spaces)
val splits = name.split(" ").filter { it.isNotBlank() }
// we need at least 2 strings (name and surname)
// you can also use != 2 if you want exactly 2
if (splits.size < 2) return false
// if any name is less than 3 chars, return false
for (split in splits) {
if (split.trim().length < 3) {
return false
}
}
// now we have at least 2 names and all of them have 3 or more chars
return true
}

Dynamic form with composable-form

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!

This `div` call produces: Html (String -> Msg) But the type annotation on `view` says it should be: Html Msg

I'm currently learning elm, I just stumbled on this problem where the div returns a Html (String -> Msg) instead of Html Msg.
error message I'm receiving
This div call produces:
Html (String -> Msg)
But the type annotation on view says it should be:
Html Msg
type alias Model =
{
firstNum: String,
secondNum: String,
answer: String
}
init: Model
init = { firstNum = "",
secondNum = "",
answer = ""}
type Msg =
Add String| Minus String
update: Msg -> Model -> Model
update msg model =
case msg of
Add x -> { model | answer = x}
Minus y -> { model | answer = y}
view : Model -> Html Msg
view model =
div []
[
input [ placeholder "Text to reverse", value model.firstNum] [],
button [onClick Add] [text "add"],
div [] [text model.answer]
]
main =
Browser.sandbox
{ init = init,
update = update,
view = view
}
You define the Msg type as
type Msg =
Add String| Minus String
with Add taking a String argument, but when you use it here:
button [onClick Add] [text "add"],
you're not giving it any argument at all.
The underlying issue seems to be that your mental model of the Elm Architecture is wrong. You seem to consider messages as "operations" or function calls rather than events, where Add is a function that takes an argument to apply to the model.
You should instead consider a message as a description of what triggered it. Instead of Add String, you might call it AddButtonClicked, with no arguments (in this case). Then have the update function do what it should based on what's in the model alone, which I'm guessing is an arithmetic operation on firstNum and secondNum.
But you're also not populating those fields. To do so you need to use the onInput event, which does ask for a message that takes a String. You might add a new message FirstNumChanged String for example, then use it with input like this:
input [ placeholder "Text to reverse", onInput FirstNumChanged, value model.firstNum] [],
I'll leave it to you to figure out how to handle it in update.

ELM Models: How Can I Transfer A Value Into A List Of Values?

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 ] }

How to migrate from working input range to elm-mdl Slider?

I currently have a function that I use for a range, and number input type for each piece of data (trait). Updates to the model are done via EditTrait Mhid Relevance message.
valueRange : String -> TraitWithRelevance -> Html Msg
valueRange typ trait =
let
( name, mhid, relevance ) =
trait
in
input [ Attr.type_ typ, Attr.min "0", Attr.max "100", Attr.value relevance, Attr.step "1", onInput <| EditTrait mhid ] []
In an attempt to bring in Google's material design through elm-mdl, I want to replace the valueRange "range" call with the valueSlider function which utilizes the Slider component from elm-mdl.
The code below compiles, but obviously doesn't work, because the important onChange handler is missing. However, it renders correctly and the slider changes, when I update a trait's relevance value through the input number element.
valueSlider trait =
let
( name, mhid, relevance ) =
trait
relAsFloat =
String.toFloat relevance |> Result.toMaybe |> Maybe.withDefault 0
in
Slider.view
[ Slider.value relAsFloat
, Slider.min 0
, Slider.max 100
, Slider.step 1
]
When I throw in Slider.onChange (EditTrait mhid), which works on the regular input, the compiler gives me this error.
The argument to function onChange is causing a mismatch.
438| Slider.onChange (EditTrait mhid)
^^^^^^^^^^^^^^ Function onChange is expecting the argument to be:
Float -> m
But it is:
Relevance -> Msg
Detected errors in 1 module.
As onInput type is (String -> msg) -> Html.Attribute msg I suppose Relevance is a String, and EditTrait is mhid -> String -> Msg.
In this case, Slider.onChange (EditTrait mhid) doesn't work because Slider.onChange expects a Float -> Msg not Relevance -> Msg (as the compiler message reads.)
To solve this issue, you should change EditTrait to receive a Float instead of String. Changing Relevance type to be a Float and updating the code accordingly should do the trick.