Let's get started creating a control panel full of “knobs” to interactively tweak values in our application.
When creating a knob, two considerations are important. The first is the type
of the value you need to control. This package currently provides knobs for
numbers, strings, booleans, enumerated choices (custom types or anything like
that) and colors out of the box, and there is a way to either transform one into
another type (map), or to create an entirely new knob
(custom).
The second important consideration is the interface you want to provide to manipulate that value, i.e. the control itself. Many knobs offer different controls for the same type, particularly number-related ones, so pick the one that best suits your needs!
First up, within our app's init let's create a Knob and put it in the model.
The following are the functions you can use to create basic knobs that map to a
single value.
👀 Tip: Check the interactive documentation to see working examples of these!
Creates an input field knob for manually entering any floating point number.
The step argument specifies the amount the number will increase or decrease
when pressing the up and down keys.
initial is just the value it takes on first load.
Creates an input field knob for manually entering numbers within a specific range,
where range = ( min, max ).
The step argument specifies the amount the number will increase or decrease
when pressing the up and down keys.
initial is just the value it takes on first load.
Creates a slider knob useful for quickly tweaking numbers when precision is not needed.
Requires a range = ( min, max ) to set the boundaries of the slider control.
initial is just the value it takes on first load.
The step argument indicates the granularity of the values the slider will allow,
so a step of 1 will produce a slider that jumps between values like 1, 2, 3 as you slide it to the right,
whereas a step of 0.1 will produce one that allows setting values like 1.1 or 2.5.
Creates an input field knob for manually entering any integer.
The step argument specifies the amount the number will increase or decrease
when pressing the up and down keys.
initial is just the value it takes on first load.
Creates an input field knob for manually entering integers within a specific range.
The step argument specifies the amount the number will increase or decrease
when pressing the up and down keys.
initial is just the value it takes on first load.
Creates a slider knob useful for quickly tweaking integers when precision is not needed.
Requires a range = ( min, max ) to set the boundaries of the slider control.
initial is just the value it takes on first load.
The step argument indicates the granularity of the values the slider will allow,
so a step of 1 will produce a slider that will set on any integer value as you slide it to the right,
whereas a step of 10 will produce one that makes bigger jumps between 10, 20, 30, etc.
Creates a small, single-line input field knob for entering text.
initial is the text it will be prefilled with.
Creates a multiline input field knob for entering text.
You can specify the dimensions of this control by setting the amount of columns and rows.
initial is the text it will be prefilled with.
Creates a checkbox representing a boolean value.
initial determines whether it will be initially checked or not.
Creates a dropdown select input for a custom type or arbitrary values of any
type you wish. You'll need to provide a List mapping unique strings (options
in the dropdown) to values of your type.
Here's a simple example mapping "yes" and "no" options to Bool values:
Knob.select
{ options =
[ ( "yes", True )
, ( "no", False )
]
, initial = False
}Represents an RGB color value. Each channel value is a Float between 0 and 1.
This is the type that the colorPicker knob uses.
Creates a color picker input.
Colors are represented using a type alias Color,
which is easily mappable into other color formats for your convenience.
Below is an example mapping it into avh4/elm-color format.
-- We set magenta as the initial color.
Knob.colorPicker { red = 1, green = 0, blue = 1 }
-- We map it into avh4/elm-color format.
|> Knob.map (\c -> Color.rgb c.red c.green c.blue)The next step is to actually display our knob in the page.
Converts a knob into HTML to put in your view. You should display a single
Knob value at any which time, so if you need multiple knobs, make
sure you compose them!
Knobs keep track of their state once they're put in the view, but for that you need to wire them up with a message, which is the second argument that this function takes.
This function produces plain HTML with no styles, so make sure you also include
styles in your page to make it display properly, or provide
your own custom styles.
-- Prepare a message for your knob:
type Msg =
KnobUpdated (Knob YourType)
-- Put this as an HTML node within your view:
Knob.view [] KnobUpdated yourKnobCheck the documentation's readme for a full demonstration on how to wire things up.
By default, it puts the panel in the lower-left corner of the viewport,
hidden until you hover over its icon. However, view takes a list of options
that change the way the knobs panel is rendered, which you can find in the
Knob.Option module. Be aware that these options may only change
the CSS classes that are added to the HTML, so if you don't use the provided
styles, the effect might not be what you expected.
Default styles for the knobs, provided as a <style> tag.
Put this as a child somewhere in your view in order to use them.
You could choose not to use these default styles and instead provide your own. I recommend you check the DOM output in your browser's inspector—the structure of the HTML produced is pretty simple!
Of course, our knobs are of no use to us if we can't read the value entered by the user.
Extract the current value out of a knob. Use it in your view to affect what you display.
Knob.int { step = 1, initial = 5 }
|> Knob.value -- Gets `5`.Most of the time you'll want to control multiple values. For that purpose we're going to “stack” our knobs together.
Creates a knob that joins multiple knobs to build up a record
(or actually any data structure you want, depending on the constructor argument you pass it!)
Pipe (|>) the knobs into it
using stack or stackLabel in order to provide the arguments.
type alias Controls =
{ someNumber : Float
, anInteger : Int
}
aKnob =
Knob.compose Controls
-- This knob will map to `someNumber`:
|> Knob.stack (Knob.float { step = 1, initial = 0 })
-- This one will map to `anInteger`:
|> Knob.stack (Knob.int { step = 1, initial = 0 })Here's how you use it to build up a different data structure, in this case a tuple. Notice that the number of arguments in the function matches the number of “stacks”.
anotherKnob =
Knob.compose (\theFloat theInt -> ( theFloat, theInt ))
|> Knob.stack (Knob.float { step = 1, initial = 0 })
|> Knob.stack (Knob.int { step = 1, initial = 0 })Adds a knob into a compose knob.
See the documentation for that for an example.
This function is called “stack” because the order you compose your knobs does matter, as they will be displayed one on top of the other!
We could have a bunch of similar knobs in our panel and not know what each of them does, so let's make sure we do!
Attaches a text description next to a knob, as a way to identify what the control is for.
The following example will produce a float knob described as “x position”.
Knob.label "x position"
(Knob.float { step = 1, initial = 0 })Analogous to List.map or other data structures' map function,
you can use this function to convert the value produced by a Knob.
The following example converts a knob that produces an Int (i.e. a Knob Int)
into one that produces a String (i.e. a Knob String.) This is achieved
because String.fromInt is a function with the type Int -> String.
Knob.int { step = 1, initial = 0 }
|> Knob.map String.fromIntThe value of your knobs will be reset every time you refresh the page, unless you persist their value somehow. Knob serialization is a way to make it easier to do this using the Web Storage API or other such techniques. Check this example to see how to do it.
Convert a knob's value into an elm/json Value.
You can then send this out to JavaScript via a port,
and store it using the browser's Web Storage API,
for example. You'll want to use in your update function whenever you get
an updated knob.
port saveKnobState : Json.Encode.Value -> Cmd msg
update msg model =
case msg of
KnobUpdated updatedKnob ->
( { model | knob = updatedKnob }
, saveKnobState (Knob.serialize updatedKnob)
)After you have used serialize to store your knob's value
somewhere, the way to get that value back into the knob is this function. You'll
probably want to use it on init with the serialized data you get from flags.
If this function fails to interpret the passed value, the knob will just
retain its initial value. It works with single, composed and
mapped knobs. For custom knobs it will only work if
serialization was properly defined when created.
Notice that you need to create your knob with initial values normally, and as a last step use this function to update it with the serialized value.
init serializedKnob =
( { knob =
Knob.int { step = 1, init = 0 }
|> readSerialized serializedKnob
}
, Cmd.none
)Creates a knob for any type, using a custom HTML view that you supply.
You can use this function if there is some kind of knob you need that is not
available in this package, and can't be created by using map over a
predefined knob.
Knobs are comprised of a value of the appropriate type, and a view which
listens to user input (typically the Html.Events.onInput event) and emits the
updated knob, instead of a regular message like is normally done. In this sense,
knobs are recursive, as their view needs to construct a new knob, typically by
calling the very same constructor function that created it in the first place.
Here's how the boolCheckbox knob would be created using custom:
ourBoolKnob : Bool -> Knob Bool
ourBoolKnob initial =
let
view : () -> Html (Knob Bool)
view () =
Html.input
[ Html.Attributes.type_ "checkbox"
, Html.Attributes.checked initial
, Html.Events.onCheck ourBoolKnob
]
[]
in
Knob.custom
{ value = initial
, view = view
, serialization = Nothing
}(Ignore the serialization part for now.)
Notice how view is a thunk—that is, a function that takes () (a placeholder
value) and returns the view. The view is just some HTML that emits knobs instead
of messages. Take a look at the line with Html.Events.onCheck and make note
of what we're doing: We're directly passing in ourBoolKnob because it's a
function that takes the new “checked” value and with it constructs the knob
anew. This is how we're transforming the contained value when the user clicks.
If you need to serialize your knob, you'll need to take care
of that by setting the serialization field in the configuration record. You'll
need elm/json for
this.
This is a record wrapped in Maybe, with an encode function which should
return the current value converted into a Json.Encode.Value, and a decoder
of type Json.Decode.Decoder that reconstructs the whole knob out of a value
created by encode. It's easy to get this pair wrong, so make sure you test
serializing and deserializing!
This would work for the above ourCustomBool example:
Just
{ encode = \() -> Json.Encode.bool initial
, decoder = Json.Decode.map ourCustomBool Json.Decode.bool
}Be careful! If you don't have experience writing forms in Elm, there's a very common pitfall you have to be aware of: When taking unconstrained user input, you can wind up making it so that the user cannot input freely. This occurs when we parse the input into a different type in a lossy manner. For example, if the input is through typing in a text field, and we convert that into a custom type, the user might not be able to actually finish typing.
Let's imagine we want to display a text field to map a String to a Vegetable
type. In the naïve case, our knob's signature could look like this, taking
Vegetable to set the initial value:
vegetableKnob : Vegetable -> Knob VegetableIt would convert the initial value to a String, and set that as the text
field's text. Then, upon user input, we'd parse the input String into our
type, and use that to construct the updated knob.
The problem with this situation is that if the conversion from String to
Vegetable and then back into String is lossy (i.e., the result is not the
same as what the user typed,) then the user won't be able to finish typing some
things, as they won't be valid in their incomplete form.
Say we use this function to parse the raw input into Vegetable:
vegetableFromString : String -> Vegetable
vegetableFromString text =
case text of
"carrot" ->
Carrot
_ ->
TomatoIf the user wants to type “carrot”, and starts typing “c”, since "c" is not
(yet) a valid value, it will be parsed as Tomato and the text field's text
will be reset to whatever the string value for Tomato is.
In order to prevent this behavior, take the raw, unparsed String as the text
field's value property, and set the parsed result as the knob's value. This
means that your knob will need to take a String as its initial value, and not
a Vegetable!
vegetableKnob : String -> Knob Vegetable
vegetableKnob initial =
Knob.custom
{ -- Parse here!
value = vegetableFromString initial
, view =
\() ->
Html.input
[ Html.Attributes.type_ "text"
-- No parsing.
, Html.Attributes.value initial
, Html.Events.onInput vegetableKnob
]
[]
, serialization = Nothing
}
Represents one user-interactive control mapped to one value of type
a, which this package refers to as a “knob”. This is the base type used to create your knobs control panel! Normally you'll have a single one of these stored in your model, but that one knob can actually represent a group ofcomposed knobs.