github.com/vugu/vugu@v0.3.5/domrender/renderer-js.go (about) 1 package domrender 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "errors" 7 "fmt" 8 "strconv" 9 "strings" 10 "sync" 11 12 "github.com/vugu/vjson" 13 14 "github.com/vugu/vugu" 15 16 js "github.com/vugu/vugu/js" 17 ) 18 19 //go:generate go run renderer-js-script-maker.go 20 21 // NewJSRenderer is an alias for New. 22 // 23 // Deprecated: Use New instead. 24 func NewJSRenderer(mountPointSelector string) (*JSRenderer, error) { 25 return New(mountPointSelector) 26 } 27 28 // New will create a new JSRenderer with the speicifc mount point selector. 29 // If an empty string is passed then the root component should include a top level <html> tag 30 // and the entire page will be rendered. 31 func New(mountPointSelector string) (*JSRenderer, error) { 32 33 ret := &JSRenderer{ 34 MountPointSelector: mountPointSelector, 35 } 36 37 ret.instructionBuffer = make([]byte, 16384) 38 // ret.instructionTypedArray = js.TypedArrayOf(ret.instructionBuffer) 39 40 ret.window = js.Global().Get("window") 41 42 ret.window.Call("eval", jsHelperScript) 43 44 ret.instructionBufferJS = ret.window.Call("vuguGetRenderArray") 45 46 ret.instructionList = newInstructionList(ret.instructionBuffer, func(il *instructionList) error { 47 48 // call vuguRender to have the instructions processed in JS 49 ret.instructionBuffer[il.pos] = 0 // ensure zero terminator 50 51 // copy the data over 52 js.CopyBytesToJS(ret.instructionBufferJS, ret.instructionBuffer) 53 54 // then call vuguRender 55 ret.window.Call("vuguRender" /*, ret.instructionBufferJS*/) 56 57 return nil 58 }) 59 60 // enable debug logging 61 // ret.instructionList.logWriter = os.Stdout 62 63 ret.eventHandlerBuffer = make([]byte, 16384) 64 // ret.eventHandlerTypedArray = js.TypedArrayOf(ret.eventHandlerBuffer) 65 66 ret.eventHandlerFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 67 if len(args) != 1 { 68 panic(fmt.Errorf("eventHandlerFunc got arg slice not exactly 1 element in length: %#v", args)) 69 } 70 bufferLength := args[0].Length() 71 if cap(ret.eventHandlerBuffer) < bufferLength+1 { 72 ret.eventHandlerBuffer = make([]byte, bufferLength+1) 73 } 74 //log.Println(cap(ret.eventHandlerBuffer)) 75 n := js.CopyBytesToGo(ret.eventHandlerBuffer, args[0]) 76 if n >= len(ret.eventHandlerBuffer) { 77 panic(errors.New("event data is too large, cannot continue, len: " + strconv.Itoa(n))) 78 } 79 ret.handleDOMEvent() // discard this and args, all data should be in eventHandlerBuffer; avoid using js.Value 80 return nil 81 // return jsEnv.handleRawDOMEvent(this, args) 82 }) 83 84 // wire up the event handler func and the array that we used to communicate with instead of js.Value 85 // ret.window.Call("vuguSetEventHandlerAndBuffer", ret.eventHandlerFunc, ret.eventHandlerTypedArray) 86 87 // wire up callback handler 88 ret.window.Call("vuguSetCallbackHandler", js.FuncOf(ret.handleCallback)) 89 90 // wire up the event handler func 91 ret.window.Call("vuguSetEventHandler", ret.eventHandlerFunc) 92 93 // log.Printf("ret.window: %#v", ret.window) 94 // log.Printf("eval: %#v", ret.window.Get("eval")) 95 96 ret.eventWaitCh = make(chan bool, 64) 97 98 ret.eventEnv = vugu.NewEventEnvImpl( 99 &ret.eventRWMU, 100 ret.eventWaitCh, 101 ) 102 103 return ret, nil 104 } 105 106 type jsRenderState struct { 107 // stores positionID to slice of DOMEventHandlerSpec 108 domHandlerMap map[string][]vugu.DOMEventHandlerSpec 109 110 // callback stuff is handled by callbackManager 111 callbackManager callbackManager 112 } 113 114 func newJsRenderState() *jsRenderState { 115 return &jsRenderState{ 116 domHandlerMap: make(map[string][]vugu.DOMEventHandlerSpec, 8), 117 } 118 } 119 120 // JSRenderer implements Renderer against the browser's DOM. 121 type JSRenderer struct { 122 MountPointSelector string 123 124 eventWaitCh chan bool // events send to this and EventWait receives from it 125 eventRWMU sync.RWMutex // make sure Render and event handling are not attempted at the same time (not totally sure if this is necessary in terms of the wasm threading model but enforce it with a rwmutex all the same) 126 eventEnv *vugu.EventEnvImpl // our EventEnv implementation that exposes eventRWMU and eventWaitCh to events in a clean way 127 128 eventHandlerFunc js.Func // the callback function for DOM events 129 eventHandlerBuffer []byte 130 // eventHandlerTypedArray js.TypedArray 131 132 instructionBuffer []byte // our local instruction buffer 133 instructionBufferJS js.Value // a Uint8Array on the JS side that we copy into 134 instructionList *instructionList 135 136 window js.Value 137 138 jsRenderState *jsRenderState 139 140 // manages the Rendered lifecycle callback stuff 141 lifecycleStateMap map[interface{}]lifecycleState 142 lifecyclePassNum uint8 143 } 144 145 type lifecycleState struct { 146 passNum uint8 147 } 148 149 // EventEnv returns an EventEnv that can be used for synchronizing updates. 150 func (r *JSRenderer) EventEnv() vugu.EventEnv { 151 return r.eventEnv 152 } 153 154 // Release calls release on any resources that this renderer allocated. 155 func (r *JSRenderer) Release() { 156 // NOTE: seems sensible to leave this here in case we do need something to be released, better than 157 // omitting it and people getting used to no release being needed and then requiring it later. 158 // r.instructionTypedArray.Release() 159 } 160 161 func (r *JSRenderer) render(buildResults *vugu.BuildResults) error { 162 163 bo := buildResults.Out 164 165 if !js.Global().Truthy() { 166 return errors.New("js environment not available") 167 } 168 169 if bo == nil { 170 return errors.New("BuildOut is nil") 171 } 172 173 if len(bo.Out) != 1 { 174 return errors.New("BuildOut.Out has bad len " + strconv.Itoa(len(bo.Out))) 175 } 176 177 if bo.Out[0].Type != vugu.ElementNode { 178 return errors.New("BuildOut.Out[0].Type is not vugu.ElementNode: " + strconv.Itoa(int(bo.Out[0].Type))) 179 } 180 181 // always make sure we have at least a non-nil render state 182 if r.jsRenderState == nil { 183 r.jsRenderState = newJsRenderState() 184 } 185 186 state := r.jsRenderState 187 188 state.callbackManager.startRender() 189 defer state.callbackManager.doneRender() 190 191 // TODO: move this next chunk out to it's own func at least 192 193 visitCSSList := func(cssList []*vugu.VGNode) error { 194 // CSS stuff first 195 for _, cssEl := range cssList { 196 197 // some basic sanity checking 198 if cssEl.Type != vugu.ElementNode || !(cssEl.Data == "style" || cssEl.Data == "link") { 199 return errors.New("CSS output must be link or style tag") 200 } 201 202 var textBuf bytes.Buffer 203 for childN := cssEl.FirstChild; childN != nil; childN = childN.NextSibling { 204 if childN.Type != vugu.TextNode { 205 return fmt.Errorf("CSS tag must contain only text children, found %v instead: %#v", childN.Type, childN) 206 } 207 textBuf.WriteString(childN.Data) 208 } 209 210 var attrPairs []string 211 if len(cssEl.Attr) > 0 { 212 attrPairs = make([]string, 0, len(cssEl.Attr)*2) 213 for _, attr := range cssEl.Attr { 214 attrPairs = append(attrPairs, attr.Key, attr.Val) 215 } 216 } 217 218 err := r.instructionList.writeSetCSSTag(cssEl.Data, textBuf.Bytes(), attrPairs) 219 if err != nil { 220 return err 221 } 222 } 223 224 return nil 225 } 226 227 var walkCSSBuildOut func(buildOut *vugu.BuildOut) error 228 walkCSSBuildOut = func(buildOut *vugu.BuildOut) error { 229 err := visitCSSList(buildOut.CSS) 230 if err != nil { 231 return err 232 } 233 for _, c := range buildOut.Components { 234 // nextBuildOut := buildResults.AllOut[c] 235 nextBuildOut := buildResults.ResultFor(c) 236 if nextBuildOut == nil { 237 panic(fmt.Errorf("walkCSSBuildOut nextBuildOut was nil for %#v", c)) 238 } 239 err := walkCSSBuildOut(nextBuildOut) 240 if err != nil { 241 return err 242 } 243 } 244 return nil 245 } 246 err := walkCSSBuildOut(bo) 247 if err != nil { 248 return err 249 } 250 251 err = r.instructionList.writeRemoveOtherCSSTags() 252 if err != nil { 253 return err 254 } 255 256 // main output 257 err = r.visitFirst(state, bo, buildResults, bo.Out[0], []byte("0")) 258 if err != nil { 259 return err 260 } 261 262 // // JS stuff last 263 // // log.Printf("TODO: handle JS") 264 265 err = r.instructionList.flush() 266 if err != nil { 267 return err 268 } 269 270 // handle Rendered lifecycle callback 271 if r.lifecycleStateMap == nil { 272 r.lifecycleStateMap = make(map[interface{}]lifecycleState, len(bo.Components)) 273 } 274 r.lifecyclePassNum++ 275 276 var rctx renderedCtx 277 278 for _, c := range bo.Components { 279 280 rctx = renderedCtx{eventEnv: r.eventEnv} 281 282 st, ok := r.lifecycleStateMap[c] 283 rctx.first = !ok 284 st.passNum = r.lifecyclePassNum 285 286 invokeRendered(c, &rctx) 287 288 r.lifecycleStateMap[c] = st 289 290 } 291 292 // now purge from lifecycleStateMap anything not touched in this pass 293 for k, st := range r.lifecycleStateMap { 294 if st.passNum != r.lifecyclePassNum { 295 delete(r.lifecycleStateMap, k) 296 } 297 } 298 299 return nil 300 301 } 302 303 // EventWait blocks until an event has occurred which causes a re-render. 304 // It returns true if the render loop should continue or false if it should exit. 305 func (r *JSRenderer) EventWait() (ok bool) { 306 307 // make sure the JS environment is still available, returning false otherwise 308 if !js.Global().Truthy() { 309 return false 310 } 311 312 // FIXME: this should probably have some sort of "debouncing" on it to handle the case of 313 // several events in rapid succession causing multiple renders - maybe we read from eventWaitCH 314 // continuously until it's empty, with a max of like 20ms pause between each or something, and then 315 // only return after we don't see anything for that time frame. 316 317 ok = <-r.eventWaitCh 318 return 319 320 } 321 322 // var window js.Value 323 324 // func init() { 325 // window = js.Global().Get("window") 326 // if window.Truthy() { 327 // js.Global().Call("eval", jsHelperScript) 328 // } 329 // } 330 331 func (r *JSRenderer) visitFirst(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error { 332 333 // log.Printf("TODO: We need to go through and optimize away unneeded calls to create elements, set attributes, set event handlers, etc. for cases where they are the same per hash") 334 335 // log.Printf("JSRenderer.visitFirst") 336 337 if n.Type != vugu.ElementNode { 338 return errors.New("root of component must be element") 339 } 340 341 err := r.instructionList.writeClearEl() 342 if err != nil { 343 return err 344 } 345 346 // first tag is html 347 if strings.ToLower(n.Data) == "html" { 348 349 err := r.syncHtml(state, n, []byte("html")) 350 if err != nil { 351 return err 352 } 353 354 for nchild := n.FirstChild; nchild != nil; nchild = nchild.NextSibling { 355 356 if strings.ToLower(nchild.Data) == "head" { 357 358 err := r.visitHead(state, bo, br, nchild, []byte("head")) 359 if err != nil { 360 return err 361 } 362 363 } else if strings.ToLower(nchild.Data) == "body" { 364 365 err := r.visitBody(state, bo, br, nchild, []byte("body")) 366 if err != nil { 367 return err 368 } 369 370 } else { 371 return fmt.Errorf("unexpected tag inside html %q (VGNode=%#v)", nchild.Data, nchild) 372 } 373 374 } 375 376 return nil 377 } 378 379 // else, first tag is anything else - try again as the element to be mounted 380 return r.visitMount(state, bo, br, n, positionID) 381 382 } 383 384 func (r *JSRenderer) syncHtml(state *jsRenderState, n *vugu.VGNode, positionID []byte) error { 385 err := r.instructionList.writeSelectQuery("html") 386 if err != nil { 387 return err 388 } 389 return r.syncElement(state, n, positionID) 390 } 391 392 func (r *JSRenderer) visitHead(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error { 393 394 err := r.instructionList.writeSelectQuery("head") 395 if err != nil { 396 return err 397 } 398 err = r.syncElement(state, n, positionID) 399 if err != nil { 400 return err 401 } 402 403 return nil 404 } 405 406 func (r *JSRenderer) visitBody(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error { 407 408 err := r.instructionList.writeSelectQuery("body") 409 if err != nil { 410 return err 411 } 412 err = r.syncElement(state, n, positionID) 413 if err != nil { 414 return err 415 } 416 417 if !(n.FirstChild != nil && n.FirstChild.NextSibling == nil) { 418 return errors.New("body tag must contain exactly one element child") 419 } 420 421 return r.visitMount(state, bo, br, n.FirstChild, positionID) 422 } 423 424 func (r *JSRenderer) visitMount(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error { 425 426 // log.Printf("visitMount got here") 427 428 err := r.instructionList.writeSelectMountPoint(r.MountPointSelector, n.Data) 429 if err != nil { 430 return err 431 } 432 433 return r.visitSyncElementEtc(state, bo, br, n, positionID) 434 435 } 436 437 func (r *JSRenderer) visitSyncNode(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error { 438 439 // log.Printf("visitSyncNode") 440 441 var err error 442 443 // check for Component, in which case we descend into it instead of processing like a regular node 444 if n.Component != nil { 445 compBuildOut := br.ResultFor(n.Component) 446 if len(compBuildOut.Out) != 1 { 447 return fmt.Errorf("component %#v expected exactly one Out element but got %d instead", 448 n.Component, len(compBuildOut.Out)) 449 } 450 return r.visitSyncNode(state, compBuildOut, br, compBuildOut.Out[0], positionID) 451 } 452 453 // check for template (used by vg-template and vg-slot) in which case we process the children directly and ignore n 454 if n.IsTemplate() { 455 456 childIndex := 1 457 for nchild := n.FirstChild; nchild != nil; nchild = nchild.NextSibling { 458 459 // use a different character here for the position to ensure it's unique 460 childPositionID := append(positionID, []byte(fmt.Sprintf("_t_%d", childIndex))...) 461 462 err = r.visitSyncNode(state, bo, br, nchild, childPositionID) 463 if err != nil { 464 return err 465 } 466 467 // if there are more children, advance to the next 468 if nchild.NextSibling != nil { 469 err = r.instructionList.writeMoveToNextSibling() 470 if err != nil { 471 return err 472 } 473 474 } 475 476 childIndex++ 477 } 478 479 // element is fully handled 480 return nil 481 } 482 483 switch n.Type { 484 case vugu.ElementNode: 485 // check if this element has a namespace set 486 if ns := namespaceToURI(n.Namespace); ns != "" { 487 err = r.instructionList.writeSetElementNS(n.Data, ns) 488 } else { 489 err = r.instructionList.writeSetElement(n.Data) 490 } 491 if err != nil { 492 return err 493 } 494 case vugu.TextNode: 495 return r.instructionList.writeSetText(n.Data) // no children possible, just return 496 case vugu.CommentNode: 497 return r.instructionList.writeSetComment(n.Data) // no children possible, just return 498 default: 499 return errors.New("unknown node type: " + strconv.Itoa(int(n.Type))) 500 } 501 502 // only elements have attributes, child or events 503 return r.visitSyncElementEtc(state, bo, br, n, positionID) 504 505 } 506 507 // visitSyncElementEtc syncs the rest of the stuff that only applies to elements 508 func (r *JSRenderer) visitSyncElementEtc(state *jsRenderState, bo *vugu.BuildOut, br *vugu.BuildResults, n *vugu.VGNode, positionID []byte) error { 509 510 err := r.syncElement(state, n, positionID) 511 if err != nil { 512 return err 513 } 514 515 if n.InnerHTML != nil { 516 return r.instructionList.writeSetInnerHTML(*n.InnerHTML) 517 } 518 519 // tell callbackManager about the create and populate functions 520 // (if present, otherwise this is a nop and will return 0,0) 521 cid, pid := state.callbackManager.addCreateAndPopulateHandlers(n.JSCreateHandler, n.JSPopulateHandler) 522 523 // for vg-js-create, send an instruction to call us back when this element is created 524 // (handled by callbackManager) 525 if cid != 0 { 526 err := r.instructionList.writeCallbackLastElement(cid) 527 if err != nil { 528 return err 529 } 530 } 531 532 if n.FirstChild != nil { 533 534 err = r.instructionList.writeMoveToFirstChild() 535 if err != nil { 536 return err 537 } 538 539 childIndex := 1 540 for nchild := n.FirstChild; nchild != nil; nchild = nchild.NextSibling { 541 542 childPositionID := append(positionID, []byte(fmt.Sprintf("_%d", childIndex))...) 543 544 err = r.visitSyncNode(state, bo, br, nchild, childPositionID) 545 if err != nil { 546 return err 547 } 548 // log.Printf("GOT HERE X: %#v", n) 549 err = r.instructionList.writeMoveToNextSibling() 550 if err != nil { 551 return err 552 } 553 childIndex++ 554 } 555 556 err = r.instructionList.writeMoveToParent() 557 if err != nil { 558 return err 559 } 560 } 561 562 // for vg-js-populate, send an instruction to call us back again with the populate flag for this same one 563 // (handled by callbackManager) 564 if pid != 0 { 565 err := r.instructionList.writeCallback(pid) 566 if err != nil { 567 return err 568 } 569 } 570 571 return nil 572 } 573 574 func (r *JSRenderer) syncElement(state *jsRenderState, n *vugu.VGNode, positionID []byte) error { 575 if namespaceToURI(n.Namespace) != "" { 576 for _, a := range n.Attr { 577 ns := namespaceToURI(a.Namespace) 578 // FIXME: we skip Namespace="" && Key = "xmlns" here, because this WILL cause an js exception 579 // the correct way would be, to parse the xmlns attribute in the generator, set the namespace of the holding element 580 // and then forget about this attribute 581 if ns == "" && a.Key == "xmlns" { 582 continue 583 } 584 err := r.instructionList.writeSetAttrNSStr(ns, a.Key, a.Val) 585 if err != nil { 586 return err 587 } 588 } 589 } else { 590 for _, a := range n.Attr { 591 err := r.instructionList.writeSetAttrStr(a.Key, a.Val) 592 if err != nil { 593 return err 594 } 595 } 596 } 597 598 err := r.instructionList.writeRemoveOtherAttrs() 599 if err != nil { 600 return err 601 } 602 603 // do any JS properties 604 for _, p := range n.Prop { 605 err := r.instructionList.writeSetProperty(p.Key, []byte(p.JSONVal)) 606 if err != nil { 607 return err 608 } 609 } 610 611 if len(n.DOMEventHandlerSpecList) > 0 { 612 613 // store in domHandlerMap 614 state.domHandlerMap[string(positionID)] = n.DOMEventHandlerSpecList 615 616 for _, hs := range n.DOMEventHandlerSpecList { 617 err := r.instructionList.writeSetEventListener(positionID, hs.EventType, hs.Capture, hs.Passive) 618 if err != nil { 619 return err 620 } 621 } 622 } 623 // always write the remove for event listeners so any previous ones are taken away 624 return r.instructionList.writeRemoveOtherEventListeners(positionID) 625 } 626 627 // // writeAllStaticAttrs is a helper to write all the static attrs from a VGNode 628 // func (r *JSRenderer) writeAllStaticAttrs(n *vugu.VGNode) error { 629 // for _, a := range n.Attr { 630 // err := r.instructionList.writeSetAttrStr(a.Key, a.Val) 631 // if err != nil { 632 // return err 633 // } 634 // } 635 // return nil 636 // } 637 638 func (r *JSRenderer) handleCallback(this js.Value, args []js.Value) interface{} { 639 return r.jsRenderState.callbackManager.callback(this, args) 640 } 641 642 func (r *JSRenderer) handleDOMEvent() { 643 644 strlen := binary.BigEndian.Uint32(r.eventHandlerBuffer[:4]) 645 b := r.eventHandlerBuffer[4 : strlen+4] 646 // log.Printf("handleDOMEvent JSON from event buffer: %q", b) 647 648 // var ee eventEnv 649 // rwmu *sync.RWMutex 650 // requestRenderCH chan bool 651 652 var eventDetail struct { 653 PositionID string // `json:"position_id"` 654 EventType string // `json:"event_type"` 655 Capture bool // `json:"capture"` 656 Passive bool // `json:"passive"` 657 658 // the event object data as extracted above 659 EventSummary map[string]interface{} // `json:"event_summary"` 660 } 661 662 edm := make(map[string]interface{}, 6) 663 // err := json.Unmarshal(b, &eventDetail) 664 err := vjson.Unmarshal(b, &edm) 665 if err != nil { 666 panic(err) 667 } 668 669 // manually extract fields 670 eventDetail.PositionID, _ = edm["position_id"].(string) 671 eventDetail.EventType, _ = edm["event_type"].(string) 672 eventDetail.Capture, _ = edm["capture"].(bool) 673 eventDetail.Passive, _ = edm["passive"].(bool) 674 eventDetail.EventSummary, _ = edm["event_summary"].(map[string]interface{}) 675 676 domEvent := vugu.NewDOMEvent(r.eventEnv, eventDetail.EventSummary) 677 678 // log.Printf("eventDetail: %#v", eventDetail) 679 680 // it is important that we lock around accessing anything that might change (domHandlerMap) 681 // and around the invokation of the handler call itself 682 683 r.eventRWMU.Lock() 684 handlers := r.jsRenderState.domHandlerMap[eventDetail.PositionID] 685 var f func(vugu.DOMEvent) 686 for _, h := range handlers { 687 if h.EventType == eventDetail.EventType && h.Capture == eventDetail.Capture { 688 f = h.Func 689 break 690 } 691 } 692 693 // make sure we found something, panic if not 694 if f == nil { 695 r.eventRWMU.Unlock() 696 panic(fmt.Errorf("Unable to find event handler for positionID=%q, eventType=%q, capture=%v", 697 eventDetail.PositionID, eventDetail.EventType, eventDetail.Capture)) 698 } 699 700 // NOTE: For tinygo support we are not using defer here for now - it would probably be better to do so since 701 // the handler can panic. However, Vugu program behavior after panicing from an event is currently 702 // undefined so whatever for now. We'll have to make a decision later about whether or not Vugu 703 // programs should keep running after an event handler panics. They do in JS after an exception, 704 // but... this is not JS. Needs more thought. 705 706 // invoke handler 707 f(domEvent) 708 709 r.eventRWMU.Unlock() 710 711 // TODO: Also give this more thought: For now we just do a non-blocking push to the 712 // eventWaitCh, telling the render loop that a render is required, but if a bunch 713 // of them stack up we don't wait 714 r.sendEventWaitCh() 715 716 }