DOM Nodes

March 7, 2025 ยท View on GitHub

A tricky problem in the domain itself.

Implementing the actual DOM tree in Go presents a problem: How to implement the DOM specification, which has a very object-oriented interface, in Go, which doesn't support "function overloading on base classes".

Unfortunately, this does lead to very large interfaces, which is very unidiomatic Go. Hopefully, new insights can over time lead to more idiomatic solutions, while still being able to provide the full DOM interface where necessary, e.g., for Script integration.

The current state

So code structure is an attempt at a solution to this problem.

type Node interface {
	AppendChild(node Node) (Node, error)
	Parent() Node
 	FirstChild() Node
	TextContent() string
	SetTextContent(string)
}

Often, but not always the Node is an Element.

type Element interface {
	Node
	GetAttribute(name string) (string, bool)
	SetAttribute(name string, value string)
}

The actual implementations have similar relationships

type node struct {
    children []Node
    parent   Node
}

type element struct {
    node
    attributes []Attr
}

Obviously methods on the node struct implement the Node interface

So why is the interface necessary? If a function like Parent() returns a node struct, that couldn't be treated as an element, if it was embedded inside an element. While an embedder receives all the methods of the embedded object; and may "override" some, the embedded object doesn't "know" anything about being embedded.

Only by returning a Node interface, does it provide for the actual return value to be a Document, Doctype, Element, an HTMLElement, or any of the special HTML elements, like HTMLButtonElement, etc.

The self problem!

When an element is appended using AppendChild it's parent is updated to the new parent. An unexported method is added to the Node interface

type Node interface {
    // ...
    setParent(Node)
}

func (n *node) AppendChild(child Node) { // Return values ignored in example
    n.children = append(n.children, child)
    child.setParent(n)
}

This doesn't work!!!

When calling AppendChild on a specialised Element, when the method implementation on Node passes itself to setParent, it is the embedded node, not the Element. So calling Parent() on the child wouldn't return an Element any longer, just a Node.

The solution

The solution is that a node has a self value.

type node struct {
    // ...
    self Node
}

Any specialised element type must pass a reference to its outer self as a function call to SetSelf after being constructed. For example:

func NewHTMLAnchorElement(ownerDoc HTMLDocument) HTMLAnchorElement {
	result := &htmlAnchorElement{
		HTMLElement: NewHTMLElement("a", ownerDoc),
		URL:         nil,
	}
	result.SetSelf(result)
	return result
}

Failure to do this will cause the system to not work correctly

Not happy about it

I am certainly not happy about this solution. Objectively the fact that every element must do something special in order to work correctly.

But it just feels wrong.

Maybe an improvement?

Actually, while writing this, I realised that Elements can't be created directly, only through a call to document.createElement. So at least the possibility of not calling SetSelf can be mitigated, as there's a single point in code that fix this for all elements.

But it still doesn't feel right.

A strategy pattern?

Maybe a strategy pattern had been a better solution, but each specialised HTML element has special methods, e.g., for reading/writing IDL attribute. E.g., the HTMLFormElement has the method and action IDL attributes, that need to exist on the Go implementation of that object as well. So all the different implementations don't have the same interface, they merely share a common subset.