virty
v0.3.1
Published
A class-based virtual DOM structure allowing for DOM manipulation in nodejs
Readme
Virty
⚠️ This library is in its early stages of development. Expect bugs, and expect them to be plentiful. Until a v1.0.0 release, this message will persist.
⚠️ Recently many methods and getters were added to the main Node class, but time is short and I haven't documented them in the README yet. Everything is JSDoc documented, so you're encouraged to install and review the source code for documentation until I get around to the README.
A class-based virtual dom structure intended to be as brain-dead as possible to use.
❔ Looking for a document parser for html/xml? Take a look at flex-parse. Flex-parse uses virty under the hood as its document model of choice and offers extensively flexible* parsing strategies.*Currently a work in progress, but basic element, text, and comment parsing is available
Installation
The release on Github prior to v1.0.0 will likely be the best place to install from. Development may be sporadic and/or rapid at times, and I won't always be pushing the latest updates to NPM.
# Latest release
pnpm i "https://github.com/jacoblockett/virty"# Release hosted on NPM
pnpm i virtyQuickstart
Say we wanted to emulate this html structure:
<body>
<div id="main">
<h1>Welcome back, Username!</h1>
<div class="form-block">
<div class="message">Not Username? Type your name below!</div>
<input placeholder="Type your name here" />
<!-- QA: should we put a submit button here? -->
</div>
</div>
</body>At the most basic level, this would be the equivalent virty code:
// COMMENT, ELEMENT, and TEXT are simple string constants exported for convenience's sake
import Node, { COMMENT, ELEMENT, TEXT } from "virty"
// element nodes
const body = new Node({ type: ELEMENT, tagName: "body" })
const mainDiv = new Node({ type: ELEMENT, tagName: "div", attributes: { id: "main" } })
const h1 = new Node({ type: ELEMENT, tagName: "h1" })
const formBlock = new Node({ type: ELEMENT, tagName: "div", attributes: { class: "form-block" } })
const message = new Node({ type: ELEMENT, tagName: "div", attributes: { class: "message" } })
const input = new Node({ type: ELEMENT, tagName: "input", attributes: { placeholder: "Type your name here" } })
// text nodes
const h1Text = new Node({ type: TEXT, value: "Welcome back, Username!" })
const msgText = new Node({ type: TEXT, value: "Not Username? Type your name below!" })
// comment nodes
const qaComment = new Node({ type: COMMENT, value: "<!-- QA: should we put a submit button here? -->" })
// putting it all together
body.appendChild(mainDiv)
mainDiv.appendChild(h1, formBlock)
h1.appendChild(h1Text)
formBlock.appendChild(message, input, qaComment)
message.appendChild(msgText)Another way to write the same thing would be to make use of the children option in the Node classes' constructors:
import Node, { COMMENT, ELEMENT, TEXT } from "virty"
const doc = new Node({
type: ELEMENT,
tagName: "body",
children: [
new Node({
type: ELEMENT,
tagName: "div",
attributes: { id: "main" },
children: [
new Node({
type: ELEMENT,
tagName: "h1",
children: [new Node({ type: TEXT, value: "Welcome back, Username!" })]
}),
new Node({
type: ELEMENT,
tagName: "div",
attributes: { class: "form-block" },
children: [
new Node({
type: ELEMENT,
tagName: "div",
attributes: { class: "message" },
children: [new Node({ type: TEXT, value: "Not Username? Type your name below!" })]
}),
new Node({
type: ELEMENT,
tagName: "input",
attributes: { placeholder: "Type your name here" }
}),
new Node({
type: COMMENT,
value: "<!-- QA: should we put a submit button here? -->"
})
]
})
]
})
]
})And finally, if you prefer a more custom functional approach:
import Node, { COMMENT, ELEMENT, TEXT } from "virty"
const createComment = value => new Node({ type: COMMENT, value })
const createElement = (tagName, attributes, children) => new Node({ type: ELEMENT, tagName, attributes, children })
const createText = value => new Node({ type: TEXT, value })
const doc = createElement("body", {}, [
createElement("div", { id: "main" }, [
createElement("h1", {}, [createText("Welcome back, Username!")]),
createElement("div", { class: "form-block" }, [
createElement("div", { class: "message" }, [createText("Not Username? Type your name below!")]),
createElement("input", { placeholder: "Type your name here" }),
createComment("<!-- QA: should we put a submit button here? -->")
])
])
])Generally speaking, this library isn't designed for you to be manually creating document structures like this. If you want to, by all means, but you should think of this library as a lower-level driver to produce something higher level, such as results for a parser, a dynamic structure generator based on user input, etc.
API
Signature:
class Node(init: {
type: "comment"|"element"|"text",
tagName?: string,
attributes?: { [name: string]: string|number|boolean },
children?: Node[],
value?: string,
}): NodeInit options:
| Name | Type | Required | Default | Description |
| ---------- | --------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------------- |
| type | Enum("comment"\|"element"\|"text") | Yes | | The case-insensitive type of the node |
| tagName | string | If the element includes attributes, yes, otherwise, no | "element": """comment"\|"text": undefined | Case-sensitive tag name to use with "element" nodes |
| attributes | { [name: string]: string\|number\|boolean } | No | "element": {}"comment"\|"text": undefined | Attribute values to use with "element" nodes |
| children | Node[] | No | "element": []"comment"\|"text": undefined | Child nodes to append to "element" nodes |
| value | string | No | "element": undefined"comment"\|"text": "" | The text content to use with "comment" or "text" nodes |
💡
"element"nodes will ignore the value option, and"comment"|"text"nodes will ignore the tagName, attributes, and children options.
Methods, Getters, Setters:
static Node.isCommentstatic Node.isElementstatic Node.isNodestatic Node.isTextget Node.prototype.attributesget Node.prototype.childrenget Node.prototype.nextget Node.prototype.parentget Node.prototype.previousget Node.prototype.rootget Node.prototype.tagNameget Node.prototype.typeget Node.prototype.valuefunction Node.prototype.appendChildfunction Node.prototype.appendSiblingfunction Node.prototype.removeChild
Node.isComment 🔝
static Node.isComment(value: unknown): booleanChecks if the given value is a comment node.
Example:
Node.isComment("<!-- comment -->") // false
Node.isComment(new Node({ type: COMMENT, value: "<!-- comment -->" })) // trueNode.isElement 🔝
static Node.isElement(value: unknown): booleanChecks if the given value is an element node.
Example:
Node.isElement("div") // false
Node.isElement(new Node({ type: ELEMENT, tagName: "div" })) // trueNode.isNode 🔝
static Node.isNode(value: unknown): booleanChecks if the given value is a node.
Example:
Node.isNode("text") // false
Node.isNode(new Node({ type: COMMENT, value: "<!-- comment -->" })) // true
Node.isNode(new Node({ type: ELEMENT, tagName: "div" })) // true
Node.isNode(new Node({ type: TEXT, value: "text" })) // trueNode.isText 🔝
static Node.isText(value: unknown): booleanChecks if the given value is a text node.
Example:
Node.isText("text") // false
Node.isText(new Node({ type: TEXT, value: "text" })) // trueNode.prototype.attributes 🔝
get attributes(): { [name: string]: string|number|boolean } | undefinedThe attributes of the node.
Example:
const n = new Node({ type: ELEMENT, tagName: "div", attributes: { class: "a b c" } })
n.attributes.class // "a b c"Node.prototype.children 🔝
get children(): Node[] | undefinedThe children of the node.
Example:
const n = new Node({ type: ELEMENT, children: [new Node({ type: ELEMENT }), new Node({ type: TEXT })] })
n.children // [ Node {}, Node {} ], i.e. [ Node { ELEMENT }, Node { TEXT } ]Node.prototype.next 🔝
get next(): Node | undefinedThe next sibling node.
Example:
const n = new Node({ type: ELEMENT, children: [new Node({ type: ELEMENT }), new Node({ type: TEXT })] })
n.next // undefined
n.children[0].next // Node {}, i.e. Node { TEXT }Node.prototype.parent 🔝
get parent(): Node | undefinedThe parent node.
Example:
const n = new Node({ type: ELEMENT, children: [new Node({ type: ELEMENT }), new Node({ type: TEXT })] })
n.parent // undefined
n.children[0].parent // Node {}, i.e. Node { ELEMENT (n) }Node.prototype.previous 🔝
get previous(): Node | undefinedThe previous sibling node.
Example:
const n = new Node({ type: ELEMENT, children: [new Node({ type: ELEMENT }), new Node({ type: TEXT })] })
n.previous // undefined
n.children[1].previous // Node {}, i.e. Node { ELEMENT }Node.prototype.root 🔝
get root(): NodeThe root node.
Example:
const n = new Node({
type: ELEMENT,
children: [new Node({ type: ELEMENT, children: [new Node({ type: ELEMENT })] }), new Node({ type: TEXT })]
})
n.children[0].children[0].root // Node {}, i.e. Node { ELEMENT (n) }Node.prototype.tagName 🔝
get tagName(): string | undefinedThe tag name of the node.
Example:
const n = new Node({ type: ELEMENT, tagName: "div" })
n.tagName // "div"Node.prototype.type 🔝
get type(): "comment" | "element" | "text"The type of the node.
Example:
const n = new Node({ type: ELEMENT })
n.type // "element"Node.prototype.value 🔝
get value(): string | undefinedThe value of the node.
⚠️ This is not the value attribute of an element node, such as an
<input>element's value, for example. To access that, use Node.prototype.attributes.value instead.
Example:
const n = new Node({ type: TEXT, value: "Lorem ipsum..." })
n.value // "Lorem ipsum..."Node.prototype.appendChild 🔝
function appendChild(...nodes: (Node | Node[])[]): NodeAppends the given child or children to the node. You can pass multiple nodes as arguments or an array of nodes. Appended children will lose any and all parent and sibling references should they already exist. "comment" and "text" nodes will do nothing. Returns itself for chaining.
Example:
const parent = new Node({ type: ELEMENT })
const child1 = new Node({ type: TEXT })
const child2 = new Node({ type: TEXT })
parent.appendChild(child1, child2)
// or parent.appendChild([child1, child2])
// or parent.appendChild(child1).appendChild(child2)
parent.children // [ Node {}, Node {} ], i.e. [ Node { TEXT (child1) }, Node { TEXT (child2) } ]Node.prototype.appendSibling 🔝
function appendSibling(...nodes: (Node | Node[])[]): NodeAppends the given sibling or siblings to the node. You can pass multiple nodes as arguments or an array of nodes. Appended siblings will lose any and all parent and sibling references should they already exist. Returns itself for chaining.
Example:
const parent = new Node({ type: ELEMENT })
const child1 = new Node({ type: TEXT })
const child2 = new Node({ type: TEXT })
parent.appendChild(child1)
child1.appendSibling(child2)
parent.children // [ Node {}, Node {} ], i.e. [ Node { TEXT (child1) }, Node { TEXT (child2) } ]Node.prototype.removeChild 🔝
function removeChild(...nodes: (Node | Node[])[]): NodeRemoves the given child or children from the node. You can pass multiple nodes as arguments or an array of nodes. Nodes of type "comment" and "text", and nodes with no children, will do nothing. Returns itself for chaining.
Example:
const parent = new Node({ type: ELEMENT })
const child1 = new Node({ type: TEXT })
const child2 = new Node({ type: TEXT })
parent.appendChild(child1, child2).removeChild(child1)
parent.children // [ Node {} ], i.e. [ Node { TEXT (child2) } ]