├── .gitignore ├── testdata ├── TestRerender_no_prevRender.want.txt ├── TestRenderBody_RenderSkipper_Skip.want.txt ├── TestSetTitle.want.txt ├── TestHTML_reconcile_nil__create_element.want.txt ├── TestHTML_reconcile_nil__create_text_node.want.txt ├── TestHTML_reconcile_std__text_identical.want.txt ├── TestRenderBody_ExpectsBody__div.want.txt ├── TestHTML_reconcile_nil__create_element_ns.want.txt ├── TestRenderBody_ExpectsBody__nil.want.txt ├── TestHTML_reconcile_std__text_diff.want.txt ├── TestHTML_reconcile_nil__inner_html.want.txt ├── TestRenderBody_ExpectsBody__text.want.txt ├── TestAddStylesheet.want.txt ├── TestHTML_reconcile_nil__properties.want.txt ├── TestHTML_reconcile_nil__dataset.want.txt ├── TestHTML_reconcile_nil__attributes.want.txt ├── TestHTML_reconcile_nil__style.want.txt ├── TestRenderBody_Nested.want.txt ├── TestHTML_reconcile_nil__add_event_listener.want.txt ├── TestRenderBody_Standard_loaded.want.txt ├── TestRenderBody_Standard_loading.want.txt ├── TestHTML_reconcile_std__properties__replaced_elem_diff.want.txt ├── TestHTML_reconcile_std__properties__replaced_elem_shared.want.txt ├── TestRerender_identical.want.txt ├── TestHTML_reconcile_std__class__map.want.txt ├── TestHTML_reconcile_std__class__remove.want.txt ├── TestHTML_reconcile_std__class__map_toggle.want.txt ├── TestHTML_reconcile_std__properties__remove.want.txt ├── TestHTML_reconcile_std__properties__diff.want.txt ├── TestHTML_reconcile_std__dataset__remove.want.txt ├── TestHTML_reconcile_std__attributes__remove.want.txt ├── TestHTML_reconcile_std__dataset__diff.want.txt ├── TestHTML_reconcile_std__attributes__diff.want.txt ├── TestHTML_reconcile_std__class__diff.want.txt ├── TestHTML_reconcile_std__class__multi.want.txt ├── TestHTML_reconcile_std__style__remove.want.txt ├── TestHTML_reconcile_std__style__diff.want.txt ├── TestHTML_reconcile_nil__children.want.txt ├── TestHTML_reconcile_nil__children_render_nil.want.txt ├── TestHTML_reconcile_std__class__combo.want.txt ├── TestHTML_reconcile_std__event_listener_diff.want.txt ├── TestRerender_Nested__new_child.want.txt ├── TestRerender_change__new_child.want.txt ├── TestRerender_Nested__component_to_html.want.txt └── TestRerender_Nested__html_to_component.want.txt ├── domutil.go ├── example ├── todomvc │ ├── node_modules │ │ ├── todomvc-common │ │ │ ├── readme.md │ │ │ ├── package.json │ │ │ ├── base.css │ │ │ └── base.js │ │ └── todomvc-app-css │ │ │ ├── readme.md │ │ │ ├── package.json │ │ │ └── index.css │ ├── store │ │ ├── model │ │ │ └── model.go │ │ ├── storeutil │ │ │ └── storeutil.go │ │ └── store.go │ ├── dispatcher │ │ └── dispatcher.go │ ├── components │ │ ├── filterbutton.go │ │ ├── itemview.go │ │ └── pageview.go │ ├── actions │ │ └── actions.go │ └── example.go └── markdown │ └── markdown.go ├── markup_test.go ├── CONTRIBUTORS ├── doc ├── projects-using-vecty.md └── CHANGELOG.md ├── .travis.yml ├── LICENSE ├── style └── style.go ├── value.go ├── README.md ├── prop └── prop.go ├── elem ├── generate.go └── elem.gen.go ├── event └── generate.go ├── testsuite_test.go ├── markup.go └── dom_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | testdata/*.got.txt 2 | .DS_store 3 | -------------------------------------------------------------------------------- /testdata/TestRerender_no_prevRender.want.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/TestRenderBody_RenderSkipper_Skip.want.txt: -------------------------------------------------------------------------------- 1 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestSetTitle.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Set("title", "foobartitle") -------------------------------------------------------------------------------- /domutil.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package vecty 4 | 5 | func replaceNode(newNode, oldNode jsObject) { 6 | if newNode == oldNode { 7 | return 8 | } 9 | oldNode.Get("parentNode").Call("replaceChild", newNode, oldNode) 10 | } 11 | -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Common TodoMVC utilities used by our apps 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install --save todomvc-common 10 | ``` 11 | 12 | 13 | ## License 14 | 15 | MIT © [TasteJS](http://tastejs.com) 16 | -------------------------------------------------------------------------------- /markup_test.go: -------------------------------------------------------------------------------- 1 | package vecty 2 | 3 | import "testing" 4 | 5 | // TODO(slimsag): tests for other Markup 6 | 7 | func TestNamespace(t *testing.T) { 8 | want := "b" 9 | h := Tag("a", Markup(Namespace(want))) 10 | if h.namespace != want { 11 | t.Fatalf("got namespace %q want %q", h.namespace, want) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__create_element.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "strong") 3 | global.Get("document").Call("createElement", "strong").Get("classList") 4 | global.Get("document").Call("createElement", "strong").Get("dataset") 5 | global.Get("document").Call("createElement", "strong").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__create_text_node.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createTextNode", "hello") 3 | global.Get("document").Call("createTextNode", "hello").Get("classList") 4 | global.Get("document").Call("createTextNode", "hello").Get("dataset") 5 | global.Get("document").Call("createTextNode", "hello").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__text_identical.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createTextNode", "foobar") 3 | global.Get("document").Call("createTextNode", "foobar").Get("classList") 4 | global.Get("document").Call("createTextNode", "foobar").Get("dataset") 5 | global.Get("document").Call("createTextNode", "foobar").Get("style") -------------------------------------------------------------------------------- /testdata/TestRenderBody_ExpectsBody__div.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__create_element_ns.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElementNS", "foobar", "strong") 3 | global.Get("document").Call("createElementNS", "foobar", "strong").Get("classList") 4 | global.Get("document").Call("createElementNS", "foobar", "strong").Get("dataset") 5 | global.Get("document").Call("createElementNS", "foobar", "strong").Get("style") -------------------------------------------------------------------------------- /testdata/TestRenderBody_ExpectsBody__nil.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "noscript") 3 | global.Get("document").Call("createElement", "noscript").Get("classList") 4 | global.Get("document").Call("createElement", "noscript").Get("dataset") 5 | global.Get("document").Call("createElement", "noscript").Get("style") 6 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__text_diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createTextNode", "bar") 3 | global.Get("document").Call("createTextNode", "bar").Get("classList") 4 | global.Get("document").Call("createTextNode", "bar").Get("dataset") 5 | global.Get("document").Call("createTextNode", "bar").Get("style") 6 | global.Get("document").Call("createTextNode", "bar").Set("nodeValue", "foo") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__inner_html.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document").Call("createElement", "div").Set("innerHTML", "

hello

") -------------------------------------------------------------------------------- /testdata/TestRenderBody_ExpectsBody__text.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createTextNode", "Hello world!") 3 | global.Get("document").Call("createTextNode", "Hello world!").Get("classList") 4 | global.Get("document").Call("createTextNode", "Hello world!").Get("dataset") 5 | global.Get("document").Call("createTextNode", "Hello world!").Get("style") 6 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestAddStylesheet.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "link") 3 | global.Get("document").Call("createElement", "link").Set("rel", "stylesheet") 4 | global.Get("document").Call("createElement", "link").Set("href", "https://google.com/foobar.css") 5 | global.Get("document") 6 | global.Get("document").Get("head") 7 | global.Get("document").Get("head").Call("appendChild", jsObject(global.Get("document").Call("createElement", "link"))) -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__properties.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Set("a", 1) 4 | global.Get("document").Call("createElement", "div").Set("b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__dataset.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("dataset").Set("a", "1") 6 | global.Get("document").Call("createElement", "div").Get("dataset").Set("b", "2foobar") 7 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__attributes.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Call("setAttribute", "a", 1) 4 | global.Get("document").Call("createElement", "div").Call("setAttribute", "b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__style.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "a", "1") 7 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "b", "2foobar") -------------------------------------------------------------------------------- /testdata/TestRenderBody_Nested.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__add_event_listener.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document").Call("createElement", "div").Call("addEventListener", "click", func(*js.Object)) 7 | global.Get("document").Call("createElement", "div").Call("addEventListener", "keydown", func(*js.Object)) -------------------------------------------------------------------------------- /testdata/TestRenderBody_Standard_loaded.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /example/todomvc/store/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Item represents a single TODO item in the store. 4 | type Item struct { 5 | Title string 6 | Completed bool 7 | } 8 | 9 | // FilterState represents a viewing filter for TODO items in the store. 10 | type FilterState int 11 | 12 | const ( 13 | // All is a FilterState which shows all items. 14 | All FilterState = iota 15 | 16 | // Active is a FilterState which shows only non-completed items. 17 | Active 18 | 19 | // Completed is a FilterState which shows only completed items. 20 | Completed 21 | ) 22 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # Names should be added to this file like so: 2 | # 3 | # Jane Doe 4 | # Jane Doe 5 | # Jane Doe 6 | # 7 | # may be the person's website, GitHub profile, or GitHub 8 | # project related to their contribution to Vecty. 9 | 10 | # Please keep the list sorted. 11 | 12 | Igor Afanasyev 13 | Peter Fern 14 | Stephen Gutekanst 15 | Thomas Bruyelle 16 | -------------------------------------------------------------------------------- /testdata/TestRenderBody_Standard_loading.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Call("addEventListener", "DOMContentLoaded", func()) 9 | global.Call("requestAnimationFrame", func(float64)) 10 | (invoking DOMContentLoaded event listener) 11 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) -------------------------------------------------------------------------------- /example/todomvc/dispatcher/dispatcher.go: -------------------------------------------------------------------------------- 1 | package dispatcher 2 | 3 | // ID is a unique identifier representing a registered callback function. 4 | type ID int 5 | 6 | var idCounter ID 7 | var callbacks = make(map[ID]func(action interface{})) 8 | 9 | // Dispatch dispatches the given action to all registered callbacks. 10 | func Dispatch(action interface{}) { 11 | for _, c := range callbacks { 12 | c(action) 13 | } 14 | } 15 | 16 | // Register registers the callback to handle dispatched actions, the returned 17 | // ID may be used to unregister the callback later. 18 | func Register(callback func(action interface{})) ID { 19 | idCounter++ 20 | id := idCounter 21 | callbacks[id] = callback 22 | return id 23 | } 24 | 25 | // Unregister unregisters the callback previously registered via a call to 26 | // Register. 27 | func Unregister(id ID) { 28 | delete(callbacks, id) 29 | } 30 | -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__properties__replaced_elem_diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Set("a", 1) 4 | global.Get("document").Call("createElement", "div").Set("b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document") 10 | global.Get("document").Call("createElement", "span") 11 | global.Get("document").Call("createElement", "span").Set("a", 3) 12 | global.Get("document").Call("createElement", "span").Set("b", "4foobar") 13 | global.Get("document").Call("createElement", "span").Get("classList") 14 | global.Get("document").Call("createElement", "span").Get("dataset") 15 | global.Get("document").Call("createElement", "span").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__properties__replaced_elem_shared.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Set("a", 1) 4 | global.Get("document").Call("createElement", "div").Set("b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document") 10 | global.Get("document").Call("createElement", "span") 11 | global.Get("document").Call("createElement", "span").Set("a", 1) 12 | global.Get("document").Call("createElement", "span").Set("b", "4foobar") 13 | global.Get("document").Call("createElement", "span").Get("classList") 14 | global.Get("document").Call("createElement", "span").Get("dataset") 15 | global.Get("document").Call("createElement", "span").Get("style") -------------------------------------------------------------------------------- /doc/projects-using-vecty.md: -------------------------------------------------------------------------------- 1 | # Projects using [Vecty](https://github.com/gowasm/vecty) 2 | 3 | The following projects use Vecty to render their frontend in some capacity. 4 | 5 | This is just a list of applications using Vecty that we're aware of. We don't strictly endorse these projects, and they don't strictly endorse us, we just think they are cool and have compiled this list _for you, the reader_! :smile: 6 | 7 | | Project | Description | 8 | |---------|-------------| 9 | | [Go Package Store](https://github.com/shurcooL/Go-Package-Store) | An app that displays updates for the Go packages in your GOPATH. | 10 | | [Go Play Space](https://goplay.space/) | An advanced Go Playground frontend. | 11 | | [qlql](https://github.com/gernest/qlql) | A GUI management tool for the [ql database](https://github.com/cznic/ql). | 12 | | [marwan.io](https://www.marwan.io/) | A portfolio website by Marwan Sulaiman. | 13 | 14 | Know of a project using Vecty that is not on this list? Please [let us know](https://github.com/gowasm/vecty/issues/new)! 15 | -------------------------------------------------------------------------------- /testdata/TestRerender_identical.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) 10 | global.Get("document").Call("createElement", "body").Get("classList") 11 | global.Get("document").Call("createElement", "body").Get("dataset") 12 | global.Get("document").Call("createElement", "body").Get("style") 13 | global.Get("document").Call("createElement", "body").Get("classList") 14 | global.Get("document").Call("createElement", "body").Get("dataset") 15 | global.Get("document").Call("createElement", "body").Get("style") 16 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__class__map.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "a") 5 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "b") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "b") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("dataset") 15 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__class__remove.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "a") 5 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "b") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "b") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("dataset") 15 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__class__map_toggle.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "a") 5 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "b") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "b") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("dataset") 15 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__properties__remove.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Set("a", 1) 4 | global.Get("document").Call("createElement", "div").Set("b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Delete("b") 10 | global.Get("document").Call("createElement", "div").Get("classList") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Set("a", 3) 14 | global.Get("document").Call("createElement", "div").Get("classList") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__properties__diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Set("a", 1) 4 | global.Get("document").Call("createElement", "div").Set("b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("style") 12 | global.Get("document").Call("createElement", "div").Set("a", 3) 13 | global.Get("document").Call("createElement", "div").Set("b", "4foobar") 14 | global.Get("document").Call("createElement", "div").Get("classList") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-app-css/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-app-css 2 | 3 | > CSS for TodoMVC apps 4 | 5 | ![](screenshot.png) 6 | 7 | 8 | ## Install 9 | 10 | 11 | ``` 12 | $ npm install --save todomvc-app-css 13 | ``` 14 | 15 | 16 | ## Getting started 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template). 23 | 24 | 25 | 26 | ## License 27 | 28 | Creative Commons License
This work by Sindre Sorhus is licensed under a Creative Commons Attribution 4.0 International License. 29 | -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__dataset__remove.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("dataset").Set("a", "1") 6 | global.Get("document").Call("createElement", "div").Get("dataset").Set("b", "2foobar") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("dataset").Delete("b") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("dataset") 15 | global.Get("document").Call("createElement", "div").Get("dataset").Set("a", "3") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__attributes__remove.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Call("setAttribute", "a", 1) 4 | global.Get("document").Call("createElement", "div").Call("setAttribute", "b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Call("removeAttribute", "b") 10 | global.Get("document").Call("createElement", "div").Get("classList") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Call("setAttribute", "a", 3) 14 | global.Get("document").Call("createElement", "div").Get("classList") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__dataset__diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("dataset").Set("a", "1") 6 | global.Get("document").Call("createElement", "div").Get("dataset").Set("b", "2foobar") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("style") 12 | global.Get("document").Call("createElement", "div").Get("classList") 13 | global.Get("document").Call("createElement", "div").Get("dataset") 14 | global.Get("document").Call("createElement", "div").Get("dataset").Set("a", "3") 15 | global.Get("document").Call("createElement", "div").Get("dataset").Set("b", "4foobar") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__attributes__diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Call("setAttribute", "a", 1) 4 | global.Get("document").Call("createElement", "div").Call("setAttribute", "b", "2foobar") 5 | global.Get("document").Call("createElement", "div").Get("classList") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("style") 12 | global.Get("document").Call("createElement", "div").Call("setAttribute", "a", 3) 13 | global.Get("document").Call("createElement", "div").Call("setAttribute", "b", "4foobar") 14 | global.Get("document").Call("createElement", "div").Get("classList") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__class__diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "a") 5 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "b") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "b") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "c") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__class__multi.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "a") 5 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "b") 6 | global.Get("document").Call("createElement", "div").Get("dataset") 7 | global.Get("document").Call("createElement", "div").Get("style") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "b") 11 | global.Get("document").Call("createElement", "div").Get("dataset") 12 | global.Get("document").Call("createElement", "div").Get("style") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "c") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__style__remove.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "a", "1") 7 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "b", "2foobar") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("style") 12 | global.Get("document").Call("createElement", "div").Get("style").Call("removeProperty", "b") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("dataset") 15 | global.Get("document").Call("createElement", "div").Get("style") 16 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "a", "3") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__style__diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "a", "1") 7 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "b", "2foobar") 8 | (first reconcile done) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("style") 12 | global.Get("document").Call("createElement", "div").Get("classList") 13 | global.Get("document").Call("createElement", "div").Get("dataset") 14 | global.Get("document").Call("createElement", "div").Get("style") 15 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "a", "3") 16 | global.Get("document").Call("createElement", "div").Get("style").Call("setProperty", "b", "4foobar") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__children.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document") 7 | global.Get("document").Call("createElement", "div") 8 | global.Get("document").Call("createElement", "div").Get("classList") 9 | global.Get("document").Call("createElement", "div").Get("dataset") 10 | global.Get("document").Call("createElement", "div").Get("style") 11 | global.Get("document") 12 | global.Get("document").Call("createElement", "div") 13 | global.Get("document").Call("createElement", "div").Get("classList") 14 | global.Get("document").Call("createElement", "div").Get("dataset") 15 | global.Get("document").Call("createElement", "div").Get("style") 16 | global.Get("document").Call("createElement", "div").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) 17 | global.Get("document").Call("createElement", "div").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.9.x 5 | install: 6 | - nvm install node 7 | - npm install -g source-map-support 8 | - go get -u github.com/gopherjs/gopherjs 9 | - go get -u github.com/golang/lint/golint 10 | - go get -u honnef.co/go/tools/cmd/megacheck 11 | - go get -u github.com/haya14busa/goverage 12 | before_script: 13 | - export NODE_PATH="/usr/local/lib/node_modules" 14 | script: 15 | # Fetch dependencies. 16 | - go get -t -v ./... 17 | 18 | # Consult Go fmt, vet, lint, megacheck tools. 19 | - diff -u <(echo -n) <(gofmt -d -s .) 20 | - go tool vet . 21 | - golint -set_exit_status . ./example/... 22 | - golint ./elem/... ./event/... ./prop/... # TODO(slimsag): address these linter errors 23 | - megacheck ./... 24 | - megacheck -tags=js ./... 25 | 26 | # Test with Go compiler and GopherJS compiler. 27 | - go test -v -race ./... 28 | - gopherjs test -v ./... 29 | 30 | # Generate and upload coverage to codecov.io 31 | - goverage -covermode=atomic -coverprofile=coverage.out $(go list ./... | grep -v -e vecty/elem -e vecty/event -e vecty/example -e vecty/prop -e vecty/style) 32 | - include_cov=coverage.out bash <(curl -s https://codecov.io/bash) 33 | -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_nil__children_render_nil.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document") 7 | global.Get("document").Call("createElement", "div") 8 | global.Get("document").Call("createElement", "div").Get("classList") 9 | global.Get("document").Call("createElement", "div").Get("dataset") 10 | global.Get("document").Call("createElement", "div").Get("style") 11 | global.Get("document") 12 | global.Get("document").Call("createElement", "noscript") 13 | global.Get("document").Call("createElement", "noscript").Get("classList") 14 | global.Get("document").Call("createElement", "noscript").Get("dataset") 15 | global.Get("document").Call("createElement", "noscript").Get("style") 16 | global.Get("document").Call("createElement", "div").Call("appendChild", jsObject(global.Get("document").Call("createElement", "noscript"))) 17 | global.Get("document").Call("createElement", "div").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) -------------------------------------------------------------------------------- /example/todomvc/components/filterbutton.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/gowasm/vecty" 5 | "github.com/gowasm/vecty/elem" 6 | "github.com/gowasm/vecty/event" 7 | "github.com/gowasm/vecty/example/todomvc/actions" 8 | "github.com/gowasm/vecty/example/todomvc/dispatcher" 9 | "github.com/gowasm/vecty/example/todomvc/store" 10 | "github.com/gowasm/vecty/example/todomvc/store/model" 11 | "github.com/gowasm/vecty/prop" 12 | ) 13 | 14 | // FilterButton is a vecty.Component which allows the user to select a filter 15 | // state. 16 | type FilterButton struct { 17 | vecty.Core 18 | 19 | Label string `vecty:"prop"` 20 | Filter model.FilterState `vecty:"prop"` 21 | } 22 | 23 | func (b *FilterButton) onClick(event *vecty.Event) { 24 | dispatcher.Dispatch(&actions.SetFilter{ 25 | Filter: b.Filter, 26 | }) 27 | } 28 | 29 | // Render implements the vecty.Component interface. 30 | func (b *FilterButton) Render() vecty.ComponentOrHTML { 31 | return elem.ListItem( 32 | elem.Anchor( 33 | vecty.Markup( 34 | vecty.MarkupIf(store.Filter == b.Filter, vecty.Class("selected")), 35 | prop.Href("#"), 36 | event.Click(b.onClick).PreventDefault(), 37 | ), 38 | 39 | vecty.Text(b.Label), 40 | ), 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /example/todomvc/actions/actions.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import "github.com/gowasm/vecty/example/todomvc/store/model" 4 | 5 | // ReplaceItems is an action that replaces all items with the specified ones. 6 | type ReplaceItems struct { 7 | Items []*model.Item 8 | } 9 | 10 | // AddItem is an action which adds a single item with the specified title. 11 | type AddItem struct { 12 | Title string 13 | } 14 | 15 | // DestroyItem is an action which destroys the item specified by the index. 16 | type DestroyItem struct { 17 | Index int 18 | } 19 | 20 | // SetTitle is an action which specifies the title of an existing item. 21 | type SetTitle struct { 22 | Index int 23 | Title string 24 | } 25 | 26 | // SetCompleted is an action which specifies the completed state of an existing 27 | // item. 28 | type SetCompleted struct { 29 | Index int 30 | Completed bool 31 | } 32 | 33 | // SetAllCompleted is an action which marks all existing items as being 34 | // completed or not. 35 | type SetAllCompleted struct { 36 | Completed bool 37 | } 38 | 39 | // ClearCompleted is an action which clears the completed items. 40 | type ClearCompleted struct{} 41 | 42 | // SetFilter is an action which sets the filter for the viewed items. 43 | type SetFilter struct { 44 | Filter model.FilterState 45 | } 46 | -------------------------------------------------------------------------------- /example/todomvc/store/storeutil/storeutil.go: -------------------------------------------------------------------------------- 1 | // Package storeutil contains a ListenerRegistry type. 2 | package storeutil 3 | 4 | // ListenerRegistry is a listener registry. 5 | // The zero value is unfit for use; use NewListenerRegistry to create an instance. 6 | type ListenerRegistry struct { 7 | listeners map[interface{}]func() 8 | } 9 | 10 | // NewListenerRegistry creates a listener registry. 11 | func NewListenerRegistry() *ListenerRegistry { 12 | return &ListenerRegistry{ 13 | listeners: make(map[interface{}]func()), 14 | } 15 | } 16 | 17 | // Add adds listener with key to the registry. 18 | // key may be nil, then an arbitrary unused key is assigned. 19 | // It panics if a listener with same key is already present. 20 | func (r *ListenerRegistry) Add(key interface{}, listener func()) { 21 | if key == nil { 22 | key = new(int) 23 | } 24 | if _, ok := r.listeners[key]; ok { 25 | panic("duplicate listener key") 26 | } 27 | r.listeners[key] = listener 28 | } 29 | 30 | // Remove removes a listener with key from the registry. 31 | func (r *ListenerRegistry) Remove(key interface{}) { 32 | delete(r.listeners, key) 33 | } 34 | 35 | // Fire invokes all listeners in the registry. 36 | func (r *ListenerRegistry) Fire() { 37 | for _, l := range r.listeners { 38 | l() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__class__combo.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "a") 5 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "b") 6 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "c") 7 | global.Get("document").Call("createElement", "div").Get("dataset") 8 | global.Get("document").Call("createElement", "div").Get("style") 9 | (first reconcile done) 10 | global.Get("document").Call("createElement", "div").Get("classList") 11 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "b") 12 | global.Get("document").Call("createElement", "div").Get("classList").Call("remove", "c") 13 | global.Get("document").Call("createElement", "div").Get("dataset") 14 | global.Get("document").Call("createElement", "div").Get("style") 15 | global.Get("document").Call("createElement", "div").Get("classList") 16 | global.Get("document").Call("createElement", "div").Get("classList").Call("add", "d") 17 | global.Get("document").Call("createElement", "div").Get("dataset") 18 | global.Get("document").Call("createElement", "div").Get("style") -------------------------------------------------------------------------------- /testdata/TestHTML_reconcile_std__event_listener_diff.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "div") 3 | global.Get("document").Call("createElement", "div").Get("classList") 4 | global.Get("document").Call("createElement", "div").Get("dataset") 5 | global.Get("document").Call("createElement", "div").Get("style") 6 | global.Get("document").Call("createElement", "div").Call("addEventListener", "click", func(*js.Object)) 7 | global.Get("document").Call("createElement", "div").Call("addEventListener", "keydown", func(*js.Object)) 8 | (expected two added event listeners above) 9 | global.Get("document").Call("createElement", "div").Get("classList") 10 | global.Get("document").Call("createElement", "div").Get("dataset") 11 | global.Get("document").Call("createElement", "div").Get("style") 12 | global.Get("document").Call("createElement", "div").Call("removeEventListener", "click", func(*js.Object)) 13 | global.Get("document").Call("createElement", "div").Call("removeEventListener", "keydown", func(*js.Object)) 14 | global.Get("document").Call("createElement", "div").Get("classList") 15 | global.Get("document").Call("createElement", "div").Get("dataset") 16 | global.Get("document").Call("createElement", "div").Get("style") 17 | global.Get("document").Call("createElement", "div").Call("addEventListener", "click", func(*js.Object)) 18 | (expected two removed, one added event listeners above) -------------------------------------------------------------------------------- /example/todomvc/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gopherjs/gopherjs/js" 7 | "github.com/gowasm/vecty" 8 | "github.com/gowasm/vecty/example/todomvc/actions" 9 | "github.com/gowasm/vecty/example/todomvc/components" 10 | "github.com/gowasm/vecty/example/todomvc/dispatcher" 11 | "github.com/gowasm/vecty/example/todomvc/store" 12 | "github.com/gowasm/vecty/example/todomvc/store/model" 13 | ) 14 | 15 | func main() { 16 | attachLocalStorage() 17 | 18 | vecty.SetTitle("GopherJS • TodoMVC") 19 | vecty.AddStylesheet("node_modules/todomvc-common/base.css") 20 | vecty.AddStylesheet("node_modules/todomvc-app-css/index.css") 21 | p := &components.PageView{} 22 | store.Listeners.Add(p, func() { 23 | p.Items = store.Items 24 | vecty.Rerender(p) 25 | }) 26 | vecty.RenderBody(p) 27 | } 28 | 29 | func attachLocalStorage() { 30 | store.Listeners.Add(nil, func() { 31 | data, err := json.Marshal(store.Items) 32 | if err != nil { 33 | println("failed to store items: " + err.Error()) 34 | } 35 | js.Global.Get("localStorage").Set("items", string(data)) 36 | }) 37 | 38 | if data := js.Global.Get("localStorage").Get("items"); data != js.Undefined { 39 | var items []*model.Item 40 | if err := json.Unmarshal([]byte(data.String()), &items); err != nil { 41 | println("failed to load items: " + err.Error()) 42 | } 43 | dispatcher.Dispatch(&actions.ReplaceItems{ 44 | Items: items, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testdata/TestRerender_Nested__new_child.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) 10 | (expect body to be set now) 11 | global.Get("document").Call("createElement", "body").Get("classList") 12 | global.Get("document").Call("createElement", "body").Get("dataset") 13 | global.Get("document").Call("createElement", "body").Get("style") 14 | global.Get("document").Call("createElement", "body").Get("classList") 15 | global.Get("document").Call("createElement", "body").Get("dataset") 16 | global.Get("document").Call("createElement", "body").Get("style") 17 | global.Get("document") 18 | global.Get("document").Call("createElement", "div") 19 | global.Get("document").Call("createElement", "div").Get("classList") 20 | global.Get("document").Call("createElement", "div").Get("dataset") 21 | global.Get("document").Call("createElement", "div").Get("style") 22 | global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) 23 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestRerender_change__new_child.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) 10 | (expect body to be set now) 11 | global.Get("document").Call("createElement", "body").Get("classList") 12 | global.Get("document").Call("createElement", "body").Get("dataset") 13 | global.Get("document").Call("createElement", "body").Get("style") 14 | global.Get("document").Call("createElement", "body").Get("classList") 15 | global.Get("document").Call("createElement", "body").Get("dataset") 16 | global.Get("document").Call("createElement", "body").Get("style") 17 | global.Get("document") 18 | global.Get("document").Call("createElement", "div") 19 | global.Get("document").Call("createElement", "div").Get("classList") 20 | global.Get("document").Call("createElement", "div").Get("dataset") 21 | global.Get("document").Call("createElement", "div").Get("style") 22 | global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) 23 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestRerender_Nested__component_to_html.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) 10 | (expect body to be set now) 11 | global.Get("document").Call("createElement", "body").Get("classList") 12 | global.Get("document").Call("createElement", "body").Get("dataset") 13 | global.Get("document").Call("createElement", "body").Get("style") 14 | global.Get("document").Call("createElement", "body").Get("classList") 15 | global.Get("document").Call("createElement", "body").Get("dataset") 16 | global.Get("document").Call("createElement", "body").Get("style") 17 | global.Get("document") 18 | global.Get("document").Call("createElement", "div") 19 | global.Get("document").Call("createElement", "div").Get("classList") 20 | global.Get("document").Call("createElement", "div").Get("dataset") 21 | global.Get("document").Call("createElement", "div").Get("style") 22 | global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) 23 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /testdata/TestRerender_Nested__html_to_component.want.txt: -------------------------------------------------------------------------------- 1 | global.Get("document") 2 | global.Get("document").Call("createElement", "body") 3 | global.Get("document").Call("createElement", "body").Get("classList") 4 | global.Get("document").Call("createElement", "body").Get("dataset") 5 | global.Get("document").Call("createElement", "body").Get("style") 6 | global.Get("document") 7 | global.Get("document").Get("readyState") 8 | global.Get("document").Set("body", jsObject(global.Get("document").Call("createElement", "body"))) 9 | global.Call("requestAnimationFrame", func(float64)) 10 | (expect body to be set now) 11 | global.Get("document").Call("createElement", "body").Get("classList") 12 | global.Get("document").Call("createElement", "body").Get("dataset") 13 | global.Get("document").Call("createElement", "body").Get("style") 14 | global.Get("document").Call("createElement", "body").Get("classList") 15 | global.Get("document").Call("createElement", "body").Get("dataset") 16 | global.Get("document").Call("createElement", "body").Get("style") 17 | global.Get("document") 18 | global.Get("document").Call("createElement", "div") 19 | global.Get("document").Call("createElement", "div").Get("classList") 20 | global.Get("document").Call("createElement", "div").Get("dataset") 21 | global.Get("document").Call("createElement", "div").Get("style") 22 | global.Get("document").Call("createElement", "body").Call("appendChild", jsObject(global.Get("document").Call("createElement", "div"))) 23 | global.Call("requestAnimationFrame", func(float64)) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Vecty Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Vecty nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /style/style.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package style 4 | 5 | import ( 6 | "strconv" 7 | 8 | "github.com/gowasm/vecty" 9 | ) 10 | 11 | type Size string 12 | 13 | func Px(pixels int) Size { 14 | return Size(strconv.Itoa(pixels) + "px") 15 | } 16 | 17 | func Color(value string) vecty.Applyer { 18 | return vecty.Style("color", value) 19 | } 20 | 21 | func Width(size Size) vecty.Applyer { 22 | return vecty.Style("width", string(size)) 23 | } 24 | 25 | func MinWidth(size Size) vecty.Applyer { 26 | return vecty.Style("min-width", string(size)) 27 | } 28 | 29 | func MaxWidth(size Size) vecty.Applyer { 30 | return vecty.Style("max-width", string(size)) 31 | } 32 | 33 | func Height(size Size) vecty.Applyer { 34 | return vecty.Style("height", string(size)) 35 | } 36 | 37 | func MinHeight(size Size) vecty.Applyer { 38 | return vecty.Style("min-height", string(size)) 39 | } 40 | 41 | func MaxHeight(size Size) vecty.Applyer { 42 | return vecty.Style("max-height", string(size)) 43 | } 44 | 45 | func Margin(size Size) vecty.Applyer { 46 | return vecty.Style("margin", string(size)) 47 | } 48 | 49 | type OverflowOption string 50 | 51 | const ( 52 | OverflowVisible OverflowOption = "visible" 53 | OverflowHidden OverflowOption = "hidden" 54 | OverflowScroll OverflowOption = "scroll" 55 | OverflowAuto OverflowOption = "auto" 56 | ) 57 | 58 | func Overflow(option OverflowOption) vecty.Applyer { 59 | return vecty.Style("overflow", string(option)) 60 | } 61 | 62 | func OverflowX(option OverflowOption) vecty.Applyer { 63 | return vecty.Style("overflow-x", string(option)) 64 | } 65 | 66 | func OverflowY(option OverflowOption) vecty.Applyer { 67 | return vecty.Style("overflow-y", string(option)) 68 | } 69 | -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "1.0.2", 4 | "description": "Common TodoMVC utilities used by our apps", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/tastejs/todomvc-common" 9 | }, 10 | "author": { 11 | "name": "TasteJS" 12 | }, 13 | "main": "base.js", 14 | "files": [ 15 | "base.js", 16 | "base.css" 17 | ], 18 | "keywords": [ 19 | "todomvc", 20 | "tastejs", 21 | "util", 22 | "utilities" 23 | ], 24 | "gitHead": "e82d0c79e01687ce7407df786cc784ad82166cb3", 25 | "bugs": { 26 | "url": "https://github.com/tastejs/todomvc-common/issues" 27 | }, 28 | "homepage": "https://github.com/tastejs/todomvc-common", 29 | "_id": "todomvc-common@1.0.2", 30 | "scripts": {}, 31 | "_shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0", 32 | "_from": "todomvc-common@*", 33 | "_npmVersion": "2.7.4", 34 | "_nodeVersion": "0.12.2", 35 | "_npmUser": { 36 | "name": "sindresorhus", 37 | "email": "sindresorhus@gmail.com" 38 | }, 39 | "dist": { 40 | "shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0", 41 | "tarball": "http://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz" 42 | }, 43 | "maintainers": [ 44 | { 45 | "name": "sindresorhus", 46 | "email": "sindresorhus@gmail.com" 47 | }, 48 | { 49 | "name": "addyosmani", 50 | "email": "addyosmani@gmail.com" 51 | }, 52 | { 53 | "name": "passy", 54 | "email": "phartig@rdrei.net" 55 | }, 56 | { 57 | "name": "stephenplusplus", 58 | "email": "sawchuk@gmail.com" 59 | } 60 | ], 61 | "directories": {}, 62 | "_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz" 63 | } 64 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package vecty 4 | 5 | import ( 6 | "reflect" 7 | "strings" 8 | 9 | "syscall/js" 10 | ) 11 | 12 | const ( 13 | structTag = "js" 14 | 15 | structTagOptionIncludeEmpty = "includeEmpty" 16 | ) 17 | 18 | const valueFieldName = "Value" 19 | 20 | var jsValueType = reflect.TypeOf(js.Value{}) 21 | 22 | type JSValuer interface { 23 | JSValue() js.Value 24 | } 25 | 26 | // Value Returns the js value of a type 27 | func Value(p interface{}) js.Value { 28 | vr, ok := p.(JSValuer) 29 | if ok { 30 | return vr.JSValue() 31 | } 32 | 33 | t := reflect.TypeOf(p) 34 | rv := reflect.ValueOf(p) 35 | 36 | switch t.Kind() { 37 | case reflect.Struct: 38 | // If the struct has an embedded js.Value then we return that. 39 | f, ok := t.FieldByName(valueFieldName) 40 | if ok && f.Anonymous && f.Type == jsValueType { 41 | return rv.FieldByName(valueFieldName).Interface().(js.Value) 42 | } 43 | 44 | v := js.Global().Get("Object").New() 45 | structValue(v, p) 46 | return v 47 | case reflect.Ptr: 48 | return Value(rv.Elem().Interface()) 49 | default: 50 | return js.ValueOf(p) 51 | } 52 | } 53 | 54 | func structValue(v js.Value, p interface{}) { 55 | t := reflect.TypeOf(p) 56 | rv := reflect.ValueOf(p) 57 | 58 | for i := 0; i < t.NumField(); i++ { 59 | field := t.Field(i) 60 | fv := rv.Field(i) 61 | if !fv.CanInterface() { 62 | continue 63 | } 64 | 65 | fn := field.Name 66 | 67 | tag := strings.Split(field.Tag.Get(structTag), ",") 68 | 69 | if len(tag[0]) > 0 { 70 | fn = tag[0] 71 | } 72 | 73 | if field.Anonymous { 74 | structValue(v, fv.Interface()) 75 | continue 76 | } 77 | 78 | includeEmpty := len(tag) > 1 && tag[1] == structTagOptionIncludeEmpty 79 | 80 | if !includeEmpty && fv.Interface() == reflect.Zero(field.Type).Interface() { 81 | continue 82 | } 83 | 84 | v.Set(fn, Value(fv.Interface())) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-app-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-app-css", 3 | "version": "2.0.1", 4 | "description": "CSS for TodoMVC apps", 5 | "license": "CC-BY-4.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/tastejs/todomvc-app-css" 9 | }, 10 | "author": { 11 | "name": "Sindre Sorhus", 12 | "email": "sindresorhus@gmail.com", 13 | "url": "sindresorhus.com" 14 | }, 15 | "files": [ 16 | "index.css" 17 | ], 18 | "keywords": [ 19 | "todomvc", 20 | "tastejs", 21 | "app", 22 | "todo", 23 | "template", 24 | "css", 25 | "style", 26 | "stylesheet" 27 | ], 28 | "gitHead": "f1bb1aa9b19888f339055418374a9b3a2d4c6fc5", 29 | "bugs": { 30 | "url": "https://github.com/tastejs/todomvc-app-css/issues" 31 | }, 32 | "homepage": "https://github.com/tastejs/todomvc-app-css", 33 | "_id": "todomvc-app-css@2.0.1", 34 | "scripts": {}, 35 | "_shasum": "f64d50b744a8a83c1151a08055b88f3aa5ccb052", 36 | "_from": "todomvc-app-css@*", 37 | "_npmVersion": "2.5.1", 38 | "_nodeVersion": "0.12.0", 39 | "_npmUser": { 40 | "name": "sindresorhus", 41 | "email": "sindresorhus@gmail.com" 42 | }, 43 | "maintainers": [ 44 | { 45 | "name": "sindresorhus", 46 | "email": "sindresorhus@gmail.com" 47 | }, 48 | { 49 | "name": "addyosmani", 50 | "email": "addyosmani@gmail.com" 51 | }, 52 | { 53 | "name": "passy", 54 | "email": "phartig@rdrei.net" 55 | }, 56 | { 57 | "name": "stephenplusplus", 58 | "email": "sawchuk@gmail.com" 59 | } 60 | ], 61 | "dist": { 62 | "shasum": "f64d50b744a8a83c1151a08055b88f3aa5ccb052", 63 | "tarball": "http://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.1.tgz" 64 | }, 65 | "directories": {}, 66 | "_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.1.tgz" 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Vecty is a [React](https://facebook.github.io/react/)-like library for [GopherJS](https://github.com/gopherjs/gopherjs) so that you can do frontend development in Go instead of writing JavaScript/HTML/CSS. 6 | 7 | [![Build Status](https://travis-ci.org/gowasm/vecty.svg?branch=master)](https://travis-ci.org/gowasm/vecty) [![GoDoc](https://godoc.org/github.com/gowasm/vecty?status.svg)](https://godoc.org/github.com/gowasm/vecty) [![codecov](https://img.shields.io/codecov/c/github/gowasm/vecty/master.svg)](https://codecov.io/gh/gowasm/vecty) 8 | 9 | Features 10 | ======== 11 | 12 | - Share frontend and backend code. 13 | - Write everything in Go -- not JS/HTML/CSS! 14 | - XSS protection: unsafe HTML must be explicitly denoted as such. 15 | - Reusability: share components by making Go packages that others can import! 16 | 17 | Goals 18 | ===== 19 | 20 | - Simplicity 21 | - Keep things as simple as possible to understand *for newcomers*. 22 | - Designed from the ground up to be easily mastered (like Go)! 23 | - Performance 24 | - As efficient as possible, make it clear what each operation in your webpage will do. 25 | - Same performance as just using plain JS/HTML/CSS. 26 | - Composability 27 | - Nest components to form your entire user interface, seperate them logically as you would any normal Go package. 28 | 29 | Current Status 30 | ============== 31 | 32 | **Vecty is currently considered to be an experimental work-in-progress.** 33 | 34 | - APIs will change. 35 | - The scope of Vecty is only ~80% defined currently. 36 | - There are a number of important [open issues](https://github.com/gowasm/vecty/issues). 37 | 38 | For a list of projects currently using Vecty, see the [doc/projects-using-vecty.md](doc/projects-using-vecty.md) file. 39 | 40 | Community 41 | ========= 42 | 43 | - Join us in the [#gopherjs](https://gophers.slack.com/messages/gopherjs/) and [#vecty](https://gophers.slack.com/messages/vecty/) channels on the [Gophers Slack](https://gophersinvite.herokuapp.com/)! 44 | 45 | Changelog 46 | ========= 47 | 48 | See the [doc/CHANGELOG.md](doc/CHANGELOG.md) file. 49 | -------------------------------------------------------------------------------- /example/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gowasm/vecty" 5 | "github.com/gowasm/vecty/elem" 6 | "github.com/gowasm/vecty/event" 7 | "github.com/microcosm-cc/bluemonday" 8 | "github.com/russross/blackfriday" 9 | ) 10 | 11 | func main() { 12 | vecty.SetTitle("Markdown Demo") 13 | vecty.RenderBody(&PageView{ 14 | Input: `# Markdown Example 15 | 16 | This is a live editor, try editing the Markdown on the right of the page. 17 | `, 18 | }) 19 | } 20 | 21 | // PageView is our main page component. 22 | type PageView struct { 23 | vecty.Core 24 | Input string 25 | } 26 | 27 | // Render implements the vecty.Component interface. 28 | func (p *PageView) Render() vecty.ComponentOrHTML { 29 | return elem.Body( 30 | // Display a textarea on the right-hand side of the page. 31 | elem.Div( 32 | vecty.Markup( 33 | vecty.Style("float", "right"), 34 | ), 35 | elem.TextArea( 36 | vecty.Markup( 37 | vecty.Style("font-family", "monospace"), 38 | vecty.Property("rows", 14), 39 | vecty.Property("cols", 70), 40 | 41 | // When input is typed into the textarea, update the local 42 | // component state and rerender. 43 | event.Input(func(e *vecty.Event) { 44 | p.Input = e.Target.Get("value").String() 45 | vecty.Rerender(p) 46 | }), 47 | ), 48 | vecty.Text(p.Input), // initial textarea text. 49 | ), 50 | ), 51 | 52 | // Render the markdown. 53 | &Markdown{Input: p.Input}, 54 | ) 55 | } 56 | 57 | // Markdown is a simple component which renders the Input markdown as sanitized 58 | // HTML into a div. 59 | type Markdown struct { 60 | vecty.Core 61 | Input string `vecty:"prop"` 62 | } 63 | 64 | // Render implements the vecty.Component interface. 65 | func (m *Markdown) Render() vecty.ComponentOrHTML { 66 | // Render the markdown input into HTML using Blackfriday. 67 | unsafeHTML := blackfriday.MarkdownCommon([]byte(m.Input)) 68 | 69 | // Sanitize the HTML. 70 | safeHTML := string(bluemonday.UGCPolicy().SanitizeBytes(unsafeHTML)) 71 | 72 | // Return the HTML, which we guarantee to be safe / sanitized. 73 | return elem.Div( 74 | vecty.Markup( 75 | vecty.UnsafeHTML(safeHTML), 76 | ), 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /prop/prop.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package prop 4 | 5 | import "github.com/gowasm/vecty" 6 | 7 | type InputType string 8 | 9 | const ( 10 | TypeButton InputType = "button" 11 | TypeCheckbox InputType = "checkbox" 12 | TypeColor InputType = "color" 13 | TypeDate InputType = "date" 14 | TypeDatetime InputType = "datetime" 15 | TypeDatetimeLocal InputType = "datetime-local" 16 | TypeEmail InputType = "email" 17 | TypeFile InputType = "file" 18 | TypeHidden InputType = "hidden" 19 | TypeImage InputType = "image" 20 | TypeMonth InputType = "month" 21 | TypeNumber InputType = "number" 22 | TypePassword InputType = "password" 23 | TypeRadio InputType = "radio" 24 | TypeRange InputType = "range" 25 | TypeMin InputType = "min" 26 | TypeMax InputType = "max" 27 | TypeValue InputType = "value" 28 | TypeStep InputType = "step" 29 | TypeReset InputType = "reset" 30 | TypeSearch InputType = "search" 31 | TypeSubmit InputType = "submit" 32 | TypeTel InputType = "tel" 33 | TypeText InputType = "text" 34 | TypeTime InputType = "time" 35 | TypeUrl InputType = "url" 36 | TypeWeek InputType = "week" 37 | ) 38 | 39 | func Autofocus(autofocus bool) vecty.Applyer { 40 | return vecty.Property("autofocus", autofocus) 41 | } 42 | 43 | func Checked(checked bool) vecty.Applyer { 44 | return vecty.Property("checked", checked) 45 | } 46 | 47 | func For(id string) vecty.Applyer { 48 | return vecty.Property("htmlFor", id) 49 | } 50 | 51 | func Href(url string) vecty.Applyer { 52 | return vecty.Property("href", url) 53 | } 54 | 55 | func ID(id string) vecty.Applyer { 56 | return vecty.Property("id", id) 57 | } 58 | 59 | func Placeholder(text string) vecty.Applyer { 60 | return vecty.Property("placeholder", text) 61 | } 62 | 63 | func Src(url string) vecty.Applyer { 64 | return vecty.Property("src", url) 65 | } 66 | 67 | func Type(t InputType) vecty.Applyer { 68 | return vecty.Property("type", string(t)) 69 | } 70 | 71 | func Value(v string) vecty.Applyer { 72 | return vecty.Property("value", v) 73 | } 74 | -------------------------------------------------------------------------------- /example/todomvc/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/gowasm/vecty/example/todomvc/actions" 5 | "github.com/gowasm/vecty/example/todomvc/dispatcher" 6 | "github.com/gowasm/vecty/example/todomvc/store/model" 7 | "github.com/gowasm/vecty/example/todomvc/store/storeutil" 8 | ) 9 | 10 | var ( 11 | // Items represents all of the TODO items in the store. 12 | Items []*model.Item 13 | 14 | // Filter represents the active viewing filter for items. 15 | Filter = model.All 16 | 17 | // Listeners is the listeners that will be invoked when the store changes. 18 | Listeners = storeutil.NewListenerRegistry() 19 | ) 20 | 21 | func init() { 22 | dispatcher.Register(onAction) 23 | } 24 | 25 | // ActiveItemCount returns the current number of items that are not completed. 26 | func ActiveItemCount() int { 27 | return count(false) 28 | } 29 | 30 | // CompletedItemCount returns the current number of items that are completed. 31 | func CompletedItemCount() int { 32 | return count(true) 33 | } 34 | 35 | func count(completed bool) int { 36 | count := 0 37 | for _, item := range Items { 38 | if item.Completed == completed { 39 | count++ 40 | } 41 | } 42 | return count 43 | } 44 | 45 | func onAction(action interface{}) { 46 | switch a := action.(type) { 47 | case *actions.ReplaceItems: 48 | Items = a.Items 49 | 50 | case *actions.AddItem: 51 | Items = append(Items, &model.Item{Title: a.Title, Completed: false}) 52 | 53 | case *actions.DestroyItem: 54 | copy(Items[a.Index:], Items[a.Index+1:]) 55 | Items = Items[:len(Items)-1] 56 | 57 | case *actions.SetTitle: 58 | Items[a.Index].Title = a.Title 59 | 60 | case *actions.SetCompleted: 61 | Items[a.Index].Completed = a.Completed 62 | 63 | case *actions.SetAllCompleted: 64 | for _, item := range Items { 65 | item.Completed = a.Completed 66 | } 67 | 68 | case *actions.ClearCompleted: 69 | var activeItems []*model.Item 70 | for _, item := range Items { 71 | if !item.Completed { 72 | activeItems = append(activeItems, item) 73 | } 74 | } 75 | Items = activeItems 76 | 77 | case *actions.SetFilter: 78 | Filter = a.Filter 79 | 80 | default: 81 | return // don't fire listeners 82 | } 83 | 84 | Listeners.Fire() 85 | } 86 | -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /example/todomvc/components/itemview.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/gowasm/vecty" 5 | "github.com/gowasm/vecty/elem" 6 | "github.com/gowasm/vecty/event" 7 | "github.com/gowasm/vecty/example/todomvc/actions" 8 | "github.com/gowasm/vecty/example/todomvc/dispatcher" 9 | "github.com/gowasm/vecty/example/todomvc/store/model" 10 | "github.com/gowasm/vecty/prop" 11 | "github.com/gowasm/vecty/style" 12 | ) 13 | 14 | // ItemView is a vecty.Component which represents a single item in the TODO 15 | // list. 16 | type ItemView struct { 17 | vecty.Core 18 | 19 | Index int `vecty:"prop"` 20 | Item *model.Item `vecty:"prop"` 21 | editing bool 22 | editTitle string 23 | input *vecty.HTML 24 | } 25 | 26 | // Key implements the vecty.Keyer interface. 27 | func (p *ItemView) Key() interface{} { 28 | return p.Index 29 | } 30 | 31 | func (p *ItemView) onDestroy(event *vecty.Event) { 32 | dispatcher.Dispatch(&actions.DestroyItem{ 33 | Index: p.Index, 34 | }) 35 | } 36 | 37 | func (p *ItemView) onToggleCompleted(event *vecty.Event) { 38 | dispatcher.Dispatch(&actions.SetCompleted{ 39 | Index: p.Index, 40 | Completed: event.Target.Get("checked").Bool(), 41 | }) 42 | } 43 | 44 | func (p *ItemView) onStartEdit(event *vecty.Event) { 45 | p.editing = true 46 | p.editTitle = p.Item.Title 47 | vecty.Rerender(p) 48 | p.input.Node().Call("focus") 49 | } 50 | 51 | func (p *ItemView) onEditInput(event *vecty.Event) { 52 | p.editTitle = event.Target.Get("value").String() 53 | vecty.Rerender(p) 54 | } 55 | 56 | func (p *ItemView) onStopEdit(event *vecty.Event) { 57 | p.editing = false 58 | vecty.Rerender(p) 59 | dispatcher.Dispatch(&actions.SetTitle{ 60 | Index: p.Index, 61 | Title: p.editTitle, 62 | }) 63 | } 64 | 65 | // Render implements the vecty.Component interface. 66 | func (p *ItemView) Render() vecty.ComponentOrHTML { 67 | p.input = elem.Input( 68 | vecty.Markup( 69 | vecty.Class("edit"), 70 | prop.Value(p.editTitle), 71 | event.Input(p.onEditInput), 72 | ), 73 | ) 74 | 75 | return elem.ListItem( 76 | vecty.Markup( 77 | vecty.ClassMap{ 78 | "completed": p.Item.Completed, 79 | "editing": p.editing, 80 | }, 81 | ), 82 | 83 | elem.Div( 84 | vecty.Markup( 85 | vecty.Class("view"), 86 | ), 87 | 88 | elem.Input( 89 | vecty.Markup( 90 | vecty.Class("toggle"), 91 | prop.Type(prop.TypeCheckbox), 92 | prop.Checked(p.Item.Completed), 93 | event.Change(p.onToggleCompleted), 94 | ), 95 | ), 96 | elem.Label( 97 | vecty.Markup( 98 | event.DoubleClick(p.onStartEdit), 99 | ), 100 | vecty.Text(p.Item.Title), 101 | ), 102 | elem.Button( 103 | vecty.Markup( 104 | vecty.Class("destroy"), 105 | event.Click(p.onDestroy), 106 | ), 107 | ), 108 | ), 109 | elem.Form( 110 | vecty.Markup( 111 | style.Margin(style.Px(0)), 112 | event.Submit(p.onStopEdit).PreventDefault(), 113 | ), 114 | p.input, 115 | ), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /example/todomvc/components/pageview.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gowasm/vecty" 7 | "github.com/gowasm/vecty/elem" 8 | "github.com/gowasm/vecty/event" 9 | "github.com/gowasm/vecty/example/todomvc/actions" 10 | "github.com/gowasm/vecty/example/todomvc/dispatcher" 11 | "github.com/gowasm/vecty/example/todomvc/store" 12 | "github.com/gowasm/vecty/example/todomvc/store/model" 13 | "github.com/gowasm/vecty/prop" 14 | "github.com/gowasm/vecty/style" 15 | ) 16 | 17 | // PageView is a vecty.Component which represents the entire page. 18 | type PageView struct { 19 | vecty.Core 20 | 21 | Items []*model.Item `vecty:"prop"` 22 | newItemTitle string 23 | } 24 | 25 | func (p *PageView) onNewItemTitleInput(event *vecty.Event) { 26 | p.newItemTitle = event.Target.Get("value").String() 27 | vecty.Rerender(p) 28 | } 29 | 30 | func (p *PageView) onAdd(event *vecty.Event) { 31 | dispatcher.Dispatch(&actions.AddItem{ 32 | Title: p.newItemTitle, 33 | }) 34 | p.newItemTitle = "" 35 | vecty.Rerender(p) 36 | } 37 | 38 | func (p *PageView) onClearCompleted(event *vecty.Event) { 39 | dispatcher.Dispatch(&actions.ClearCompleted{}) 40 | } 41 | 42 | func (p *PageView) onToggleAllCompleted(event *vecty.Event) { 43 | dispatcher.Dispatch(&actions.SetAllCompleted{ 44 | Completed: event.Target.Get("checked").Bool(), 45 | }) 46 | } 47 | 48 | // Render implements the vecty.Component interface. 49 | func (p *PageView) Render() vecty.ComponentOrHTML { 50 | return elem.Body( 51 | elem.Section( 52 | vecty.Markup( 53 | vecty.Class("todoapp"), 54 | ), 55 | 56 | p.renderHeader(), 57 | vecty.If(len(store.Items) > 0, 58 | p.renderItemList(), 59 | p.renderFooter(), 60 | ), 61 | ), 62 | 63 | p.renderInfo(), 64 | ) 65 | } 66 | 67 | func (p *PageView) renderHeader() *vecty.HTML { 68 | return elem.Header( 69 | vecty.Markup( 70 | vecty.Class("header"), 71 | ), 72 | 73 | elem.Heading1( 74 | vecty.Text("todos"), 75 | ), 76 | elem.Form( 77 | vecty.Markup( 78 | style.Margin(style.Px(0)), 79 | event.Submit(p.onAdd).PreventDefault(), 80 | ), 81 | 82 | elem.Input( 83 | vecty.Markup( 84 | vecty.Class("new-todo"), 85 | prop.Placeholder("What needs to be done?"), 86 | prop.Autofocus(true), 87 | prop.Value(p.newItemTitle), 88 | event.Input(p.onNewItemTitleInput), 89 | ), 90 | ), 91 | ), 92 | ) 93 | } 94 | 95 | func (p *PageView) renderFooter() *vecty.HTML { 96 | count := store.ActiveItemCount() 97 | var itemsLeftText = " items left" 98 | if count == 1 { 99 | itemsLeftText = " item left" 100 | } 101 | 102 | return elem.Footer( 103 | vecty.Markup( 104 | vecty.Class("footer"), 105 | ), 106 | 107 | elem.Span( 108 | vecty.Markup( 109 | vecty.Class("todo-count"), 110 | ), 111 | 112 | elem.Strong( 113 | vecty.Text(strconv.Itoa(count)), 114 | ), 115 | vecty.Text(itemsLeftText), 116 | ), 117 | 118 | elem.UnorderedList( 119 | vecty.Markup( 120 | vecty.Class("filters"), 121 | ), 122 | &FilterButton{Label: "All", Filter: model.All}, 123 | vecty.Text(" "), 124 | &FilterButton{Label: "Active", Filter: model.Active}, 125 | vecty.Text(" "), 126 | &FilterButton{Label: "Completed", Filter: model.Completed}, 127 | ), 128 | 129 | vecty.If(store.CompletedItemCount() > 0, 130 | elem.Button( 131 | vecty.Markup( 132 | vecty.Class("clear-completed"), 133 | event.Click(p.onClearCompleted), 134 | ), 135 | vecty.Text("Clear completed ("+strconv.Itoa(store.CompletedItemCount())+")"), 136 | ), 137 | ), 138 | ) 139 | } 140 | 141 | func (p *PageView) renderInfo() *vecty.HTML { 142 | return elem.Footer( 143 | vecty.Markup( 144 | vecty.Class("info"), 145 | ), 146 | 147 | elem.Paragraph( 148 | vecty.Text("Double-click to edit a todo"), 149 | ), 150 | elem.Paragraph( 151 | vecty.Text("Created by "), 152 | elem.Anchor( 153 | vecty.Markup( 154 | prop.Href("http://github.com/neelance"), 155 | ), 156 | vecty.Text("Richard Musiol"), 157 | ), 158 | ), 159 | elem.Paragraph( 160 | vecty.Text("Part of "), 161 | elem.Anchor( 162 | vecty.Markup( 163 | prop.Href("http://todomvc.com"), 164 | ), 165 | vecty.Text("TodoMVC"), 166 | ), 167 | ), 168 | ) 169 | } 170 | 171 | func (p *PageView) renderItemList() *vecty.HTML { 172 | var items vecty.List 173 | for i, item := range store.Items { 174 | if (store.Filter == model.Active && item.Completed) || (store.Filter == model.Completed && !item.Completed) { 175 | continue 176 | } 177 | items = append(items, &ItemView{Index: i, Item: item}) 178 | } 179 | 180 | return elem.Section( 181 | vecty.Markup( 182 | vecty.Class("main"), 183 | ), 184 | 185 | elem.Input( 186 | vecty.Markup( 187 | vecty.Class("toggle-all"), 188 | prop.ID("toggle-all"), 189 | prop.Type(prop.TypeCheckbox), 190 | prop.Checked(store.CompletedItemCount() == len(store.Items)), 191 | event.Change(p.onToggleAllCompleted), 192 | ), 193 | ), 194 | elem.Label( 195 | vecty.Markup( 196 | prop.For("toggle-all"), 197 | ), 198 | vecty.Text("Mark all as complete"), 199 | ), 200 | 201 | elem.UnorderedList( 202 | vecty.Markup( 203 | vecty.Class("todo-list"), 204 | ), 205 | items, 206 | ), 207 | ) 208 | } 209 | -------------------------------------------------------------------------------- /doc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Although v1.0.0 [is not yet out](https://github.com/gowasm/vecty/milestone/1), we do not expect many breaking changes. When there is one, however, it is documented clearly here. 5 | 6 | Pre-v1.0.0 Breaking Changes 7 | --------------------------- 8 | 9 | ## Nov 4, 2017 ([PR #158](https://github.com/gowasm/vecty/pull/158)): major breaking change 10 | 11 | All `Component`s must now have a `Render` method which returns `vecty.ComponentOrHTML` instead of the prior `*vecty.HTML` type. 12 | 13 | This change allows for higher order components (components that themselves render components), which is useful for many more advanced uses of Vecty. 14 | 15 | ### Upgrading 16 | 17 | Upgrading most codebases should be trivial with a find-and-replace across all files. 18 | 19 | From your editor: 20 | * Find `) Render() *vecty.HTML` and replace with `) Render() vecty.ComponentOrHTML`. 21 | 22 | From the __Linux__ command line: 23 | ```bash 24 | git grep -l ') Render() \*vecty.HTML' | xargs sed -i 's/) Render() \*vecty.HTML/) Render() vecty.ComponentOrHTML/g' 25 | ``` 26 | 27 | From the __Mac__ command line: 28 | ```bash 29 | git grep -l ') Render() \*vecty.HTML' | xargs sed -i '' -e 's/) Render() \*vecty.HTML/) Render() vecty.ComponentOrHTML/g' 30 | ``` 31 | 32 | Obviously, you'll still need to verify that this only modifies your `Component` implementations. No other changes are needed, and no behavior change is expected for components that return `*vecty.HTML` (as the new `vecty.ComponentOrHTML` interface return type). 33 | 34 | ## Oct 14, 2017 ([PR #155](https://github.com/gowasm/vecty/pull/155)): major breaking change 35 | 36 | The function `prop.Class(string)` has been removed and replaced with `vecty.Class(...string)`. Migrating users must use the new function and split their classes into separate strings, rather than a single space-separated string. 37 | 38 | ## Oct 1, 2017 ([PR #147](https://github.com/gowasm/vecty/pull/147)): minor breaking change 39 | 40 | `MarkupOrChild` and `ComponentOrHTML` can both now contain `KeyedList` (a new type that has been added) 41 | 42 | ## Sept 5, 2017 ([PR #140](https://github.com/gowasm/vecty/pull/140)): minor breaking change 43 | 44 | Package `storeutil` has been moved to `github.com/gowasm/vecty/example/todomvc/store/storeutil` import path. 45 | 46 | 47 | ## Sept 2, 2017 ([PR #134](https://github.com/gowasm/vecty/pull/134)): major breaking change 48 | 49 | Several breaking changes have been made. Below, we describe how to upgrade your Vecty code to reflect each of these changes. 50 | 51 | On the surface, these changes _may_ appear to be needless or simple API changes, however when combined they in fact resolve one of the last major open issues about how Vecty fundamentally operates. With this change, Vecty now ensures that the persistent pointer to your component instances remain the same regardless of e.g. the styles that you pass into element constructors. 52 | 53 | ### constructors no longer accept markup directly 54 | 55 | `Tag`, `Text`, and `elem.Foo` constructors no longer accept markup (styles, properties, etc.) directly. You must now specify them via `vecty.Markup`. For example, this code: 56 | 57 | ```Go 58 | func (p *PageView) Render() *vecty.HTML { 59 | return elem.Body( 60 | vecty.Style("background", "red"), 61 | vecty.Text("Hello World"), 62 | ) 63 | } 64 | ``` 65 | 66 | Must now be written as: 67 | 68 | ```Go 69 | func (p *PageView) Render() *vecty.HTML { 70 | return elem.Body( 71 | vecty.Markup( 72 | vecty.Style("background", "red"), 73 | ), 74 | vecty.Text("Hello World"), 75 | ) 76 | } 77 | ``` 78 | 79 | ### If no longer works for markup 80 | 81 | `If` now only accepts `ComponentOrHTML` (meaning `Component`, `*HTML`, `List` or `nil`). It does not accept markup anymore (styles, properties, etc). A new `MarkupIf` function is added for this purpose. For example you would need to make a change like this to your code: 82 | 83 | ```diff 84 | func (p *PageView) Render() *vecty.HTML { 85 | return elem.Body( 86 | vecty.Markup( 87 | - vecty.If(isBackgroundRed, vecty.Style("background", "red")), 88 | + vecty.MarkupIf(isBackgroundRed, vecty.Style("background", "red")), 89 | ), 90 | vecty.Text("Hello World"), 91 | ) 92 | } 93 | ``` 94 | 95 | ### Other breaking changes 96 | 97 | - `ComponentOrHTML` now includes `nil` and the new `List` type, rather than just `Component` and `*HTML`. 98 | - `MarkupOrComponentOrHTML` has been renamed to `MarkupOrChild`, and now includes `nil` and the new `List` and `MarkupList` (instead of `Markup`, see below) types. 99 | - The `Markup` _interface_ has been renamed to `Applyer`, and a `Markup` _function_ has been added to create a `MarkupList`. 100 | 101 | 102 | ## Aug 6, 2017 ([PR #130](https://github.com/gowasm/vecty/pull/130)): minor breaking change 103 | 104 | The `Restorer` interface has been removed, component instances are now persistent. Properties should be denoted via ``` `vecty:"prop"` ``` struct field tags. 105 | 106 | 107 | ## Jun 17, 2017 ([PR #117](https://github.com/gowasm/vecty/pull/117)): minor breaking change 108 | 109 | `(*HTML).Restore` is no longer exported, this method was not generally used externally. 110 | 111 | 112 | ## May 11, 2017 ([PR #108](https://github.com/gowasm/vecty/pull/108)): minor breaking change 113 | 114 | `(*HTML).Node` is now a function instead of a struct field. 115 | -------------------------------------------------------------------------------- /elem/generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | 11 | "github.com/PuerkitoBio/goquery" 12 | ) 13 | 14 | // elemNameMap translates lowercase HTML tag names from the MDN source into a 15 | // proper Go style name with MixedCaps and initialisms: 16 | // 17 | // https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps 18 | // https://github.com/golang/go/wiki/CodeReviewComments#initialisms 19 | // 20 | var elemNameMap = map[string]string{ 21 | "a": "Anchor", 22 | "abbr": "Abbreviation", 23 | "b": "Bold", 24 | "bdi": "BidirectionalIsolation", 25 | "bdo": "BidirectionalOverride", 26 | "blockquote": "BlockQuote", 27 | "br": "Break", 28 | "cite": "Citation", 29 | "col": "Column", 30 | "colgroup": "ColumnGroup", 31 | "datalist": "DataList", 32 | "dd": "Description", 33 | "del": "DeletedText", 34 | "dfn": "Definition", 35 | "dl": "DescriptionList", 36 | "dt": "DefinitionTerm", 37 | "em": "Emphasis", 38 | "fieldset": "FieldSet", 39 | "figcaption": "FigureCaption", 40 | "h1": "Heading1", 41 | "h2": "Heading2", 42 | "h3": "Heading3", 43 | "h4": "Heading4", 44 | "h5": "Heading5", 45 | "h6": "Heading6", 46 | "hgroup": "HeadingsGroup", 47 | "hr": "HorizontalRule", 48 | "i": "Italic", 49 | "iframe": "InlineFrame", 50 | "img": "Image", 51 | "ins": "InsertedText", 52 | "kbd": "KeyboardInput", 53 | "li": "ListItem", 54 | "menuitem": "MenuItem", 55 | "nav": "Navigation", 56 | "noframes": "NoFrames", 57 | "noscript": "NoScript", 58 | "ol": "OrderedList", 59 | "optgroup": "OptionsGroup", 60 | "p": "Paragraph", 61 | "param": "Parameter", 62 | "pre": "Preformatted", 63 | "q": "Quote", 64 | "rp": "RubyParenthesis", 65 | "rt": "RubyText", 66 | "rtc": "RubyTextContainer", 67 | "s": "Strikethrough", 68 | "samp": "Sample", 69 | "sub": "Subscript", 70 | "sup": "Superscript", 71 | "tbody": "TableBody", 72 | "textarea": "TextArea", 73 | "td": "TableData", 74 | "tfoot": "TableFoot", 75 | "th": "TableHeader", 76 | "thead": "TableHead", 77 | "tr": "TableRow", 78 | "u": "Underline", 79 | "ul": "UnorderedList", 80 | "var": "Variable", 81 | "wbr": "WordBreakOpportunity", 82 | } 83 | 84 | func main() { 85 | doc, err := goquery.NewDocument("https://developer.mozilla.org/en-US/docs/Web/HTML/Element") 86 | if err != nil { 87 | panic(err) 88 | } 89 | 90 | file, err := os.Create("elem.gen.go") 91 | if err != nil { 92 | panic(err) 93 | } 94 | defer file.Close() 95 | 96 | fmt.Fprint(file, `//go:generate go run generate.go 97 | 98 | // Package elem defines markup to create DOM elements. 99 | // 100 | // Generated from "HTML element reference" by Mozilla Contributors, 101 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element, licensed under 102 | // CC-BY-SA 2.5. 103 | package elem 104 | 105 | import "github.com/gowasm/vecty" 106 | `) 107 | 108 | doc.Find(".quick-links a").Each(func(i int, s *goquery.Selection) { 109 | link, _ := s.Attr("href") 110 | if !strings.HasPrefix(link, "/en-US/docs/Web/HTML/Element/") { 111 | return 112 | } 113 | 114 | if s.Parent().Find(".icon-trash, .icon-thumbs-down-alt, .icon-warning-sign").Length() > 0 { 115 | return 116 | } 117 | 118 | desc, _ := s.Attr("title") 119 | 120 | text := s.Text() 121 | if text == "

" { 122 | writeElem(file, "h1", desc, link) 123 | writeElem(file, "h2", desc, link) 124 | writeElem(file, "h3", desc, link) 125 | writeElem(file, "h4", desc, link) 126 | writeElem(file, "h5", desc, link) 127 | writeElem(file, "h6", desc, link) 128 | return 129 | } 130 | 131 | name := text[1 : len(text)-1] 132 | if name == "html" || name == "head" { 133 | return 134 | } 135 | 136 | writeElem(file, name, desc, link) 137 | }) 138 | } 139 | 140 | func writeElem(w io.Writer, name, desc, link string) { 141 | funName := elemNameMap[name] 142 | if funName == "" { 143 | funName = capitalize(name) 144 | } 145 | 146 | // Descriptions for elements generally read as: 147 | // 148 | // The HTML element ... 149 | // 150 | // Because these are consistent (sometimes with varying captalization, 151 | // however) we can exploit that fact to reword the documentation in proper 152 | // Go style: 153 | // 154 | // Foobar ... 155 | // 156 | generalLowercase := fmt.Sprintf("the html <%s> element", strings.ToLower(name)) 157 | 158 | // Replace a number of 'no-break space' unicode characters which exist in 159 | // the descriptions with normal spaces. 160 | desc = strings.Replace(desc, "\u00a0", " ", -1) 161 | if l := len(generalLowercase); len(desc) > l && strings.HasPrefix(strings.ToLower(desc), generalLowercase) { 162 | desc = fmt.Sprintf("%s%s", funName, desc[l:]) 163 | } 164 | 165 | fmt.Fprintf(w, `%s 166 | // 167 | // https://developer.mozilla.org%s 168 | func %s(markup ...vecty.MarkupOrChild) *vecty.HTML { 169 | return vecty.Tag("%s", markup...) 170 | } 171 | `, descToComments(desc), link, funName, name) 172 | } 173 | 174 | func capitalize(s string) string { 175 | return strings.ToUpper(s[:1]) + s[1:] 176 | } 177 | 178 | func descToComments(desc string) string { 179 | c := "" 180 | length := 80 181 | for _, word := range strings.Fields(desc) { 182 | if length+len(word)+1 > 80 { 183 | length = 3 184 | c += "\n//" 185 | } 186 | c += " " + word 187 | length += len(word) + 1 188 | } 189 | return c 190 | } 191 | -------------------------------------------------------------------------------- /event/generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/PuerkitoBio/goquery" 12 | ) 13 | 14 | type Event struct { 15 | Name string 16 | Link string 17 | Desc string 18 | Spec string 19 | } 20 | 21 | func main() { 22 | // nameMap translates lowercase HTML attribute names from the MDN source 23 | // into a proper Go style name with MixedCaps and initialisms: 24 | // 25 | // https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps 26 | // https://github.com/golang/go/wiki/CodeReviewComments#initialisms 27 | // 28 | nameMap := map[string]string{ 29 | "afterprint": "AfterPrint", 30 | "animationend": "AnimationEnd", 31 | "animationiteration": "AnimationIteration", 32 | "animationstart": "AnimationStart", 33 | "audioprocess": "AudioProcess", 34 | "audioend": "AudioEnd", 35 | "audiostart": "AudioStart", 36 | "beforeprint": "BeforePrint", 37 | "beforeunload": "BeforeUnload", 38 | "canplay": "CanPlay", 39 | "canplaythrough": "CanPlayThrough", 40 | "chargingchange": "ChargingChange", 41 | "chargingtimechange": "ChargingTimeChange", 42 | "compassneedscalibration": "CompassNeedsCalibration", 43 | "compositionend": "CompositionEnd", 44 | "compositionstart": "CompositionStart", 45 | "compositionupdate": "CompositionUpdate", 46 | "contextmenu": "ContextMenu", 47 | "dblclick": "DoubleClick", 48 | "devicelight": "DeviceLight", 49 | "devicemotion": "DeviceMotion", 50 | "deviceorientation": "DeviceOrientation", 51 | "deviceproximity": "DeviceProximity", 52 | "dischargingtimechange": "DischargingTimeChange", 53 | "dragend": "DragEnd", 54 | "dragenter": "DragEnter", 55 | "dragleave": "DragLeave", 56 | "dragover": "DragOver", 57 | "dragstart": "DragStart", 58 | "durationchange": "DurationChange", 59 | "focusin": "FocusIn", 60 | "focusout": "FocusOut", 61 | "fullscreenchange": "FullScreenChange", 62 | "fullscreenerror": "FullScreenError", 63 | "gamepadconnected": "GamepadConnected", 64 | "gamepaddisconnected": "GamepadDisconnected", 65 | "gotpointercapture": "GotPointerCapture", 66 | "hashchange": "HashChange", 67 | "keydown": "KeyDown", 68 | "keypress": "KeyPress", 69 | "keyup": "KeyUp", 70 | "languagechange": "LanguageChange", 71 | "levelchange": "LevelChange", 72 | "loadeddata": "LoadedData", 73 | "loadedmetadata": "LoadedMetadata", 74 | "loadend": "LoadEnd", 75 | "loadstart": "LoadStart", 76 | "lostpointercapture": "LostPointerCapture", 77 | "mousedown": "MouseDown", 78 | "mouseenter": "MouseEnter", 79 | "mouseleave": "MouseLeave", 80 | "mousemove": "MouseMove", 81 | "mouseout": "MouseOut", 82 | "mouseover": "MouseOver", 83 | "mouseup": "MouseUp", 84 | "noupdate": "NoUpdate", 85 | "nomatch": "NoMatch", 86 | "notificationclick": "NotificationClick", 87 | "orientationchange": "OrientationChange", 88 | "pagehide": "PageHide", 89 | "pageshow": "PageShow", 90 | "pointercancel": "PointerCancel", 91 | "pointerdown": "PointerDown", 92 | "pointerenter": "PointerEnter", 93 | "pointerleave": "PointerLeave", 94 | "pointerlockchange": "PointerLockChange", 95 | "pointerlockerror": "PointerLockError", 96 | "pointermove": "PointerMove", 97 | "pointerout": "PointerOut", 98 | "pointerover": "PointerOver", 99 | "pointerup": "PointerUp", 100 | "popstate": "PopState", 101 | "pushsubscriptionchange": "PushSubscriptionChange", 102 | "ratechange": "RateChange", 103 | "readystatechange": "ReadyStateChange", 104 | "resourcetimingbufferfull": "ResourceTimingBufferFull", 105 | "selectstart": "SelectStart", 106 | "selectionchange": "SelectionChange", 107 | "soundend": "SoundEnd", 108 | "soundstart": "SoundStart", 109 | "speechend": "SpeechEnd", 110 | "speechstart": "SpeechStart", 111 | "timeupdate": "TimeUpdate", 112 | "touchcancel": "TouchCancel", 113 | "touchend": "TouchEnd", 114 | "touchenter": "TouchEnter", 115 | "touchleave": "TouchLeave", 116 | "touchmove": "TouchMove", 117 | "touchstart": "TouchStart", 118 | "transitionend": "TransitionEnd", 119 | "updateready": "UpdateReady", 120 | "upgradeneeded": "UpgradeNeeded", 121 | "userproximity": "UserProximity", 122 | "versionchange": "VersionChange", 123 | "visibilitychange": "VisibilityChange", 124 | "voiceschanged": "VoicesChanged", 125 | "volumechange": "VolumeChange", 126 | "vrdisplayconnected": "VRDisplayConnected", 127 | "vrdisplaydisconnected": "VRDisplayDisconnected", 128 | "vrdisplaypresentchange": "VRDisplayPresentChange", 129 | } 130 | 131 | doc, err := goquery.NewDocument("https://developer.mozilla.org/en-US/docs/Web/Events") 132 | if err != nil { 133 | panic(err) 134 | } 135 | 136 | events := make(map[string]*Event) 137 | 138 | doc.Find(".standard-table").Eq(0).Find("tr").Each(func(i int, s *goquery.Selection) { 139 | cols := s.Find("td") 140 | if cols.Length() == 0 || cols.Find(".icon-thumbs-down-alt").Length() != 0 { 141 | return 142 | } 143 | link := cols.Eq(0).Find("a").Eq(0) 144 | var e Event 145 | e.Name = link.Text() 146 | e.Link, _ = link.Attr("href") 147 | e.Desc = strings.TrimSpace(cols.Eq(3).Text()) 148 | e.Spec = strings.TrimSpace(cols.Eq(2).Text()) 149 | 150 | funName := nameMap[e.Name] 151 | if funName == "" { 152 | funName = capitalize(e.Name) 153 | } 154 | 155 | if e.Desc != "" { 156 | e.Desc = fmt.Sprintf("%s is an event fired when %s", funName, lowercase(e.Desc)) 157 | } else { 158 | e.Desc = "(no documentation)" 159 | } 160 | events[funName] = &e 161 | }) 162 | 163 | var names []string 164 | for name := range events { 165 | names = append(names, name) 166 | } 167 | sort.Strings(names) 168 | 169 | file, err := os.Create("event.gen.go") 170 | if err != nil { 171 | panic(err) 172 | } 173 | defer file.Close() 174 | 175 | fmt.Fprint(file, `//go:generate go run generate.go 176 | 177 | // Package event defines markup to bind DOM events. 178 | // 179 | // Generated from "Event reference" by Mozilla Contributors, 180 | // https://developer.mozilla.org/en-US/docs/Web/Events, licensed under 181 | // CC-BY-SA 2.5. 182 | package event 183 | 184 | import "github.com/gowasm/vecty" 185 | `) 186 | 187 | for _, name := range names { 188 | e := events[name] 189 | if e.Spec == "WebVR API" { 190 | continue // not stabilized 191 | } 192 | fmt.Fprintf(file, `%s 193 | // 194 | // https://developer.mozilla.org%s 195 | func %s(listener func(*vecty.Event)) *vecty.EventListener { 196 | return &vecty.EventListener{Name: "%s", Listener: listener} 197 | } 198 | `, descToComments(e.Desc), e.Link[6:], name, e.Name) 199 | } 200 | } 201 | 202 | func capitalize(s string) string { 203 | return strings.ToUpper(s[:1]) + s[1:] 204 | } 205 | 206 | func lowercase(s string) string { 207 | return strings.ToLower(s[:1]) + s[1:] 208 | } 209 | 210 | func descToComments(desc string) string { 211 | c := "" 212 | length := 80 213 | for _, word := range strings.Fields(desc) { 214 | if length+len(word)+1 > 80 { 215 | length = 3 216 | c += "\n//" 217 | } 218 | c += " " + word 219 | length += len(word) + 1 220 | } 221 | return c 222 | } 223 | -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-common/base.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | (function () { 3 | 'use strict'; 4 | 5 | /* jshint ignore:start */ 6 | // Underscore's Template Module 7 | // Courtesy of underscorejs.org 8 | var _ = (function (_) { 9 | _.defaults = function (object) { 10 | if (!object) { 11 | return object; 12 | } 13 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { 14 | var iterable = arguments[argsIndex]; 15 | if (iterable) { 16 | for (var key in iterable) { 17 | if (object[key] == null) { 18 | object[key] = iterable[key]; 19 | } 20 | } 21 | } 22 | } 23 | return object; 24 | } 25 | 26 | // By default, Underscore uses ERB-style template delimiters, change the 27 | // following template settings to use alternative delimiters. 28 | _.templateSettings = { 29 | evaluate : /<%([\s\S]+?)%>/g, 30 | interpolate : /<%=([\s\S]+?)%>/g, 31 | escape : /<%-([\s\S]+?)%>/g 32 | }; 33 | 34 | // When customizing `templateSettings`, if you don't want to define an 35 | // interpolation, evaluation or escaping regex, we need one that is 36 | // guaranteed not to match. 37 | var noMatch = /(.)^/; 38 | 39 | // Certain characters need to be escaped so that they can be put into a 40 | // string literal. 41 | var escapes = { 42 | "'": "'", 43 | '\\': '\\', 44 | '\r': 'r', 45 | '\n': 'n', 46 | '\t': 't', 47 | '\u2028': 'u2028', 48 | '\u2029': 'u2029' 49 | }; 50 | 51 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 52 | 53 | // JavaScript micro-templating, similar to John Resig's implementation. 54 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 55 | // and correctly escapes quotes within interpolated code. 56 | _.template = function(text, data, settings) { 57 | var render; 58 | settings = _.defaults({}, settings, _.templateSettings); 59 | 60 | // Combine delimiters into one regular expression via alternation. 61 | var matcher = new RegExp([ 62 | (settings.escape || noMatch).source, 63 | (settings.interpolate || noMatch).source, 64 | (settings.evaluate || noMatch).source 65 | ].join('|') + '|$', 'g'); 66 | 67 | // Compile the template source, escaping string literals appropriately. 68 | var index = 0; 69 | var source = "__p+='"; 70 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 71 | source += text.slice(index, offset) 72 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 73 | 74 | if (escape) { 75 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 76 | } 77 | if (interpolate) { 78 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 79 | } 80 | if (evaluate) { 81 | source += "';\n" + evaluate + "\n__p+='"; 82 | } 83 | index = offset + match.length; 84 | return match; 85 | }); 86 | source += "';\n"; 87 | 88 | // If a variable is not specified, place data values in local scope. 89 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 90 | 91 | source = "var __t,__p='',__j=Array.prototype.join," + 92 | "print=function(){__p+=__j.call(arguments,'');};\n" + 93 | source + "return __p;\n"; 94 | 95 | try { 96 | render = new Function(settings.variable || 'obj', '_', source); 97 | } catch (e) { 98 | e.source = source; 99 | throw e; 100 | } 101 | 102 | if (data) return render(data, _); 103 | var template = function(data) { 104 | return render.call(this, data, _); 105 | }; 106 | 107 | // Provide the compiled function source as a convenience for precompilation. 108 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 109 | 110 | return template; 111 | }; 112 | 113 | return _; 114 | })({}); 115 | 116 | if (location.hostname === 'todomvc.com') { 117 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 118 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 119 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 120 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 121 | ga('create', 'UA-31081062-1', 'auto'); 122 | ga('send', 'pageview'); 123 | } 124 | /* jshint ignore:end */ 125 | 126 | function redirect() { 127 | if (location.hostname === 'tastejs.github.io') { 128 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); 129 | } 130 | } 131 | 132 | function findRoot() { 133 | var base = location.href.indexOf('examples/'); 134 | return location.href.substr(0, base); 135 | } 136 | 137 | function getFile(file, callback) { 138 | if (!location.host) { 139 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); 140 | } 141 | 142 | var xhr = new XMLHttpRequest(); 143 | 144 | xhr.open('GET', findRoot() + file, true); 145 | xhr.send(); 146 | 147 | xhr.onload = function () { 148 | if (xhr.status === 200 && callback) { 149 | callback(xhr.responseText); 150 | } 151 | }; 152 | } 153 | 154 | function Learn(learnJSON, config) { 155 | if (!(this instanceof Learn)) { 156 | return new Learn(learnJSON, config); 157 | } 158 | 159 | var template, framework; 160 | 161 | if (typeof learnJSON !== 'object') { 162 | try { 163 | learnJSON = JSON.parse(learnJSON); 164 | } catch (e) { 165 | return; 166 | } 167 | } 168 | 169 | if (config) { 170 | template = config.template; 171 | framework = config.framework; 172 | } 173 | 174 | if (!template && learnJSON.templates) { 175 | template = learnJSON.templates.todomvc; 176 | } 177 | 178 | if (!framework && document.querySelector('[data-framework]')) { 179 | framework = document.querySelector('[data-framework]').dataset.framework; 180 | } 181 | 182 | this.template = template; 183 | 184 | if (learnJSON.backend) { 185 | this.frameworkJSON = learnJSON.backend; 186 | this.frameworkJSON.issueLabel = framework; 187 | this.append({ 188 | backend: true 189 | }); 190 | } else if (learnJSON[framework]) { 191 | this.frameworkJSON = learnJSON[framework]; 192 | this.frameworkJSON.issueLabel = framework; 193 | this.append(); 194 | } 195 | 196 | this.fetchIssueCount(); 197 | } 198 | 199 | Learn.prototype.append = function (opts) { 200 | var aside = document.createElement('aside'); 201 | aside.innerHTML = _.template(this.template, this.frameworkJSON); 202 | aside.className = 'learn'; 203 | 204 | if (opts && opts.backend) { 205 | // Remove demo link 206 | var sourceLinks = aside.querySelector('.source-links'); 207 | var heading = sourceLinks.firstElementChild; 208 | var sourceLink = sourceLinks.lastElementChild; 209 | // Correct link path 210 | var href = sourceLink.getAttribute('href'); 211 | sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); 212 | sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; 213 | } else { 214 | // Localize demo links 215 | var demoLinks = aside.querySelectorAll('.demo-link'); 216 | Array.prototype.forEach.call(demoLinks, function (demoLink) { 217 | if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { 218 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); 219 | } 220 | }); 221 | } 222 | 223 | document.body.className = (document.body.className + ' learn-bar').trim(); 224 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); 225 | }; 226 | 227 | Learn.prototype.fetchIssueCount = function () { 228 | var issueLink = document.getElementById('issue-count-link'); 229 | if (issueLink) { 230 | var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); 231 | var xhr = new XMLHttpRequest(); 232 | xhr.open('GET', url, true); 233 | xhr.onload = function (e) { 234 | var parsedResponse = JSON.parse(e.target.responseText); 235 | if (parsedResponse instanceof Array) { 236 | var count = parsedResponse.length; 237 | if (count !== 0) { 238 | issueLink.innerHTML = 'This app has ' + count + ' open issues'; 239 | document.getElementById('issue-count').style.display = 'inline'; 240 | } 241 | } 242 | }; 243 | xhr.send(); 244 | } 245 | }; 246 | 247 | redirect(); 248 | getFile('learn.json', Learn); 249 | })(); 250 | -------------------------------------------------------------------------------- /example/todomvc/node_modules/todomvc-app-css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /testsuite_test.go: -------------------------------------------------------------------------------- 1 | package vecty 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "path/filepath" 10 | "reflect" 11 | "sort" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | var _ = func() bool { 17 | isTest = true 18 | return true 19 | }() 20 | 21 | // recoverStr runs f and returns the recovered panic as a string. 22 | func recoverStr(f func()) (s string) { 23 | defer func() { 24 | s = fmt.Sprint(recover()) 25 | }() 26 | f() 27 | return 28 | } 29 | 30 | type componentFunc struct { 31 | Core 32 | id string 33 | render func() ComponentOrHTML 34 | skipRender func(prev Component) bool 35 | } 36 | 37 | func (c *componentFunc) Render() ComponentOrHTML { return c.render() } 38 | func (c *componentFunc) SkipRender(prev Component) bool { return c.skipRender(prev) } 39 | 40 | func TestMain(m *testing.M) { 41 | // Try to remove all testdata/*.got.txt files now. 42 | matches, _ := filepath.Glob("testdata/*.got.txt") 43 | for _, match := range matches { 44 | os.Remove(match) 45 | } 46 | 47 | os.Exit(m.Run()) 48 | } 49 | 50 | func testSuite(t *testing.T, testName string) *testSuiteT { 51 | ts := &testSuiteT{ 52 | t: t, 53 | testName: testName, 54 | callbacks: make(map[string]interface{}), 55 | strings: &valueMocker{}, 56 | bools: &valueMocker{}, 57 | floats: &valueMocker{}, 58 | ints: &valueMocker{}, 59 | } 60 | global = &objectRecorder{ 61 | ts: ts, 62 | name: "global", 63 | } 64 | return ts 65 | } 66 | 67 | // mockedValue represents a mocked value. 68 | type mockedValue struct { 69 | invocation string 70 | value interface{} 71 | } 72 | 73 | // valueMocker keeps tracked of mocked values for method invocations on 74 | // jsObject's. 75 | type valueMocker struct { 76 | values []mockedValue 77 | } 78 | 79 | // mock adds an entry to mock the specified invocation to return the given 80 | // value. 81 | func (v *valueMocker) mock(invocation string, value interface{}) { 82 | v.values = append(v.values, mockedValue{invocation, value}) 83 | } 84 | 85 | // get gets the mocked value for the specified invocation. 86 | func (v *valueMocker) get(invocation string) interface{} { 87 | for i, value := range v.values { 88 | if value.invocation == invocation { 89 | // Found the right invocation. 90 | v.values = append(v.values[:i], v.values[i+1:]...) 91 | return value.value 92 | } 93 | } 94 | panic(fmt.Sprintf("expected mocked value for invocation: %s", invocation)) 95 | } 96 | 97 | type testSuiteT struct { 98 | t *testing.T 99 | testName string 100 | callbacks map[string]interface{} 101 | strings, bools, floats, ints *valueMocker 102 | 103 | got string 104 | isDone bool 105 | } 106 | 107 | func (ts *testSuiteT) done() { 108 | ts.multiSortedDone() 109 | } 110 | 111 | // sortedDone is just like done(), except it sorts the specified line range first. 112 | func (ts *testSuiteT) sortedDone(sortStartLine, sortEndLine int) { 113 | ts.multiSortedDone([2]int{sortStartLine, sortEndLine}) 114 | } 115 | 116 | // multiSortedDone is just like done(), except it sorts the specified line range first. 117 | func (ts *testSuiteT) multiSortedDone(linesToSort ...[2]int) { 118 | if ts.isDone { 119 | panic("testSuite done methods called multiple times") 120 | } 121 | ts.isDone = true 122 | // Read the want file or create it if it does not exist. 123 | wantFileName := path.Join("testdata", ts.testName+".want.txt") 124 | wantBytes, err := ioutil.ReadFile(wantFileName) 125 | if err != nil { 126 | if os.IsNotExist(err) { 127 | // Touch the file 128 | f, err := os.Create(wantFileName) 129 | f.Close() 130 | if err != nil { 131 | ts.t.Fatal(err) 132 | } 133 | } else { 134 | ts.t.Fatal(err) 135 | } 136 | } 137 | want := strings.TrimSpace(string(wantBytes)) 138 | 139 | // Ensure output is properly sorted. 140 | split := strings.Split(strings.TrimSpace(ts.got), "\n") 141 | for _, pair := range linesToSort { 142 | sortStartLine := pair[0] - 1 // to match editor line numbers 143 | if sortStartLine < 0 { 144 | sortStartLine = 0 145 | } 146 | sortEndLine := pair[1] 147 | if sortEndLine > len(split) { 148 | sortEndLine = len(split) 149 | } 150 | sorted := split[sortStartLine:sortEndLine] 151 | ts.t.Logf("lines selected for sorting (%d-%d):\n%s\n\n", sortStartLine, sortEndLine, strings.Join(sorted, "\n")) 152 | sort.Strings(sorted) 153 | for i := sortStartLine; i < sortEndLine; i++ { 154 | split[i] = sorted[i-sortStartLine] 155 | } 156 | } 157 | got := strings.Join(split, "\n") 158 | 159 | // Check if we got what we wanted. 160 | if got == want { 161 | // Successful test. 162 | 163 | // Ensure there are no unused mocked values. 164 | for _, v := range ts.strings.values { 165 | ts.t.Errorf("unused mocked string value %q %v", v.invocation, v.value) 166 | } 167 | for _, v := range ts.bools.values { 168 | ts.t.Errorf("unused mocked bool value %q %v", v.invocation, v.value) 169 | } 170 | for _, v := range ts.floats.values { 171 | ts.t.Errorf("unused mocked float value %q %v", v.invocation, v.value) 172 | } 173 | for _, v := range ts.ints.values { 174 | ts.t.Errorf("unused mocked int value %q %v", v.invocation, v.value) 175 | } 176 | return 177 | } 178 | 179 | // Write what we got to disk. 180 | gotFileName := path.Join("testdata", ts.testName+".got.txt") 181 | err = ioutil.WriteFile(gotFileName, []byte(got), 0777) 182 | if err != nil { 183 | ts.t.Fatal(err) 184 | } 185 | 186 | // Print a nice diff for easy comparison. 187 | cmd := exec.Command("git", "-c", "color.ui=always", "diff", "--no-index", wantFileName, gotFileName) 188 | out, _ := cmd.CombinedOutput() 189 | ts.t.Log("\n" + string(out)) 190 | 191 | ts.t.Fatalf("to accept these changes:\n\n$ mv %s %s", gotFileName, wantFileName) 192 | } 193 | 194 | // record records the invocation to the test suite and returns the string 195 | // unmodified. 196 | func (ts *testSuiteT) record(invocation string) string { 197 | ts.got += "\n" + invocation 198 | return invocation 199 | } 200 | 201 | // addCallbacks adds the first function in args to ts.callbacks[invocation], if there is one. 202 | func (ts *testSuiteT) addCallbacks(invocation string, args ...interface{}) { 203 | for _, a := range args { 204 | if reflect.TypeOf(a).Kind() == reflect.Func { 205 | ts.callbacks[invocation] = a 206 | return 207 | } 208 | } 209 | } 210 | 211 | // objectRecorder implements the jsObject interface by recording method 212 | // invocations to the test suite. 213 | type objectRecorder struct { 214 | ts *testSuiteT 215 | name string 216 | } 217 | 218 | // Set implements the jsObject interface. 219 | func (r *objectRecorder) Set(key string, value interface{}) { 220 | invocation := r.ts.record(fmt.Sprintf("%s.Set(%q, %+v)", r.name, key, stringify(value))) 221 | r.ts.addCallbacks(invocation, value) 222 | } 223 | 224 | // Get implements the jsObject interface. 225 | func (r *objectRecorder) Get(key string) jsObject { 226 | invocation := r.ts.record(fmt.Sprintf("%s.Get(%q)", r.name, key)) 227 | return &objectRecorder{ 228 | ts: r.ts, 229 | name: invocation, 230 | } 231 | } 232 | 233 | // Delete implements the jsObject interface. 234 | func (r *objectRecorder) Delete(key string) { 235 | r.ts.record(fmt.Sprintf("%s.Delete(%q)", r.name, key)) 236 | } 237 | 238 | // Call implements the jsObject interface. 239 | func (r *objectRecorder) Call(name string, args ...interface{}) jsObject { 240 | invocation := r.ts.record(fmt.Sprintf("%s.Call(%q, %s)", r.name, name, stringify(args...))) 241 | r.ts.addCallbacks(invocation, args...) 242 | return &objectRecorder{ 243 | ts: r.ts, 244 | name: invocation, 245 | } 246 | } 247 | 248 | // String implements the jsObject interface. 249 | func (r *objectRecorder) String() string { return r.ts.strings.get(r.name).(string) } 250 | 251 | // Bool implements the jsObject interface. 252 | func (r *objectRecorder) Bool() bool { return r.ts.bools.get(r.name).(bool) } 253 | 254 | // Int implements the jsObject interface. 255 | func (r *objectRecorder) Int() int { return r.ts.ints.get(r.name).(int) } 256 | 257 | // Float implements the jsObject interface. 258 | func (r *objectRecorder) Float() float64 { return r.ts.floats.get(r.name).(float64) } 259 | 260 | func stringify(args ...interface{}) string { 261 | var s []string 262 | for _, a := range args { 263 | if reflect.TypeOf(a).Kind() == reflect.Func { 264 | s = append(s, reflect.TypeOf(a).String()) 265 | continue 266 | } 267 | switch v := a.(type) { 268 | case string: 269 | s = append(s, fmt.Sprintf("%q", v)) 270 | case *objectRecorder: 271 | s = append(s, fmt.Sprintf("jsObject(%s)", v.name)) 272 | default: 273 | s = append(s, fmt.Sprintf("%v", v)) 274 | } 275 | } 276 | return strings.Join(s, ", ") 277 | } 278 | -------------------------------------------------------------------------------- /markup.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package vecty 4 | 5 | import ( 6 | "reflect" 7 | "syscall/js" 8 | 9 | ) 10 | 11 | // EventListener is markup that specifies a callback function to be invoked when 12 | // the named DOM event is fired. 13 | type EventListener struct { 14 | Name string 15 | Listener func(*Event) 16 | callPreventDefault bool 17 | callStopPropagation bool 18 | wrapper js.Func //func(this js.Value, args []js.Value) 19 | } 20 | 21 | // PreventDefault prevents the default behavior of the event from occurring. 22 | // 23 | // See https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault. 24 | func (l *EventListener) PreventDefault() *EventListener { 25 | l.callPreventDefault = true 26 | return l 27 | } 28 | 29 | // StopPropagation prevents further propagation of the current event in the 30 | // capturing and bubbling phases. 31 | // 32 | // See https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation. 33 | func (l *EventListener) StopPropagation() *EventListener { 34 | l.callStopPropagation = true 35 | return l 36 | } 37 | 38 | // Apply implements the Applyer interface. 39 | func (l *EventListener) Apply(h *HTML) { 40 | h.eventListeners = append(h.eventListeners, l) 41 | } 42 | 43 | // Event represents a DOM event. 44 | type Event struct { 45 | js.Value 46 | Target js.Value 47 | } 48 | 49 | // MarkupOrChild represents one of: 50 | // 51 | // Component 52 | // *HTML 53 | // List 54 | // KeyedList 55 | // nil 56 | // MarkupList 57 | // 58 | // An unexported method on this interface ensures at compile time that the 59 | // underlying value must be one of these types. 60 | type MarkupOrChild interface { 61 | isMarkupOrChild() 62 | } 63 | 64 | func apply(m MarkupOrChild, h *HTML) { 65 | switch m := m.(type) { 66 | case MarkupList: 67 | m.Apply(h) 68 | case nil: 69 | h.children = append(h.children, nil) 70 | case Component, *HTML, List, KeyedList: 71 | h.children = append(h.children, m.(ComponentOrHTML)) 72 | default: 73 | panic("vecty: internal error (unexpected MarkupOrChild type " + reflect.TypeOf(m).String() + ")") 74 | } 75 | } 76 | 77 | // Applyer represents some type of markup (a style, property, data, etc) which 78 | // can be applied to a given HTML element or text node. 79 | type Applyer interface { 80 | // Apply applies the markup to the given HTML element or text node. 81 | Apply(h *HTML) 82 | } 83 | 84 | type markupFunc func(h *HTML) 85 | 86 | func (m markupFunc) Apply(h *HTML) { m(h) } 87 | 88 | // Style returns an Applyer which applies the given CSS style. Generally, this 89 | // function is not used directly but rather the style subpackage (which is type 90 | // safe) should be used instead. 91 | func Style(key, value string) Applyer { 92 | return markupFunc(func(h *HTML) { 93 | if h.styles == nil { 94 | h.styles = make(map[string]string) 95 | } 96 | h.styles[key] = value 97 | }) 98 | } 99 | 100 | // Key returns an Applyer that uniquely identifies the HTML element amongst its 101 | // siblings. When used, all other sibling elements and components must also be 102 | // keyed. 103 | func Key(key interface{}) Applyer { 104 | return markupFunc(func(h *HTML) { 105 | h.key = key 106 | }) 107 | } 108 | 109 | // Property returns an Applyer which applies the given JavaScript property to an 110 | // HTML element or text node. Generally, this function is not used directly but 111 | // rather the prop and style subpackages (which are type safe) should be used instead. 112 | // 113 | // To set style, use style package or Style. Property panics if key is "style". 114 | func Property(key string, value interface{}) Applyer { 115 | if key == "style" { 116 | panic(`vecty: Property called with key "style"; style package or Style should be used instead`) 117 | } 118 | return markupFunc(func(h *HTML) { 119 | if h.properties == nil { 120 | h.properties = make(map[string]interface{}) 121 | } 122 | h.properties[key] = value 123 | }) 124 | } 125 | 126 | // Attribute returns an Applyer which applies the given attribute to an element. 127 | // 128 | // In most situations, you should use Property function, or the prop subpackage 129 | // (which is type-safe) instead. There are only a few attributes (aria-*, role, 130 | // etc) which do not have equivalent properties. Always opt for the property 131 | // first, before relying on an attribute. 132 | func Attribute(key string, value interface{}) Applyer { 133 | return markupFunc(func(h *HTML) { 134 | if h.attributes == nil { 135 | h.attributes = make(map[string]interface{}) 136 | } 137 | h.attributes[key] = value 138 | }) 139 | } 140 | 141 | // Data returns an Applyer which applies the given data attribute. 142 | func Data(key, value string) Applyer { 143 | return markupFunc(func(h *HTML) { 144 | if h.dataset == nil { 145 | h.dataset = make(map[string]string) 146 | } 147 | h.dataset[key] = value 148 | }) 149 | } 150 | 151 | // Class returns an Applyer which applies the provided classes. Subsequent 152 | // calls to this function will append additional classes. To toggle classes, 153 | // use ClassMap instead. Each class name must be passed as a separate argument. 154 | func Class(class ...string) Applyer { 155 | mustValidateClassNames(class) 156 | return markupFunc(func(h *HTML) { 157 | if h.classes == nil { 158 | h.classes = make(map[string]struct{}) 159 | } 160 | for _, name := range class { 161 | h.classes[name] = struct{}{} 162 | } 163 | }) 164 | } 165 | 166 | // mustValidateClassNames ensures no class names have spaces 167 | // and panics with clear instructions on how to fix this user error. 168 | func mustValidateClassNames(class []string) { 169 | for _, name := range class { 170 | if containsSpace(name) { 171 | panic(`vecty: invalid argument to vecty.Class "` + name + `" (string may not contain spaces)`) 172 | } 173 | } 174 | } 175 | 176 | // containsSpace reports whether s contains a space character. 177 | func containsSpace(s string) bool { 178 | for _, c := range s { 179 | if c == ' ' { 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | 186 | // ClassMap is markup that specifies classes to be applied to an element if 187 | // their boolean value are true. 188 | type ClassMap map[string]bool 189 | 190 | // Apply implements the Applyer interface. 191 | func (m ClassMap) Apply(h *HTML) { 192 | if h.classes == nil { 193 | h.classes = make(map[string]struct{}) 194 | } 195 | for name, active := range m { 196 | if !active { 197 | delete(h.classes, name) 198 | continue 199 | } 200 | h.classes[name] = struct{}{} 201 | } 202 | } 203 | 204 | // MarkupList represents a list of Applyer which is individually 205 | // applied to an HTML element or text node. 206 | // 207 | // It may only be created through the Markup function. 208 | type MarkupList struct { 209 | list []Applyer 210 | } 211 | 212 | // Apply implements the Applyer interface. 213 | func (m MarkupList) Apply(h *HTML) { 214 | for _, a := range m.list { 215 | if a == nil { 216 | continue 217 | } 218 | a.Apply(h) 219 | } 220 | } 221 | 222 | // isMarkupOrChild implements MarkupOrChild 223 | func (m MarkupList) isMarkupOrChild() {} 224 | 225 | // Markup wraps a list of Applyer which is individually 226 | // applied to an HTML element or text node. 227 | func Markup(m ...Applyer) MarkupList { 228 | // returns public non-pointer struct value with private field so that users 229 | // must acquire a MarkupList only from this function, and so that it can 230 | // never be nil (which would make it indistinguishable from (*HTML)(nil) in 231 | // a call to e.g. Tag). 232 | return MarkupList{list: m} 233 | } 234 | 235 | // If returns nil if cond is false, otherwise it returns the given children. 236 | func If(cond bool, children ...ComponentOrHTML) MarkupOrChild { 237 | if cond { 238 | return List(children) 239 | } 240 | return nil 241 | } 242 | 243 | // MarkupIf returns nil if cond is false, otherwise it returns the given markup. 244 | func MarkupIf(cond bool, markup ...Applyer) Applyer { 245 | if cond { 246 | return Markup(markup...) 247 | } 248 | return nil 249 | } 250 | 251 | // UnsafeHTML is Applyer which unsafely sets the inner HTML of an HTML element. 252 | // 253 | // It is entirely up to the caller to ensure the input HTML is properly 254 | // sanitized. 255 | // 256 | // It is akin to innerHTML in standard JavaScript and dangerouslySetInnerHTML 257 | // in React, and is said to be unsafe because Vecty makes no effort to validate 258 | // or ensure the HTML is safe for insertion in the DOM. If the HTML came from a 259 | // user, for example, it would create a cross-site-scripting (XSS) exploit in 260 | // the application. 261 | // 262 | // The returned Applyer can only be applied to HTML, not vecty.Text, or else a 263 | // panic will occur. 264 | func UnsafeHTML(html string) Applyer { 265 | return markupFunc(func(h *HTML) { 266 | h.innerHTML = html 267 | }) 268 | } 269 | 270 | // Namespace is Applyer which sets the namespace URI to associate with the 271 | // created element. This is primarily used when working with, e.g., SVG. 272 | // 273 | // See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#Valid Namespace URIs 274 | func Namespace(uri string) Applyer { 275 | return markupFunc(func(h *HTML) { 276 | h.namespace = uri 277 | }) 278 | } 279 | -------------------------------------------------------------------------------- /dom_test.go: -------------------------------------------------------------------------------- 1 | package vecty 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gopherjs/gopherwasm/js" 8 | ) 9 | 10 | type testCore struct{ Core } 11 | 12 | func (testCore) Render() ComponentOrHTML { return Tag("p") } 13 | 14 | type testCorePtr struct{ *Core } 15 | 16 | func (testCorePtr) Render() ComponentOrHTML { return Tag("p") } 17 | 18 | func TestCore(t *testing.T) { 19 | // Test that a standard *MyComponent with embedded Core works as we expect. 20 | t.Run("comp_ptr_and_core", func(t *testing.T) { 21 | v1 := Tag("v1") 22 | valid := Component(&testCore{}) 23 | valid.Context().prevRender = v1 24 | if valid.Context().prevRender != v1 { 25 | t.Fatal("valid.Context().prevRender != v1") 26 | } 27 | }) 28 | 29 | // Test that a non-pointer MyComponent with embedded Core does not satisfy 30 | // the Component interface: 31 | // 32 | // testCore does not implement Component (Context method has pointer receiver) 33 | // 34 | t.Run("comp_and_core", func(t *testing.T) { 35 | isComponent := func(x interface{}) bool { 36 | _, ok := x.(Component) 37 | return ok 38 | } 39 | if isComponent(testCore{}) { 40 | t.Fatal("expected !isComponent(testCompCore{})") 41 | } 42 | }) 43 | 44 | // Test what happens when a user accidentally embeds *Core instead of Core in 45 | // their component. 46 | t.Run("comp_ptr_and_core_ptr", func(t *testing.T) { 47 | v1 := Tag("v1") 48 | invalid := Component(&testCorePtr{}) 49 | got := recoverStr(func() { 50 | invalid.Context().prevRender = v1 51 | }) 52 | // TODO(slimsag): This would happen in user-facing code too. We should 53 | // create a helper for when we access a component's context, which 54 | // would panic with a more helpful message. 55 | want := "runtime error: invalid memory address or nil pointer dereference" 56 | if got != want { 57 | t.Fatalf("got panic %q want %q", got, want) 58 | } 59 | }) 60 | t.Run("comp_and_core_ptr", func(t *testing.T) { 61 | v1 := Tag("v1") 62 | invalid := Component(testCorePtr{}) 63 | got := recoverStr(func() { 64 | invalid.Context().prevRender = v1 65 | }) 66 | // TODO(slimsag): This would happen in user-facing code too. We should 67 | // create a helper for when we access a component's context, which 68 | // would panic with a more helpful message. 69 | want := "runtime error: invalid memory address or nil pointer dereference" 70 | if got != want { 71 | t.Fatalf("got panic %q want %q", got, want) 72 | } 73 | }) 74 | } 75 | 76 | // TODO(slimsag): TestUnmounter; Unmounter.Unmount 77 | 78 | func TestHTML_Node(t *testing.T) { 79 | // Create a non-nil *js.Object. For 'gopherjs test', &js.Object{} == nil 80 | // because it is special-cased; but for 'go test' js.Global == nil. 81 | x := js.Global() // used for 'gopherjs test' 82 | if x == js.Null() { 83 | x = js.Value{} // used for 'go test' 84 | } 85 | h := &HTML{node: wrapObject(x)} 86 | if h.Node() != x { 87 | t.Fatal("h.Node() != x") 88 | } 89 | } 90 | 91 | // TestHTML_reconcile_std tests that (*HTML).reconcile against an old HTML instance 92 | // works as expected (i.e. that it updates nodes correctly). 93 | func TestHTML_reconcile_std(t *testing.T) { 94 | t.Run("text_identical", func(t *testing.T) { 95 | ts := testSuite(t, "TestHTML_reconcile_std__text_identical") 96 | defer ts.done() 97 | 98 | init := Text("foobar") 99 | init.reconcile(nil) 100 | 101 | target := Text("foobar") 102 | target.reconcile(init) 103 | }) 104 | t.Run("text_diff", func(t *testing.T) { 105 | ts := testSuite(t, "TestHTML_reconcile_std__text_diff") 106 | defer ts.done() 107 | 108 | init := Text("bar") 109 | init.reconcile(nil) 110 | 111 | target := Text("foo") 112 | target.reconcile(init) 113 | }) 114 | t.Run("properties", func(t *testing.T) { 115 | cases := []struct { 116 | name string 117 | initHTML *HTML 118 | targetHTML *HTML 119 | sortedLines [][2]int 120 | }{ 121 | { 122 | name: "diff", 123 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 124 | targetHTML: Tag("div", Markup(Property("a", 3), Property("b", "4foobar"))), 125 | sortedLines: [][2]int{{3, 4}, {12, 13}}, 126 | }, 127 | { 128 | name: "remove", 129 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 130 | targetHTML: Tag("div", Markup(Property("a", 3))), 131 | sortedLines: [][2]int{{3, 4}}, 132 | }, 133 | { 134 | name: "replaced_elem_diff", 135 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 136 | targetHTML: Tag("span", Markup(Property("a", 3), Property("b", "4foobar"))), 137 | sortedLines: [][2]int{{3, 4}, {11, 12}}, 138 | }, 139 | { 140 | name: "replaced_elem_shared", 141 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 142 | targetHTML: Tag("span", Markup(Property("a", 1), Property("b", "4foobar"))), 143 | sortedLines: [][2]int{{3, 4}, {11, 12}}, 144 | }, 145 | } 146 | for _, tst := range cases { 147 | t.Run(tst.name, func(t *testing.T) { 148 | ts := testSuite(t, "TestHTML_reconcile_std__properties__"+tst.name) 149 | defer ts.multiSortedDone(tst.sortedLines...) 150 | 151 | tst.initHTML.reconcile(nil) 152 | ts.record("(first reconcile done)") 153 | tst.targetHTML.reconcile(tst.initHTML) 154 | }) 155 | } 156 | }) 157 | t.Run("attributes", func(t *testing.T) { 158 | cases := []struct { 159 | name string 160 | initHTML *HTML 161 | targetHTML *HTML 162 | sortedLines [][2]int 163 | }{ 164 | { 165 | name: "diff", 166 | initHTML: Tag("div", Markup(Attribute("a", 1), Attribute("b", "2foobar"))), 167 | targetHTML: Tag("div", Markup(Attribute("a", 3), Attribute("b", "4foobar"))), 168 | sortedLines: [][2]int{{3, 4}, {12, 13}}, 169 | }, 170 | { 171 | name: "remove", 172 | initHTML: Tag("div", Markup(Attribute("a", 1), Attribute("b", "2foobar"))), 173 | targetHTML: Tag("div", Markup(Attribute("a", 3))), 174 | sortedLines: [][2]int{{3, 4}}, 175 | }, 176 | } 177 | for _, tst := range cases { 178 | t.Run(tst.name, func(t *testing.T) { 179 | ts := testSuite(t, "TestHTML_reconcile_std__attributes__"+tst.name) 180 | defer ts.multiSortedDone(tst.sortedLines...) 181 | 182 | tst.initHTML.reconcile(nil) 183 | ts.record("(first reconcile done)") 184 | tst.targetHTML.reconcile(tst.initHTML) 185 | }) 186 | } 187 | }) 188 | t.Run("class", func(t *testing.T) { 189 | cases := []struct { 190 | name string 191 | initHTML *HTML 192 | targetHTML *HTML 193 | sortedLines [][2]int 194 | }{ 195 | { 196 | name: "multi", 197 | initHTML: Tag("div", Markup(Class("a"), Class("b"))), 198 | targetHTML: Tag("div", Markup(Class("a"), Class("c"))), 199 | sortedLines: [][2]int{{4, 5}}, 200 | }, 201 | { 202 | name: "diff", 203 | initHTML: Tag("div", Markup(Class("a", "b"))), 204 | targetHTML: Tag("div", Markup(Class("a", "c"))), 205 | sortedLines: [][2]int{{4, 5}}, 206 | }, 207 | { 208 | name: "remove", 209 | initHTML: Tag("div", Markup(Class("a", "b"))), 210 | targetHTML: Tag("div", Markup(Class("a"))), 211 | sortedLines: [][2]int{{4, 5}}, 212 | }, 213 | { 214 | name: "map", 215 | initHTML: Tag("div", Markup(ClassMap{"a": true, "b": true})), 216 | targetHTML: Tag("div", Markup(ClassMap{"a": true})), 217 | sortedLines: [][2]int{{4, 5}}, 218 | }, 219 | { 220 | name: "map_toggle", 221 | initHTML: Tag("div", Markup(ClassMap{"a": true, "b": true})), 222 | targetHTML: Tag("div", Markup(ClassMap{"a": true, "b": false})), 223 | sortedLines: [][2]int{{4, 5}}, 224 | }, 225 | { 226 | name: "combo", 227 | initHTML: Tag("div", Markup(ClassMap{"a": true, "b": true}, Class("c"))), 228 | targetHTML: Tag("div", Markup(ClassMap{"a": true, "b": false}, Class("d"))), 229 | sortedLines: [][2]int{{4, 6}, {11, 12}}, 230 | }, 231 | } 232 | for _, tst := range cases { 233 | t.Run(tst.name, func(t *testing.T) { 234 | ts := testSuite(t, "TestHTML_reconcile_std__class__"+tst.name) 235 | defer ts.multiSortedDone(tst.sortedLines...) 236 | 237 | tst.initHTML.reconcile(nil) 238 | ts.record("(first reconcile done)") 239 | tst.targetHTML.reconcile(tst.initHTML) 240 | }) 241 | } 242 | }) 243 | t.Run("dataset", func(t *testing.T) { 244 | cases := []struct { 245 | name string 246 | initHTML *HTML 247 | targetHTML *HTML 248 | sortedLines [][2]int 249 | }{ 250 | { 251 | name: "diff", 252 | initHTML: Tag("div", Markup(Data("a", "1"), Data("b", "2foobar"))), 253 | targetHTML: Tag("div", Markup(Data("a", "3"), Data("b", "4foobar"))), 254 | sortedLines: [][2]int{{5, 6}, {14, 15}}, 255 | }, 256 | { 257 | name: "remove", 258 | initHTML: Tag("div", Markup(Data("a", "1"), Data("b", "2foobar"))), 259 | targetHTML: Tag("div", Markup(Data("a", "3"))), 260 | sortedLines: [][2]int{{5, 6}}, 261 | }, 262 | } 263 | for _, tst := range cases { 264 | t.Run(tst.name, func(t *testing.T) { 265 | ts := testSuite(t, "TestHTML_reconcile_std__dataset__"+tst.name) 266 | defer ts.multiSortedDone(tst.sortedLines...) 267 | 268 | tst.initHTML.reconcile(nil) 269 | ts.record("(first reconcile done)") 270 | tst.targetHTML.reconcile(tst.initHTML) 271 | }) 272 | } 273 | }) 274 | t.Run("style", func(t *testing.T) { 275 | cases := []struct { 276 | name string 277 | initHTML *HTML 278 | targetHTML *HTML 279 | sortedLines [][2]int 280 | }{ 281 | { 282 | name: "diff", 283 | initHTML: Tag("div", Markup(Style("a", "1"), Style("b", "2foobar"))), 284 | targetHTML: Tag("div", Markup(Style("a", "3"), Style("b", "4foobar"))), 285 | sortedLines: [][2]int{{6, 7}, {15, 16}}, 286 | }, 287 | { 288 | name: "remove", 289 | initHTML: Tag("div", Markup(Style("a", "1"), Style("b", "2foobar"))), 290 | targetHTML: Tag("div", Markup(Style("a", "3"))), 291 | sortedLines: [][2]int{{6, 7}}, 292 | }, 293 | } 294 | for _, tst := range cases { 295 | t.Run(tst.name, func(t *testing.T) { 296 | ts := testSuite(t, "TestHTML_reconcile_std__style__"+tst.name) 297 | defer ts.multiSortedDone(tst.sortedLines...) 298 | 299 | tst.initHTML.reconcile(nil) 300 | ts.record("(first reconcile done)") 301 | tst.targetHTML.reconcile(tst.initHTML) 302 | }) 303 | } 304 | }) 305 | t.Run("event_listener", func(t *testing.T) { 306 | // TODO(pdf): Mock listener functions for equality testing 307 | ts := testSuite(t, "TestHTML_reconcile_std__event_listener_diff") 308 | defer ts.done() 309 | 310 | initEventListeners := []Applyer{ 311 | &EventListener{Name: "click"}, 312 | &EventListener{Name: "keydown"}, 313 | } 314 | prev := Tag("div", Markup(initEventListeners...)) 315 | prev.reconcile(nil) 316 | ts.record("(expected two added event listeners above)") 317 | for i, m := range initEventListeners { 318 | listener := m.(*EventListener) 319 | if listener.wrapper.Value == js.Null() { 320 | t.Fatalf("listener %d wrapper == nil: %+v", i, listener) 321 | } 322 | } 323 | 324 | targetEventListeners := []Applyer{ 325 | &EventListener{Name: "click"}, 326 | } 327 | h := Tag("div", Markup(targetEventListeners...)) 328 | h.reconcile(prev) 329 | ts.record("(expected two removed, one added event listeners above)") 330 | for i, m := range targetEventListeners { 331 | listener := m.(*EventListener) 332 | if listener.wrapper.Value == js.Null() { 333 | t.Fatalf("listener %d wrapper == nil: %+v", i, listener) 334 | } 335 | } 336 | }) 337 | 338 | // TODO(pdf): test (*HTML).reconcile child mutations, and value/checked properties 339 | // TODO(pdf): test multi-pass reconcile of persistent component pointer children, ref: https://github.com/gowasm/vecty/pull/124 340 | } 341 | 342 | // TestHTML_reconcile_nil tests that (*HTML).reconcile(nil) works as expected (i.e. 343 | // that it creates nodes correctly). 344 | func TestHTML_reconcile_nil(t *testing.T) { 345 | t.Run("one_of_tag_or_text", func(t *testing.T) { 346 | got := recoverStr(func() { 347 | h := &HTML{text: "hello", tag: "div"} 348 | h.reconcile(nil) 349 | }) 350 | want := "vecty: internal error (only one of HTML.tag or HTML.text may be set)" 351 | if got != want { 352 | t.Fatalf("got panic %q want %q", got, want) 353 | } 354 | }) 355 | t.Run("unsafe_text", func(t *testing.T) { 356 | got := recoverStr(func() { 357 | h := &HTML{text: "hello", innerHTML: "foobar"} 358 | h.reconcile(nil) 359 | }) 360 | want := "vecty: only HTML may have UnsafeHTML attribute" 361 | if got != want { 362 | t.Fatalf("got panic %q want %q", got, want) 363 | } 364 | }) 365 | t.Run("create_element", func(t *testing.T) { 366 | ts := testSuite(t, "TestHTML_reconcile_nil__create_element") 367 | defer ts.done() 368 | 369 | h := Tag("strong") 370 | h.reconcile(nil) 371 | }) 372 | t.Run("create_element_ns", func(t *testing.T) { 373 | ts := testSuite(t, "TestHTML_reconcile_nil__create_element_ns") 374 | defer ts.done() 375 | 376 | h := Tag("strong", Markup(Namespace("foobar"))) 377 | h.reconcile(nil) 378 | }) 379 | t.Run("create_text_node", func(t *testing.T) { 380 | ts := testSuite(t, "TestHTML_reconcile_nil__create_text_node") 381 | defer ts.done() 382 | 383 | h := Text("hello") 384 | h.reconcile(nil) 385 | }) 386 | t.Run("inner_html", func(t *testing.T) { 387 | ts := testSuite(t, "TestHTML_reconcile_nil__inner_html") 388 | defer ts.done() 389 | 390 | h := Tag("div", Markup(UnsafeHTML("

