├── .github └── FUNDING.yml ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── doc ├── CHANGELOG.md └── projects-using-vecty.md ├── dom.go ├── dom_js.go ├── dom_native.go ├── dom_no_tinygo.go ├── dom_test.go ├── dom_tinygo.go ├── domutil.go ├── elem ├── elem.gen.go └── generate.go ├── event ├── event.gen.go └── generate.go ├── example ├── README.md ├── go.mod ├── go.sum ├── hellovecty │ └── hellovecty.go ├── markdown │ └── markdown.go ├── mod.go └── todomvc │ ├── actions │ └── actions.go │ ├── components │ ├── filterbutton.go │ ├── itemview.go │ └── pageview.go │ ├── dispatcher │ └── dispatcher.go │ ├── example.go │ └── store │ ├── model │ └── model.go │ ├── store.go │ └── storeutil │ └── storeutil.go ├── go.mod ├── markup.go ├── markup_test.go ├── prop └── prop.go ├── require_go_1_14.go ├── style └── style.go ├── testdata ├── TestAddStylesheet.want.txt ├── TestHTML_Node.want.txt ├── TestHTML_reconcile_nil__add_event_listener.want.txt ├── TestHTML_reconcile_nil__attributes.want.txt ├── TestHTML_reconcile_nil__children.want.txt ├── TestHTML_reconcile_nil__children_render_nil.want.txt ├── TestHTML_reconcile_nil__create_element.want.txt ├── TestHTML_reconcile_nil__create_element_ns.want.txt ├── TestHTML_reconcile_nil__create_text_node.want.txt ├── TestHTML_reconcile_nil__dataset.want.txt ├── TestHTML_reconcile_nil__inner_html.want.txt ├── TestHTML_reconcile_nil__properties.want.txt ├── TestHTML_reconcile_nil__style.want.txt ├── TestHTML_reconcile_std__attributes__diff.want.txt ├── TestHTML_reconcile_std__attributes__remove.want.txt ├── TestHTML_reconcile_std__class__combo.want.txt ├── TestHTML_reconcile_std__class__diff.want.txt ├── TestHTML_reconcile_std__class__map.want.txt ├── TestHTML_reconcile_std__class__map_toggle.want.txt ├── TestHTML_reconcile_std__class__multi.want.txt ├── TestHTML_reconcile_std__class__remove.want.txt ├── TestHTML_reconcile_std__dataset__diff.want.txt ├── TestHTML_reconcile_std__dataset__remove.want.txt ├── TestHTML_reconcile_std__event_listener.want.txt ├── TestHTML_reconcile_std__properties__diff.want.txt ├── TestHTML_reconcile_std__properties__remove.want.txt ├── TestHTML_reconcile_std__properties__replaced_elem_diff.want.txt ├── TestHTML_reconcile_std__properties__replaced_elem_shared.want.txt ├── TestHTML_reconcile_std__style__diff.want.txt ├── TestHTML_reconcile_std__style__remove.want.txt ├── TestHTML_reconcile_std__text_diff.want.txt ├── TestHTML_reconcile_std__text_identical.want.txt ├── TestKeyedChild_DifferentType.want.txt ├── TestRenderBody_ExpectsBody__div.want.txt ├── TestRenderBody_ExpectsBody__nil.want.txt ├── TestRenderBody_ExpectsBody__text.want.txt ├── TestRenderBody_Nested.want.txt ├── TestRenderBody_RenderSkipper_Skip.want.txt ├── TestRenderBody_Standard_loaded.want.txt ├── TestRenderBody_Standard_loading.want.txt ├── TestRerender_Nested__component_to_html.want.txt ├── TestRerender_Nested__html_to_component.want.txt ├── TestRerender_Nested__new_child.want.txt ├── TestRerender_change__new_child.want.txt ├── TestRerender_identical.want.txt ├── TestRerender_no_prevRender.want.txt ├── TestRerender_persistent.want.txt ├── TestRerender_persistent_direct.want.txt └── TestSetTitle.want.txt ├── testsuite_js_test.go ├── testsuite_native_test.go └── testsuite_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: slimsag 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | testdata/*.got.txt 2 | .DS_store 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains our configuration for golangci-lint; see https://github.com/golangci/golangci-lint 2 | # for more information. 3 | 4 | linters: 5 | enable: 6 | - golint 7 | 8 | issues: 9 | # We do not use golangci-lint's default list of exclusions because some of 10 | # them are not good for us (we prefer strictness): 11 | # 12 | # - Not requiring published functions to have comments. 13 | # - Not warning about ineffective break statements. 14 | # - And more that don't effect Vecty itself. 15 | # 16 | exclude-use-default: false 17 | 18 | # List of regexps of issue texts to exclude, empty list by default. 19 | exclude: 20 | # https://github.com/hexops/vecty/issues/226 21 | - "comment on exported function Timeout should be of the form" 22 | 23 | # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok 24 | - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked 25 | 26 | # golint: False positive when tests are defined in package 'test' 27 | - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | install: 3 | # Manually download and install Go because the Travis / gimme version has been broken in the past: 4 | # https://travis-ci.community/t/goos-js-goarch-wasm-go-run-fails-panic-newosproc-not-implemented/1651/6 5 | - wget -O go.tar.gz https://dl.google.com/go/go1.15.linux-amd64.tar.gz 6 | - tar -C ~ -xzf go.tar.gz 7 | - rm go.tar.gz 8 | - export GOROOT=~/go 9 | - export PATH=$GOROOT/bin:$PATH 10 | - go version 11 | - go env 12 | 13 | # Install NodeJS (for testing WebAssembly support) 14 | - nvm install 14.7.0 15 | 16 | # Linters, etc. 17 | - go get -u github.com/golangci/golangci-lint/cmd/golangci-lint 18 | - go get -u github.com/haya14busa/goverage 19 | - go get -u mvdan.cc/gofumpt 20 | - go get -u mvdan.cc/gofumpt/gofumports 21 | script: 22 | # Fetch dependencies. 23 | - go get -d . 24 | - GOOS=js GOARCH=wasm go get -d ./... 25 | 26 | # Ensure consistent code style. 27 | - diff -u <(echo -n) <(gofumpt -d -s .) 28 | - diff -u <(echo -n) <(gofumports -d .) 29 | 30 | # Consult golangci-lint (multiple Go linting tools). 31 | - golangci-lint run . ./elem/... ./event/... 32 | - golangci-lint run --exclude 'exported .* should have comment .*or be unexported' ./prop/... ./style/... # https://github.com/hexops/vecty/issues/227 33 | - GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' . ./elem/... ./event/... 34 | - GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' --exclude 'exported .* should have comment .*or be unexported' ./prop/... ./style/... # https://github.com/hexops/vecty/issues/227 35 | - bash -c 'cd example && golangci-lint run ./markdown' 36 | - bash -c 'cd example && GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' ./...' 37 | 38 | # Test with Go compiler (under amd64 and wasm architectures.) 39 | - go test -race ./... 40 | - GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" ./... 41 | 42 | # Generate and upload coverage to codecov.io 43 | - 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) 44 | - include_cov=coverage.out bash <(curl -s https://codecov.io/bash) 45 | -------------------------------------------------------------------------------- /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 | Andrea Manzini 13 | Igor Afanasyev 14 | Peter Fern 15 | Stephen Gutekanst 16 | Thomas Bruyelle 17 | Marwan Sulaiman 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Vecty lets you build responsive and dynamic web frontends in Go using WebAssembly, competing with modern web frameworks like React & VueJS. 6 | 7 | [![Build Status](https://travis-ci.org/hexops/vecty.svg?branch=master)](https://travis-ci.org/hexops/vecty) [![PkgGoDev](https://pkg.go.dev/badge/github.com/hexops/vecty)](https://pkg.go.dev/github.com/hexops/vecty) [![GoDoc](https://godoc.org/github.com/hexops/vecty?status.svg)](https://godoc.org/github.com/hexops/vecty) [![codecov](https://img.shields.io/codecov/c/github/hexops/vecty/master.svg)](https://codecov.io/gh/hexops/vecty) 8 | 9 | Benefits 10 | ======== 11 | 12 | - Go developers can be competitive frontend developers. 13 | - Share Go code between your frontend & backend. 14 | - Reusability by sharing components via Go packages so that others can simply import them. 15 | 16 | Goals 17 | ===== 18 | 19 | - _Simple_ 20 | - Designed from the ground up to be easily mastered _by newcomers_ (like Go). 21 | - _Performant_ 22 | - Efficient & understandable performance, small bundle sizes, same performance as raw JS/HTML/CSS. 23 | - _Composable_ 24 | - Nest components to form your entire user interface, seperating them logically as you would any normal Go package. 25 | - _Designed for Go (implicit)_ 26 | - Written from the ground up asking the question _"What is the best way to solve this problem in Go?"_, not simply asking _"How do we translate $POPULAR_LIBRARY to Go?"_ 27 | 28 | Features 29 | ======== 30 | 31 | - Compiles to WebAssembly (via standard Go compiler). 32 | - Small bundle sizes: 0.5 MB hello world (see section below). 33 | - Fast expectation-based browser DOM diffing ('virtual DOM', but less resource usage). 34 | 35 | Vecty vs. Vugu 36 | ============== 37 | 38 | If you're wondering if you should use Vecty or [Vugu](https://www.vugu.org/), consider reading [this Twitter thread](https://twitter.com/JohanBrandhorst/status/1452393594283831297) for advice from both myself and the creator of Vugu. 39 | 40 | Current Status 41 | ============== 42 | 43 | **Vecty is currently considered to be an experimental work-in-progress.** Prior to widespread production use, we must meet our [v1.0.0 milestone](https://github.com/hexops/vecty/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0.0) goals, which are being completed slowly and steadily as contributors have time (Vecty is over 4 years in the making!). 44 | 45 | Early adopters may make use of it for real applications today as long as they are understanding and accepting of the fact that: 46 | 47 | - APIs will change (maybe extensively). 48 | - A number of important things are not ready: 49 | - Extensive documentation, examples and tutorials 50 | - URL-based component routing 51 | - Ready-to-use component libraries (e.g. material UI) 52 | - Server-side rendering 53 | - And more, see [milestone: v1.0.0 ](https://github.com/hexops/vecty/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0.0) 54 | - The scope of Vecty is only ~80% defined currently. 55 | - There are a number of important [open issues](https://github.com/hexops/vecty/issues). 56 | 57 | For a list of projects currently using Vecty, see the [doc/projects-using-vecty.md](doc/projects-using-vecty.md) file. 58 | 59 | Near-zero dependencies 60 | ====================== 61 | 62 | Vecty has nearly zero dependencies, it only relies on `reflect` from the Go stdlib. Through this, it is able to produce the smallest bundle sizes for Go frontend applications out there, limited only by the Go compiler itself: 63 | 64 | | Example binary | Compiler | uncompressed | `gzip --best` | `brotli` | 65 | |-------------------|-----------------|--------------|---------------|----------| 66 | | `hellovecty.wasm` | `tinygo 0.14.0` | 252K | 97K | 81K | 67 | | `hellovecty.wasm` | `go 1.15` | 2.1M | 587K | 443K | 68 | | `markdown.wasm` | `go 1.19` | 5.9M | 1.3M | 952K | 69 | | `todomvc.wasm` | `go 1.15` | 2.9M | 825K | 617K | 70 | 71 | You can find these examples under the [example](./example) directory along with a readme on how to go about running a vecty project. 72 | 73 | Note: Bundle sizes above do not scale linearly with more code/dependencies in your Vecty project. `hellovecty` is the smallest base-line bundle that the compiler can produce with just Vecty as a dependency, other examples above pull in more of the Go standard library and you would not e.g. have to pay that total cost again. 74 | 75 | Community 76 | ========= 77 | 78 | - Join us in the [#vecty](https://gophers.slack.com/messages/vecty/) channel on the [Gophers Slack](https://gophersinvite.herokuapp.com/)! 79 | - See what [projects use Vecty in the wild](https://github.com/hexops/vecty/blob/main/doc/projects-using-vecty.md) 80 | 81 | Changelog 82 | ========= 83 | 84 | See the [doc/CHANGELOG.md](doc/CHANGELOG.md) file. 85 | -------------------------------------------------------------------------------- /doc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Although v1.0.0 [is not yet out](https://github.com/hexops/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 | ## October 25, 2020 10 | 11 | * The `master` branch has been renamed to `main`. 12 | * `v0.6.0` has been tagged/released. 13 | 14 | ## August 15, 2020 ([PR #266](https://github.com/hexops/vecty/pull/266)): minor breaking change 15 | 16 | Vecty has moved to the [github.com/hexops](https://github.com/hexops) organization. Update your import paths: 17 | 18 | ```diff 19 | -import "github.com/gopherjs/vecty" 20 | +import "github.com/hexops/vecty" 21 | ``` 22 | 23 | And update your `go.mod` as required. 24 | 25 | For more information see [issue #230](https://github.com/hexops/vecty/issues/230#issuecomment-674474753). 26 | 27 | ## August 15, 2020 ([PR #265](https://github.com/hexops/vecty/pull/265)): minor breaking change 28 | 29 | Deprecated and removed official support for GopherJS. 30 | 31 | New versions of Vecty _may_ compile with GopherJS, but we do not officially support it and it is dependent on GopherJS being compatible with the official Go compiler. 32 | 33 | If your application cannot compile without GopherJS, you may continue to use the tag `v0.5.0` which is the last version of Vecty which officially supported GopherJS at the time. 34 | 35 | For more information please see [issue #264](https://github.com/hexops/vecty/issues/264). 36 | 37 | ## February 28, 2020 ([PR #256](https://github.com/hexops/vecty/pull/256)): indirect breaking change 38 | 39 | - Go 1.14+ is now required by Vecty. Users of older Go versions and/or GopherJS (until https://github.com/gopherjs/gopherjs/issues/962 is fixed) may wish to continue using commit `6a0a25ee5a96ce029e684c7da6333aa1f34f8f96`. 40 | 41 | ## Nov 30, 2019 ([PR #249](https://github.com/hexops/vecty/pull/249)): minor breaking change 42 | 43 | - `vecty.RenderBody(comp)` is now a blocking function call. Users that rely on it being non-blocking can instead now use `if err := vecty.RenderInto("body", comp); err != nil { panic(err) }` 44 | 45 | ## June 30, 2019 ([PR #232](https://github.com/hexops/vecty/pull/232)): major breaking change 46 | 47 | - `(*HTML).Node` now returns a `syscall/js.Value` instead of `*gopherjs/js.Object`. Users will need to update to the new `syscall/js` API in their applications. 48 | - Go 1.12+ is now required by Vecty, as we make use of [synchronous callback support](https://go-review.googlesource.com/c/go/+/142004) not present in earlier versions. 49 | 50 | ## May 25, 2019 ([PR #235](https://github.com/hexops/vecty/pull/235)): minor breaking change 51 | 52 | - `prop.TypeUrl` has been renamed to `prop.TypeURL`. 53 | 54 | ## Nov 4, 2017 ([PR #158](https://github.com/hexops/vecty/pull/158)): major breaking change 55 | 56 | All `Component`s must now have a `Render` method which returns `vecty.ComponentOrHTML` instead of the prior `*vecty.HTML` type. 57 | 58 | This change allows for higher order components (components that themselves render components), which is useful for many more advanced uses of Vecty. 59 | 60 | ### Upgrading 61 | 62 | Upgrading most codebases should be trivial with a find-and-replace across all files. 63 | 64 | From your editor: 65 | * Find `) Render() *vecty.HTML` and replace with `) Render() vecty.ComponentOrHTML`. 66 | 67 | From the __Linux__ command line: 68 | ```bash 69 | git grep -l ') Render() \*vecty.HTML' | xargs sed -i 's/) Render() \*vecty.HTML/) Render() vecty.ComponentOrHTML/g' 70 | ``` 71 | 72 | From the __Mac__ command line: 73 | ```bash 74 | git grep -l ') Render() \*vecty.HTML' | xargs sed -i '' -e 's/) Render() \*vecty.HTML/) Render() vecty.ComponentOrHTML/g' 75 | ``` 76 | 77 | 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). 78 | 79 | ## Oct 14, 2017 ([PR #155](https://github.com/hexops/vecty/pull/155)): major breaking change 80 | 81 | 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. 82 | 83 | ## Oct 1, 2017 ([PR #147](https://github.com/hexops/vecty/pull/147)): minor breaking change 84 | 85 | `MarkupOrChild` and `ComponentOrHTML` can both now contain `KeyedList` (a new type that has been added) 86 | 87 | ## Sept 5, 2017 ([PR #140](https://github.com/hexops/vecty/pull/140)): minor breaking change 88 | 89 | Package `storeutil` has been moved to `github.com/hexops/vecty/example/todomvc/store/storeutil` import path. 90 | 91 | 92 | ## Sept 2, 2017 ([PR #134](https://github.com/hexops/vecty/pull/134)): major breaking change 93 | 94 | Several breaking changes have been made. Below, we describe how to upgrade your Vecty code to reflect each of these changes. 95 | 96 | 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. 97 | 98 | ### constructors no longer accept markup directly 99 | 100 | `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: 101 | 102 | ```Go 103 | func (p *PageView) Render() *vecty.HTML { 104 | return elem.Body( 105 | vecty.Style("background", "red"), 106 | vecty.Text("Hello World"), 107 | ) 108 | } 109 | ``` 110 | 111 | Must now be written as: 112 | 113 | ```Go 114 | func (p *PageView) Render() *vecty.HTML { 115 | return elem.Body( 116 | vecty.Markup( 117 | vecty.Style("background", "red"), 118 | ), 119 | vecty.Text("Hello World"), 120 | ) 121 | } 122 | ``` 123 | 124 | ### If no longer works for markup 125 | 126 | `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: 127 | 128 | ```diff 129 | func (p *PageView) Render() *vecty.HTML { 130 | return elem.Body( 131 | vecty.Markup( 132 | - vecty.If(isBackgroundRed, vecty.Style("background", "red")), 133 | + vecty.MarkupIf(isBackgroundRed, vecty.Style("background", "red")), 134 | ), 135 | vecty.Text("Hello World"), 136 | ) 137 | } 138 | ``` 139 | 140 | ### Other breaking changes 141 | 142 | - `ComponentOrHTML` now includes `nil` and the new `List` type, rather than just `Component` and `*HTML`. 143 | - `MarkupOrComponentOrHTML` has been renamed to `MarkupOrChild`, and now includes `nil` and the new `List` and `MarkupList` (instead of `Markup`, see below) types. 144 | - The `Markup` _interface_ has been renamed to `Applyer`, and a `Markup` _function_ has been added to create a `MarkupList`. 145 | 146 | 147 | ## Aug 6, 2017 ([PR #130](https://github.com/hexops/vecty/pull/130)): minor breaking change 148 | 149 | The `Restorer` interface has been removed, component instances are now persistent. Properties should be denoted via ``` `vecty:"prop"` ``` struct field tags. 150 | 151 | 152 | ## Jun 17, 2017 ([PR #117](https://github.com/hexops/vecty/pull/117)): minor breaking change 153 | 154 | `(*HTML).Restore` is no longer exported, this method was not generally used externally. 155 | 156 | 157 | ## May 11, 2017 ([PR #108](https://github.com/hexops/vecty/pull/108)): minor breaking change 158 | 159 | `(*HTML).Node` is now a function instead of a struct field. 160 | -------------------------------------------------------------------------------- /doc/projects-using-vecty.md: -------------------------------------------------------------------------------- 1 | # Projects using [Vecty](https://github.com/hexops/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 | | [Vocdoni Block Explorer](https://gitlab.com/vocdoni/vocexplorer) | A custom blockchain explorer for decentralized digital governance toolkit [Vocdoni.io](https://Vocdoni.io). | 14 | 15 | Know of a project using Vecty that is not on this list? Please [let us know](https://github.com/hexops/vecty/issues/new)! 16 | 17 | ## Libraries 18 | 19 | | Package | Description | 20 | |---------|-------------| 21 | | [`github.com/soypat/mdc`](https://github.com/soypat/mdc) | Material Design Components for use with Vecty in a dead-simple fashion. | 22 | | [`github.com/vecty-components/material`](https://github.com/vecty-components/material) | Vecty Material is Vecty bindings for the material-components-web JavaScript library (MDC). | 23 | | [`github.com/wizenerd/ui`](https://github.com/wizenerd/ui) | Material design lite components for gopherjs and vecty | 24 | 25 | ## Tools 26 | 27 | | Tool | Description | 28 | |---------|-------------| 29 | | [`github.com/soypat/vectytemplater`](https://github.com/soypat/vectytemplater) | Executable to generate a basic working vecty application. | 30 | -------------------------------------------------------------------------------- /dom_js.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package vecty 4 | 5 | import "syscall/js" 6 | 7 | // Event represents a DOM event. 8 | type Event struct { 9 | js.Value 10 | Target js.Value 11 | } 12 | 13 | // Node returns the underlying JavaScript Element or TextNode. 14 | // 15 | // It panics if it is called before the DOM node has been attached, i.e. before 16 | // the associated component's Mounter interface would be invoked. 17 | func (h *HTML) Node() js.Value { 18 | if h.node == nil { 19 | panic("vecty: cannot call (*HTML).Node() before DOM node creation / component mount") 20 | } 21 | return h.node.(wrappedObject).j 22 | } 23 | 24 | // RenderIntoNode renders the given component into the existing HTML element by 25 | // replacing it. 26 | // 27 | // If the Component's Render method does not return an element of the same type, 28 | // an error of type ElementMismatchError is returned. 29 | func RenderIntoNode(node js.Value, c Component) error { 30 | return renderIntoNode("RenderIntoNode", wrapObject(node), c) 31 | } 32 | 33 | func toLower(s string) string { 34 | // We must call the prototype method here to workaround a limitation of 35 | // syscall/js in both Go and GopherJS where we cannot call the 36 | // `toLowerCase` string method. See https://github.com/golang/go/issues/35917 37 | return js.Global().Get("String").Get("prototype").Get("toLowerCase").Call("call", js.ValueOf(s)).String() 38 | } 39 | 40 | var globalValue jsObject 41 | 42 | func global() jsObject { 43 | if globalValue == nil { 44 | globalValue = wrapObject(js.Global()) 45 | } 46 | return globalValue 47 | } 48 | 49 | func undefined() wrappedObject { 50 | return wrappedObject{js.Undefined()} 51 | } 52 | 53 | func funcOf(fn func(this jsObject, args []jsObject) interface{}) jsFunc { 54 | return &jsFuncImpl{ 55 | f: js.FuncOf(func(this js.Value, args []js.Value) interface{} { 56 | wrappedArgs := make([]jsObject, len(args)) 57 | for i, arg := range args { 58 | wrappedArgs[i] = wrapObject(arg) 59 | } 60 | return unwrap(fn(wrapObject(this), wrappedArgs)) 61 | }), 62 | goFunc: fn, 63 | } 64 | } 65 | 66 | type jsFuncImpl struct { 67 | f js.Func 68 | goFunc func(this jsObject, args []jsObject) interface{} 69 | } 70 | 71 | func (j *jsFuncImpl) String() string { 72 | // fmt.Sprint(j) would produce the actual implementation of the function in 73 | // JS code which differs across WASM/GopherJS/TinyGo so we instead just 74 | // return an opaque string for testing purposes. 75 | return "func" 76 | } 77 | 78 | func (j *jsFuncImpl) Release() { j.f.Release() } 79 | 80 | func valueOf(v interface{}) jsObject { 81 | return wrapObject(js.ValueOf(v)) 82 | } 83 | 84 | func wrapObject(j js.Value) jsObject { 85 | if j.IsNull() { 86 | return nil 87 | } 88 | return wrappedObject{j: j} 89 | } 90 | 91 | func unwrap(value interface{}) interface{} { 92 | if v, ok := value.(wrappedObject); ok { 93 | return v.j 94 | } 95 | if v, ok := value.(*jsFuncImpl); ok { 96 | return v.f 97 | } 98 | return value 99 | } 100 | 101 | type wrappedObject struct { 102 | j js.Value 103 | } 104 | 105 | func (w wrappedObject) Set(key string, value interface{}) { 106 | w.j.Set(key, unwrap(value)) 107 | } 108 | 109 | func (w wrappedObject) Get(key string) jsObject { 110 | return wrapObject(w.j.Get(key)) 111 | } 112 | 113 | func (w wrappedObject) Delete(key string) { 114 | w.j.Delete(key) 115 | } 116 | 117 | func (w wrappedObject) Call(name string, args ...interface{}) jsObject { 118 | for i, arg := range args { 119 | args[i] = unwrap(arg) 120 | } 121 | return wrapObject(w.j.Call(name, args...)) 122 | } 123 | 124 | func (w wrappedObject) String() string { 125 | return w.j.String() 126 | } 127 | 128 | func (w wrappedObject) Truthy() bool { 129 | return w.j.Truthy() 130 | } 131 | 132 | func (w wrappedObject) IsUndefined() bool { 133 | return w.j.IsUndefined() 134 | } 135 | 136 | func (w wrappedObject) Equal(other jsObject) bool { 137 | if w.j.IsNull() != (other == nil) { 138 | return false 139 | } 140 | return w.j.Equal(unwrap(other).(js.Value)) 141 | } 142 | 143 | func (w wrappedObject) Bool() bool { 144 | return w.j.Bool() 145 | } 146 | 147 | func (w wrappedObject) Int() int { 148 | return w.j.Int() 149 | } 150 | 151 | func (w wrappedObject) Float() float64 { 152 | return w.j.Float() 153 | } 154 | -------------------------------------------------------------------------------- /dom_native.go: -------------------------------------------------------------------------------- 1 | // +build !js 2 | 3 | package vecty 4 | 5 | import "strings" 6 | 7 | // Stubs for building Vecty under a native GOOS and GOARCH, so that Vecty 8 | // type-checks, lints, auto-completes, and serves documentation under godoc.org 9 | // as with any other normal Go package that is not under GOOS=js and 10 | // GOARCH=wasm. 11 | 12 | // SyscallJSValue is an actual syscall/js.Value type under WebAssembly compilation. 13 | // 14 | // It is declared here just for purposes of testing Vecty under native 15 | // 'go test', linting, and serving documentation under godoc.org. 16 | type SyscallJSValue jsObject 17 | 18 | // Event represents a DOM event. 19 | type Event struct { 20 | Value SyscallJSValue 21 | Target SyscallJSValue 22 | } 23 | 24 | // Node returns the underlying JavaScript Element or TextNode. 25 | // 26 | // It panics if it is called before the DOM node has been attached, i.e. before 27 | // the associated component's Mounter interface would be invoked. 28 | func (h *HTML) Node() SyscallJSValue { 29 | return htmlNodeImpl(h) 30 | } 31 | 32 | // RenderIntoNode renders the given component into the existing HTML element by 33 | // replacing it. 34 | // 35 | // If the Component's Render method does not return an element of the same type, 36 | // an error of type ElementMismatchError is returned. 37 | func RenderIntoNode(node SyscallJSValue, c Component) error { 38 | return renderIntoNode("RenderIntoNode", node, c) 39 | } 40 | 41 | func toLower(s string) string { 42 | return strings.ToLower(s) 43 | } 44 | 45 | var globalValue jsObject 46 | 47 | func global() jsObject { 48 | return globalValue 49 | } 50 | 51 | func undefined() wrappedObject { 52 | return wrappedObject{j: &jsObjectImpl{}} 53 | } 54 | 55 | func funcOf(fn func(this jsObject, args []jsObject) interface{}) jsFunc { 56 | return funcOfImpl(fn) 57 | } 58 | 59 | type jsFuncImpl struct { 60 | goFunc func(this jsObject, args []jsObject) interface{} 61 | } 62 | 63 | func (j *jsFuncImpl) String() string { return "func" } 64 | func (j *jsFuncImpl) Release() {} 65 | 66 | func valueOf(v interface{}) jsObject { return valueOfImpl(v) } 67 | 68 | type wrappedObject struct { 69 | jsObject 70 | j jsObject 71 | } 72 | 73 | type jsObjectImpl struct { 74 | jsObject 75 | } 76 | 77 | func (e *jsObjectImpl) Equal(other jsObject) bool { 78 | return e == other.(*jsObjectImpl) 79 | } 80 | 81 | var ( 82 | htmlNodeImpl = func(h *HTML) SyscallJSValue { 83 | panic("not implemented on this architecture in non-testing environment") 84 | } 85 | funcOfImpl = func(fn func(this jsObject, args []jsObject) interface{}) jsFunc { 86 | panic("not implemented on this architecture in non-testing environment") 87 | } 88 | valueOfImpl = func(v interface{}) jsObject { 89 | panic("not implemented on this architecture in non-testing environment") 90 | } 91 | ) 92 | -------------------------------------------------------------------------------- /dom_no_tinygo.go: -------------------------------------------------------------------------------- 1 | // +build !tinygo 2 | 3 | package vecty 4 | 5 | func init() { 6 | if isTest { 7 | return 8 | } 9 | if global() == nil { 10 | panic("vecty: only WebAssembly, TinyGo, and testing compilation is supported") 11 | } 12 | if global().Get("document").IsUndefined() { 13 | panic("vecty: only running inside a browser is supported") 14 | } 15 | } 16 | 17 | func (h *HTML) tinyGoCannotIterateNilMaps() {} 18 | 19 | func tinyGoAssertCopier(c Component) {} 20 | -------------------------------------------------------------------------------- /dom_test.go: -------------------------------------------------------------------------------- 1 | package vecty 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type testCore struct{ Core } 9 | 10 | func (testCore) Render() ComponentOrHTML { return Tag("p") } 11 | 12 | type testCorePtr struct{ *Core } 13 | 14 | func (testCorePtr) Render() ComponentOrHTML { return Tag("p") } 15 | 16 | func TestCore(t *testing.T) { 17 | // Test that a standard *MyComponent with embedded Core works as we expect. 18 | t.Run("comp_ptr_and_core", func(t *testing.T) { 19 | v1 := Tag("v1") 20 | valid := Component(&testCore{}) 21 | valid.Context().prevRender = v1 22 | if valid.Context().prevRender != v1 { 23 | t.Fatal("valid.Context().prevRender != v1") 24 | } 25 | }) 26 | 27 | // Test that a non-pointer MyComponent with embedded Core does not satisfy 28 | // the Component interface: 29 | // 30 | // testCore does not implement Component (Context method has pointer receiver) 31 | // 32 | t.Run("comp_and_core", func(t *testing.T) { 33 | isComponent := func(x interface{}) bool { 34 | _, ok := x.(Component) 35 | return ok 36 | } 37 | if isComponent(testCore{}) { 38 | t.Fatal("expected !isComponent(testCompCore{})") 39 | } 40 | }) 41 | 42 | // Test what happens when a user accidentally embeds *Core instead of Core in 43 | // their component. 44 | t.Run("comp_ptr_and_core_ptr", func(t *testing.T) { 45 | v1 := Tag("v1") 46 | invalid := Component(&testCorePtr{}) 47 | got := recoverStr(func() { 48 | invalid.Context().prevRender = v1 49 | }) 50 | // TODO(slimsag): This would happen in user-facing code too. We should 51 | // create a helper for when we access a component's context, which 52 | // would panic with a more helpful message. 53 | want := "runtime error: invalid memory address or nil pointer dereference" 54 | if got != want { 55 | t.Fatalf("got panic %q want %q", got, want) 56 | } 57 | }) 58 | t.Run("comp_and_core_ptr", func(t *testing.T) { 59 | v1 := Tag("v1") 60 | invalid := Component(testCorePtr{}) 61 | got := recoverStr(func() { 62 | invalid.Context().prevRender = v1 63 | }) 64 | // TODO(slimsag): This would happen in user-facing code too. We should 65 | // create a helper for when we access a component's context, which 66 | // would panic with a more helpful message. 67 | want := "runtime error: invalid memory address or nil pointer dereference" 68 | if got != want { 69 | t.Fatalf("got panic %q want %q", got, want) 70 | } 71 | }) 72 | } 73 | 74 | // TODO(slimsag): TestUnmounter; Unmounter.Unmount 75 | 76 | func TestHTML_Node(t *testing.T) { 77 | ts := testSuite(t) 78 | defer ts.done() 79 | 80 | x := undefined() 81 | h := &HTML{node: x} 82 | if !h.Node().Equal(x.j) { 83 | t.Fatal("h.Node() != x") 84 | } 85 | } 86 | 87 | // TestHTML_reconcile_std tests that (*HTML).reconcile against an old HTML instance 88 | // works as expected (i.e. that it updates nodes correctly). 89 | func TestHTML_reconcile_std(t *testing.T) { 90 | t.Run("text_identical", func(t *testing.T) { 91 | ts := testSuite(t) 92 | defer ts.done() 93 | 94 | init := Text("foobar") 95 | init.reconcile(nil) 96 | 97 | target := Text("foobar") 98 | target.reconcile(init) 99 | }) 100 | t.Run("text_diff", func(t *testing.T) { 101 | ts := testSuite(t) 102 | defer ts.done() 103 | 104 | init := Text("bar") 105 | init.reconcile(nil) 106 | 107 | target := Text("foo") 108 | target.reconcile(init) 109 | }) 110 | t.Run("properties", func(t *testing.T) { 111 | cases := []struct { 112 | name string 113 | initHTML *HTML 114 | targetHTML *HTML 115 | sortedLines [][2]int 116 | }{ 117 | { 118 | name: "diff", 119 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 120 | targetHTML: Tag("div", Markup(Property("a", 3), Property("b", "4foobar"))), 121 | sortedLines: [][2]int{{3, 4}, {12, 13}}, 122 | }, 123 | { 124 | name: "remove", 125 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 126 | targetHTML: Tag("div", Markup(Property("a", 3))), 127 | sortedLines: [][2]int{{3, 4}}, 128 | }, 129 | { 130 | name: "replaced_elem_diff", 131 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 132 | targetHTML: Tag("span", Markup(Property("a", 3), Property("b", "4foobar"))), 133 | sortedLines: [][2]int{{3, 4}, {11, 12}}, 134 | }, 135 | { 136 | name: "replaced_elem_shared", 137 | initHTML: Tag("div", Markup(Property("a", 1), Property("b", "2foobar"))), 138 | targetHTML: Tag("span", Markup(Property("a", 1), Property("b", "4foobar"))), 139 | sortedLines: [][2]int{{3, 4}, {11, 12}}, 140 | }, 141 | } 142 | for _, tst := range cases { 143 | t.Run(tst.name, func(t *testing.T) { 144 | ts := testSuite(t) 145 | defer ts.multiSortedDone(tst.sortedLines...) 146 | 147 | tst.initHTML.reconcile(nil) 148 | ts.record("(first reconcile done)") 149 | tst.targetHTML.reconcile(tst.initHTML) 150 | }) 151 | } 152 | }) 153 | t.Run("attributes", func(t *testing.T) { 154 | cases := []struct { 155 | name string 156 | initHTML *HTML 157 | targetHTML *HTML 158 | sortedLines [][2]int 159 | }{ 160 | { 161 | name: "diff", 162 | initHTML: Tag("div", Markup(Attribute("a", 1), Attribute("b", "2foobar"))), 163 | targetHTML: Tag("div", Markup(Attribute("a", 3), Attribute("b", "4foobar"))), 164 | sortedLines: [][2]int{{3, 4}, {12, 13}}, 165 | }, 166 | { 167 | name: "remove", 168 | initHTML: Tag("div", Markup(Attribute("a", 1), Attribute("b", "2foobar"))), 169 | targetHTML: Tag("div", Markup(Attribute("a", 3))), 170 | sortedLines: [][2]int{{3, 4}}, 171 | }, 172 | } 173 | for _, tst := range cases { 174 | t.Run(tst.name, func(t *testing.T) { 175 | ts := testSuite(t) 176 | defer ts.multiSortedDone(tst.sortedLines...) 177 | 178 | tst.initHTML.reconcile(nil) 179 | ts.record("(first reconcile done)") 180 | tst.targetHTML.reconcile(tst.initHTML) 181 | }) 182 | } 183 | }) 184 | t.Run("class", func(t *testing.T) { 185 | cases := []struct { 186 | name string 187 | initHTML *HTML 188 | targetHTML *HTML 189 | sortedLines [][2]int 190 | }{ 191 | { 192 | name: "multi", 193 | initHTML: Tag("div", Markup(Class("a"), Class("b"))), 194 | targetHTML: Tag("div", Markup(Class("a"), Class("c"))), 195 | sortedLines: [][2]int{{4, 5}}, 196 | }, 197 | { 198 | name: "diff", 199 | initHTML: Tag("div", Markup(Class("a", "b"))), 200 | targetHTML: Tag("div", Markup(Class("a", "c"))), 201 | sortedLines: [][2]int{{4, 5}}, 202 | }, 203 | { 204 | name: "remove", 205 | initHTML: Tag("div", Markup(Class("a", "b"))), 206 | targetHTML: Tag("div", Markup(Class("a"))), 207 | sortedLines: [][2]int{{4, 5}}, 208 | }, 209 | { 210 | name: "map", 211 | initHTML: Tag("div", Markup(ClassMap{"a": true, "b": true})), 212 | targetHTML: Tag("div", Markup(ClassMap{"a": true})), 213 | sortedLines: [][2]int{{4, 5}}, 214 | }, 215 | { 216 | name: "map_toggle", 217 | initHTML: Tag("div", Markup(ClassMap{"a": true, "b": true})), 218 | targetHTML: Tag("div", Markup(ClassMap{"a": true, "b": false})), 219 | sortedLines: [][2]int{{4, 5}}, 220 | }, 221 | { 222 | name: "combo", 223 | initHTML: Tag("div", Markup(ClassMap{"a": true, "b": true}, Class("c"))), 224 | targetHTML: Tag("div", Markup(ClassMap{"a": true, "b": false}, Class("d"))), 225 | sortedLines: [][2]int{{4, 6}, {11, 12}}, 226 | }, 227 | } 228 | for _, tst := range cases { 229 | t.Run(tst.name, func(t *testing.T) { 230 | ts := testSuite(t) 231 | defer ts.multiSortedDone(tst.sortedLines...) 232 | 233 | tst.initHTML.reconcile(nil) 234 | ts.record("(first reconcile done)") 235 | tst.targetHTML.reconcile(tst.initHTML) 236 | }) 237 | } 238 | }) 239 | t.Run("dataset", func(t *testing.T) { 240 | cases := []struct { 241 | name string 242 | initHTML *HTML 243 | targetHTML *HTML 244 | sortedLines [][2]int 245 | }{ 246 | { 247 | name: "diff", 248 | initHTML: Tag("div", Markup(Data("a", "1"), Data("b", "2foobar"))), 249 | targetHTML: Tag("div", Markup(Data("a", "3"), Data("b", "4foobar"))), 250 | sortedLines: [][2]int{{5, 6}, {14, 15}}, 251 | }, 252 | { 253 | name: "remove", 254 | initHTML: Tag("div", Markup(Data("a", "1"), Data("b", "2foobar"))), 255 | targetHTML: Tag("div", Markup(Data("a", "3"))), 256 | sortedLines: [][2]int{{5, 6}}, 257 | }, 258 | } 259 | for _, tst := range cases { 260 | t.Run(tst.name, func(t *testing.T) { 261 | ts := testSuite(t) 262 | defer ts.multiSortedDone(tst.sortedLines...) 263 | 264 | tst.initHTML.reconcile(nil) 265 | ts.record("(first reconcile done)") 266 | tst.targetHTML.reconcile(tst.initHTML) 267 | }) 268 | } 269 | }) 270 | t.Run("style", func(t *testing.T) { 271 | cases := []struct { 272 | name string 273 | initHTML *HTML 274 | targetHTML *HTML 275 | sortedLines [][2]int 276 | }{ 277 | { 278 | name: "diff", 279 | initHTML: Tag("div", Markup(Style("a", "1"), Style("b", "2foobar"))), 280 | targetHTML: Tag("div", Markup(Style("a", "3"), Style("b", "4foobar"))), 281 | sortedLines: [][2]int{{6, 7}, {15, 16}}, 282 | }, 283 | { 284 | name: "remove", 285 | initHTML: Tag("div", Markup(Style("a", "1"), Style("b", "2foobar"))), 286 | targetHTML: Tag("div", Markup(Style("a", "3"))), 287 | sortedLines: [][2]int{{6, 7}}, 288 | }, 289 | } 290 | for _, tst := range cases { 291 | t.Run(tst.name, func(t *testing.T) { 292 | ts := testSuite(t) 293 | defer ts.multiSortedDone(tst.sortedLines...) 294 | 295 | tst.initHTML.reconcile(nil) 296 | ts.record("(first reconcile done)") 297 | tst.targetHTML.reconcile(tst.initHTML) 298 | }) 299 | } 300 | }) 301 | t.Run("event_listener", func(t *testing.T) { 302 | // TODO(pdf): Mock listener functions for equality testing 303 | ts := testSuite(t) 304 | defer ts.done() 305 | 306 | initEventListeners := []Applyer{ 307 | &EventListener{Name: "click"}, 308 | &EventListener{Name: "keydown"}, 309 | } 310 | prev := Tag("div", Markup(initEventListeners...)) 311 | prev.reconcile(nil) 312 | ts.record("(expected two added event listeners above)") 313 | for i, m := range initEventListeners { 314 | listener := m.(*EventListener) 315 | if listener.wrapper == nil { 316 | t.Fatalf("listener %d wrapper == nil: %+v", i, listener) 317 | } 318 | } 319 | 320 | targetEventListeners := []Applyer{ 321 | &EventListener{Name: "click"}, 322 | } 323 | h := Tag("div", Markup(targetEventListeners...)) 324 | h.reconcile(prev) 325 | ts.record("(expected two removed, one added event listeners above)") 326 | for i, m := range targetEventListeners { 327 | listener := m.(*EventListener) 328 | if listener.wrapper == nil { 329 | t.Fatalf("listener %d wrapper == nil: %+v", i, listener) 330 | } 331 | } 332 | }) 333 | 334 | // TODO(pdf): test (*HTML).reconcile child mutations, and value/checked properties 335 | // TODO(pdf): test multi-pass reconcile of persistent component pointer children, ref: https://github.com/hexops/vecty/pull/124 336 | } 337 | 338 | // TestHTML_reconcile_nil tests that (*HTML).reconcile(nil) works as expected (i.e. 339 | // that it creates nodes correctly). 340 | func TestHTML_reconcile_nil(t *testing.T) { 341 | t.Run("one_of_tag_or_text", func(t *testing.T) { 342 | got := recoverStr(func() { 343 | h := &HTML{text: "hello", tag: "div"} 344 | h.reconcile(nil) 345 | }) 346 | want := "vecty: internal error (only one of HTML.tag or HTML.text may be set)" 347 | if got != want { 348 | t.Fatalf("got panic %q want %q", got, want) 349 | } 350 | }) 351 | t.Run("unsafe_text", func(t *testing.T) { 352 | got := recoverStr(func() { 353 | h := &HTML{text: "hello", innerHTML: "foobar"} 354 | h.reconcile(nil) 355 | }) 356 | want := "vecty: only HTML may have UnsafeHTML attribute" 357 | if got != want { 358 | t.Fatalf("got panic %q want %q", got, want) 359 | } 360 | }) 361 | t.Run("create_element", func(t *testing.T) { 362 | ts := testSuite(t) 363 | defer ts.done() 364 | 365 | h := Tag("strong") 366 | h.reconcile(nil) 367 | }) 368 | t.Run("create_element_ns", func(t *testing.T) { 369 | ts := testSuite(t) 370 | defer ts.done() 371 | 372 | h := Tag("strong", Markup(Namespace("foobar"))) 373 | h.reconcile(nil) 374 | }) 375 | t.Run("create_text_node", func(t *testing.T) { 376 | ts := testSuite(t) 377 | defer ts.done() 378 | 379 | h := Text("hello") 380 | h.reconcile(nil) 381 | }) 382 | t.Run("inner_html", func(t *testing.T) { 383 | ts := testSuite(t) 384 | defer ts.done() 385 | 386 | h := Tag("div", Markup(UnsafeHTML("

hello

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