├── .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 | 
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 | 
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 | [](https://travis-ci.org/gowasm/vecty) [](https://godoc.org/github.com/gowasm/vecty) [](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