From 866ec269e4f2f98a877c72eb7588eecd35200fcc Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 24 Jul 2024 10:33:58 +0200 Subject: [PATCH 1/6] WIP use attributes instead of properties (allows removal) --- src/Html/Attributes.elm | 119 +++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 63 deletions(-) diff --git a/src/Html/Attributes.elm b/src/Html/Attributes.elm index 652fc62..da828f2 100644 --- a/src/Html/Attributes.elm +++ b/src/Html/Attributes.elm @@ -159,11 +159,6 @@ property = VirtualDom.property -stringProperty : String -> String -> Attribute msg -stringProperty key string = - Elm.Kernel.VirtualDom.property key (Json.string string) - - boolProperty : String -> Bool -> Attribute msg boolProperty key bool = Elm.Kernel.VirtualDom.property key (Json.bool bool) @@ -204,7 +199,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 +213,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,7 +229,7 @@ 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. -} @@ -256,7 +251,7 @@ contextmenu = -} dir : String -> Attribute msg dir = - stringProperty "dir" + Elm.Kernel.VirtualDom.attribute "dir" {-| Defines whether the element can be dragged. -} @@ -265,10 +260,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,7 +278,7 @@ 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. -} @@ -306,7 +304,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 +328,7 @@ width n = -} alt : String -> Attribute msg alt = - stringProperty "alt" + Elm.Kernel.VirtualDom.attribute "alt" @@ -362,7 +360,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 +368,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 +382,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 +408,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 +416,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 +428,15 @@ 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 = + Elm.Kernel.VirtualDom.property "value" (Json.string string) {-| Indicates whether an `input` of type checkbox is checked. -} @@ -452,7 +450,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 +464,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 +519,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 +553,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 +570,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 +586,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 +617,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 +638,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 +646,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 +654,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 +677,7 @@ values are "hard" and "soft". -} wrap : String -> Attribute msg wrap = - stringProperty "wrap" + Elm.Kernel.VirtualDom.attribute "wrap" @@ -697,7 +699,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 +708,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 +716,7 @@ shape = -} coords : String -> Attribute msg coords = - stringProperty "coords" + Elm.Kernel.VirtualDom.attribute "coords" @@ -727,7 +729,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 +737,7 @@ align = -} cite : String -> Attribute msg cite = - stringProperty "cite" + Elm.Kernel.VirtualDom.attribute "cite" @@ -746,7 +748,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 +763,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 +778,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 +801,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 +849,7 @@ besides 1. -} start : Int -> Attribute msg start n = - stringProperty "start" (String.fromInt n) + Elm.Kernel.VirtualDom.attribute "start" (String.fromInt n) @@ -876,7 +869,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 +885,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 +898,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) --} From cc9238e09b79debd6562e82a5befb3515e8dfbb2 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Wed, 24 Jul 2024 14:13:08 +0200 Subject: [PATCH 2/6] Go through boolProperty --- src/Html/Attributes.elm | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Html/Attributes.elm b/src/Html/Attributes.elm index da828f2..40e7808 100644 --- a/src/Html/Attributes.elm +++ b/src/Html/Attributes.elm @@ -159,6 +159,22 @@ property = VirtualDom.property +{-| 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) @@ -232,10 +248,16 @@ accesskey 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 autocomplete 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" + -- Note: `node.contentEditable = 'bad'` thrown 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 @@ -283,8 +305,11 @@ 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 @@ -436,6 +461,8 @@ type_ = -} value : String -> Attribute msg value string = + -- Note: `.value` has no corresponding attribute. It can also be modified by + -- the user by typing in inputs. Elm.Kernel.VirtualDom.property "value" (Json.string string) From e6db1f1b4edb377a3972f549fe62e2d34fdc6a46 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 4 Aug 2024 21:08:26 +0200 Subject: [PATCH 3/6] Clarify value property --- src/Html/Attributes.elm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Html/Attributes.elm b/src/Html/Attributes.elm index 40e7808..5e4896d 100644 --- a/src/Html/Attributes.elm +++ b/src/Html/Attributes.elm @@ -461,8 +461,10 @@ type_ = -} value : String -> Attribute msg value string = - -- Note: `.value` has no corresponding attribute. It can also be modified by - -- the user by typing in inputs. + -- 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) From 3c9742fce28645f42b371d0e83c9ac1dd7a5b803 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 19 Jan 2025 14:21:18 +0100 Subject: [PATCH 4/6] Add missing bool parameter to contenteditable --- src/Html/Attributes.elm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Html/Attributes.elm b/src/Html/Attributes.elm index 5e4896d..b8e6a79 100644 --- a/src/Html/Attributes.elm +++ b/src/Html/Attributes.elm @@ -255,7 +255,7 @@ Note: These days, the autocomplete attribute can take more values than a boolean attribute "contenteditable" "inherit" -} contenteditable : Bool -> Attribute msg -contenteditable = +contenteditable bool = -- Note: `node.contentEditable = 'bad'` thrown an error! Elm.Kernel.VirtualDom.attribute "contenteditable" (if bool then "true" else "false") From 6d02bc5dda833b93ccb86c2f2c11369820e30703 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sat, 25 Jan 2025 22:01:04 +0100 Subject: [PATCH 5/6] Document why attributes are preferred --- properties-vs-attributes.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/properties-vs-attributes.md b/properties-vs-attributes.md index 41f7565..e1213ff 100644 --- a/properties-vs-attributes.md +++ b/properties-vs-attributes.md @@ -12,4 +12,14 @@ Notice that the attribute is called `class` but the property is called `classNam It is actually a bit crazier than that though. **Sometimes an attribute exists, but there is no corresponding property.** For example, as of this writing the `webkit-playsinline` can be added with `setAttribute`, but there is no corresponding property. And with SVG, you cannot use properties at all, you must to use `setAttributeNS` for everything. -With all the corner cases here, it makes sense to have access to both approaches. \ No newline at end of file +With all the corner cases here, it makes sense to have access to both approaches. + +The functions in `Html.Attributes` are generally implemented using attributes (except for a few things that _have_ to be properties). Using attributes have a couple of benefits for standard attributes: + + 1. It’s possible to remove an attribute, but not always possible to remove a property. For example, the HTML `` results in a link without any `href`. The specification defines that as a link placeholder, and a use case is a menu where the current page does not need to link to itself. But `.href` is actually set to the empty string (`""`) on the DOM node. The HTML `` results in a link _with_ a `href`. If you have just ``, you can either do `.href = "/about"` or `.setAttribute("href", "/about")` to set the `href`. But how do you remove the attribute again? If you try `.href = ""`, that actually results in `` (which is not the same as the `href` attribute being missing), while `.removeAttribute("href")` does result in ``. + + 2. When initializing an Elm app, you give it a DOM node to render into, or the `` node is used. What happens if the node to render into isn’t empty? Elm then _virtualizes_ the DOM into virtual DOM. If the DOM nodes match what your app’s first render, the virtual DOM diffing won’t find any changes to make. This allows for server side rendering HTML, and then have Elm take over that content and avoiding lots of work at startup. Let’s take `` 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 `