From 5066a6fb6d78b29f6ac9da8b1fa705c99981ec33 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sat, 30 Nov 2019 17:13:51 -0700 Subject: [PATCH 1/4] Add support for rendering into non-body DOM nodes This PR adds support for rendering into arbitrary DOM nodes in the same way that rendering into the `body` worked previously, with one exception: blocking. `RenderBody` remains blocking, since it effectively becomes a helper that 90% of Vecty applications will rely on, whereas `RenderInto` and `RenderIntoNode` which are introduced in this PR do not block. This is because: a. Anyone rendering Vecty into arbitrary DOM nodes will likely want to do it more than once, so making the API of these functions non-blocking makes sense. b. Anyone using these functions will be a more advanced user and understand when blocking is / is not needed. In order to land this PR earlier for users who need this functionality, tests will not be added. They will be added in a follow-up as part of #168. Fixes #81 Fixes #247 --- dom.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/dom.go b/dom.go index 4acaa2de..f9a1c973 100644 --- a/dom.go +++ b/dom.go @@ -1,6 +1,9 @@ package vecty -import "reflect" +import ( + "reflect" + "syscall/js" +) // batch renderer singleton var batch = &batchRenderer{idx: make(map[Component]int)} @@ -1155,27 +1158,91 @@ func requestAnimationFrame(callback func(float64)) int { } // RenderBody renders the given component as the document body. The given -// Component's Render method must return a "body" element. +// Component's Render method must return a "body" element or a panic will +// occur. +// +// This function blocks forever in order to prevent the program from exiting, +// which would prevent components from rerendering themselves in the future. +// +// It is a short-handed form for writing: +// +// err := vecty.RenderInto("body", body) +// if err !== nil { +// panic(err) +// } +// select{} // run Go forever +// func RenderBody(body Component) { + err := RenderInto("body", body) + if err != nil { + panic(err) + } + if !isTest { + select {} // run Go forever + } +} + +// ElementMismatchError is returned when the element returned by a component +// does not match what is required for rendering. +type ElementMismatchError struct { + method, got, want string +} + +func (e ElementMismatchError) Error() string { + return "vecty: " + e.method + `: expected Component.Render to return a "` + e.want + `", found "` + e.got + `"` +} + +// InvalidTargetError is returned when the element targeted by a render is +// invalid because it is null or undefined. +type InvalidTargetError struct { + method string +} + +func (e InvalidTargetError) Error() string { + return "vecty: " + e.method + `: invalid target element is null or undefined` +} + +// RenderInto renders the given component into the existing HTML element found +// by the CSS selector (e.g. "#id", ".class-name") by replacing it. +// +// If there is more than one element found, the first is used. If no element is +// found, an error of type InvalidTargetError is returned. +// +// If the Component's Render method does not return an element of the same type, +// an error of type ElementMismatchError is returned. +func RenderInto(selector string, c Component) error { + target := js.Global.Get("document").Call("querySelector", selector) + return RenderInto(target, c) +} + +// RenderIntoNode renders the given component into the existing HTML element by +// replacing it. +// +// If the Component's Render method does not return an element of the same type, +// an error of type ElementMismatchError is returned. +func RenderIntoNode(node js.Value, c Component) error { + if !node.Truthy() { + return InvalidTargetError{method: "RenderIntoNode"} + } // block batch until we're done batch.scheduled = true - nextRender, skip, pendingMounts := renderComponent(body, nil) + nextRender, skip, pendingMounts := renderComponent(c, nil) if skip { - panic("vecty: RenderBody Component.SkipRender illegally returned true") + panic("vecty: RenderIntoNode: Component.SkipRender illegally returned true") } - if nextRender.tag != "body" { - panic("vecty: RenderBody expected Component.Render to return a body tag, found \"" + nextRender.tag + "\"") + expectTag := node.Get("nodeName").String() + if nextRender.tag != expectTag { + return ElementMismatchError{method: `RenderIntoNode`, got: nextRender.tag, want: expectTag} } doc := global.Get("document") if doc.Get("readyState").String() == "loading" { - // avoid duplicate body var cb jsFunc cb = funcOf(func(this jsObject, args []jsObject) interface{} { cb.Release() - doc.Set("body", nextRender.node) + replaceNode(nextRender.node, node) mount(pendingMounts...) - if m, ok := body.(Mounter); ok { + if m, ok := c.(Mounter); ok { mount(m) } requestAnimationFrame(batch.render) @@ -1184,16 +1251,12 @@ func RenderBody(body Component) { doc.Call("addEventListener", "DOMContentLoaded", cb) return } - doc.Set("body", nextRender.node) + replaceNode(nextRender.node, node) mount(pendingMounts...) - if m, ok := body.(Mounter); ok { + if m, ok := c.(Mounter); ok { mount(m) } requestAnimationFrame(batch.render) - if !isTest { - // run Go forever - select {} - } } // SetTitle sets the title of the document. From 02970fb6fdc5133dc73257f91e52f638dbeb3379 Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sat, 30 Nov 2019 17:50:59 -0700 Subject: [PATCH 2/4] fix tests --- dom.go | 16 ++++++---------- dom_native.go | 9 +++++++++ dom_wasmjs_gopherjs.go | 13 +++++++++++++ testsuite_test.go | 3 +++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/dom.go b/dom.go index f9a1c973..c96e2dbd 100644 --- a/dom.go +++ b/dom.go @@ -2,7 +2,6 @@ package vecty import ( "reflect" - "syscall/js" ) // batch renderer singleton @@ -1211,16 +1210,11 @@ func (e InvalidTargetError) Error() string { // If the Component's Render method does not return an element of the same type, // an error of type ElementMismatchError is returned. func RenderInto(selector string, c Component) error { - target := js.Global.Get("document").Call("querySelector", selector) - return RenderInto(target, c) + target := global.Get("document").Call("querySelector", selector) + return renderIntoNode(target, c) } -// RenderIntoNode renders the given component into the existing HTML element by -// replacing it. -// -// If the Component's Render method does not return an element of the same type, -// an error of type ElementMismatchError is returned. -func RenderIntoNode(node js.Value, c Component) error { +func renderIntoNode(node jsObject, c Component) error { if !node.Truthy() { return InvalidTargetError{method: "RenderIntoNode"} } @@ -1249,7 +1243,7 @@ func RenderIntoNode(node js.Value, c Component) error { return undefined }) doc.Call("addEventListener", "DOMContentLoaded", cb) - return + return nil } replaceNode(nextRender.node, node) mount(pendingMounts...) @@ -1257,6 +1251,7 @@ func RenderIntoNode(node js.Value, c Component) error { mount(m) } requestAnimationFrame(batch.render) + return nil } // SetTitle sets the title of the document. @@ -1282,6 +1277,7 @@ type jsObject interface { Delete(key string) Call(name string, args ...interface{}) jsObject String() string + Truthy() bool Bool() bool Int() int Float() float64 diff --git a/dom_native.go b/dom_native.go index dca76e64..eecd306b 100755 --- a/dom_native.go +++ b/dom_native.go @@ -28,6 +28,15 @@ func (h *HTML) Node() SyscallJSValue { return htmlNodeImpl(h) } +// RenderIntoNode renders the given component into the existing HTML element by +// replacing it. +// +// If the Component's Render method does not return an element of the same type, +// an error of type ElementMismatchError is returned. +func RenderIntoNode(node SyscallJSValue, c Component) error { + return renderIntoNode(node, c) +} + var ( global jsObject undefined wrappedObject diff --git a/dom_wasmjs_gopherjs.go b/dom_wasmjs_gopherjs.go index baa8ef86..7c49ceb9 100644 --- a/dom_wasmjs_gopherjs.go +++ b/dom_wasmjs_gopherjs.go @@ -21,6 +21,15 @@ func (h *HTML) Node() js.Value { return h.node.(wrappedObject).j } +// RenderIntoNode renders the given component into the existing HTML element by +// replacing it. +// +// If the Component's Render method does not return an element of the same type, +// an error of type ElementMismatchError is returned. +func RenderIntoNode(node js.Value, c Component) error { + return renderIntoNode(wrapObject(node), c) +} + var ( global = wrapObject(js.Global()) undefined = wrappedObject{js.Undefined()} @@ -103,6 +112,10 @@ func (w wrappedObject) String() string { return w.j.String() } +func (w wrappedObject) Truthy() bool { + return w.j.Truthy() +} + func (w wrappedObject) Bool() bool { return w.j.Bool() } diff --git a/testsuite_test.go b/testsuite_test.go index 4eb836b7..75387201 100644 --- a/testsuite_test.go +++ b/testsuite_test.go @@ -264,6 +264,9 @@ func (r *objectRecorder) Call(name string, args ...interface{}) jsObject { // String implements the jsObject interface. func (r *objectRecorder) String() string { return r.ts.strings.get(r.name).(string) } +// Truthy implements the jsObject interface. +func (r *objectRecorder) Truthy() bool { panic("not implemented") } + // Bool implements the jsObject interface. func (r *objectRecorder) Bool() bool { return r.ts.bools.get(r.name).(bool) } From 87242bb17aa4df11b47937a127a7841a489e738a Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sat, 30 Nov 2019 19:34:44 -0700 Subject: [PATCH 3/4] fix tests / update test data --- dom_test.go | 33 ++++++++++++++++--- .../TestKeyedChild_DifferentType.want.txt | 6 +++- .../TestRenderBody_ExpectsBody__div.want.txt | 5 ++- .../TestRenderBody_ExpectsBody__nil.want.txt | 5 ++- .../TestRenderBody_ExpectsBody__text.want.txt | 5 ++- testdata/TestRenderBody_Nested.want.txt | 6 +++- ...TestRenderBody_RenderSkipper_Skip.want.txt | 2 ++ .../TestRenderBody_Standard_loaded.want.txt | 6 +++- .../TestRenderBody_Standard_loading.want.txt | 6 +++- ...erender_Nested__component_to_html.want.txt | 6 +++- ...erender_Nested__html_to_component.want.txt | 6 +++- .../TestRerender_change__new_child.want.txt | 6 +++- testdata/TestRerender_identical.want.txt | 6 +++- testdata/TestRerender_persistent.want.txt | 6 +++- .../TestRerender_persistent_direct.want.txt | 6 +++- testsuite_test.go | 9 ++--- 16 files changed, 97 insertions(+), 22 deletions(-) diff --git a/dom_test.go b/dom_test.go index c4afa90e..cf815f65 100644 --- a/dom_test.go +++ b/dom_test.go @@ -563,6 +563,8 @@ func TestRerender_identical(t *testing.T) { ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) // Perform the initial render of the component. render := Tag("body") @@ -650,6 +652,8 @@ func TestRerender_change(t *testing.T) { ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) // Perform the initial render of the component. render := Tag("body") @@ -753,6 +757,8 @@ func TestRerender_Nested(t *testing.T) { ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) // Perform the initial render of the component. var renderCalled, skipRenderCalled int @@ -851,6 +857,8 @@ func TestRerender_persistent(t *testing.T) { ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) lastRenderedComponent = nil renderCount = 0 @@ -913,6 +921,8 @@ func TestRerender_persistent_direct(t *testing.T) { ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) lastRenderedComponent = nil renderCount = 0 @@ -959,17 +969,17 @@ func TestRenderBody_ExpectsBody(t *testing.T) { { name: "text", render: Text("Hello world!"), - wantPanic: "vecty: RenderBody expected Component.Render to return a body tag, found \"\"", // TODO(slimsag): error message bug + wantPanic: "vecty: RenderIntoNode: expected Component.Render to return a \"body\", found \"\"", // TODO(slimsag): error message bug }, { name: "div", render: Tag("div"), - wantPanic: "vecty: RenderBody expected Component.Render to return a body tag, found \"div\"", + wantPanic: "vecty: RenderIntoNode: expected Component.Render to return a \"body\", found \"div\"", }, { name: "nil", render: nil, - wantPanic: "vecty: RenderBody expected Component.Render to return a body tag, found \"noscript\"", + wantPanic: "vecty: RenderIntoNode: expected Component.Render to return a \"body\", found \"noscript\"", }, } for _, c := range cases { @@ -977,6 +987,9 @@ func TestRenderBody_ExpectsBody(t *testing.T) { ts := testSuite(t) defer ts.done() + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) + var gotPanic string func() { defer func() { @@ -993,7 +1006,7 @@ func TestRenderBody_ExpectsBody(t *testing.T) { }) }() if c.wantPanic != gotPanic { - t.Fatalf("want panic %q got panic %q", c.wantPanic, gotPanic) + t.Fatalf("want panic:\n%q\ngot panic:\n%q", c.wantPanic, gotPanic) } }) } @@ -1005,6 +1018,8 @@ func TestRenderBody_RenderSkipper_Skip(t *testing.T) { ts := testSuite(t) defer ts.done() + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) + comp := &componentFunc{ render: func() ComponentOrHTML { return Tag("body") @@ -1018,7 +1033,7 @@ func TestRenderBody_RenderSkipper_Skip(t *testing.T) { got := recoverStr(func() { RenderBody(comp) }) - want := "vecty: RenderBody Component.SkipRender illegally returned true" + want := "vecty: RenderIntoNode: Component.SkipRender illegally returned true" if got != want { t.Fatalf("got panic %q want %q", got, want) } @@ -1033,6 +1048,8 @@ func TestRenderBody_Standard_loaded(t *testing.T) { ts.strings.mock(`global.Get("document").Get("readyState")`, "loaded") ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) RenderBody(&componentFunc{ render: func() ComponentOrHTML { @@ -1050,6 +1067,8 @@ func TestRenderBody_Standard_loading(t *testing.T) { ts.strings.mock(`global.Get("document").Get("readyState")`, "loading") ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) RenderBody(&componentFunc{ render: func() ComponentOrHTML { @@ -1069,6 +1088,8 @@ func TestRenderBody_Nested(t *testing.T) { ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) RenderBody(&componentFunc{ render: func() ComponentOrHTML { @@ -1109,6 +1130,8 @@ func TestKeyedChild_DifferentType(t *testing.T) { ts.ints.mock(`global.Call("requestAnimationFrame", func)`, 0) ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") + ts.strings.mock(`global.Get("document").Call("querySelector", "body").Get("nodeName")`, "body") + ts.truthies.mock(`global.Get("document").Call("querySelector", "body")`, true) comp := &componentFunc{ render: func() ComponentOrHTML { diff --git a/testdata/TestKeyedChild_DifferentType.want.txt b/testdata/TestKeyedChild_DifferentType.want.txt index cc1241d2..43aa84c5 100755 --- a/testdata/TestKeyedChild_DifferentType.want.txt +++ b/testdata/TestKeyedChild_DifferentType.want.txt @@ -1,4 +1,6 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") @@ -9,9 +11,11 @@ global.Get("document").Call("createElement", "tag1").Get("classList") global.Get("document").Call("createElement", "tag1").Get("dataset") global.Get("document").Call("createElement", "tag1").Get("style") global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "tag1"))) +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") diff --git a/testdata/TestRenderBody_ExpectsBody__div.want.txt b/testdata/TestRenderBody_ExpectsBody__div.want.txt index 5ba345c9..5eca4735 100755 --- a/testdata/TestRenderBody_ExpectsBody__div.want.txt +++ b/testdata/TestRenderBody_ExpectsBody__div.want.txt @@ -1,5 +1,8 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "div") global.Get("document").Call("createElement", "div").Get("classList") global.Get("document").Call("createElement", "div").Get("dataset") -global.Get("document").Call("createElement", "div").Get("style") \ No newline at end of file +global.Get("document").Call("createElement", "div").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") \ No newline at end of file diff --git a/testdata/TestRenderBody_ExpectsBody__nil.want.txt b/testdata/TestRenderBody_ExpectsBody__nil.want.txt index 3c7ad931..aa93e937 100755 --- a/testdata/TestRenderBody_ExpectsBody__nil.want.txt +++ b/testdata/TestRenderBody_ExpectsBody__nil.want.txt @@ -1,5 +1,8 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "noscript") global.Get("document").Call("createElement", "noscript").Get("classList") global.Get("document").Call("createElement", "noscript").Get("dataset") -global.Get("document").Call("createElement", "noscript").Get("style") \ No newline at end of file +global.Get("document").Call("createElement", "noscript").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") \ No newline at end of file diff --git a/testdata/TestRenderBody_ExpectsBody__text.want.txt b/testdata/TestRenderBody_ExpectsBody__text.want.txt index 5548a1f4..1ee9b10c 100755 --- a/testdata/TestRenderBody_ExpectsBody__text.want.txt +++ b/testdata/TestRenderBody_ExpectsBody__text.want.txt @@ -1,5 +1,8 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createTextNode", "Hello world!") global.Get("document").Call("createTextNode", "Hello world!").Get("classList") global.Get("document").Call("createTextNode", "Hello world!").Get("dataset") -global.Get("document").Call("createTextNode", "Hello world!").Get("style") \ No newline at end of file +global.Get("document").Call("createTextNode", "Hello world!").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") \ No newline at end of file diff --git a/testdata/TestRenderBody_Nested.want.txt b/testdata/TestRenderBody_Nested.want.txt index 7629b9db..53d7028b 100755 --- a/testdata/TestRenderBody_Nested.want.txt +++ b/testdata/TestRenderBody_Nested.want.txt @@ -1,9 +1,13 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) \ No newline at end of file diff --git a/testdata/TestRenderBody_RenderSkipper_Skip.want.txt b/testdata/TestRenderBody_RenderSkipper_Skip.want.txt index e69de29b..bfb00f8f 100755 --- a/testdata/TestRenderBody_RenderSkipper_Skip.want.txt +++ b/testdata/TestRenderBody_RenderSkipper_Skip.want.txt @@ -0,0 +1,2 @@ +global.Get("document") +global.Get("document").Call("querySelector", "body") \ No newline at end of file diff --git a/testdata/TestRenderBody_Standard_loaded.want.txt b/testdata/TestRenderBody_Standard_loaded.want.txt index 7629b9db..53d7028b 100755 --- a/testdata/TestRenderBody_Standard_loaded.want.txt +++ b/testdata/TestRenderBody_Standard_loaded.want.txt @@ -1,9 +1,13 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) \ No newline at end of file diff --git a/testdata/TestRenderBody_Standard_loading.want.txt b/testdata/TestRenderBody_Standard_loading.want.txt index c76c1242..188d4e6c 100755 --- a/testdata/TestRenderBody_Standard_loading.want.txt +++ b/testdata/TestRenderBody_Standard_loading.want.txt @@ -1,11 +1,15 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") global.Get("document").Call("addEventListener", "DOMContentLoaded", func) (invoking DOMContentLoaded event listener) -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) \ No newline at end of file diff --git a/testdata/TestRerender_Nested__component_to_html.want.txt b/testdata/TestRerender_Nested__component_to_html.want.txt index 265a94a8..4d00c3ab 100755 --- a/testdata/TestRerender_Nested__component_to_html.want.txt +++ b/testdata/TestRerender_Nested__component_to_html.want.txt @@ -1,11 +1,15 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) (expect body to be set now) global.Get("document").Call("createElement", "body").Get("classList") diff --git a/testdata/TestRerender_Nested__html_to_component.want.txt b/testdata/TestRerender_Nested__html_to_component.want.txt index 265a94a8..4d00c3ab 100755 --- a/testdata/TestRerender_Nested__html_to_component.want.txt +++ b/testdata/TestRerender_Nested__html_to_component.want.txt @@ -1,11 +1,15 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) (expect body to be set now) global.Get("document").Call("createElement", "body").Get("classList") diff --git a/testdata/TestRerender_change__new_child.want.txt b/testdata/TestRerender_change__new_child.want.txt index 265a94a8..4d00c3ab 100755 --- a/testdata/TestRerender_change__new_child.want.txt +++ b/testdata/TestRerender_change__new_child.want.txt @@ -1,11 +1,15 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) (expect body to be set now) global.Get("document").Call("createElement", "body").Get("classList") diff --git a/testdata/TestRerender_identical.want.txt b/testdata/TestRerender_identical.want.txt index 444b4392..3ee9e545 100755 --- a/testdata/TestRerender_identical.want.txt +++ b/testdata/TestRerender_identical.want.txt @@ -1,11 +1,15 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") global.Get("document").Call("createElement", "body").Get("style") +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") diff --git a/testdata/TestRerender_persistent.want.txt b/testdata/TestRerender_persistent.want.txt index 3c045299..d08c2440 100755 --- a/testdata/TestRerender_persistent.want.txt +++ b/testdata/TestRerender_persistent.want.txt @@ -1,4 +1,6 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") @@ -9,9 +11,11 @@ global.Get("document").Call("createElement", "div").Get("classList") global.Get("document").Call("createElement", "div").Get("dataset") global.Get("document").Call("createElement", "div").Get("style") global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") diff --git a/testdata/TestRerender_persistent_direct.want.txt b/testdata/TestRerender_persistent_direct.want.txt index 3c045299..d08c2440 100755 --- a/testdata/TestRerender_persistent_direct.want.txt +++ b/testdata/TestRerender_persistent_direct.want.txt @@ -1,4 +1,6 @@ global.Get("document") +global.Get("document").Call("querySelector", "body") +global.Get("document") global.Get("document").Call("createElement", "body") global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") @@ -9,9 +11,11 @@ global.Get("document").Call("createElement", "div").Get("classList") global.Get("document").Call("createElement", "div").Get("dataset") global.Get("document").Call("createElement", "div").Get("style") global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) +global.Get("document").Call("querySelector", "body").Get("nodeName") global.Get("document") global.Get("document").Get("readyState") -global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) +global.Get("document").Call("querySelector", "body").Get("parentNode") +global.Get("document").Call("querySelector", "body").Get("parentNode").Call("replaceChild", jsObject(global.Get("document").Call("createElement", "body")), jsObject(global.Get("document").Call("querySelector", "body"))) global.Call("requestAnimationFrame", func) global.Get("document").Call("createElement", "body").Get("classList") global.Get("document").Call("createElement", "body").Get("dataset") diff --git a/testsuite_test.go b/testsuite_test.go index 75387201..0028360e 100644 --- a/testsuite_test.go +++ b/testsuite_test.go @@ -53,6 +53,7 @@ func testSuite(t *testing.T) *testSuiteT { bools: &valueMocker{}, floats: &valueMocker{}, ints: &valueMocker{}, + truthies: &valueMocker{}, } global = &objectRecorder{ ts: ts, @@ -92,9 +93,9 @@ func (v *valueMocker) get(invocation string) interface{} { } type testSuiteT struct { - t *testing.T - callbacks map[string]interface{} - strings, bools, floats, ints *valueMocker + t *testing.T + callbacks map[string]interface{} + strings, bools, floats, ints, truthies *valueMocker got string isDone bool @@ -265,7 +266,7 @@ func (r *objectRecorder) Call(name string, args ...interface{}) jsObject { func (r *objectRecorder) String() string { return r.ts.strings.get(r.name).(string) } // Truthy implements the jsObject interface. -func (r *objectRecorder) Truthy() bool { panic("not implemented") } +func (r *objectRecorder) Truthy() bool { return r.ts.truthies.get(r.name).(bool) } // Bool implements the jsObject interface. func (r *objectRecorder) Bool() bool { return r.ts.bools.get(r.name).(bool) } From 864c5b501d177c7c711632a5e451f3621c0e088d Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Sat, 30 Nov 2019 19:37:45 -0700 Subject: [PATCH 4/4] improve panic / error naming (use correct method name always) --- dom.go | 13 +++++++------ dom_native.go | 2 +- dom_test.go | 8 ++++---- dom_wasmjs_gopherjs.go | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dom.go b/dom.go index c96e2dbd..0eb000e0 100644 --- a/dom.go +++ b/dom.go @@ -1172,7 +1172,8 @@ func requestAnimationFrame(callback func(float64)) int { // select{} // run Go forever // func RenderBody(body Component) { - err := RenderInto("body", body) + target := global.Get("document").Call("querySelector", "body") + err := renderIntoNode("RenderBody", target, body) if err != nil { panic(err) } @@ -1211,22 +1212,22 @@ func (e InvalidTargetError) Error() string { // an error of type ElementMismatchError is returned. func RenderInto(selector string, c Component) error { target := global.Get("document").Call("querySelector", selector) - return renderIntoNode(target, c) + return renderIntoNode("RenderInto", target, c) } -func renderIntoNode(node jsObject, c Component) error { +func renderIntoNode(methodName string, node jsObject, c Component) error { if !node.Truthy() { - return InvalidTargetError{method: "RenderIntoNode"} + return InvalidTargetError{method: methodName} } // block batch until we're done batch.scheduled = true nextRender, skip, pendingMounts := renderComponent(c, nil) if skip { - panic("vecty: RenderIntoNode: Component.SkipRender illegally returned true") + panic("vecty: " + methodName + ": Component.SkipRender illegally returned true") } expectTag := node.Get("nodeName").String() if nextRender.tag != expectTag { - return ElementMismatchError{method: `RenderIntoNode`, got: nextRender.tag, want: expectTag} + return ElementMismatchError{method: methodName, got: nextRender.tag, want: expectTag} } doc := global.Get("document") if doc.Get("readyState").String() == "loading" { diff --git a/dom_native.go b/dom_native.go index eecd306b..0e53d7f0 100755 --- a/dom_native.go +++ b/dom_native.go @@ -34,7 +34,7 @@ func (h *HTML) Node() SyscallJSValue { // If the Component's Render method does not return an element of the same type, // an error of type ElementMismatchError is returned. func RenderIntoNode(node SyscallJSValue, c Component) error { - return renderIntoNode(node, c) + return renderIntoNode("RenderIntoNode", node, c) } var ( diff --git a/dom_test.go b/dom_test.go index cf815f65..77444c3c 100644 --- a/dom_test.go +++ b/dom_test.go @@ -969,17 +969,17 @@ func TestRenderBody_ExpectsBody(t *testing.T) { { name: "text", render: Text("Hello world!"), - wantPanic: "vecty: RenderIntoNode: expected Component.Render to return a \"body\", found \"\"", // TODO(slimsag): error message bug + wantPanic: "vecty: RenderBody: expected Component.Render to return a \"body\", found \"\"", // TODO(slimsag): error message bug }, { name: "div", render: Tag("div"), - wantPanic: "vecty: RenderIntoNode: expected Component.Render to return a \"body\", found \"div\"", + wantPanic: "vecty: RenderBody: expected Component.Render to return a \"body\", found \"div\"", }, { name: "nil", render: nil, - wantPanic: "vecty: RenderIntoNode: expected Component.Render to return a \"body\", found \"noscript\"", + wantPanic: "vecty: RenderBody: expected Component.Render to return a \"body\", found \"noscript\"", }, } for _, c := range cases { @@ -1033,7 +1033,7 @@ func TestRenderBody_RenderSkipper_Skip(t *testing.T) { got := recoverStr(func() { RenderBody(comp) }) - want := "vecty: RenderIntoNode: Component.SkipRender illegally returned true" + want := "vecty: RenderBody: Component.SkipRender illegally returned true" if got != want { t.Fatalf("got panic %q want %q", got, want) } diff --git a/dom_wasmjs_gopherjs.go b/dom_wasmjs_gopherjs.go index 7c49ceb9..098cbacf 100644 --- a/dom_wasmjs_gopherjs.go +++ b/dom_wasmjs_gopherjs.go @@ -27,7 +27,7 @@ func (h *HTML) Node() js.Value { // If the Component's Render method does not return an element of the same type, // an error of type ElementMismatchError is returned. func RenderIntoNode(node js.Value, c Component) error { - return renderIntoNode(wrapObject(node), c) + return renderIntoNode("RenderIntoNode", wrapObject(node), c) } var (