├── LICENSE ├── README.md ├── bus.go ├── component.go ├── context.go ├── event.go ├── examples ├── 01-declarative-rendering │ ├── index.wasmgo.html │ └── main.go ├── 02-attribute-data-binding │ ├── index.wasmgo.html │ └── main.go ├── 03-conditionals-with-methods │ ├── index.wasmgo.html │ └── main.go ├── 04-loops │ ├── index.wasmgo.html │ └── main.go ├── 05-handling-user-input │ ├── index.wasmgo.html │ └── main.go ├── 06-two-way-data-binding │ ├── index.wasmgo.html │ └── main.go ├── 07-composing-with-components │ ├── index.wasmgo.html │ └── main.go ├── 08-raw-html │ ├── index.wasmgo.html │ └── main.go ├── 09-computed-properties │ ├── index.wasmgo.html │ └── main.go ├── 10-watchers │ ├── index.wasmgo.html │ └── main.go ├── 11-class-binding │ ├── index.wasmgo.html │ └── main.go └── 12-style-binding │ ├── index.wasmgo.html │ └── main.go ├── go.mod ├── go.sum ├── option.go ├── render.go ├── subcomponent.go ├── template.go ├── vnode.go └── vue.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue 2 | [![GoDoc](https://godoc.org/github.com/norunners/vue?status.svg)](https://godoc.org/github.com/norunners/vue) 3 | 4 | Package `vue` is the progressive framework for [WebAssembly](https://github.com/golang/go/wiki/WebAssembly) applications. 5 | 6 | ## Install 7 | ```bash 8 | GOARCH=wasm GOOS=js go get github.com/norunners/vue 9 | ``` 10 | *Requires Go 1.12 or higher.* 11 | 12 | ## Goals 13 | * Provide a unified solution for a framework, state manager and router in the frontend space. 14 | * Leverage [templating](https://github.com/norunners/vueg) to separate application logic from frontend rendering. 15 | * Simplify data binding to ease the relation of state management to rendering. 16 | * Encourage component reuse to promote development productivity. 17 | * Follow an idiomatic Go translation of the familiar Vue API. 18 | 19 | ## Hello World! 20 | The `main.go` file is compiled to a `.wasm` WebAssembly file. 21 | ```go 22 | package main 23 | 24 | import "github.com/norunners/vue" 25 | 26 | type Data struct { 27 | Message string 28 | } 29 | 30 | func main() { 31 | vue.New( 32 | vue.El("#app"), 33 | vue.Template("

{{ Message }}

"), 34 | vue.Data(Data{Message: "Hello WebAssembly!"}), 35 | ) 36 | 37 | select {} 38 | } 39 | ``` 40 | 41 | The `index.wasmgo.html` file fetches and runs a `.wasm` WebAssembly file. 42 | ```html 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | ``` 55 | *Note, the example above is compatible with [wasmgo](https://github.com/dave/wasmgo).* 56 | 57 | ## Serve Examples 58 | Install `wasmgo` to serve examples. 59 | ```bash 60 | go get -u github.com/dave/wasmgo 61 | ``` 62 | 63 | Serve an example [locally](http://localhost:8080/). 64 | ```bash 65 | cd examples/01-declarative-rendering 66 | wasmgo serve 67 | ``` 68 | 69 | ## Status 70 | Alpha - The state of this project is experimental until the common features of Vue are implemented. 71 | The plan is to follow the Vue API closely except for areas of major simplification, which may lead to a subset of the Vue API. 72 | During this stage, the API is expected to encounter minor breaking changes but increase in stability as the project progresses. 73 | 74 | ## F.A.Q. 75 | 76 | #### Why Vue? 77 | One of the common themes of existing frameworks is to combine component application logic with frontend rendering. 78 | This can lead to a confusing mental model to reason about because both concerns may be mixed together in the same logic. 79 | By design, Vue renders components with templates which ensures application logic is developed separately from frontend rending. 80 | 81 | Another commonality of existing frameworks is to unnecessarily expose the relation of state management to rendering in the API. 82 | By design, Vue binds data in both directions which ensures automatic updating and rendering when state changes. 83 | 84 | This project aims to combine the simplicity of Vue with the power of Go WebAssembly. 85 | 86 | License 87 | ------- 88 | * [MIT License](LICENSE) 89 | -------------------------------------------------------------------------------- /bus.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // bus contains subscriptions of events to methods. 8 | type bus struct { 9 | parent *bus 10 | caller caller 11 | subs map[string]map[string]struct{} 12 | } 13 | 14 | // caller calls a method with optional arguments. 15 | type caller interface { 16 | call(method string, args []reflect.Value) 17 | } 18 | 19 | // newBus creates a new event bus. 20 | func newBus(parent *bus, caller caller) *bus { 21 | subs := make(map[string]map[string]struct{}, 0) 22 | return &bus{parent: parent, caller: caller, subs: subs} 23 | } 24 | 25 | // pub publishes the event with the method and optional arguments. 26 | // All event subscriber methods are called if the method is empty. 27 | // When the component is not subscribed to the event, it propagates to parent components. 28 | func (bus *bus) pub(event, method string, args []interface{}) { 29 | if bus == nil { 30 | return 31 | } 32 | 33 | methods, ok := bus.subs[event] 34 | if !ok { 35 | bus.parent.pub(event, method, args) 36 | return 37 | } 38 | 39 | values := make([]reflect.Value, 0, len(args)) 40 | for _, arg := range args { 41 | values = append(values, reflect.ValueOf(arg)) 42 | } 43 | 44 | if method != "" { 45 | bus.caller.call(method, values) 46 | return 47 | } 48 | 49 | for method := range methods { 50 | bus.caller.call(method, values) 51 | } 52 | } 53 | 54 | // sub subscribes the component to the event with the method. 55 | func (bus *bus) sub(event, method string) { 56 | if methods, ok := bus.subs[event]; ok { 57 | methods[method] = struct{}{} 58 | } else { 59 | bus.subs[event] = map[string]struct{}{method: {}} 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /component.go: -------------------------------------------------------------------------------- 1 | // Package vue is the progressive framework for wasm applications. 2 | package vue 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // Comp is a vue component. 10 | type Comp struct { 11 | el string 12 | tmpl string 13 | data interface{} 14 | methods map[string]reflect.Value 15 | computed map[string]reflect.Value 16 | watchers map[string]reflect.Value 17 | props map[string]struct{} 18 | subs map[string]*Comp 19 | isSub bool 20 | } 21 | 22 | // Component creates a new component from the given options. 23 | func Component(options ...Option) *Comp { 24 | methods := make(map[string]reflect.Value, 0) 25 | computed := make(map[string]reflect.Value, 0) 26 | watches := make(map[string]reflect.Value, 0) 27 | props := make(map[string]struct{}, 0) 28 | subs := make(map[string]*Comp, 0) 29 | 30 | comp := &Comp{ 31 | data: struct{}{}, 32 | methods: methods, 33 | computed: computed, 34 | watchers: watches, 35 | props: props, 36 | subs: subs, 37 | } 38 | for _, option := range options { 39 | option(comp) 40 | } 41 | return comp 42 | } 43 | 44 | // newData creates new data from the function. 45 | // Without a function the data of the component is returned. 46 | func (comp *Comp) newData() reflect.Value { 47 | value := reflect.ValueOf(comp.data) 48 | if value.Type().Kind() != reflect.Func { 49 | return value 50 | } 51 | rets := value.Call(nil) 52 | if n := len(rets); n != 1 { 53 | must(fmt.Errorf("invalid return length: %d", n)) 54 | } 55 | return rets[0] 56 | } 57 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Context is received by functions to interact with the component. 9 | type Context interface { 10 | Data() interface{} 11 | Get(field string) interface{} 12 | Set(field string, value interface{}) 13 | Go(method string, args ...interface{}) 14 | Emit(event string, args ...interface{}) 15 | } 16 | 17 | // Data returns the data for the component. 18 | // Props and computed are excluded from data. 19 | func (vm *ViewModel) Data() interface{} { 20 | return vm.data.Interface() 21 | } 22 | 23 | // Get returns the data field value. 24 | // Props and computed are included to get. 25 | // Computed may be calculated as needed. 26 | func (vm *ViewModel) Get(field string) interface{} { 27 | if value, ok := vm.state[field]; ok { 28 | return value 29 | } 30 | 31 | function, ok := vm.comp.computed[field] 32 | if !ok { 33 | must(fmt.Errorf("unknown data field: %s", field)) 34 | } 35 | value := vm.compute(function) 36 | vm.mapField(field, value) 37 | return value 38 | } 39 | 40 | // Set assigns the data field to the given value. 41 | // Props and computed are excluded to set. 42 | func (vm *ViewModel) Set(field string, value interface{}) { 43 | data := reflect.Indirect(vm.data) 44 | oldVal := reflect.Indirect(data.FieldByName(field)) 45 | newVal := reflect.Indirect(reflect.ValueOf(value)) 46 | 47 | oldVal.Set(newVal) 48 | vm.mapField(field, value) 49 | } 50 | 51 | // Go asynchronously calls the given method with optional arguments. 52 | // Blocking functions must be called asynchronously. 53 | func (vm *ViewModel) Go(method string, args ...interface{}) { 54 | values := make([]reflect.Value, 0, len(args)) 55 | for _, arg := range args { 56 | values = append(values, reflect.ValueOf(arg)) 57 | } 58 | go vm.call(method, values) 59 | } 60 | 61 | // Emit dispatches the given event with optional arguments. 62 | func (vm *ViewModel) Emit(event string, args ...interface{}) { 63 | vm.bus.pub(event, "", args) 64 | } 65 | 66 | // call calls the given method with optional values then calls render. 67 | func (vm *ViewModel) call(method string, values []reflect.Value) { 68 | if function, ok := vm.comp.methods[method]; ok { 69 | values = append([]reflect.Value{reflect.ValueOf(vm)}, values...) 70 | function.Call(values) 71 | vm.render() 72 | } 73 | } 74 | 75 | // compute calls the given function and returns the first element. 76 | func (vm *ViewModel) compute(function reflect.Value) interface{} { 77 | values := []reflect.Value{reflect.ValueOf(vm)} 78 | rets := function.Call(values) 79 | return rets[0].Interface() 80 | } 81 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "github.com/gowasm/go-js-dom" 5 | "strings" 6 | "syscall/js" 7 | ) 8 | 9 | // keyboardEvent is the keyboard event type. 10 | var keyboardEvent = js.Global().Get("KeyboardEvent") 11 | 12 | // addEventListener adds the callback to the element as an event listener unless the type was previously added. 13 | func (vm *ViewModel) addEventListener(typ string, cb func(dom.Event)) { 14 | if _, ok := vm.funcs[typ]; ok { 15 | return 16 | } 17 | fn := vm.vnode.node.AddEventListener(typ, cb, false) 18 | vm.funcs[typ] = fn 19 | } 20 | 21 | // vModel is the vue model event callback. 22 | func (vm *ViewModel) vModel(event dom.Event) { 23 | event.StopImmediatePropagation() 24 | 25 | target := event.Target() 26 | _, field, ok := findAttr(target, event.Type()) 27 | if !ok { 28 | return 29 | } 30 | 31 | value := target.Underlying().Get("value").String() 32 | vm.Set(field, value) 33 | vm.render() 34 | } 35 | 36 | // vOn is the vue on event callback. 37 | func (vm *ViewModel) vOn(event dom.Event) { 38 | event.StopImmediatePropagation() 39 | 40 | typ := event.Type() 41 | attrKey, method, ok := findAttr(event.Target(), typ) 42 | if !ok { 43 | return 44 | } 45 | 46 | modifiers := strings.TrimPrefix(attrKey, typ) 47 | modSet := modSet(modifiers) 48 | 49 | if event.Underlying().InstanceOf(keyboardEvent) { 50 | keyType := event.Underlying().Get("key").String() 51 | if _, ok := modSet[keyType]; !ok && len(modSet) > 0 { 52 | return 53 | } 54 | } 55 | 56 | vm.bus.pub(typ, method, nil) 57 | } 58 | 59 | // release removes all the event listeners. 60 | func (vm *ViewModel) release() { 61 | for typ, fn := range vm.funcs { 62 | vm.vnode.node.RemoveEventListener(typ, fn, false) 63 | } 64 | } 65 | 66 | // findAttr finds the attribute from the given prefix by searching up the dom tree. 67 | func findAttr(elem dom.Element, prefix string) (string, string, bool) { 68 | if elem == nil { 69 | return "", "", false 70 | } 71 | for attrKey, attrVal := range elem.Attributes() { 72 | if strings.HasPrefix(attrKey, prefix) { 73 | return attrKey, attrVal, true 74 | } 75 | } 76 | return findAttr(elem.ParentElement(), prefix) 77 | } 78 | 79 | // modSet converts modifiers to a set, includes title conversion. 80 | // For example: hello.world -> {"Hello", "World"} 81 | func modSet(modifiers string) map[string]struct{} { 82 | if modifiers == "" { 83 | return nil 84 | } 85 | mods := strings.Split(modifiers, ".") 86 | set := make(map[string]struct{}, len(mods)) 87 | for _, mod := range mods { 88 | set[modTitle(mod)] = struct{}{} 89 | } 90 | return set 91 | } 92 | 93 | // modTitle converts modifiers to title style. 94 | // For example: page-down -> PageDown 95 | func modTitle(modifier string) string { 96 | mods := strings.Split(modifier, "-") 97 | sb := &strings.Builder{} 98 | for _, mod := range mods { 99 | sb.WriteString(strings.Title(mod)) 100 | } 101 | return sb.String() 102 | } 103 | -------------------------------------------------------------------------------- /examples/01-declarative-rendering/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1 - Declarative Rendering 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/01-declarative-rendering/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/norunners/vue" 4 | 5 | type Data struct { 6 | Message string 7 | } 8 | 9 | func main() { 10 | vue.New( 11 | vue.El("#app"), 12 | vue.Template("

{{ Message }}

"), 13 | vue.Data(Data{Message: "Hello WebAssembly!"}), 14 | ) 15 | 16 | select {} 17 | } 18 | -------------------------------------------------------------------------------- /examples/02-attribute-data-binding/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2 - Attribute Data Binding 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/02-attribute-data-binding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | "time" 6 | ) 7 | 8 | const tmpl = ` 9 | 10 | Hover your mouse over me for a few seconds 11 | to see my dynamically bound title! 12 | 13 | ` 14 | 15 | type Data struct { 16 | Message string 17 | } 18 | 19 | func main() { 20 | vue.New( 21 | vue.El("#app"), 22 | vue.Template(tmpl), 23 | vue.Data(Data{Message: "You loaded this page on " + time.Now().Format(time.ANSIC)}), 24 | ) 25 | 26 | select {} 27 | } 28 | -------------------------------------------------------------------------------- /examples/03-conditionals-with-methods/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3 - Conditionals with Methods 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/03-conditionals-with-methods/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | "time" 6 | ) 7 | 8 | const tmpl = ` 9 | Now you see me 10 | ` 11 | 12 | type Data struct { 13 | Seen bool 14 | } 15 | 16 | func ToggleSeen(vctx vue.Context) { 17 | data := vctx.Data().(*Data) 18 | data.Seen = !data.Seen 19 | } 20 | 21 | func main() { 22 | vm := vue.New( 23 | vue.El("#app"), 24 | vue.Template(tmpl), 25 | vue.Data(&Data{Seen: true}), 26 | vue.Methods(ToggleSeen), 27 | ) 28 | 29 | for tick := time.Tick(time.Second); ; { 30 | select { 31 | case <-tick: 32 | vm.Go("ToggleSeen") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/04-loops/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 4 - Loops 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/04-loops/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | "time" 6 | ) 7 | 8 | const tmpl = ` 9 |
    10 |
  1. 11 | {{ Todo.Text }} 12 |
  2. 13 |
14 | ` 15 | 16 | type Data struct { 17 | Todos []Todo 18 | } 19 | 20 | type Todo struct { 21 | Text string 22 | } 23 | 24 | func Add(vctx vue.Context) { 25 | data := vctx.Data().(*Data) 26 | data.Todos = append(data.Todos, Todo{"Build something wasm!"}) 27 | } 28 | 29 | func main() { 30 | data := &Data{ 31 | Todos: []Todo{ 32 | {Text: "Learn Go"}, 33 | {Text: "Learn Vue"}, 34 | }, 35 | } 36 | 37 | vm := vue.New( 38 | vue.El("#app"), 39 | vue.Template(tmpl), 40 | vue.Data(data), 41 | vue.Methods(Add), 42 | ) 43 | 44 | time.AfterFunc(time.Second, func() { 45 | vm.Go("Add") 46 | }) 47 | select {} 48 | } 49 | -------------------------------------------------------------------------------- /examples/05-handling-user-input/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 5 - Handling User Input 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/05-handling-user-input/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | ) 6 | 7 | const tmpl = ` 8 |
9 |

{{ Message }}

10 | 13 |
14 | ` 15 | 16 | type Data struct { 17 | Message string 18 | } 19 | 20 | func ReverseMessage(vctx vue.Context) { 21 | data := vctx.Data().(*Data) 22 | runes := []rune(data.Message) 23 | for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { 24 | runes[i], runes[j] = runes[j], runes[i] 25 | } 26 | data.Message = string(runes) 27 | } 28 | 29 | func main() { 30 | vue.New( 31 | vue.El("#app"), 32 | vue.Template(tmpl), 33 | vue.Data(&Data{Message: "Hello WebAssembly!"}), 34 | vue.Methods(ReverseMessage), 35 | ) 36 | 37 | select {} 38 | } 39 | -------------------------------------------------------------------------------- /examples/06-two-way-data-binding/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 - Two Way Data Binding 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/06-two-way-data-binding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | ) 6 | 7 | const tmpl = ` 8 |
9 |

{{ Message }}

10 | 11 |
12 | ` 13 | 14 | type Data struct { 15 | Message string 16 | } 17 | 18 | func main() { 19 | vue.New( 20 | vue.El("#app"), 21 | vue.Template(tmpl), 22 | vue.Data(&Data{Message: "Hello WebAssembly!"}), 23 | ) 24 | 25 | select {} 26 | } 27 | -------------------------------------------------------------------------------- /examples/07-composing-with-components/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 - Composing with Components 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/07-composing-with-components/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | ) 6 | 7 | const tmpl = ` 8 |
    9 | 12 | 13 |
14 | ` 15 | 16 | type Data struct { 17 | Todos []Todo 18 | } 19 | 20 | type Todo struct { 21 | Text string 22 | } 23 | 24 | func main() { 25 | data := &Data{ 26 | Todos: []Todo{ 27 | {Text: "Vegetables"}, 28 | {Text: "Cheese"}, 29 | {Text: "Whatever else humans are supposed to eat"}, 30 | }, 31 | } 32 | 33 | vue.New( 34 | vue.El("#app"), 35 | vue.Template(tmpl), 36 | vue.Data(data), 37 | vue.Sub("todo-item", vue.Component( 38 | vue.Props("Todo"), 39 | vue.Template("
  • {{ Todo.Text }}
  • "), 40 | )), 41 | ) 42 | 43 | select {} 44 | } 45 | -------------------------------------------------------------------------------- /examples/08-raw-html/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 - Raw Html 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/08-raw-html/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | ) 6 | 7 | const ( 8 | tmpl = ` 9 |
    10 |

    Using mustaches: {{{ RawHtml }}}

    11 |

    Using v-html directive:

    12 |
    13 | ` 14 | rawHtml = ` 15 | This should be red. 16 | ` 17 | ) 18 | 19 | type Data struct { 20 | RawHtml string 21 | } 22 | 23 | func main() { 24 | vue.New( 25 | vue.El("#app"), 26 | vue.Template(tmpl), 27 | vue.Data(Data{RawHtml: rawHtml}), 28 | ) 29 | 30 | select {} 31 | } 32 | -------------------------------------------------------------------------------- /examples/09-computed-properties/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 - Computed Properties 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/09-computed-properties/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | ) 6 | 7 | const tmpl = ` 8 |
    9 |

    Original message: "{{ Message }}"

    10 |

    Computed reversed message: "{{ ReversedMessage }}"

    11 |
    12 | ` 13 | 14 | type Data struct { 15 | Message string 16 | } 17 | 18 | func ReversedMessage(vctx vue.Context) string { 19 | message := vctx.Get("Message").(string) 20 | runes := []rune(message) 21 | for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { 22 | runes[i], runes[j] = runes[j], runes[i] 23 | } 24 | return string(runes) 25 | } 26 | 27 | func main() { 28 | vue.New( 29 | vue.El("#app"), 30 | vue.Template(tmpl), 31 | vue.Data(Data{Message: "Hello WebAssembly!"}), 32 | vue.Computeds(ReversedMessage), 33 | ) 34 | 35 | select {} 36 | } 37 | -------------------------------------------------------------------------------- /examples/10-watchers/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 - Watchers 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/10-watchers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/norunners/vue" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | const tmpl = ` 11 |
    12 |

    13 | Ask a yes or no question: 14 | 15 |

    16 |

    {{ Answer }}

    17 |
    18 | ` 19 | 20 | type Data struct { 21 | Question string 22 | Answer string 23 | } 24 | 25 | type yesno struct { 26 | Answer string `json:"answer"` 27 | } 28 | 29 | func Answer(vctx vue.Context, newQuestion, _ string) { 30 | if !strings.HasSuffix(newQuestion, "?") { 31 | vctx.Set("Answer", "Questions usually contain a question mark.") 32 | return 33 | } 34 | 35 | vctx.Go("AsyncAnswer") 36 | } 37 | 38 | func AsyncAnswer(vctx vue.Context) { 39 | data := vctx.Data().(*Data) 40 | res, err := http.Get("https://yesno.wtf/api") 41 | if err != nil { 42 | data.Answer = err.Error() 43 | return 44 | } 45 | defer res.Body.Close() 46 | 47 | dec := json.NewDecoder(res.Body) 48 | yesno := &yesno{} 49 | err = dec.Decode(yesno) 50 | if err != nil { 51 | data.Answer = err.Error() 52 | return 53 | } 54 | data.Answer = yesno.Answer 55 | } 56 | 57 | func main() { 58 | vue.New( 59 | vue.El("#app"), 60 | vue.Template(tmpl), 61 | vue.Data(&Data{Answer: "I cannot give you an answer until you ask a question!"}), 62 | vue.Watch("Question", Answer), 63 | vue.Methods(AsyncAnswer), 64 | ) 65 | 66 | select {} 67 | } 68 | -------------------------------------------------------------------------------- /examples/11-class-binding/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 - Class Binding 6 | 7 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/11-class-binding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/norunners/vue" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | const tmpl = ` 10 |

    11 | Hello WebAssembly! 12 |

    13 | ` 14 | 15 | type Data struct { 16 | Class Class 17 | } 18 | 19 | type Class struct { 20 | Active bool `css:"active"` 21 | TextDanger bool `css:"text-danger"` 22 | } 23 | 24 | func Change(vctx vue.Context) { 25 | data := vctx.Data().(*Data) 26 | data.Class.Active = rand.Intn(2) == 1 27 | data.Class.TextDanger = rand.Intn(2) == 1 28 | } 29 | 30 | func main() { 31 | vm := vue.New( 32 | vue.El("#app"), 33 | vue.Template(tmpl), 34 | vue.Data(&Data{}), 35 | vue.Methods(Change), 36 | ) 37 | 38 | for tick := time.Tick(time.Second); ; { 39 | select { 40 | case <-tick: 41 | vm.Go("Change") 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/12-style-binding/index.wasmgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 - Style Binding 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/12-style-binding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/norunners/vue" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | const tmpl = ` 11 |

    12 | Hello WebAssembly! 13 |

    14 | ` 15 | 16 | type Data struct { 17 | r, g, b int 18 | px int 19 | } 20 | 21 | type Styles struct { 22 | Color string `css:"color"` 23 | FontSize string `css:"font-size"` 24 | } 25 | 26 | func Style(vctx vue.Context) *Styles { 27 | data := vctx.Data().(*Data) 28 | hex := fmt.Sprintf("#%02x%02x%02x", data.r, data.g, data.b) 29 | size := fmt.Sprintf("%dpx", data.px) 30 | return &Styles{ 31 | Color: hex, 32 | FontSize: size, 33 | } 34 | } 35 | 36 | func Change(vctx vue.Context) { 37 | data := vctx.Data().(*Data) 38 | data.r = int(rand.Float32() * 0xff) 39 | data.g = int(rand.Float32() * 0xff) 40 | data.b = int(rand.Float32() * 0xff) 41 | data.px = 8 + (data.px-7)%64 42 | } 43 | 44 | func main() { 45 | vm := vue.New( 46 | vue.El("#app"), 47 | vue.Template(tmpl), 48 | vue.Data(&Data{px: 8}), 49 | vue.Computeds(Style), 50 | vue.Methods(Change), 51 | ) 52 | 53 | for tick := time.Tick(200 * time.Millisecond); ; { 54 | select { 55 | case <-tick: 56 | vm.Go("Change") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/norunners/vue 2 | 3 | require ( 4 | github.com/cbroglie/mustache v1.0.1 5 | github.com/gowasm/go-js-dom v0.0.3 6 | golang.org/x/net v0.0.0-20190311031020-56fb01167e7d 7 | ) 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw= 2 | github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g= 3 | github.com/gowasm/go-js-dom v0.0.3 h1:5TDTkogeJ137AMChH7/cxYIBM1hTitz2rd44j28+Cr0= 4 | github.com/gowasm/go-js-dom v0.0.3/go.mod h1:K37PTzggLHdwZwVKIlgreQbR7b1pwrudrZEFYcPifKE= 5 | golang.org/x/net v0.0.0-20190311031020-56fb01167e7d h1:vQJbQvu6+H699vOmHa20TEBI9nEqroRbMtf/9biIE3A= 6 | golang.org/x/net v0.0.0-20190311031020-56fb01167e7d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 7 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "strings" 7 | ) 8 | 9 | // Option uses the option pattern for components. 10 | type Option func(*Comp) 11 | 12 | // El is the element option for components. 13 | // The root element of a component is query selected from the value, e.g. #app or body. 14 | func El(el string) Option { 15 | return func(comp *Comp) { 16 | comp.el = el 17 | } 18 | } 19 | 20 | // Template is the template option for components. 21 | // The template uses the mustache syntax for rendering. 22 | // The template must have a single root element. 23 | func Template(tmpl string) Option { 24 | return func(comp *Comp) { 25 | comp.tmpl = tmpl 26 | } 27 | } 28 | 29 | // Data is the data option for components. 30 | // This option accepts either a function or a struct. 31 | // The data function is expected to return a new data value. 32 | // For example: func() *Type { return &Type{...} } 33 | // Without a function the data is shared across components. 34 | // The scope of the data is within the component. 35 | // Data must be a pointer to be mutable by methods. 36 | func Data(data interface{}) Option { 37 | return func(comp *Comp) { 38 | comp.data = data 39 | } 40 | } 41 | 42 | // Method is the method option for components. 43 | // The given name and function is registered as a method for the component. 44 | // The function is required to accept context and allows optional arguments. 45 | // For example: func(vctx vue.Context) or func(vctx vue.Context, a1 Arg1, ..., ak ArgK) 46 | func Method(name string, function interface{}) Option { 47 | return func(comp *Comp) { 48 | comp.methods[name] = reflect.ValueOf(function) 49 | } 50 | } 51 | 52 | // Methods is the methods option for components. 53 | // The given functions are registered as methods for the component. 54 | // The functions are required to accept context and allows optional arguments. 55 | // For example: func(vctx vue.Context) or func(vctx vue.Context, a1 Arg1, ..., ak ArgK) 56 | func Methods(functions ...interface{}) Option { 57 | return func(comp *Comp) { 58 | for _, function := range functions { 59 | fn := reflect.ValueOf(function) 60 | name := funcName(fn) 61 | comp.methods[name] = fn 62 | } 63 | } 64 | } 65 | 66 | // Computed is the computed option for components. 67 | // The given name and function is registered as a computed property for the component. 68 | // The function is required to accept context and return a value. 69 | // For example: func(vctx vue.Context) Type 70 | func Computed(name string, function interface{}) Option { 71 | return func(comp *Comp) { 72 | fn := reflect.ValueOf(function) 73 | comp.computed[name] = fn 74 | } 75 | } 76 | 77 | // Computeds is the computeds option for components. 78 | // The given functions are registered as computed properties for the component. 79 | // The functions are required to accept context and return a value. 80 | // For example: func(vctx vue.Context) Type 81 | func Computeds(functions ...interface{}) Option { 82 | return func(comp *Comp) { 83 | for _, function := range functions { 84 | fn := reflect.ValueOf(function) 85 | name := funcName(fn) 86 | comp.computed[name] = fn 87 | } 88 | } 89 | } 90 | 91 | // Watch is the watch option for components. 92 | // The given function is registered as a watcher for the data field. 93 | // All data fields are watchable, e.g. data, props and computed. 94 | // The function is required to accept context and both the new and old values. 95 | // For example: func(vctx vue.Context, newVal, oldVal Type) 96 | func Watch(field string, function interface{}) Option { 97 | return func(comp *Comp) { 98 | fn := reflect.ValueOf(function) 99 | comp.watchers[field] = fn 100 | } 101 | } 102 | 103 | // Sub is the subcomponent option for components. 104 | func Sub(element string, sub *Comp) Option { 105 | return func(comp *Comp) { 106 | sub.isSub = true 107 | comp.subs[element] = sub 108 | } 109 | } 110 | 111 | // Props is the props option for subcomponents. 112 | func Props(props ...string) Option { 113 | return func(sub *Comp) { 114 | for _, prop := range props { 115 | sub.props[prop] = struct{}{} 116 | } 117 | } 118 | } 119 | 120 | // funcName returns the name of the given function. 121 | func funcName(function reflect.Value) string { 122 | name := runtime.FuncForPC(function.Pointer()).Name() 123 | return stripMetadata(name) 124 | } 125 | 126 | // stripMetadata returns the function name without metadata. 127 | func stripMetadata(name string) string { 128 | parts := strings.Split(name, ".") 129 | name = parts[len(parts)-1] 130 | return strings.TrimSuffix(name, "-fm") 131 | } 132 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // render executes and renders the prepared state. 9 | func (vm *ViewModel) render() { 10 | vm.mapState() 11 | node := vm.execute(vm.state) 12 | vm.subs.reset() 13 | if vm.comp.isSub { 14 | var ok bool 15 | if node, ok = firstElement(node); !ok { 16 | must(fmt.Errorf("failed to find first element from node: %s", node.Data)) 17 | } 18 | } 19 | vm.vnode.render(node, vm.subs) 20 | vm.subs.reset() 21 | } 22 | 23 | // mapData creates a map of state from data, props and computed. 24 | func (vm *ViewModel) mapState() { 25 | elem := reflect.Indirect(vm.data) 26 | typ := elem.Type() 27 | n := elem.NumField() 28 | vm.state = make(map[string]interface{}, n) 29 | for i := 0; i < n; i++ { 30 | field := elem.Field(i) 31 | if field.CanInterface() { 32 | name := typ.Field(i).Name 33 | value := field.Interface() 34 | vm.mapField(name, value) 35 | } 36 | } 37 | vm.mapProps() 38 | vm.mapComputed() 39 | } 40 | 41 | // mapProps maps props to state. 42 | func (vm *ViewModel) mapProps() { 43 | for field, prop := range vm.props { 44 | vm.mapField(field, prop) 45 | } 46 | } 47 | 48 | // mapComputed maps computed to state. 49 | func (vm *ViewModel) mapComputed() { 50 | for computed, function := range vm.comp.computed { 51 | if _, ok := vm.state[computed]; !ok { 52 | value := vm.compute(function) 53 | vm.mapField(computed, value) 54 | } 55 | } 56 | } 57 | 58 | // mapField maps a field to state. 59 | // Watchers are called on field changes. 60 | func (vm *ViewModel) mapField(field string, value interface{}) { 61 | oldField, ok := vm.state[field] 62 | vm.state[field] = value 63 | if !ok { 64 | return 65 | } 66 | 67 | if watcher, ok := vm.comp.watchers[field]; ok { 68 | newVal := reflect.ValueOf(value) 69 | oldVal := reflect.ValueOf(oldField) 70 | if reflect.DeepEqual(newVal, oldVal) { 71 | return 72 | } 73 | values := append([]reflect.Value{reflect.ValueOf(vm)}, newVal, oldVal) 74 | watcher.Call(values) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /subcomponent.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | // subs maps elements to subcomponents 4 | type subs map[string]*sub 5 | 6 | // sub contains all the subcomponent instances for a component. 7 | type sub struct { 8 | comp *Comp 9 | index int 10 | instances map[int]*instance 11 | } 12 | 13 | // instance contains a view model with props. 14 | type instance struct { 15 | props map[string]interface{} 16 | vm *ViewModel 17 | } 18 | 19 | // newSubs creates a new map of subcomponents. 20 | func newSubs(comps map[string]*Comp) subs { 21 | subs := make(subs, len(comps)) 22 | for element, comp := range comps { 23 | subs[element] = newSub(comp) 24 | } 25 | return subs 26 | } 27 | 28 | // newSub creates a new subcomponent. 29 | func newSub(comp *Comp) *sub { 30 | instances := make(map[int]*instance, 0) 31 | return &sub{comp: comp, instances: instances} 32 | } 33 | 34 | // putProp puts the props in the subcomponent. 35 | // Returns false if the element is not a subcomponent 36 | // or the subcomponent is not expecting the prop. 37 | func (subs subs) putProp(element, field string, data interface{}) bool { 38 | sub, ok := subs[element] 39 | if !ok { 40 | return false 41 | } 42 | return sub.putProp(field, data) 43 | } 44 | 45 | // putProp puts the props in the instance. 46 | // Returns false if the subcomponent is not expecting the prop. 47 | func (sub *sub) putProp(field string, data interface{}) bool { 48 | if _, ok := sub.comp.props[field]; !ok { 49 | return false 50 | } 51 | 52 | if inst, ok := sub.instances[sub.index]; ok { 53 | if inst.props == nil { 54 | inst.props = map[string]interface{}{field: data} 55 | } else { 56 | inst.props[field] = data 57 | } 58 | } else { 59 | sub.instances[sub.index] = &instance{props: map[string]interface{}{field: data}} 60 | } 61 | return true 62 | } 63 | 64 | // newInstance creates a new instance of the subcomponent with props. 65 | // // Returns false if the element is not a subcomponent. 66 | func (subs subs) newInstance(element string, bus *bus) bool { 67 | sub, ok := subs[element] 68 | if !ok { 69 | return false 70 | } 71 | return sub.newInstance(bus) 72 | } 73 | 74 | // newInstance creates a new instance of the subcomponent with props. 75 | func (sub *sub) newInstance(bus *bus) bool { 76 | if inst, ok := sub.instances[sub.index]; ok { 77 | if inst.vm == nil { 78 | inst.vm = newViewModel(sub.comp, bus, inst.props) 79 | } else { 80 | inst.vm.props = inst.props 81 | inst.vm.render() 82 | } 83 | } else { 84 | vm := newViewModel(sub.comp, bus, nil) 85 | sub.instances[sub.index] = &instance{vm: vm} 86 | } 87 | sub.index++ 88 | return true 89 | } 90 | 91 | // vnode retrieves a virtual node of the subcomponent. 92 | // // Returns false if the element is not a subcomponent. 93 | func (subs subs) vnode(element string) (*vnode, bool) { 94 | sub, ok := subs[element] 95 | if !ok { 96 | return nil, false 97 | } 98 | vnode, ok := sub.vnode() 99 | return vnode, ok 100 | } 101 | 102 | // vnode retrieves a virtual node of the subcomponent. 103 | func (sub *sub) vnode() (*vnode, bool) { 104 | inst, ok := sub.instances[sub.index] 105 | if !ok { 106 | return nil, false 107 | } 108 | sub.index++ 109 | return inst.vm.vnode, true 110 | } 111 | 112 | // reset resets all subcomponents. 113 | func (subs subs) reset() { 114 | for _, sub := range subs { 115 | sub.reset() 116 | } 117 | } 118 | 119 | // reset cleans up and unmounts unused subcomponent instances. 120 | func (sub *sub) reset() { 121 | for i := sub.index; i < len(sub.instances); { 122 | sub.instances[i].vm.release() 123 | delete(sub.instances, i) 124 | } 125 | sub.index = 0 126 | } 127 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/cbroglie/mustache" 7 | "golang.org/x/net/html" 8 | "golang.org/x/net/html/atom" 9 | "io" 10 | "reflect" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | v = "v-" 16 | vBind = "v-bind" 17 | vFor = "v-for" 18 | vHtml = "v-html" 19 | vIf = "v-if" 20 | vModel = "v-model" 21 | vOn = "v-on" 22 | ) 23 | 24 | var attrOrder = []string{vFor, vIf, vModel, vOn, vBind, vHtml} 25 | 26 | // execute executes the template with the given data to be rendered. 27 | func (vm *ViewModel) execute(data map[string]interface{}) *html.Node { 28 | node := parseNode(vm.comp.tmpl) 29 | 30 | vm.executeElement(node, data) 31 | executeText(node, data) 32 | 33 | return node 34 | } 35 | 36 | // executeElement recursively traverses the html node and templates the elements. 37 | // The next node is always returned which allows execution to jump around as needed. 38 | func (vm *ViewModel) executeElement(node *html.Node, data map[string]interface{}) *html.Node { 39 | // Leave the text nodes to be executed. 40 | if node.Type != html.ElementNode { 41 | return node.NextSibling 42 | } 43 | 44 | // Order attributes before execution. 45 | orderAttrs(node) 46 | 47 | // Execute attributes. 48 | for i := 0; i < len(node.Attr); i++ { 49 | attr := node.Attr[i] 50 | if strings.HasPrefix(attr.Key, v) { 51 | deleteAttr(node, i) 52 | i-- 53 | next, modified := vm.executeAttr(node, attr, data) 54 | // The current node is not longer valid in favor of the next node. 55 | if modified { 56 | return next 57 | } 58 | } 59 | } 60 | 61 | // Execute subcomponent. 62 | if vm.subs.newInstance(node.Data, vm.bus) { 63 | return node.NextSibling 64 | } 65 | 66 | // Execute children. 67 | for child := node.FirstChild; child != nil; { 68 | child = vm.executeElement(child, data) 69 | } 70 | 71 | return node.NextSibling 72 | } 73 | 74 | // executeText recursively executes the text node. 75 | func executeText(node *html.Node, data map[string]interface{}) { 76 | switch node.Type { 77 | case html.TextNode: 78 | if strings.TrimSpace(node.Data) == "" { 79 | return 80 | } 81 | 82 | var err error 83 | node.Data, err = mustache.Render(node.Data, data) 84 | must(err) 85 | case html.ElementNode: 86 | for child := node.FirstChild; child != nil; child = child.NextSibling { 87 | executeText(child, data) 88 | } 89 | } 90 | } 91 | 92 | // executeAttr executes the given vue attribute. 93 | // The next node will be executed next if the html was modified unless it is nil. 94 | func (vm *ViewModel) executeAttr(node *html.Node, attr html.Attribute, data map[string]interface{}) (*html.Node, bool) { 95 | vals := strings.Split(attr.Key, ":") 96 | typ, part := vals[0], "" 97 | if len(vals) > 1 { 98 | part = vals[1] 99 | } 100 | var next *html.Node 101 | var modified bool 102 | switch typ { 103 | case vBind: 104 | vm.executeAttrBind(node, part, attr.Val, data) 105 | case vFor: 106 | next, modified = vm.executeAttrFor(node, attr.Val, data) 107 | case vHtml: 108 | executeAttrHtml(node, attr.Val, data) 109 | case vIf: 110 | next, modified = vm.executeAttrIf(node, attr.Val, data) 111 | case vModel: 112 | vm.executeAttrModel(node, attr.Val, data) 113 | case vOn: 114 | vm.executeAttrOn(node, part, attr.Val) 115 | default: 116 | must(fmt.Errorf("unknown vue attribute: %v", typ)) 117 | } 118 | return next, modified 119 | } 120 | 121 | // executeAttrBind executes the vue bind attribute. 122 | func (vm *ViewModel) executeAttrBind(node *html.Node, key, field string, data map[string]interface{}) { 123 | value, ok := data[field] 124 | if !ok { 125 | must(fmt.Errorf("unknown data field: %s", field)) 126 | } 127 | 128 | prop := strings.Title(key) 129 | if ok := vm.subs.putProp(node.Data, prop, value); ok { 130 | return 131 | } 132 | 133 | if key == "class" { 134 | class := formatAttrClass(value) 135 | node.Attr = append(node.Attr, html.Attribute{Key: key, Val: class}) 136 | return 137 | } 138 | 139 | if key == "style" { 140 | style := formatAttrStyle(value) 141 | node.Attr = append(node.Attr, html.Attribute{Key: key, Val: style}) 142 | return 143 | } 144 | 145 | // Remove attribute if bound to a false value of type bool. 146 | if val, ok := value.(bool); ok && !val { 147 | return 148 | } 149 | 150 | val := fmt.Sprintf("%v", value) 151 | node.Attr = append(node.Attr, html.Attribute{Key: key, Val: val}) 152 | } 153 | 154 | // executeAttrFor executes the vue for attribute. 155 | func (vm *ViewModel) executeAttrFor(node *html.Node, value string, data map[string]interface{}) (*html.Node, bool) { 156 | vals := strings.Split(value, "in") 157 | name := bytes.TrimSpace([]byte(vals[0])) 158 | field := strings.TrimSpace(vals[1]) 159 | 160 | slice, ok := data[field] 161 | if !ok { 162 | must(fmt.Errorf("slice not found for field: %s", field)) 163 | } 164 | 165 | elem := bytes.NewBuffer(nil) 166 | err := html.Render(elem, node) 167 | must(err) 168 | 169 | buf := bytes.NewBuffer(nil) 170 | values := reflect.ValueOf(slice) 171 | n := values.Len() 172 | for i := 0; i < n; i++ { 173 | key := fmt.Sprintf("%s%d", name, vm.index) 174 | vm.index++ 175 | 176 | b := bytes.Replace(elem.Bytes(), name, []byte(key), -1) 177 | _, err := buf.Write(b) 178 | must(err) 179 | 180 | data[key] = values.Index(i).Interface() 181 | } 182 | 183 | nodes := parseNodes(buf) 184 | for _, child := range nodes { 185 | node.Parent.InsertBefore(child, node) 186 | } 187 | node.Parent.RemoveChild(node) 188 | // The first child is the next node to execute. 189 | return nodes[0], true 190 | } 191 | 192 | // executeAttrHtml executes the vue html attribute. 193 | func executeAttrHtml(node *html.Node, field string, data map[string]interface{}) { 194 | value, ok := data[field] 195 | if !ok { 196 | must(fmt.Errorf("unknown data field: %s", field)) 197 | } 198 | html, ok := value.(string) 199 | if !ok { 200 | must(fmt.Errorf("data field is not of type string: %T", field)) 201 | } 202 | 203 | nodes := parseNodes(strings.NewReader(html)) 204 | for _, child := range nodes { 205 | node.AppendChild(child) 206 | } 207 | } 208 | 209 | // executeAttrIf executes the vue if attribute. 210 | func (vm *ViewModel) executeAttrIf(node *html.Node, field string, data map[string]interface{}) (*html.Node, bool) { 211 | if value, ok := data[field]; ok { 212 | if val, ok := value.(bool); ok && val { 213 | return nil, false 214 | } 215 | } 216 | next := node.NextSibling 217 | node.Parent.RemoveChild(node) 218 | return next, true 219 | } 220 | 221 | // executeAttrModel executes the vue model attribute. 222 | func (vm *ViewModel) executeAttrModel(node *html.Node, field string, data map[string]interface{}) { 223 | typ := "input" 224 | node.Attr = append(node.Attr, html.Attribute{Key: typ, Val: field}) 225 | 226 | value, ok := data[field] 227 | if !ok { 228 | must(fmt.Errorf("unknown data field: %s", field)) 229 | } 230 | val, ok := value.(string) 231 | if !ok { 232 | must(fmt.Errorf("data field is not of type string: %T", field)) 233 | } 234 | node.Attr = append(node.Attr, html.Attribute{Key: "value", Val: val}) 235 | 236 | vm.addEventListener(typ, vm.vModel) 237 | } 238 | 239 | // executeAttrOn executes the vue on attribute. 240 | func (vm *ViewModel) executeAttrOn(node *html.Node, typ, method string) { 241 | event := strings.Split(typ, ".")[0] 242 | node.Attr = append(node.Attr, html.Attribute{Key: typ, Val: method}) 243 | 244 | vm.addEventListener(event, vm.vOn) 245 | vm.bus.sub(event, method) 246 | } 247 | 248 | // parseNode parses the template into an html node. 249 | // The node returned is a placeholder, not to be rendered. 250 | func parseNode(tmpl string) *html.Node { 251 | nodes := parseNodes(strings.NewReader(tmpl)) 252 | node := &html.Node{Type: html.ElementNode} 253 | for _, child := range nodes { 254 | node.AppendChild(child) 255 | } 256 | return node 257 | } 258 | 259 | // parseNodes parses the reader into html nodes. 260 | func parseNodes(reader io.Reader) []*html.Node { 261 | nodes, err := html.ParseFragment(reader, &html.Node{ 262 | Type: html.ElementNode, 263 | Data: "div", 264 | DataAtom: atom.Div, 265 | }) 266 | must(err) 267 | return nodes 268 | } 269 | 270 | // firstElement finds the first child element of a node. 271 | // Returns false if a child element is not found. 272 | func firstElement(node *html.Node) (*html.Node, bool) { 273 | for child := node.FirstChild; child != nil; child = child.NextSibling { 274 | if child.Type == html.ElementNode { 275 | return child, true 276 | } 277 | } 278 | return nil, false 279 | } 280 | 281 | // orderAttrs orders the attributes of the node which orders the template execution. 282 | func orderAttrs(node *html.Node) { 283 | n := len(node.Attr) 284 | if n == 0 { 285 | return 286 | } 287 | attrs := make([]html.Attribute, 0, n) 288 | for _, prefix := range attrOrder { 289 | for _, attr := range node.Attr { 290 | if strings.HasPrefix(attr.Key, prefix) { 291 | attrs = append(attrs, attr) 292 | } 293 | } 294 | } 295 | // Append other attributes which are not vue attributes. 296 | for _, attr := range node.Attr { 297 | if !strings.HasPrefix(attr.Key, v) { 298 | attrs = append(attrs, attr) 299 | } 300 | } 301 | node.Attr = attrs 302 | } 303 | 304 | // deleteAttr deletes the attribute of the node at the index. 305 | // Attribute order is preserved. 306 | func deleteAttr(node *html.Node, i int) { 307 | node.Attr = append(node.Attr[:i], node.Attr[i+1:]...) 308 | } 309 | 310 | // formatAttrClass formats the value into a class attribute. 311 | // For example: { Active: true, DangerText: true } -> "active danger-text" 312 | // For type: struct { Active: bool `css:"active"`, DangerText: bool `css:"danger-text"` } 313 | func formatAttrClass(value interface{}) string { 314 | elem := reflect.Indirect(reflect.ValueOf(value)) 315 | typ := elem.Type() 316 | n := elem.NumField() 317 | buf := bytes.NewBuffer(nil) 318 | format := "%s" 319 | for i := 0; i < n; i++ { 320 | if field := elem.Field(i); field.CanInterface() { 321 | value := field.Interface() 322 | if val, ok := value.(bool); ok && val { 323 | typ := typ.Field(i) 324 | class := typ.Tag.Get("css") 325 | if class == "" { 326 | class = strings.ToLower(typ.Name) 327 | } 328 | fmt.Fprintf(buf, format, class) 329 | format = " %s" 330 | } 331 | } 332 | } 333 | return buf.String() 334 | } 335 | 336 | // formatAttrStyle formats the value into a style attribute. 337 | // For example: { Color: red, FontSize: 8px } -> "color: red; font-size: 8px" 338 | // For type: struct { Color: string `css:"color"`, FontSize: string `css:"font-size"` } 339 | func formatAttrStyle(value interface{}) string { 340 | elem := reflect.Indirect(reflect.ValueOf(value)) 341 | typ := elem.Type() 342 | n := elem.NumField() 343 | buf := bytes.NewBuffer(nil) 344 | format := "%s: %v" 345 | for i := 0; i < n; i++ { 346 | if field := elem.Field(i); field.CanInterface() { 347 | typ := typ.Field(i) 348 | style := typ.Tag.Get("css") 349 | if style == "" { 350 | style = strings.ToLower(typ.Name) 351 | } 352 | value := field.Interface() 353 | fmt.Fprintf(buf, format, style, value) 354 | format = "; %s: %v" 355 | } 356 | } 357 | return buf.String() 358 | } 359 | -------------------------------------------------------------------------------- /vnode.go: -------------------------------------------------------------------------------- 1 | package vue 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gowasm/go-js-dom" 6 | "golang.org/x/net/html" 7 | "syscall/js" 8 | ) 9 | 10 | var document dom.Document 11 | 12 | type vnode struct { 13 | parent, firstChild, lastChild, prevSibling, nextSibling *vnode 14 | 15 | attrs map[string]string 16 | typ html.NodeType 17 | data string 18 | 19 | node dom.Node 20 | } 21 | 22 | func init() { 23 | doc := js.Global().Get("document") 24 | if doc == js.Undefined() || doc == js.Null() { 25 | panic("failed to initialize document") 26 | } 27 | document = dom.WrapDocument(doc) 28 | } 29 | 30 | // newNode creates a virtual node by query selecting the given element. 31 | func newNode(el string) *vnode { 32 | node := document.QuerySelector(el) 33 | return &vnode{attrs: node.Attributes(), node: node} 34 | } 35 | 36 | // newSubNode creates a virtual subcomponent node from the given template. 37 | func newSubNode(tmpl string) *vnode { 38 | node := parseNode(tmpl) 39 | var ok bool 40 | if node, ok = firstElement(node); !ok { 41 | must(fmt.Errorf("failed to find first element from template: %s", tmpl)) 42 | } 43 | return createElement(node) 44 | } 45 | 46 | // createElement creates a virtual node element without children nor attributes. 47 | func createElement(node *html.Node) *vnode { 48 | el := document.CreateElement(node.Data) 49 | attrs := make(map[string]string, len(node.Attr)) 50 | return &vnode{ 51 | typ: node.Type, 52 | data: node.Data, 53 | attrs: attrs, 54 | node: el, 55 | } 56 | } 57 | 58 | // createNode recursively creates a virtual node from the html node. 59 | func createNode(node *html.Node, subs subs) *vnode { 60 | vnode := &vnode{typ: node.Type, data: node.Data} 61 | switch node.Type { 62 | case html.ElementNode: 63 | if subNode, ok := subs.vnode(node.Data); ok { 64 | subNode.renderAttributes(node.Attr) 65 | return subNode 66 | } else { 67 | vnode.node = document.CreateElement(node.Data) 68 | vnode.attrs = make(map[string]string, len(node.Attr)) 69 | for _, attr := range node.Attr { 70 | vnode.setAttr(attr.Key, attr.Val) 71 | } 72 | for child := node.FirstChild; child != nil; child = child.NextSibling { 73 | vnode.append(createNode(child, subs)) 74 | } 75 | } 76 | case html.TextNode: 77 | vnode.node = document.CreateTextNode(node.Data) 78 | default: 79 | must(fmt.Errorf("unknown node type: %v", node.Type)) 80 | } 81 | return vnode 82 | } 83 | 84 | // render recursively renders the virtual node. 85 | func (dst *vnode) render(src *html.Node, subs subs) { 86 | for dstChild, srcChild := dst.firstChild, src.FirstChild; dstChild != nil || srcChild != nil; { 87 | switch { 88 | case dstChild == nil: 89 | dst.append(createNode(srcChild, subs)) 90 | case srcChild == nil: 91 | dst.remove(dstChild) 92 | case dstChild.typ != srcChild.Type: 93 | dst.replace(createNode(srcChild, subs), dstChild) 94 | default: 95 | switch srcChild.Type { 96 | case html.ElementNode: 97 | if subNode, ok := subs.vnode(srcChild.Data); ok { 98 | subNode.renderAttributes(srcChild.Attr) 99 | dst.replace(subNode, dstChild) 100 | } else if dstChild.data != srcChild.Data { 101 | dst.replace(createNode(srcChild, subs), dstChild) 102 | } else { 103 | dstChild.renderAttributes(srcChild.Attr) 104 | dstChild.render(srcChild, subs) 105 | } 106 | case html.TextNode: 107 | if dstChild.data != srcChild.Data { 108 | dstChild.setText(srcChild.Data) 109 | } 110 | default: 111 | must(fmt.Errorf("unknown html node type: %v", srcChild.Type)) 112 | } 113 | } 114 | if dstChild != nil { 115 | dstChild = dstChild.nextSibling 116 | } 117 | if srcChild != nil { 118 | srcChild = srcChild.NextSibling 119 | } 120 | } 121 | } 122 | 123 | // renderAttributes renders the attributes. 124 | func (vnode *vnode) renderAttributes(attrs []html.Attribute) { 125 | keys := make(map[string]struct{}, len(vnode.attrs)+len(attrs)) 126 | srcAttrs := make(map[string]string, len(attrs)) 127 | for _, attr := range attrs { 128 | keys[attr.Key] = struct{}{} 129 | srcAttrs[attr.Key] = attr.Val 130 | } 131 | for key := range vnode.attrs { 132 | keys[key] = struct{}{} 133 | } 134 | 135 | for key := range keys { 136 | if srcVal, ok := srcAttrs[key]; ok { 137 | if dstVal, ok := vnode.attrs[key]; !ok || dstVal != srcVal { 138 | vnode.setAttr(key, srcVal) 139 | } 140 | } else { 141 | vnode.remAttr(key) 142 | } 143 | } 144 | } 145 | 146 | // setAttr sets an attribute of the element. 147 | func (vnode *vnode) setAttr(key, val string) { 148 | vnode.attrs[key] = val 149 | if vnode.node != nil { 150 | if key == "value" { 151 | vnode.node.Underlying().Set(key, val) 152 | } 153 | vnode.node.(dom.Element).SetAttribute(key, val) 154 | } 155 | } 156 | 157 | // remAttr removes an attribute from the element. 158 | func (vnode *vnode) remAttr(key string) { 159 | delete(vnode.attrs, key) 160 | if vnode.node != nil { 161 | vnode.node.(dom.Element).RemoveAttribute(key) 162 | } 163 | } 164 | 165 | // setText sets the content of the text. 166 | func (vnode *vnode) setText(content string) { 167 | vnode.data = content 168 | if vnode.node != nil { 169 | vnode.node.SetTextContent(content) 170 | } 171 | } 172 | 173 | // append appends the child to the node. 174 | func (vnode *vnode) append(child *vnode) { 175 | prev := vnode.lastChild 176 | if prev == nil { 177 | vnode.firstChild = child 178 | } else { 179 | prev.nextSibling = child 180 | } 181 | vnode.lastChild = child 182 | child.parent = vnode 183 | child.prevSibling = prev 184 | 185 | if vnode.node != nil { 186 | vnode.node.AppendChild(child.node) 187 | } 188 | } 189 | 190 | // replace replaces a child with a new child. 191 | func (vnode *vnode) replace(newChild, oldChild *vnode) { 192 | prev, next := oldChild.prevSibling, oldChild.nextSibling 193 | if prev == nil { 194 | vnode.firstChild = newChild 195 | } else { 196 | prev.nextSibling = newChild 197 | } 198 | if next == nil { 199 | vnode.lastChild = newChild 200 | } else { 201 | next.prevSibling = newChild 202 | } 203 | newChild.parent = vnode 204 | newChild.prevSibling = prev 205 | newChild.nextSibling = next 206 | 207 | if vnode.node != nil { 208 | vnode.node.ReplaceChild(newChild.node, oldChild.node) 209 | } 210 | } 211 | 212 | // remove removes a child from the node. 213 | func (vnode *vnode) remove(child *vnode) { 214 | if vnode.firstChild == child { 215 | vnode.firstChild = child.nextSibling 216 | } 217 | if child.nextSibling != nil { 218 | child.nextSibling.prevSibling = child.prevSibling 219 | } 220 | if vnode.lastChild == child { 221 | vnode.lastChild = child.prevSibling 222 | } 223 | if child.prevSibling != nil { 224 | child.prevSibling.nextSibling = child.nextSibling 225 | } 226 | 227 | if vnode.node != nil { 228 | vnode.node.RemoveChild(child.node) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /vue.go: -------------------------------------------------------------------------------- 1 | // Package vue is the progressive framework for wasm applications. 2 | package vue 3 | 4 | import ( 5 | "reflect" 6 | "syscall/js" 7 | ) 8 | 9 | // ViewModel is a vue view model, e.g. VM. 10 | type ViewModel struct { 11 | comp *Comp 12 | vnode *vnode 13 | data reflect.Value 14 | state map[string]interface{} 15 | funcs map[string]js.Func 16 | props map[string]interface{} 17 | subs subs 18 | bus *bus 19 | 20 | index int 21 | } 22 | 23 | // New creates a new view model from the given options. 24 | func New(options ...Option) *ViewModel { 25 | comp := Component(options...) 26 | return newViewModel(comp, nil, nil) 27 | } 28 | 29 | // newViewModel creates a new view model from the given component with props. 30 | func newViewModel(comp *Comp, bus *bus, props map[string]interface{}) *ViewModel { 31 | var vnode *vnode 32 | if comp.isSub { 33 | vnode = newSubNode(comp.tmpl) 34 | } else { 35 | vnode = newNode(comp.el) 36 | } 37 | data := comp.newData() 38 | funcs := make(map[string]js.Func, 0) 39 | subs := newSubs(comp.subs) 40 | 41 | vm := &ViewModel{ 42 | comp: comp, 43 | vnode: vnode, 44 | data: data, 45 | funcs: funcs, 46 | props: props, 47 | subs: subs, 48 | } 49 | vm.bus = newBus(bus, vm) 50 | vm.render() 51 | return vm 52 | } 53 | 54 | // must panics on errors. 55 | func must(err error) { 56 | if err != nil { 57 | panic(err) 58 | } 59 | } 60 | --------------------------------------------------------------------------------