Skip to content

Commit 993a9f2

Browse files
authored
Add NodeComponent class (#17)
2 parents 93134fd + a1bd9ae commit 993a9f2

File tree

6 files changed

+159
-56
lines changed

6 files changed

+159
-56
lines changed

src/Component.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
1515
* If not, see <https://www.gnu.org/licenses/>.
1616
*/
17-
import {BaseComponent} from "./index.js";
17+
import {ElementComponent} from "./index.js";
1818

1919
type ElementToTagName<T extends HTMLElement> = {
2020
[K in keyof HTMLElementTagNameMap]: HTMLElementTagNameMap[K] extends T ? K : never
@@ -30,7 +30,7 @@ type HtmlTagString<T extends HTMLElement> =
3030
* To create your own HTML component, it's recommended to extend this class.
3131
* @typeParam T Component element type
3232
*/
33-
export class Component<T extends HTMLElement = HTMLElement> extends BaseComponent<T> {
33+
export class Component<T extends HTMLElement = HTMLElement> extends ElementComponent<T> {
3434
/**
3535
* Create Component instance
3636
* @param element Instance or tag name
@@ -61,7 +61,7 @@ export class Component<T extends HTMLElement = HTMLElement> extends BaseComponen
6161
* @typeParam T Component element type
6262
*/
6363
public select<T extends HTMLElement = HTMLElement>(selectors: string): Component<T> | null {
64-
const element = this.element.querySelector<T>(selectors);
64+
const element = this.node.querySelector<T>(selectors);
6565
if (element == null) return null;
6666
return new Component<T>(element);
6767
}
@@ -74,7 +74,7 @@ export class Component<T extends HTMLElement = HTMLElement> extends BaseComponen
7474
* @typeParam T Component element type
7575
*/
7676
public selectAll<T extends HTMLElement = HTMLElement>(selectors: string): Component<T>[] {
77-
return [...this.element.querySelectorAll<T>(selectors)].map(e => new Component<T>(e));
77+
return [...this.node.querySelectorAll<T>(selectors)].map(e => new Component<T>(e));
7878
}
7979

8080
/**
@@ -103,7 +103,7 @@ export class Component<T extends HTMLElement = HTMLElement> extends BaseComponen
103103
const name: string = args[0];
104104
const value: string = args[1];
105105
const priority: boolean = args[2] ?? false;
106-
this.element.style.setProperty(name, value, priority ? "important" : void 0);
106+
this.node.style.setProperty(name, value, priority ? "important" : undefined);
107107
}
108108
else {
109109
const properties: Record<string, string> = args[0];
@@ -113,7 +113,10 @@ export class Component<T extends HTMLElement = HTMLElement> extends BaseComponen
113113
return this;
114114
}
115115

116-
public override on<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K], component: this) => any, options?: boolean | AddEventListenerOptions) {
117-
return super.on(type as any, listener, options);
116+
public override on<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K], component: this) => any): typeof this;
117+
public override on<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K], component: this) => any, options: AddEventListenerOptions): typeof this;
118+
public override on<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K], component: this) => any, useCapture: boolean): typeof this;
119+
public override on<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K], component: this) => any, c?: boolean | AddEventListenerOptions): typeof this {
120+
return super.on(type as any, listener, c as any);
118121
}
119122
}
Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
1515
* If not, see <https://www.gnu.org/licenses/>.
1616
*/
17+
import {NodeComponent} from "./NodeComponent.js";
1718

1819
/**
1920
* Non-readonly non-method keys
@@ -40,49 +41,36 @@ type ReadableKeys<T> = {
4041
* An {@link !Element} component
4142
* @typeParam T Component element type
4243
*/
43-
export abstract class BaseComponent<T extends Element> {
44-
/**
45-
* This component's element
46-
*/
47-
public readonly element: T;
48-
44+
export abstract class ElementComponent<T extends Element> extends NodeComponent<T> {
4945
/**
5046
* @param element Initial element for this component
5147
* @protected
5248
*/
5349
protected constructor(element: T) {
54-
this.element = element;
55-
}
56-
57-
/**
58-
* Insert component after the last child
59-
*/
60-
public append(...components: BaseComponent<any>[]) {
61-
components.forEach((component) => this.element.appendChild(component.element))
62-
return this;
50+
super(element);
6351
}
6452

6553
/**
6654
* Insert component before the first child
6755
*/
68-
public prepend(...components: BaseComponent<any>[]) {
69-
components.forEach((component) => this.element.prepend(component.element))
56+
public prepend(...components: NodeComponent<any>[]) {
57+
components.forEach((component) => this.node.prepend(component.node))
7058
return this;
7159
}
7260

7361
/**
7462
* Add classes
7563
*/
7664
public class(...classes: string[]) {
77-
this.element.classList.add(...classes.flatMap(c => c.split(" ")));
65+
this.node.classList.add(...classes.flatMap(c => c.split(" ")));
7866
return this;
7967
}
8068

8169
/**
8270
* Remove classes
8371
*/
8472
public removeClass(...classes: string[]) {
85-
this.element.classList.remove(...classes.flatMap(c => c.split(" ")));
73+
this.node.classList.remove(...classes.flatMap(c => c.split(" ")));
8674
return this;
8775
}
8876

@@ -91,7 +79,7 @@ export abstract class BaseComponent<T extends Element> {
9179
*/
9280
public toggleClass(...classes: string[]) {
9381
for (const c of new Set(classes.flatMap(c => c.split(" "))))
94-
this.element.classList.toggle(c);
82+
this.node.classList.toggle(c);
9583
return this;
9684
}
9785

@@ -116,7 +104,7 @@ export abstract class BaseComponent<T extends Element> {
116104
* @returns true if component has all the specified classes
117105
*/
118106
public hasClass(...classes: string[]) {
119-
return classes.every(c => this.element.classList.contains(c));
107+
return classes.every(c => this.node.classList.contains(c));
120108
}
121109

122110
/**
@@ -125,7 +113,7 @@ export abstract class BaseComponent<T extends Element> {
125113
* @param [value] attribute value
126114
*/
127115
public attr(name: string, value?: string) {
128-
this.element.setAttribute(name, value ?? "");
116+
this.node.setAttribute(name, value ?? "");
129117
return this;
130118
}
131119

@@ -134,23 +122,15 @@ export abstract class BaseComponent<T extends Element> {
134122
* @param name attribute name
135123
*/
136124
public removeAttr(name: string) {
137-
this.element.removeAttribute(name);
138-
return this;
139-
}
140-
141-
/**
142-
* Set text content
143-
*/
144-
public text(text: string) {
145-
this.element.textContent = text;
125+
this.node.removeAttribute(name);
146126
return this;
147127
}
148128

149129
/**
150130
* Set inner HTML
151131
*/
152132
public html(html: string) {
153-
this.element.innerHTML = html;
133+
this.node.innerHTML = html;
154134
return this;
155135
}
156136

@@ -160,7 +140,7 @@ export abstract class BaseComponent<T extends Element> {
160140
* @param value property value
161141
*/
162142
public set<K extends WritableKeys<T>>(name: K, value: T[K]) {
163-
this.element[name] = value;
143+
this.node[name] = value;
164144
return this;
165145
}
166146

@@ -169,32 +149,28 @@ export abstract class BaseComponent<T extends Element> {
169149
* @param name property name
170150
*/
171151
public get<K extends ReadableKeys<T>>(name: K): T[K] {
172-
return this.element[name];
152+
return this.node[name];
173153
}
174154

175155
/**
176156
* Remove the element
177157
*/
178158
public remove(): this {
179-
this.element.remove();
159+
this.node.remove();
180160
return this;
181161
}
182162

183-
/**
184-
* Add event listener
185-
* @param type
186-
* @param listener
187-
* @param options
188-
*/
189-
public on<K extends keyof ElementEventMap>(type: K, listener: (ev: ElementEventMap[K], component: this) => any, options?: boolean | AddEventListenerOptions) {
190-
this.element.addEventListener(type, e => listener(e, this), options);
191-
return this;
163+
public override on<K extends keyof ElementEventMap>(type: K, listener: (ev: ElementEventMap[K], component: this) => any): typeof this;
164+
public override on<K extends keyof ElementEventMap>(type: K, listener: (ev: ElementEventMap[K], component: this) => any, options: AddEventListenerOptions): typeof this;
165+
public override on<K extends keyof ElementEventMap>(type: K, listener: (ev: ElementEventMap[K], component: this) => any, useCapture: boolean): typeof this;
166+
public override on<K extends keyof ElementEventMap>(type: K, listener: (ev: ElementEventMap[K], component: this) => any, c?: boolean | AddEventListenerOptions): typeof this {
167+
return super.on(type as any, listener, c as any);
192168
}
193169

194170
/**
195171
* Get this component's outer HTML
196172
*/
197-
public toString() {
198-
return this.element.outerHTML;
173+
public override toString() {
174+
return this.node.outerHTML;
199175
}
200176
}

src/NodeComponent.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Copyright © 2024 Cloudnode OÜ
3+
*
4+
* This file is part of @cldn/components.
5+
*
6+
* \@cldn/components is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
7+
* General Public License as published by the Free Software Foundation, either version 3 of the License,
8+
* or (at your option) any later version.
9+
*
10+
* \@cldn/components is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
11+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12+
* for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
15+
* If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
/**
19+
* A {@link !Node} component
20+
* @typeParam T Node type
21+
*/
22+
export abstract class NodeComponent<T extends Node> {
23+
/**
24+
* This component's node
25+
*/
26+
public readonly node: T;
27+
28+
/**
29+
* @param node Initial node for this component
30+
* @protected
31+
*/
32+
protected constructor(node: T) {
33+
this.node = node;
34+
}
35+
36+
/**
37+
* Insert component after the last child
38+
*/
39+
public append(...components: NodeComponent<any>[]) {
40+
components.forEach((component) => this.node.appendChild(component.node))
41+
return this;
42+
}
43+
44+
/**
45+
* Set text content
46+
*/
47+
public text(text: string) {
48+
this.node.textContent = text;
49+
return this;
50+
}
51+
52+
/**
53+
* Add event listener
54+
* @param type A case-sensitive string representing the event type to listen for.
55+
* @param listener The function that is called when an event of the specified type occurs.
56+
*/
57+
public on(type: string, listener: (ev: Event, component: this) => any): typeof this;
58+
/**
59+
* Add event listener
60+
* @param type A case-sensitive string representing the event type to listen for.
61+
* @param listener The function that is called when an event of the specified type occurs.
62+
* @param options An object that specifies characteristics about the event listener. See {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options `options` on MDN}
63+
*/
64+
public on(type: string, listener: (ev: Event, component: this) => any, options: AddEventListenerOptions): typeof this;
65+
/**
66+
* Add event listener
67+
* @param type A case-sensitive string representing the event type to listen for.
68+
* @param listener The function that is called when an event of the specified type occurs.
69+
* @param useCapture See {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture `useCapture` on MDN}
70+
*/
71+
public on(type: string, listener: (ev: Event, component: this) => any, useCapture: boolean): typeof this;
72+
public on(type: string, listener: (ev: Event, component: this) => any, c?: boolean | AddEventListenerOptions) {
73+
this.node.addEventListener(type, e => listener(e, this), c);
74+
return this;
75+
}
76+
}

src/SvgComponent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
1515
* If not, see <https://www.gnu.org/licenses/>.
1616
*/
17-
import {BaseComponent} from "./index.js";
17+
import {ElementComponent} from "./index.js";
1818

1919
/**
2020
* An SVG component (`<svg>`)
2121
*/
22-
export class SvgComponent extends BaseComponent<SVGSVGElement> {
22+
export class SvgComponent extends ElementComponent<SVGSVGElement> {
2323
public constructor(element?: SVGSVGElement) {
2424
super(element ?? document.createElementNS("http://www.w3.org/2000/svg", "svg"));
2525
}

src/TextComponent.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright © 2024 Cloudnode OÜ
3+
*
4+
* This file is part of @cldn/components.
5+
*
6+
* \@cldn/components is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
7+
* General Public License as published by the Free Software Foundation, either version 3 of the License,
8+
* or (at your option) any later version.
9+
*
10+
* \@cldn/components is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
11+
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
12+
* for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
15+
* If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
import {NodeComponent} from "./NodeComponent.js";
18+
19+
/**
20+
* A text node component
21+
*/
22+
export class TextComponent extends NodeComponent<Text> {
23+
/**
24+
* Create component
25+
* @param text Text node instance or text content string
26+
*/
27+
public constructor(text: Text | string) {
28+
super(typeof text === "string" ? document.createTextNode(text) : text);
29+
}
30+
31+
/**
32+
* @deprecated Cannot add children to a TextComponent
33+
*
34+
* @throws {@link !DOMException} Always
35+
*/
36+
public override append(): never {
37+
throw new DOMException(`NodeComponent.append: Cannot add children to a ${this.constructor.name}`);
38+
}
39+
40+
/**
41+
* Get the text content
42+
*/
43+
public override toString() {
44+
return this.node.textContent;
45+
}
46+
}

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* You should have received a copy of the GNU Lesser General Public License along with @cldn/components.
1515
* If not, see <https://www.gnu.org/licenses/>.
1616
*/
17-
export {BaseComponent} from "./BaseComponent.js";
1817
export {Component} from "./Component.js";
18+
export {ElementComponent} from "./ElementComponent.js";
19+
export {NodeComponent} from "./NodeComponent.js";
1920
export {SvgComponent} from "./SvgComponent.js";
21+
export {TextComponent} from "./TextComponent.js";

0 commit comments

Comments
 (0)