fix multiselect combobox
This commit is contained in:
@@ -7,6 +7,7 @@ package vdom
|
||||
import (
|
||||
"html"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -100,6 +101,41 @@ func (p propMod) apply(n *VNode) { n.Props[p.k] = p.v }
|
||||
// Prop sets a live DOM property (e.g. an input's value).
|
||||
func Prop(k, v string) Mod { return propMod{k, v} }
|
||||
|
||||
// BoolProp sets a boolean DOM property: checked, disabled, open, and the rest of
|
||||
// boolProps below.
|
||||
//
|
||||
// Use this rather than Prop for those — a boolean written as a string is wrong in BOTH
|
||||
// directions, and wrong in the same direction both times, which is what makes it such a
|
||||
// good hiding place for a bug. In HTML a boolean attribute is presence-based, so
|
||||
// `checked="false"` renders a TICKED box. In JS every non-empty string is truthy, so
|
||||
// `el.checked = "false"` also ticks it. A checkbox bound with Prop is therefore ticked
|
||||
// forever, and looks fine until you try to untick it.
|
||||
func BoolProp(k string, on bool) Mod { return propMod{k, strconv.FormatBool(on)} }
|
||||
|
||||
// boolProps are the DOM properties whose type is boolean, so both the reconciler and
|
||||
// SSR can special-case them (see BoolProp for why they must).
|
||||
//
|
||||
// This is a name table because the DOM is a name table: there is no way to ask, of a
|
||||
// VNode alone, whether `checked` on this tag is a boolean. Every framework carries the
|
||||
// same list.
|
||||
var boolProps = map[string]bool{
|
||||
"checked": true,
|
||||
"disabled": true,
|
||||
"readOnly": true,
|
||||
"required": true,
|
||||
"selected": true,
|
||||
"multiple": true,
|
||||
"hidden": true,
|
||||
"open": true,
|
||||
"autofocus": true,
|
||||
"indeterminate": true,
|
||||
"defaultChecked": true,
|
||||
}
|
||||
|
||||
// IsBoolProp reports whether a prop name is a boolean DOM property. The reconciler
|
||||
// needs it to write a real bool instead of a string.
|
||||
func IsBoolProp(k string) bool { return boolProps[k] }
|
||||
|
||||
type htmlMod struct{ html string }
|
||||
|
||||
func (h htmlMod) apply(n *VNode) { n.HTML = h.html }
|
||||
@@ -147,7 +183,7 @@ func writeNode(b *strings.Builder, n *VNode) {
|
||||
b.WriteByte('<')
|
||||
b.WriteString(n.Tag)
|
||||
writeAttrs(b, n.Attrs)
|
||||
writeAttrs(b, n.Props) // props like input value show up as attributes in SSR
|
||||
writeProps(b, n.Props) // props like an input's value show up as attributes in SSR
|
||||
b.WriteByte('>')
|
||||
if voidTags[n.Tag] {
|
||||
return
|
||||
@@ -165,16 +201,44 @@ func writeNode(b *strings.Builder, n *VNode) {
|
||||
}
|
||||
|
||||
func writeAttrs(b *strings.Builder, m map[string]string) {
|
||||
for _, k := range sortedKeys(m) {
|
||||
writeAttr(b, k, m[k])
|
||||
}
|
||||
}
|
||||
|
||||
// writeProps serializes live DOM properties as HTML attributes for SSR, so the
|
||||
// server's markup shows what the client's props will hold.
|
||||
//
|
||||
// A boolean prop is emitted as a BARE attribute when true and omitted entirely when
|
||||
// false — that is what the HTML boolean-attribute rule means. Writing checked="false"
|
||||
// would render a ticked checkbox, which is the exact opposite of what was asked for.
|
||||
func writeProps(b *strings.Builder, m map[string]string) {
|
||||
for _, k := range sortedKeys(m) {
|
||||
v := m[k]
|
||||
if boolProps[k] {
|
||||
if v == "true" {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(strings.ToLower(k))
|
||||
}
|
||||
continue
|
||||
}
|
||||
writeAttr(b, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func writeAttr(b *strings.Builder, k, v string) {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(k)
|
||||
b.WriteString(`="`)
|
||||
b.WriteString(html.EscapeString(v))
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(k)
|
||||
b.WriteString(`="`)
|
||||
b.WriteString(html.EscapeString(m[k]))
|
||||
b.WriteByte('"')
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
46
go/vdom/vnode_test.go
Normal file
46
go/vdom/vnode_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package vdom
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A boolean DOM property is a trap in HTML: the attribute's PRESENCE is what means
|
||||
// true. checked="false" is a ticked checkbox — the value is not even read. So a false
|
||||
// boolean prop must be omitted entirely, and a true one written bare.
|
||||
func TestBoolPropSSR(t *testing.T) {
|
||||
ticked := RenderHTML(Input(Attr("type", "checkbox"), BoolProp("checked", true)))
|
||||
if !strings.Contains(ticked, " checked") {
|
||||
t.Errorf("a checked box did not render the attribute: %s", ticked)
|
||||
}
|
||||
if strings.Contains(ticked, `checked="`) {
|
||||
t.Errorf("a boolean attribute must be bare, not valued: %s", ticked)
|
||||
}
|
||||
|
||||
unticked := RenderHTML(Input(Attr("type", "checkbox"), BoolProp("checked", false)))
|
||||
if strings.Contains(unticked, "checked") {
|
||||
t.Errorf(`an unchecked box must omit the attribute entirely — checked="false" renders as TICKED: %s`, unticked)
|
||||
}
|
||||
}
|
||||
|
||||
// Non-boolean props keep their values: an input's value is a string, and dropping it
|
||||
// when empty would be just as wrong as writing checked="false".
|
||||
func TestValuePropSSRKeepsItsValue(t *testing.T) {
|
||||
got := RenderHTML(Input(Prop("value", "false")))
|
||||
if !strings.Contains(got, `value="false"`) {
|
||||
t.Errorf(`value="false" is a string, not a boolean, and must survive: %s`, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBoolProp(t *testing.T) {
|
||||
for _, k := range []string{"checked", "disabled", "selected", "open"} {
|
||||
if !IsBoolProp(k) {
|
||||
t.Errorf("%q should be known as a boolean property — the reconciler writes it as a string otherwise", k)
|
||||
}
|
||||
}
|
||||
for _, k := range []string{"value", "className", "id"} {
|
||||
if IsBoolProp(k) {
|
||||
t.Errorf("%q is not a boolean property", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user