Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7750b31

Browse files
feat: implement ParentNode.children getter (#960)
Implements the [`ParentNode.children`](https://dom.spec.whatwg.org/#dom-parentnode-children) getter on `Element`, `Document`, and `DocumentFragment`. Returns a `LiveNodeList` of direct element children, filtering out text, comment, processing instruction, and other non-element nodes. ### Approach Uses `Object.defineProperty` getters backed by `LiveNodeList` — no extra state is maintained on the node. This follows the getter/setter approach [discussed in #652](#652 (comment)): > *"with the amount of state maintenance in this lib I'm leaning towards doing as many things as possible using a getter/setter approach"* The getter computes from `firstChild`/`nextSibling` on access. `LiveNodeList` handles cache invalidation automatically via the `_inc` counter on DOM mutations. ### Deviation from spec The DOM spec marks `.children` with [`[SameObject]`](https://webidl.spec.whatwg.org/#SameObject) (same reference on each access). This implementation returns a new `LiveNodeList` per access, matching the existing `getElementsByTagName` pattern. Caching per-node was avoided to keep zero extra state on nodes, consistent with the getter/setter preference above. Fixes #410
1 parent aa806cd commit 7750b31

6 files changed

Lines changed: 135 additions & 0 deletions

File tree

index.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,15 @@ declare module '@xmldom/xmldom' {
827827
*/
828828
readonly tagName: string;
829829

830+
/**
831+
* Returns a live collection of the direct child elements of this element.
832+
*
833+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/children)
834+
*
835+
* @see https://dom.spec.whatwg.org/#dom-parentnode-children
836+
*/
837+
readonly children: LiveNodeList<Element>;
838+
830839
/**
831840
* Returns element's first attribute whose qualified name is qualifiedName, and null if there
832841
* is no such attribute otherwise.
@@ -1085,6 +1094,16 @@ declare module '@xmldom/xmldom' {
10851094
*/
10861095
interface DocumentFragment extends Node {
10871096
readonly ownerDocument: Document;
1097+
1098+
/**
1099+
* Returns a live collection of the direct child elements of this document fragment.
1100+
*
1101+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentFragment/children)
1102+
*
1103+
* @see https://dom.spec.whatwg.org/#dom-parentnode-children
1104+
*/
1105+
readonly children: LiveNodeList<Element>;
1106+
10881107
getElementById(elementId: string): Element | null;
10891108
}
10901109
var DocumentFragment: InstanceOf<DocumentFragment>;
@@ -1152,6 +1171,15 @@ declare module '@xmldom/xmldom' {
11521171
*/
11531172
readonly documentElement: Element | null;
11541173

1174+
/**
1175+
* Returns a live collection of the direct child elements of this document.
1176+
*
1177+
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/children)
1178+
*
1179+
* @see https://dom.spec.whatwg.org/#dom-parentnode-children
1180+
*/
1181+
readonly children: LiveNodeList<Element>;
1182+
11551183
/**
11561184
* Creates an attribute object with a specified name.
11571185
*

lib/dom.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3051,6 +3051,22 @@ function cloneNode(doc, node, deep) {
30513051
function __set__(object, key, value) {
30523052
object[key] = value;
30533053
}
3054+
3055+
// Returns a new array of direct Element children.
3056+
// Passed to LiveNodeList to implement ParentNode.children.
3057+
// https://dom.spec.whatwg.org/#dom-parentnode-children
3058+
function childrenRefresh(node) {
3059+
var ls = [];
3060+
var child = node.firstChild;
3061+
while (child) {
3062+
if (child.nodeType === ELEMENT_NODE) {
3063+
ls.push(child);
3064+
}
3065+
child = child.nextSibling;
3066+
}
3067+
return ls;
3068+
}
3069+
30543070
//do dynamic
30553071
try {
30563072
if (Object.defineProperty) {
@@ -3104,6 +3120,22 @@ try {
31043120
}
31053121
}
31063122

3123+
Object.defineProperty(Element.prototype, 'children', {
3124+
get: function () {
3125+
return new LiveNodeList(this, childrenRefresh);
3126+
},
3127+
});
3128+
Object.defineProperty(Document.prototype, 'children', {
3129+
get: function () {
3130+
return new LiveNodeList(this, childrenRefresh);
3131+
},
3132+
});
3133+
Object.defineProperty(DocumentFragment.prototype, 'children', {
3134+
get: function () {
3135+
return new LiveNodeList(this, childrenRefresh);
3136+
},
3137+
});
3138+
31073139
__set__ = function (object, key, value) {
31083140
//console.log(value)
31093141
object['$$' + key] = value;

readme.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,13 @@ import { DOMParser } from '@xmldom/xmldom'
289289
- `isDefaultNamespace(namespaceURI)`
290290
- `lookupNamespaceURI(prefix)`
291291

292+
### DOM Living Standard support:
293+
294+
* [ParentNode](https://dom.spec.whatwg.org/#interface-parentnode) mixin (on `Document`, `DocumentFragment`, `Element`)
295+
296+
readonly attribute:
297+
- `children`
298+
292299
### DOM extension by xmldom
293300

294301
* [Node] Source position extension;

test/dom/document.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,14 @@ describe('Document.prototype', () => {
491491
);
492492
});
493493
});
494+
describe('children', () => {
495+
test('should return only direct element children of the document', () => {
496+
const doc = new DOMParser().parseFromString('<?xml version="1.0"?><!-- comment --><root/>', MIME_TYPE.XML_TEXT);
497+
const children = doc.children;
498+
expect(children).toHaveLength(1);
499+
expect(children.item(0).tagName).toBe('root');
500+
});
501+
});
494502
describe('removeChild', () => {
495503
test('should remove all connections to node', () => {
496504
const doc = new DOMImplementation().createDocument('', 'xml');

test/dom/element.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,50 @@ describe('Element', () => {
408408
expect(doc.documentElement.childNodes[1].childNodes[0].hasAttributes()).toBe(true);
409409
});
410410
});
411+
describe('children', () => {
412+
test('should return only element children, filtering text, comments, and processing instructions', () => {
413+
const doc = new DOMParser().parseFromString('<root>text<a/><!-- comment --><b/><?pi data?><c/></root>', MIME_TYPE.XML_TEXT);
414+
const root = doc.documentElement;
415+
const children = root.children;
416+
expect(children).toHaveLength(3);
417+
expect(children.item(0).tagName).toBe('a');
418+
expect(children.item(1).tagName).toBe('b');
419+
expect(children.item(2).tagName).toBe('c');
420+
});
421+
422+
test('should return empty list for element with only text content', () => {
423+
const doc = new DOMParser().parseFromString('<root>just text</root>', MIME_TYPE.XML_TEXT);
424+
expect(doc.documentElement.children).toHaveLength(0);
425+
});
426+
427+
test('should return empty list for element with no children', () => {
428+
const doc = new DOMParser().parseFromString('<root/>', MIME_TYPE.XML_TEXT);
429+
expect(doc.documentElement.children).toHaveLength(0);
430+
});
431+
432+
test('should return correct length', () => {
433+
const doc = new DOMParser().parseFromString('<root><a/><b/><c/><d/></root>', MIME_TYPE.XML_TEXT);
434+
expect(doc.documentElement.children.length).toBe(4);
435+
});
436+
437+
test('should be live — reflects appendChild', () => {
438+
const doc = new DOMParser().parseFromString('<root><a/></root>', MIME_TYPE.XML_TEXT);
439+
const root = doc.documentElement;
440+
const children = root.children;
441+
expect(children).toHaveLength(1);
442+
root.appendChild(doc.createElement('b'));
443+
expect(children).toHaveLength(2);
444+
expect(children.item(1).tagName).toBe('b');
445+
});
446+
447+
test('should be live — reflects removeChild', () => {
448+
const doc = new DOMParser().parseFromString('<root><a/><b/></root>', MIME_TYPE.XML_TEXT);
449+
const root = doc.documentElement;
450+
const children = root.children;
451+
expect(children).toHaveLength(2);
452+
root.removeChild(root.firstChild);
453+
expect(children).toHaveLength(1);
454+
expect(children.item(0).tagName).toBe('b');
455+
});
456+
});
411457
});

test/dom/fragment.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ describe('DOM DocumentFragment', () => {
1212
expect(() => new DocumentFragment()).toThrow(TypeError);
1313
});
1414
});
15+
describe('children', () => {
16+
test('should return only direct element children of the fragment', () => {
17+
const doc = new DOMParser().parseFromString('<root/>', MIME_TYPE.XML_TEXT);
18+
const fragment = doc.createDocumentFragment();
19+
fragment.appendChild(doc.createTextNode('text'));
20+
fragment.appendChild(doc.createElement('a'));
21+
fragment.appendChild(doc.createComment('comment'));
22+
fragment.appendChild(doc.createElement('b'));
23+
const children = fragment.children;
24+
expect(children).toHaveLength(2);
25+
expect(children.item(0).tagName).toBe('a');
26+
expect(children.item(1).tagName).toBe('b');
27+
});
28+
});
1529
// see: http://jsfiddle.net/9Wmh2/1/
1630
test('append empty fragment', () => {
1731
const document = new DOMParser().parseFromString('<p id="p"/>', MIME_TYPE.XML_TEXT);

0 commit comments

Comments
 (0)