Elm: Use `Address String Action` - elm

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.

Related

How to trigger Cmd from view in Elm

I'm new to Elm and I ran into this problem...
We get translations for our page using something like:
case (translate translation.id) of
Success: -> translation
Failure: -> translation.id
Where translate just finds translation.id in a dictionary and it may or may not be there.
There are no runtime errors because you get a string either way, but we would like to log the missing translation to a rest service logger. But Elm hates side effects in the view that doesn't stem from html events so I'm not sure how to handle this.
Obviously in regular JS you could just crowbar in a fetch inside the failure case block and then return a string afterwards but that doesn't seem to be possible in Elm?
You need to move the effects part of your code into the update function. In this case I suggest doing it when the translation arrives. There you will want something like
OnTranslation translation ->
( { model | translation = translation } -- attach to model
, case translate translation.id of
Ok _ ->
Cmd.none
Err err ->
-- register with the error logger
logMissingTranslation translation.id
)
#Odin Thorsen, use Html.node and Html.Attributes.attribute to reference a custom element in Elm:
-- VIEW
view : Model -> Html.Html ()
view model =
Html.div []
[ translation model "key"
, translation model "junk key"
]
translation : Model -> String -> Html.Html ()
translation { translations } id =
Html.node "translated-text"
[ Html.Attributes.attribute "translation-id" id
, Html.Attributes.attribute "translation-text" <| translate translations id
]
[]
translate : Dict.Dict String String -> String -> String
translate translations id =
case Dict.get id translations of
Just value ->
value
Nothing ->
""
And in Javascript, use customElements.define to create the custom element:
customElements.define("translated-text", class extends HTMLElement {
constructor() { super(); }
connectedCallback() { }
attributeChangedCallback() { this.setTextAndLogFailure(); }
static get observedAttributes() { return ["translation-id", "translation-text"]; }
setTextAndLogFailure() {
var id = this.getAttribute("translation-id");
var text = this.getAttribute("translation-text");
if (text === null) return;
this.textContent = text;
if (!text.length) alert("Unkown translation id: " + id);
}
});
You'd replace alert("Unkown translation id: " + id); with the logging fetch.
Here is an Ellie with a full solution.

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.

onInput argument failure (by Elm n00b)

I am experimenting with Elm based on their tutorial, and is encountering a problem with function argument declaration.
Basically I just extracted a function within the tutorial code. It works fine without function declaration, but fails when I include it.
The essence of the code is:
type Msg
= Name String
| Password String
view : Model -> Html Msg
view model =
div []
[ myInput "text" "Name" Name
]
myInput : String -> String -> Msg -> Html Msg
myInput type__ label handle =
input [ type_ type__, placeholder label, onInput Name ] []
And the error message is:
The 3rd argument to function myInput is causing a mismatch.
47| myInput "text" "Name" Name
Function myInput is expecting the 3rd argument to be:
Msg
But it is:
String -> Msg
Hint: It looks like a function needs 1 more argument.
Ideally I would also like the argument to onInput be the argument called "handle", declared in myInput.
The type signature for myInput is incorrect. The constructor Name has a single argument, which means when used as a function its signature is (String -> Msg). That is what you should use for the annotation of the handle parameter.
myInput : String -> String -> (String -> Msg) -> Html Msg
myInput type__ label handle =
input [ type_ type__, placeholder label, onInput handle ] []

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