TypeScriptで快適DOM操作生活を送りたい

TypeScript便利ですね。 自動で型チェックしてくれたり、クラスベース言語ぽい書き方ができたりと、BabelだけでJavaScript書いていた頃の小さなストレスが無くなり、とっても快適なのですが jQueryやReactDOMの ↓ みたいなDOM操作を、生Typescriptでできたら、もっと最高なのにな…(・_・)

jQuery

$('#container').append('<div>Hello world!</div>');

ReactDOM

render(
    <div>Hello world!</div>,
    document.getElementById('container')
);
と思い、次のようなクラスを定義してみました↓

クラスの定義

/**
### DOM
    Substitute of querySelector, appendChild.
*/
export type NodeType = Element | HTMLElement | DocumentFragment | null;

export class DOM {
    private parser: DOMParser = new DOMParser();
    el: NodeType = null;

    constructor(node?: NodeType | string) {
        if (typeof node === 'string' && !/[<>]/.test(node)) {
            this.el = document.querySelector(node) as NodeType;
        } else if (typeof node === 'string' && /[<>]/.test(node)) {
            const collection: NodeList = this.parser.parseFromString(node, 'text/html').body.childNodes;
            const doms: Element[] = [].slice.call(collection) as Element[];
            if (doms.filter((e: Element) => !(e instanceof Text)).length === 1) {
                this.el = doms[0];
            } else {
                this.el = document.createElement('div');
                const fragment: DocumentFragment = document.createDocumentFragment();
                for (let [i, l]: number[] = [0, doms.length]; i < l; i++) fragment.appendChild(doms[i]);
                this.el.appendChild(fragment);
            }
        } else if (node instanceof Element || node instanceof DocumentFragment) {
            this.el = node;
        } else {
            this.el = document.createDocumentFragment();
        }
    }
    /**
        Substitute of append - childNode:(string | Element | HTMLElement | DocumentFragment)
    */
    public append(childNode: NodeType | string): Element | DocumentFragment | boolean {
        if (typeof childNode === 'string' && this.el) {
            const fragment: DocumentFragment = document.createDocumentFragment();
            const collection: NodeList = this.parser.parseFromString(childNode, 'text/html').body.childNodes;
            const doms: Element[] = [].slice.call(collection) as Element[];
            for (let [i, l]: number[] = [0, doms.length]; i < l; i++) fragment.appendChild(doms[i]);
            this.el.appendChild(fragment);
            return false;
        } else if (childNode instanceof Element || childNode instanceof DocumentFragment) {
            this.el.appendChild(childNode);
            return false;
        } else {
            return this.el;
        }
    }
}

使い方

例1 新しい要素の作成

let container = new DOM('<div id="container"></div>');
new DOM('body').append(container.el);

例2 要素の追加

new DOM('#container').append('<div>Hello world!</div>');

例3 DocumentFragmentによる要素の追加

let fragment = new DOM();
for(let i=0; i<10; i++){
   fragment.append(`
      <div>要素その${i}</div>
   `);
}
new DOM('#container').append(fragment.el);
ReactのようなVirtualDOMの恩恵はありませんが、動的に要素を追加したりするのが格段に楽になりました。
IEでテンプレートリテラルが使えたらいいのになあ・・ ちなみに写真は クア・アイナ のハンバーガーとガーリックシュリンプです。

おまけ

要素の上書きをしたい場合は上記クラスに以下のプロパティrewriteを追加
/**
    innerHTML like method - childNode: (string | Element | HTMLElement | DocumentFragment)
*/
public rewrite(childNode: NodeType | string): Element | DocumentFragment | boolean {
    if (typeof childNode === 'string') {
        const fragment: DocumentFragment = document.createDocumentFragment();
        const collection: NodeList = this.parser.parseFromString(childNode, 'text/html').body.childNodes;
        const doms: Element[] = [].slice.call(collection) as Element[];
        for (let [i, l]: number[] = [0, doms.length]; i < l; i++) fragment.appendChild(doms[i]);
        while (this.el.firstChild) this.el.removeChild(this.el.firstChild);
        this.el.appendChild(fragment);
        return false;
    } else if (childNode instanceof Element || childNode instanceof DocumentFragment) {
        while (this.el.firstChild) this.el.removeChild(this.el.firstChild);
        this.el.appendChild(childNode);
        return false;
    } else {
        return this.el;
    }
}
要素を手前に追加したい場合は上記クラスに以下プロパティprependを追加
/**
    Substitute of prepend - childNode:(string | Element | HTMLElement | DocumentFragment)
*/
public prepend(childNode: NodeType | string): Element | DocumentFragment | boolean {
    if (typeof childNode === 'string') {
        const fragment: DocumentFragment = document.createDocumentFragment();
        const collection: NodeList = this.parser.parseFromString(childNode, 'text/html').body.childNodes;
        const doms: Element[] = [].slice.call(collection) as Element[];
        for (let [i, l]: number[] = [0, doms.length]; i < l; i++) fragment.appendChild(doms[i]);
        this.el.insertBefore(fragment, this.el.firstChild);
        return false;
    } else if (childNode instanceof Element || childNode instanceof DocumentFragment) {
        this.el.insertBefore(childNode, this.el.firstChild);
        return false;
    } else {
        return this.el;
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *