` as an example again. When virtualizing, Elm has to make a guess: Did you use `property "href" (Json.Encode.string "/about")` or `attribute "href" "/about"` in your Elm code? If we guess wrong, the first render will do an unnecessary DOM update, and if we guess right the first render won’t do anything as expected. Another complication is that attributes and properties don’t always have the same name, as mentioned above. When virtualizing DOM nodes, it’s possible to loop over all _attributes_ that are set, but it’s not possible to loop over properties. For ``, a loop over attributes would get that `class` attribute, but if `Html.Attributes.class` is implemented with a property, we would have to translate `class` into `className` with a lookup table. By primarily using attributes in `Html.Attributes`, we don’t need a lookup table (except for a couple of edge cases).
+
+ 3. Some properties are read only – if you try to assign them an error is thrown. The most notable example of this is trying to do `.className = "my-class"` on an SVG element. That throws an error, while `.setAttribute("class", "my-class")` works. In Elm, both `Html msg` and `Svg msg` are type aliases for the same virtual DOM node type, so you can mix functions from elm/html and elm/svg without getting type errors. `Html.Attributes.class` used to be implemented by setting the `className` property, and a common mistake was accidentally using using `Html.Attributes.class` on an SVG element, instead of `Svg.Attributes.class` (which is implemented by setting the `class` attribute), which would then cause hard to debug runtime errors. By setting the `class` attribute in `Html.Attributes.class` this issue is avoided.
+
+ 4. Attributes are easier to diff. If we take the example with the link again, if you do `.href = "/about"`, then what is the value of `.href` afterwards? You might think it’s `"/about"`, but it’s actually `"https://example.com/about"` (if the code is running on `https://example.com`). `.href` returns the _full_ URL. If you set it to a non-full URL, the browser resolves it to the full URL. This is important because some properties change automatically through user interactions. For example, `.value` of a text input updates automatically as the user types into it. But if you have code like `Html.input [ Html.Attributes.value "hardcoded" ]` you would expect the input to always say “hardcoded”. If the virtual DOM diffing compares the old and new virtual DOM, it’ll see that both of them say that `value` should be `"hardcoded"` and decided that no update is needed. But in reality, the input displays whatever the user has typed. For this reason, it sounds like a good idea to instead diff the new virtual DOM against the actual DOM node when it comes to properties. The downside is the `href` example, though: If the virtual DOM says that `href` should be `"/about"` it will never be equal to the `.href` property on the DOM node, since it’s `"https://example.com/about"`, which would result in us setting `.href = "/about"` on _every_ render. Again, this is a reason to prefer attributes where possible. `value` is a good example of where a _property_ is used on purpose: We _need_ to set it on every render to make sure that it is set to the desired value.
diff --git a/src/Html/Attributes.elm b/src/Html/Attributes.elm
index 652fc62..2b561d1 100644
--- a/src/Html/Attributes.elm
+++ b/src/Html/Attributes.elm
@@ -159,11 +159,22 @@ property =
VirtualDom.property
-stringProperty : String -> String -> Attribute msg
-stringProperty key string =
- Elm.Kernel.VirtualDom.property key (Json.string string)
+{-| This is used for attributes that have a property that:
+- Is boolean.
+- Defaults to `false`.
+- Removes the attribute when setting to `false`.
+Note:
+
+- Some properties, like `checked`, can be modified by the user.
+- `.setAttribute(property, "false")` does not set the property to `false` – we have to remove the attribute. (Except `spellcheck` which explicitly has a "false" (and "true") value.)
+
+Consider `hidden : Bool -> Attribute msg`. When that `Bool` is `True`, we could implement the function with `attribute "hidden" ""`. (Using the empty string seems to be “canonical”, but any string would make the element hidden.) But what do we do when the `Bool` is `False`? The intention is to make the element _not_ hidden. The only way of doing that is to remove the `hidden` attribute, but we cannot do that with `attribute` – it always results in the attribute being present (we can only choose its value, but no value will result in the element _not_ being hidden). To keep this API, we _have_ to use the `hidden` _property_ instead, which (like mentioned above) automatically removes the attribute when set to `false`.
+
+An alternative would be to have `hidden : Attribute msg` and let users do `if shouldHide then hidden else ???` where `???` would have to be a way to express a no-op `Attribute msg`, or the user has to resort to list manipulation.
+
+-}
boolProperty : String -> Bool -> Attribute msg
boolProperty key bool =
Elm.Kernel.VirtualDom.property key (Json.bool bool)
@@ -204,7 +215,7 @@ you will get both classes!
-}
class : String -> Attribute msg
class =
- stringProperty "className"
+ Elm.Kernel.VirtualDom.attribute "class"
{-| Indicates the relevance of an element. -}
@@ -218,13 +229,13 @@ attribute must be unique.
-}
id : String -> Attribute msg
id =
- stringProperty "id"
+ Elm.Kernel.VirtualDom.attribute "id"
{-| Text to be displayed in a tooltip when hovering over the element. -}
title : String -> Attribute msg
title =
- stringProperty "title"
+ Elm.Kernel.VirtualDom.attribute "title"
@@ -234,13 +245,19 @@ title =
{-| Defines a keyboard shortcut to activate or add focus to the element. -}
accesskey : Char -> Attribute msg
accesskey char =
- stringProperty "accessKey" (String.fromChar char)
+ Elm.Kernel.VirtualDom.attribute "accesskey" (String.fromChar char)
-{-| Indicates whether the element's content is editable. -}
+{-| Indicates whether the element's content is editable.
+
+Note: These days, the contenteditable attribute can take more values than a boolean, like "inherit" and "plaintext-only". You can set those values like this:
+
+ attribute "contenteditable" "inherit"
+-}
contenteditable : Bool -> Attribute msg
-contenteditable =
- boolProperty "contentEditable"
+contenteditable bool =
+ -- Note: `node.contentEditable = 'bad'` throws an error!
+ Elm.Kernel.VirtualDom.attribute "contenteditable" (if bool then "true" else "false")
{-| Defines the ID of a `menu` element which will serve as the element's
@@ -256,7 +273,7 @@ contextmenu =
-}
dir : String -> Attribute msg
dir =
- stringProperty "dir"
+ Elm.Kernel.VirtualDom.attribute "dir"
{-| Defines whether the element can be dragged. -}
@@ -265,10 +282,13 @@ draggable =
Elm.Kernel.VirtualDom.attribute "draggable"
-{-| Indicates that the element accept the dropping of content on it. -}
+{-| Indicates that the element accept the dropping of content on it.
+
+Note: This attribute or property seems to no longer exist.
+-}
dropzone : String -> Attribute msg
dropzone =
- stringProperty "dropzone"
+ Elm.Kernel.VirtualDom.attribute "dropzone"
{-|-}
@@ -280,13 +300,16 @@ itemprop =
{-| Defines the language used in the element. -}
lang : String -> Attribute msg
lang =
- stringProperty "lang"
+ Elm.Kernel.VirtualDom.attribute "lang"
{-| Indicates whether spell checking is allowed for the element. -}
spellcheck : Bool -> Attribute msg
-spellcheck =
- boolProperty "spellcheck"
+spellcheck bool =
+ -- Note: The spellcheck _property_ defaults to `true`, unlike other boolean properties.
+ -- Setting it back to the default value does _not_ remove the attribute.
+ -- Because of this, we set it using an attribute instead.
+ Elm.Kernel.VirtualDom.attribute "spellcheck" (if bool then "true" else "false")
{-| Overrides the browser's default tab order and follows the one specified
@@ -306,7 +329,7 @@ tabindex n =
-}
src : String -> Attribute msg
src url =
- stringProperty "src" (Elm.Kernel.VirtualDom.noJavaScriptOrHtmlUri url)
+ Elm.Kernel.VirtualDom.attribute "src" (Elm.Kernel.VirtualDom.noJavaScriptOrHtmlUri url)
{-| Declare the height of a `canvas`, `embed`, `iframe`, `img`, `input`,
@@ -330,7 +353,7 @@ width n =
-}
alt : String -> Attribute msg
alt =
- stringProperty "alt"
+ Elm.Kernel.VirtualDom.attribute "alt"
@@ -362,7 +385,7 @@ loop =
{-| Control how much of an `audio` or `video` resource should be preloaded. -}
preload : String -> Attribute msg
preload =
- stringProperty "preload"
+ Elm.Kernel.VirtualDom.attribute "preload"
{-| A URL indicating a poster frame to show until the user plays or seeks the
@@ -370,7 +393,7 @@ preload =
-}
poster : String -> Attribute msg
poster =
- stringProperty "poster"
+ Elm.Kernel.VirtualDom.attribute "poster"
{-| Indicates that the `track` should be enabled unless the user's preferences
@@ -384,21 +407,21 @@ default =
{-| Specifies the kind of text `track`. -}
kind : String -> Attribute msg
kind =
- stringProperty "kind"
+ Elm.Kernel.VirtualDom.attribute "kind"
{-- TODO: maybe reintroduce once there's a better way to disambiguate imports
{-| Specifies a user-readable title of the text `track`. -}
label : String -> Attribute msg
label =
- stringProperty "label"
+ Elm.Kernel.VirtualDom.attribute "label"
--}
{-| A two letter language code indicating the language of the `track` text data.
-}
srclang : String -> Attribute msg
srclang =
- stringProperty "srclang"
+ Elm.Kernel.VirtualDom.attribute "srclang"
@@ -410,7 +433,7 @@ srclang =
-}
sandbox : String -> Attribute msg
sandbox =
- stringProperty "sandbox"
+ Elm.Kernel.VirtualDom.attribute "sandbox"
{-| An HTML document that will be displayed as the body of an `iframe`. It will
@@ -418,7 +441,7 @@ override the content of the `src` attribute if it has been specified.
-}
srcdoc : String -> Attribute msg
srcdoc =
- stringProperty "srcdoc"
+ Elm.Kernel.VirtualDom.attribute "srcdoc"
@@ -430,15 +453,19 @@ srcdoc =
-}
type_ : String -> Attribute msg
type_ =
- stringProperty "type"
+ Elm.Kernel.VirtualDom.attribute "type"
{-| Defines a default value which will be displayed in a `button`, `option`,
`input`, `li`, `meter`, `progress`, or `param`.
-}
value : String -> Attribute msg
-value =
- stringProperty "value"
+value string =
+ -- Note: `.value` has no corresponding attribute, so we have to set it
+ -- using a property. It can also be modified by the user by typing in inputs.
+ -- Properties are diffed against the actual DOM, not the virtual DOM, so
+ -- this ensures that the DOM is up-to-date with the model.
+ Elm.Kernel.VirtualDom.property "value" (Json.string string)
{-| Indicates whether an `input` of type checkbox is checked. -}
@@ -452,7 +479,7 @@ checked =
-}
placeholder : String -> Attribute msg
placeholder =
- stringProperty "placeholder"
+ Elm.Kernel.VirtualDom.attribute "placeholder"
{-| Defines which `option` will be selected on page load. -}
@@ -466,33 +493,37 @@ selected =
{-| List of types the server accepts, typically a file type.
-For `form` and `input`.
+For `input`.
-}
accept : String -> Attribute msg
accept =
- stringProperty "accept"
+ Elm.Kernel.VirtualDom.attribute "accept"
{-| List of supported charsets in a `form`.
-}
acceptCharset : String -> Attribute msg
acceptCharset =
- stringProperty "acceptCharset"
+ Elm.Kernel.VirtualDom.attribute "accept-charset"
{-| The URI of a program that processes the information submitted via a `form`.
-}
action : String -> Attribute msg
action uri =
- stringProperty "action" (Elm.Kernel.VirtualDom.noJavaScriptUri uri)
+ Elm.Kernel.VirtualDom.attribute "action" (Elm.Kernel.VirtualDom.noJavaScriptUri uri)
{-| Indicates whether a `form` or an `input` can have their values automatically
completed by the browser.
+
+Note: These days, the autocomplete attribute can take more values than a boolean. For example, you can use this to autocomplete a street address:
+
+ attribute "autocomplete" "street-address"
-}
autocomplete : Bool -> Attribute msg
autocomplete bool =
- stringProperty "autocomplete" (if bool then "on" else "off")
+ Elm.Kernel.VirtualDom.attribute "autocomplete" (if bool then "on" else "off")
{-| The element should be automatically focused after the page loaded.
@@ -517,7 +548,7 @@ text/plain.
-}
enctype : String -> Attribute msg
enctype =
- stringProperty "enctype"
+ Elm.Kernel.VirtualDom.attribute "enctype"
{-| Associates an `input` with a `datalist` tag. The datalist gives some
@@ -551,7 +582,7 @@ maxlength n =
-}
method : String -> Attribute msg
method =
- stringProperty "method"
+ Elm.Kernel.VirtualDom.attribute "method"
{-| Indicates whether multiple values can be entered in an `input` of type
@@ -568,7 +599,7 @@ in form submits. For `button`, `form`, `fieldset`, `iframe`, `input`,
-}
name : String -> Attribute msg
name =
- stringProperty "name"
+ Elm.Kernel.VirtualDom.attribute "name"
{-| This attribute indicates that a `form` shouldn't be validated when
@@ -584,7 +615,7 @@ against.
-}
pattern : String -> Attribute msg
pattern =
- stringProperty "pattern"
+ Elm.Kernel.VirtualDom.attribute "pattern"
{-| Indicates whether an `input` or `textarea` can be edited. -}
@@ -615,7 +646,7 @@ for an `output`.
-}
for : String -> Attribute msg
for =
- stringProperty "htmlFor"
+ Elm.Kernel.VirtualDom.attribute "for"
{-| Indicates the element ID of the `form` that owns this particular `button`,
@@ -636,7 +667,7 @@ date, the max value must be a number or date. For `input`, `meter`, and `progres
-}
max : String -> Attribute msg
max =
- stringProperty "max"
+ Elm.Kernel.VirtualDom.attribute "max"
{-| Indicates the minimum value allowed. When using an input of type number or
@@ -644,7 +675,7 @@ date, the min value must be a number or date. For `input` and `meter`.
-}
min : String -> Attribute msg
min =
- stringProperty "min"
+ Elm.Kernel.VirtualDom.attribute "min"
{-| Add a step size to an `input`. Use `step "any"` to allow any floating-point
@@ -652,7 +683,7 @@ number to be used in the input.
-}
step : String -> Attribute msg
step n =
- stringProperty "step" n
+ Elm.Kernel.VirtualDom.attribute "step" n
--------------------------
@@ -675,7 +706,7 @@ values are "hard" and "soft".
-}
wrap : String -> Attribute msg
wrap =
- stringProperty "wrap"
+ Elm.Kernel.VirtualDom.attribute "wrap"
@@ -697,7 +728,7 @@ E.g. `"#planet-map"`.
-}
usemap : String -> Attribute msg
usemap =
- stringProperty "useMap"
+ Elm.Kernel.VirtualDom.attribute "usemap"
{-| Declare the shape of the clickable area in an `a` or `area`. Valid values
@@ -706,7 +737,7 @@ include: default, rect, circle, poly. This attribute can be paired with
-}
shape : String -> Attribute msg
shape =
- stringProperty "shape"
+ Elm.Kernel.VirtualDom.attribute "shape"
{-| A set of values specifying the coordinates of the hot-spot region in an
@@ -714,7 +745,7 @@ shape =
-}
coords : String -> Attribute msg
coords =
- stringProperty "coords"
+ Elm.Kernel.VirtualDom.attribute "coords"
@@ -727,7 +758,7 @@ coords =
-}
align : String -> Attribute msg
align =
- stringProperty "align"
+ Elm.Kernel.VirtualDom.attribute "align"
{-| Contains a URI which points to the source of the quote or change in a
@@ -735,7 +766,7 @@ align =
-}
cite : String -> Attribute msg
cite =
- stringProperty "cite"
+ Elm.Kernel.VirtualDom.attribute "cite"
@@ -746,7 +777,7 @@ cite =
{-| The URL of a linked resource, such as `a`, `area`, `base`, or `link`. -}
href : String -> Attribute msg
href url =
- stringProperty "href" (Elm.Kernel.VirtualDom.noJavaScriptUri url)
+ Elm.Kernel.VirtualDom.attribute "href" (Elm.Kernel.VirtualDom.noJavaScriptUri url)
{-| Specify where the results of clicking an `a`, `area`, `base`, or `form`
@@ -761,7 +792,7 @@ You can also give the name of any `frame` you have created.
-}
target : String -> Attribute msg
target =
- stringProperty "target"
+ Elm.Kernel.VirtualDom.attribute "target"
{-| Indicates that clicking an `a` and `area` will download the resource
@@ -776,23 +807,14 @@ The empty `String` says to just name it whatever it was called on the server.
-}
download : String -> Attribute msg
download fileName =
- stringProperty "download" fileName
-
-
-{-| Indicates that clicking an `a` and `area` will download the resource
-directly, and that the downloaded resource with have the given filename.
-So `downloadAs "hats.json"` means the person gets a file named `hats.json`.
--}
-downloadAs : String -> Attribute msg
-downloadAs =
- stringProperty "download"
+ Elm.Kernel.VirtualDom.attribute "download" fileName
{-| Two-letter language code of the linked resource of an `a`, `area`, or `link`.
-}
hreflang : String -> Attribute msg
hreflang =
- stringProperty "hreflang"
+ Elm.Kernel.VirtualDom.attribute "hreflang"
{-| Specifies a hint of the target media of a `a`, `area`, `link`, `source`,
@@ -808,7 +830,7 @@ media =
-}
ping : String -> Attribute msg
ping =
- stringProperty "ping"
+ Elm.Kernel.VirtualDom.attribute "ping"
{-| Specifies the relationship of the target object to the link object.
@@ -856,7 +878,7 @@ besides 1.
-}
start : Int -> Attribute msg
start n =
- stringProperty "start" (String.fromInt n)
+ Elm.Kernel.VirtualDom.attribute "start" (String.fromInt n)
@@ -876,7 +898,7 @@ headers for this cell. For `td` and `th`.
-}
headers : String -> Attribute msg
headers =
- stringProperty "headers"
+ Elm.Kernel.VirtualDom.attribute "headers"
{-| Defines the number of rows a table cell should span over.
@@ -892,7 +914,7 @@ colgroup, rowgroup.
-}
scope : String -> Attribute msg
scope =
- stringProperty "scope"
+ Elm.Kernel.VirtualDom.attribute "scope"
{-| Specifies the URL of the cache manifest for an `html` tag. -}
@@ -905,5 +927,5 @@ manifest =
{-| The number of columns a `col` or `colgroup` should span. -}
span : Int -> Attribute msg
span n =
- stringProperty "span" (String.fromInt n)
+ Elm.Kernel.VirtualDom.attribute "span" (String.fromInt n)
--}