github.com/vugu/vugu@v0.3.5/vgnode.go (about) 1 package vugu 2 3 import ( 4 "fmt" 5 "html" 6 "reflect" 7 "strconv" 8 9 "github.com/vugu/vugu/js" 10 ) 11 12 // VGNodeType is one of the valid node types (error, text, document, element, comment, doctype). 13 // Note that only text, element and comment are currently used. 14 type VGNodeType uint32 15 16 // Available VGNodeTypes. 17 const ( 18 ErrorNode = VGNodeType(0 /*html.ErrorNode*/) 19 TextNode = VGNodeType(1 /*html.TextNode*/) 20 DocumentNode = VGNodeType(2 /*html.DocumentNode*/) 21 ElementNode = VGNodeType(3 /*html.ElementNode*/) 22 CommentNode = VGNodeType(4 /*html.CommentNode*/) 23 DoctypeNode = VGNodeType(5 /*html.DoctypeNode*/) 24 ) 25 26 // VGAtom is an integer corresponding to golang.org/x/net/html/atom.Atom. 27 // Note that this may be removed for simplicity and to remove the dependency 28 // on the package above. Suggest you don't use it. 29 // type VGAtom uint32 30 31 // VGAttribute is the attribute on an HTML tag. 32 type VGAttribute struct { 33 Namespace, Key, Val string 34 } 35 36 // VGProperty is a JS property to be set on a DOM element. 37 type VGProperty struct { 38 Key string 39 JSONVal []byte // value as JSON expression 40 } 41 42 // VGNode represents a node from our virtual DOM with the dynamic parts wired up into functions. 43 // 44 // For the static parts, an instance of VGNode corresponds directly to the DOM representation of 45 // an HTML node. The pointers to other VGNode instances (Parent, FirstChild, etc.) are used to manage the tree. 46 // Type, Data, Namespace and Attr have the usual meanings for nodes. 47 // 48 // The Component field, if not-nil indicates that rendering should be delegated to the specified component, 49 // all other fields are ignored. 50 // 51 // Another special case is when Type is ElementNode and Data is an empty string (and Component is nil) then this node is a "template" 52 // (i.e. <vg-template>) and its children will be "flattened" into the DOM in position of this element 53 // and attributes, events, etc. ignored. 54 // 55 // Prop contains JavaScript property values to be assigned during render. InnerHTML provides alternate 56 // HTML content instead of children. DOMEventHandlerSpecList specifies DOM handlers to register. 57 // And the JS...Handler fields are used to register callbacks to obtain information at JS render-time. 58 // 59 // TODO: This and its related parts should probably move into a sub-package (vgnode?) and 60 // the "VG" prefixes removed. 61 type VGNode struct { 62 Parent, FirstChild, LastChild, PrevSibling, NextSibling *VGNode 63 64 Type VGNodeType 65 // DataAtom VGAtom // this needed to come out, we're not using it and without well-defined behavior it just becomes confusing and problematic 66 Data string 67 Namespace string 68 Attr []VGAttribute 69 70 // JS properties to e set during render 71 Prop []VGProperty 72 73 // Props Props // dynamic attributes, used as input for components or converted to attributes for regular HTML elements 74 75 InnerHTML *string // indicates that children should be ignored and this raw HTML is the children of this tag; nil means not set, empty string means explicitly set to empty string 76 77 DOMEventHandlerSpecList []DOMEventHandlerSpec // describes invocations when DOM events happen 78 79 // indicates this node's output should be delegated to the specified component 80 Component interface{} 81 82 // if not-nil, called when element is created (but before examining child nodes) 83 JSCreateHandler JSValueHandler 84 // if not-nil, called after children have been visited 85 JSPopulateHandler JSValueHandler 86 } 87 88 // IsComponent returns true if this is a component (Component != nil). 89 // Components have rendering delegated to them instead of processing this node. 90 func (n *VGNode) IsComponent() bool { 91 return n.Component != nil 92 } 93 94 // IsTemplate returns true if this is a template (Type is ElementNode and Data is an empty string and not a Component). 95 // Templates have their children flattened into the output DOM instead of being processed directly. 96 func (n *VGNode) IsTemplate() bool { 97 if n.Type == ElementNode && n.Data == "" && n.Component == nil { 98 return true 99 } 100 return false 101 } 102 103 // InsertBefore inserts newChild as a child of n, immediately before oldChild 104 // in the sequence of n's children. oldChild may be nil, in which case newChild 105 // is appended to the end of n's children. 106 // 107 // It will panic if newChild already has a parent or siblings. 108 func (n *VGNode) InsertBefore(newChild, oldChild *VGNode) { 109 if newChild.Parent != nil || newChild.PrevSibling != nil || newChild.NextSibling != nil { 110 panic("html: InsertBefore called for an attached child Node") 111 } 112 var prev, next *VGNode 113 if oldChild != nil { 114 prev, next = oldChild.PrevSibling, oldChild 115 } else { 116 prev = n.LastChild 117 } 118 if prev != nil { 119 prev.NextSibling = newChild 120 } else { 121 n.FirstChild = newChild 122 } 123 if next != nil { 124 next.PrevSibling = newChild 125 } else { 126 n.LastChild = newChild 127 } 128 newChild.Parent = n 129 newChild.PrevSibling = prev 130 newChild.NextSibling = next 131 } 132 133 // AppendChild adds a node c as a child of n. 134 // 135 // It will panic if c already has a parent or siblings. 136 func (n *VGNode) AppendChild(c *VGNode) { 137 if c.Parent != nil || c.PrevSibling != nil || c.NextSibling != nil { 138 panic("html: AppendChild called for an attached child Node") 139 } 140 last := n.LastChild 141 if last != nil { 142 last.NextSibling = c 143 } else { 144 n.FirstChild = c 145 } 146 n.LastChild = c 147 c.Parent = n 148 c.PrevSibling = last 149 } 150 151 // RemoveChild removes a node c that is a child of n. Afterwards, c will have 152 // no parent and no siblings. 153 // 154 // It will panic if c's parent is not n. 155 func (n *VGNode) RemoveChild(c *VGNode) { 156 if c.Parent != n { 157 panic("html: RemoveChild called for a non-child Node") 158 } 159 if n.FirstChild == c { 160 n.FirstChild = c.NextSibling 161 } 162 if c.NextSibling != nil { 163 c.NextSibling.PrevSibling = c.PrevSibling 164 } 165 if n.LastChild == c { 166 n.LastChild = c.PrevSibling 167 } 168 if c.PrevSibling != nil { 169 c.PrevSibling.NextSibling = c.NextSibling 170 } 171 c.Parent = nil 172 c.PrevSibling = nil 173 c.NextSibling = nil 174 } 175 176 // type IfFunc func(data interface{}) (bool, error) 177 178 // Walk will walk the tree under a VGNode using the specified callback function. 179 // If the function provided returns a non-nil error then walking will be stopped 180 // and this error will be returned. Only FirstChild and NextSibling are used 181 // while walking and so with well-formed documents should not loop. (But loops 182 // created manually by modifying FirstChild or NextSibling pointers could cause 183 // this function to recurse indefinitely.) Note that f may modify nodes as it 184 // visits them with predictable results as long as it does not modify elements 185 // higher on the tree (up, toward the parent); it is safe to modify self and children. 186 func (n *VGNode) Walk(f func(*VGNode) error) error { 187 if n == nil { 188 return nil 189 } 190 err := f(n) 191 if err != nil { 192 return err 193 } 194 err = n.FirstChild.Walk(f) 195 if err != nil { 196 return err 197 } 198 err = n.NextSibling.Walk(f) 199 if err != nil { 200 return err 201 } 202 return nil 203 } 204 205 // AddAttrInterface sets an attribute based on the given interface. The followings types are supported 206 // - string - value is used as attr value as it is 207 // - int,float,... - the value is converted to string with strconv and used as attr value 208 // - bool - treat the attribute as a flag. If false, the attribute will be ignored, if true outputs the attribute without a value 209 // - fmt.Stringer - if the value implements fmt.Stringer, the returned string of StringVar() is used 210 // - ptr - If the ptr is nil, the attribute will be ignored. Else, the rules above apply 211 // any other type is handled via fmt.Sprintf() 212 func (n *VGNode) AddAttrInterface(key string, val interface{}) { 213 // ignore nil attributes 214 if val == nil { 215 return 216 } 217 218 nattr := VGAttribute{ 219 Key: key, 220 } 221 222 switch v := val.(type) { 223 case string: 224 nattr.Val = v 225 case int: 226 nattr.Val = strconv.Itoa(v) 227 case int8: 228 nattr.Val = strconv.Itoa(int(v)) 229 case int16: 230 nattr.Val = strconv.Itoa(int(v)) 231 case int32: 232 nattr.Val = strconv.Itoa(int(v)) 233 case int64: 234 nattr.Val = strconv.FormatInt(v, 10) 235 case uint: 236 nattr.Val = strconv.FormatUint(uint64(v), 10) 237 case uint8: 238 nattr.Val = strconv.FormatUint(uint64(v), 10) 239 case uint16: 240 nattr.Val = strconv.FormatUint(uint64(v), 10) 241 case uint32: 242 nattr.Val = strconv.FormatUint(uint64(v), 10) 243 case uint64: 244 nattr.Val = strconv.FormatUint(v, 10) 245 case float32: 246 nattr.Val = strconv.FormatFloat(float64(v), 'f', 6, 32) 247 case float64: 248 nattr.Val = strconv.FormatFloat(v, 'f', 6, 64) 249 case bool: 250 if !v { 251 return 252 } 253 // FIXME: according to the HTML spec this is valid for flags, but not pretty 254 // To change this, we have to adopt the changes inside the domrender and staticrender too. 255 nattr.Val = key 256 case fmt.Stringer: 257 // we have to check that the given interface does not hide a nil ptr 258 if p := reflect.ValueOf(val); p.Kind() == reflect.Ptr && p.IsNil() { 259 return 260 } 261 nattr.Val = v.String() 262 default: 263 // check if this is a ptr 264 rv := reflect.ValueOf(val) 265 if rv.Kind() == reflect.Ptr { 266 if rv.IsNil() { 267 return 268 } 269 // indirect the ptr and recurse 270 if !rvIsZero(rv) { 271 n.AddAttrInterface(key, reflect.Indirect(rv).Interface()) 272 } 273 return 274 } 275 // fall back to fmt 276 nattr.Val = fmt.Sprintf("%v", val) 277 } 278 279 n.Attr = append(n.Attr, nattr) 280 } 281 282 // AddAttrList takes a VGAttributeLister and sets the returned attributes to the node 283 func (n *VGNode) AddAttrList(lister VGAttributeLister) { 284 n.Attr = append(n.Attr, lister.AttributeList()...) 285 } 286 287 // SetInnerHTML assigns the InnerHTML field with useful logic based on the type of input. 288 // Values of string type are escaped using html.EscapeString(). Values in the 289 // int or float type families or bool are converted to a string using the strconv package 290 // (no escaping is required). Nil values set InnerHTML to nil. Non-nil pointers 291 // are followed and the same rules applied. Values implementing HTMLer will have their 292 // HTML() method called and the result put into InnerHTML without escaping. 293 // Values implementing fmt.Stringer have thier String() method called and the escaped result 294 // used as in string above. 295 // 296 // All other values have undefined behavior but are currently handled by setting InnerHTML 297 // to the result of: `html.EscapeString(fmt.Sprintf("%v", val))` 298 func (n *VGNode) SetInnerHTML(val interface{}) { 299 300 var s string 301 302 if val == nil { 303 n.InnerHTML = nil 304 return 305 } 306 307 switch v := val.(type) { 308 case string: 309 s = html.EscapeString(v) 310 case int: 311 s = strconv.Itoa(v) 312 case int8: 313 s = strconv.Itoa(int(v)) 314 case int16: 315 s = strconv.Itoa(int(v)) 316 case int32: 317 s = strconv.Itoa(int(v)) 318 case int64: 319 s = strconv.FormatInt(v, 10) 320 case uint: 321 s = strconv.FormatUint(uint64(v), 10) 322 case uint8: 323 s = strconv.FormatUint(uint64(v), 10) 324 case uint16: 325 s = strconv.FormatUint(uint64(v), 10) 326 case uint32: 327 s = strconv.FormatUint(uint64(v), 10) 328 case uint64: 329 s = strconv.FormatUint(v, 10) 330 case float32: 331 s = strconv.FormatFloat(float64(v), 'f', 6, 32) 332 case float64: 333 s = strconv.FormatFloat(v, 'f', 6, 64) 334 case bool: 335 // I don't see any use in making false result in no output in this case. 336 // I'm guessing if someone puts a bool in vg-content/vg-html 337 // they are expecting it to print out the text "true" or "false". 338 // It's different from AddAttrInterface but I think appropriate here. 339 s = strconv.FormatBool(v) 340 341 case HTMLer: 342 s = v.HTML() 343 344 case fmt.Stringer: 345 // we have to check that the given interface does not hide a nil ptr 346 if p := reflect.ValueOf(val); p.Kind() == reflect.Ptr && p.IsNil() { 347 n.InnerHTML = nil 348 return 349 } 350 s = html.EscapeString(v.String()) 351 default: 352 // check if this is a ptr 353 rv := reflect.ValueOf(val) 354 355 if rv.Kind() == reflect.Ptr { 356 if rv.IsNil() { 357 return 358 } 359 if !rvIsZero(rv) { 360 indirected := reflect.Indirect(rv) 361 if !indirected.IsNil() { 362 n.SetInnerHTML(indirected.Interface()) 363 } 364 } 365 return 366 } 367 368 // fall back to fmt 369 s = html.EscapeString(fmt.Sprintf("%v", val)) 370 } 371 372 n.InnerHTML = &s 373 } 374 375 // JSValueHandler does something with a js.Value 376 type JSValueHandler interface { 377 JSValueHandle(js.Value) 378 } 379 380 // JSValueFunc implements JSValueHandler as a function. 381 type JSValueFunc func(js.Value) 382 383 // JSValueHandle implements the JSValueHandler interface. 384 func (f JSValueFunc) JSValueHandle(v js.Value) { f(v) }