hello

"))) 391 | h.reconcile(nil) 392 | }) 393 | t.Run("properties", func(t *testing.T) { 394 | ts := testSuite(t, "TestHTML_reconcile_nil__properties") 395 | defer ts.sortedDone(3, 4) 396 | 397 | h := Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))) 398 | h.reconcile(nil) 399 | }) 400 | t.Run("attributes", func(t *testing.T) { 401 | ts := testSuite(t, "TestHTML_reconcile_nil__attributes") 402 | defer ts.sortedDone(3, 4) 403 | 404 | h := Tag("div", Markup(Attribute("a", 1), Attribute("b", "2foobar"))) 405 | h.reconcile(nil) 406 | }) 407 | t.Run("dataset", func(t *testing.T) { 408 | ts := testSuite(t, "TestHTML_reconcile_nil__dataset") 409 | defer ts.sortedDone(5, 6) 410 | 411 | h := Tag("div", Markup(Data("a", "1"), Data("b", "2foobar"))) 412 | h.reconcile(nil) 413 | }) 414 | t.Run("style", func(t *testing.T) { 415 | ts := testSuite(t, "TestHTML_reconcile_nil__style") 416 | defer ts.sortedDone(6, 7) 417 | 418 | h := Tag("div", Markup(Style("a", "1"), Style("b", "2foobar"))) 419 | h.reconcile(nil) 420 | }) 421 | t.Run("add_event_listener", func(t *testing.T) { 422 | ts := testSuite(t, "TestHTML_reconcile_nil__add_event_listener") 423 | defer ts.done() 424 | 425 | e0 := &EventListener{Name: "click"} 426 | e1 := &EventListener{Name: "keydown"} 427 | h := Tag("div", Markup(e0, e1)) 428 | h.reconcile(nil) 429 | if e0.wrapper.Value == js.Null() { 430 | t.Fatal("e0.wrapper == nil") 431 | } 432 | if e1.wrapper.Value == js.Null() { 433 | t.Fatal("e1.wrapper == nil") 434 | } 435 | }) 436 | t.Run("children", func(t *testing.T) { 437 | ts := testSuite(t, "TestHTML_reconcile_nil__children") 438 | defer ts.done() 439 | 440 | var compRenderCalls int 441 | compRender := Tag("div") 442 | comp := &componentFunc{ 443 | id: "foobar", 444 | render: func() ComponentOrHTML { 445 | compRenderCalls++ 446 | return compRender 447 | }, 448 | } 449 | h := Tag("div", Tag("div", comp)) 450 | h.reconcile(nil) 451 | if compRenderCalls != 1 { 452 | t.Fatal("compRenderCalls != 1") 453 | } 454 | if comp.Context().prevRenderComponent.(*componentFunc).id != comp.id { 455 | t.Fatal("comp.Context().prevRenderComponent.(*componentFunc).id != comp.id") 456 | } 457 | if comp.Context().prevRender != compRender { 458 | t.Fatal("comp.Context().prevRender != compRender") 459 | } 460 | }) 461 | t.Run("children_render_nil", func(t *testing.T) { 462 | ts := testSuite(t, "TestHTML_reconcile_nil__children_render_nil") 463 | defer ts.done() 464 | 465 | var compRenderCalls int 466 | comp := &componentFunc{ 467 | id: "foobar", 468 | render: func() ComponentOrHTML { 469 | compRenderCalls++ 470 | return nil 471 | }, 472 | } 473 | h := Tag("div", Tag("div", comp)) 474 | h.reconcile(nil) 475 | if compRenderCalls != 1 { 476 | t.Fatal("compRenderCalls != 1") 477 | } 478 | if comp.Context().prevRenderComponent.(*componentFunc).id != comp.id { 479 | t.Fatal("comp.Context().prevRenderComponent.(*componentFunc).id != comp.id") 480 | } 481 | if comp.Context().prevRender == nil { 482 | t.Fatal("comp.Context().prevRender == nil") 483 | } 484 | }) 485 | } 486 | 487 | func TestTag(t *testing.T) { 488 | markupCalled := false 489 | want := "foobar" 490 | h := Tag(want, Markup(markupFunc(func(h *HTML) { 491 | markupCalled = true 492 | }))) 493 | if !markupCalled { 494 | t.Fatal("expected markup to be applied") 495 | } 496 | if h.tag != want { 497 | t.Fatalf("got tag %q want tag %q", h.text, want) 498 | } 499 | if h.text != "" { 500 | t.Fatal("expected no text") 501 | } 502 | } 503 | 504 | func TestText(t *testing.T) { 505 | markupCalled := false 506 | want := "Hello world!" 507 | h := Text(want, Markup(markupFunc(func(h *HTML) { 508 | markupCalled = true 509 | }))) 510 | if !markupCalled { 511 | t.Fatal("expected markup to be applied") 512 | } 513 | if h.text != want { 514 | t.Fatalf("got text %q want text %q", h.text, want) 515 | } 516 | if h.tag != "" { 517 | t.Fatal("expected no tag") 518 | } 519 | } 520 | 521 | // TestRerender_nil tests that Rerender panics when the component argument is 522 | // nil. 523 | func TestRerender_nil(t *testing.T) { 524 | gotPanic := "" 525 | func() { 526 | defer func() { 527 | r := recover() 528 | if r != nil { 529 | gotPanic = fmt.Sprint(r) 530 | } 531 | }() 532 | Rerender(nil) 533 | }() 534 | expected := "vecty: Rerender illegally called with a nil Component argument" 535 | if gotPanic != expected { 536 | t.Fatalf("got panic %q expected %q", gotPanic, expected) 537 | } 538 | } 539 | 540 | // TestRerender_no_prevRender tests the behavior of Rerender when there is no 541 | // previous render. 542 | func TestRerender_no_prevRender(t *testing.T) { 543 | ts := testSuite(t, "TestRerender_no_prevRender") 544 | defer ts.done() 545 | 546 | got := recoverStr(func() { 547 | Rerender(&componentFunc{ 548 | render: func() ComponentOrHTML { 549 | panic("expected no Render call") 550 | }, 551 | skipRender: func(prev Component) bool { 552 | panic("expected no SkipRender call") 553 | }, 554 | }) 555 | }) 556 | want := "vecty: Rerender invoked on Component that has never been rendered" 557 | if got != want { 558 | t.Fatalf("got panic %q expected %q", got, want) 559 | } 560 | } 561 | 562 | // TestRerender_identical tests the behavior of Rerender when there is a 563 | // previous render which is identical to the new render. 564 | func TestRerender_identical(t *testing.T) { 565 | ts := testSuite(t, "TestRerender_identical") 566 | defer ts.done() 567 | 568 | ts.ints.mock(`global.Call("requestAnimationFrame", func(float64))`, 0) 569 | ts.strings.mock(`global.Get("document").Get("readyState")`, "complete") 570 | 571 | // Perform the initial render of the component. 572 | render := Tag("body") 573 | var renderCalled, skipRenderCalled int 574 | comp := &componentFunc{ 575 | id: "original", 576 | render: func() ComponentOrHTML { 577 | renderCalled++ 578 | return render 579 | }, 580 | } 581 | RenderBody(comp) 582 | if renderCalled != 1 { 583 | t.Fatal("renderCalled != 1") 584 | } 585 | if comp.Context().prevRender != render { 586 | t.Fatal("comp.Context().prevRender != render") 587 | } 588 | if comp.Context().prevRenderComponent.(*componentFunc).id != "original" { 589 | t.Fatal(`comp.Context().prevRenderComponent.(*componentFunc).id != "original"`) 590 | } 591 | 592 | // Perform a re-render. 593 | newRender := Tag("body") 594 | comp.id = "modified" 595 | comp.render = func() ComponentOrHTML { 596 | renderCalled++ 597 | return newRender 598 | } 599 | comp.skipRender = func(prev Component) bool { 600 | if comp.id != "modified" { 601 | panic(`comp.id != "modified"`) 602 | } 603 | if comp.Context().prevRenderComponent.(*componentFunc).id != "original" { 604 | panic(`comp.Context().prevRenderComponent.(*componentFunc).id != "original"`) 605 | } 606 | if prev.(*componentFunc).id != "original" { 607 | panic(`prev.(*componentFunc).id != "original"`) 608 | } 609 | skipRenderCalled++ 610 | return false 611 | } 612 | Rerender(comp) 613 | 614 | // Invoke the render callback. 615 | ts.ints.mock(`global.Call("requestAnimationFrame", func(float64))`, 0) 616 | ts.callbacks[`global.Call("requestAnimationFrame", func(float64))`].(func(float64))(0) 617 | 618 | if renderCalled != 2 { 619 | t.Fatal("renderCalled != 2") 620 | } 621 | if skipRenderCalled != 1 { 622 | t.Fatal("skipRenderCalled != 1") 623 | } 624 | if comp.Context().prevRender != newRender { 625 | t.Fatal("comp.Context().prevRender != newRender") 626 | } 627 | if comp.Context().prevRenderComponent.(*componentFunc).id != "modified" { 628 | t.Fatal(`comp.Context().prevRenderComponent.(*componentFunc).id != "modified"`) 629 | } 630 | } 631 | 632 | // TestRerender_change tests the behavior of Rerender when there is a 633 | // previous render which is different from the new render. 634 | func TestRerender_change(t *testing.T) { 635 | cases := []struct { 636 | name string 637 | newRender *HTML 638 | }{ 639 | { 640 | name: "new_child", 641 | newRender: Tag("body", Tag("div")), 642 | }, 643 | // TODO(slimsag): bug! nil produces