【Angular基礎講座】レンダリング後のhtmlの無駄にラップされている要素を無理やり剥がしてみる


2019/09/19

今回はAngularで、レンダリング出力後のDOMを直接操作するのに便利な
Renderer2モジュールを利用して、要らないhtml要素のラッピング剥がしを力技でやってみます。

ちなみに、ムダ要素のラッピング剥がし程度ならば、
Renderer2でDOMを無理クソ引っこ抜かなくても、`<ng-content>` + カスタムディレクテブというエレガントな方法もあるようです。

今回は
Renderer2で泥臭くラップ要素剥がしをやってみます。


検証 - 通常のレンダリング後のhtml出力

先ずレンダリングの後で、無駄にラップされているhtmlの一例を挙げます。

以下のように、孫コンポネート、子コンポネート、基準コンポネート(自分)、親コンポネート、親の親コンポネートを定義して、階層構造を作ります。

            
            import { Component } from '@angular/core';

@Component({
    selector: 'app-my-grandchild-comp',
    template: `<div>my-grandchild</div>`
})
export class MyGrandChildComponent {}

@Component({
    selector: 'app-my-child-comp',
    template: `
    <div>
        my-child
        <app-my-grandchild-comp></app-my-grandchild-comp>
    </div>
    `
})
export class MyChildComponent {}

@Component({
    selector: 'app-myself-comp',
    template: `
    <div>
        me
        <app-my-child-comp></app-my-child-comp>
    </div>
    `
})
export class MyselfComponent {}

@Component({
    selector: 'app-my-parent-comp',
    template: `
    <div>
        my-parent
        <app-myself-comp></app-myself-comp>
    </div>
    `
})
export class MyParentComponent {}

@Component({
    selector: 'app-my-grandparent-comp',
    template: `
    <div>
        my-grandparent
        <app-my-parent-comp></app-my-parent-comp>
    </div>
    `
})
export class MyGrandParentComponent {}
        
app.component.htmlの方からの呼び出しは、

            
            <app-my-grandparent-comp></app-my-grandparent-comp>
        
とすることで、レンダリングすると、以下のようなDOM構造になります。

合同会社タコスキングダム|蛸壺の技術ブログ

これは5回の階層構造で事足りるものが、コンポネート自体の要素のタグ名も合わさって、計10段の構造になってしまいました。

とはいえ、レンダリング後のHTMLが気になる方は、

            
            <div>
    my-grandparent
    <div>
        my-parent
        <div>
            me
            <div>
                my-child
                <div>
                    my-grandchild
                </div>
            </div>
        </div>
    </div>
</div>
        
のようにスッキリと出力したい場合もあると思います。

例えば、階層構造を持つ
<table>要素の段組みの際には<thead>, <tbody>, <tfoot>をそれぞれコンポネート化し、さらに<tr>の行要素もコンポネート化する…という応用などに今回のテクニックが利用できます。


Renderer2の基本

本題をご説明する前に、Renderer2の基本的な利用方法に言及します。

説明をしやすくするために、上のソースコードで定義した
MyselfComponentのタグ<app-myself-comp>を基点にしてみます。

先ずは以下のような属性ディレクテブを作成してDOMの情報をサンプリングしてみましょう。

            
            import {
    Directive, AfterViewInit, ElementRef, Renderer2
} from '@angular/core';

@Directive({
    selector: '[appDomManipulator]'
})
export class DomManipulatorDirective implements AfterViewInit {

    constructor(
        private el: ElementRef,
        private renderer: Renderer2
    ) { }

    ngAfterViewInit() {
        const itsMe = this.el.nativeElement; // 自分本体(この場合<app-myself-comp>)

        const parentsDiv = this.renderer.parentNode(itsMe); // <app-parent-comp>内の<div>
        const parent = this.renderer.parentNode(parentsDiv); // <app-parent-comp>本体

        const grandparentsDiv = this.renderer.parentNode(parent); // <app-grandparent-comp>内の<div>
        const grandparent = this.renderer.parentNode(grandparentsDiv); // <app-grandparent-comp>本体

        const myDiv = itsMe.childNodes[0]; // 自分の中の<div>
        const child = myDiv.childNodes[1]; // <app-child-comp>本体

        const childsDiv = child.childNodes[0]; // <app-child-comp>内の<div>
        const grandchild = childsDiv.childNodes[1]; // <app-grandchild-comp>本体

        console.log(grandparent);
        console.log(parent);
        console.log(itsMe);
        console.log(child);
        console.log(grandchild);
    }
}
        
この属性ディレクテブをMyParentComponent内に記述していた<app-myself-comp>に付与します。

            
            ...

@Component({
    selector: 'app-my-parent-comp',
    template: `
    <div>
        my-parent
        <app-myself-comp appDomManipulator></app-myself-comp>
    </div>
    `
})
export class MyParentComponent {}

...
        
さて、これをビルドしてブラウザのコンソールを確認すると、

合同会社タコスキングダム|蛸壺の技術ブログ

となって、基点のDOM(
<app-myself-comp>)からみて、親、親の親、子供、孫のDOMコンポネートがキャプチャされております。

ここでのポイントとして、
Renderer2の関数のうち、parentNodeを使って自分からみた一つ上の階層のDOM要素を手繰れます。

再度
parentNodeを繰り返すと、さらに上へ、さらに上へ...をキャプチャできる仕組みです。

子DOM要素も理屈は同じですが、
childNodesプロパティが子要素のノードが入った配列を返すので、お目当の要素番号を指定する必要があります。


insertBeforeで親子の縁を切る

Renderer2の組み込み関数insertBeforeで、DOM要素を移動することができます。

この関数を利用して、親子関係にあるDOMを移し変えることを考えます。

合同会社タコスキングダム|蛸壺の技術ブログ

この関数の用法をかいつまむと、

            
            Method:
    insertBefore(parent: any, newChild: any, refChild: any): void

Parameters:
    parent:
        新しく親にしたいDOMノード
    newChild:
        新たに子ノードとして移動したいDOMノード
    refChild:
        もともと親だったDOMノード
        
今回は、子DOM要素をもともとの親DOM要素の階層に移動したいので、insertBefore(親の親ノード, 子ノード, 親ノード)という感じで呼び出します。

ではこれを実行する属性ディレクテブを以下に示します。

            
            import {
    Directive, AfterViewInit, ElementRef, Renderer2
} from '@angular/core';

@Directive({
    selector: '[appUnwrapDom]'
})
export class UnwrapDomDirective implements AfterViewInit {

    constructor(
        private el: ElementRef,
        private renderer: Renderer2
    ) { }

    ngAfterViewInit() {
        const myDiv = this.el.nativeElement; // <app-***>の中身(=この場合指定した<div>)
        const parent = this.renderer.parentNode(myDiv); // 親要素の<app-*****>
        const superparent = this.renderer.parentNode(parent); // 親要素の一つ上
        this.renderer.insertBefore(superparent, myDiv, parent);
    }

}
        
そして、この属性ディレクテブを子要素(<div>...</div>)に割り当てて、親要素<app-***>から剥ぎ取りましょう。

具体的には以下のように変更します。

            
            import { Component } from '@angular/core';

@Component({
    selector: 'app-my-grandchild-comp',
    template: `<div appUnwrapDom>my-grandchild</div>`
})
export class MyGrandChildComponent {}

@Component({
    selector: 'app-my-child-comp',
    template: `
    <div appUnwrapDom>
        my-child
        <app-my-grandchild-comp></app-my-grandchild-comp>
    </div>
    `
})
export class MyChildComponent {}

@Component({
    selector: 'app-myself-comp',
    template: `
    <div appUnwrapDom>
        me
        <app-my-child-comp></app-my-child-comp>
    </div>
    `
})
export class MyselfComponent {}

@Component({
    selector: 'app-my-parent-comp',
    template: `
    <div appUnwrapDom>
        my-parent
        <app-myself-comp></app-myself-comp>
    </div>
    `
})
export class MyParentComponent {}

@Component({
    selector: 'app-my-grandparent-comp',
    template: `
    <div appUnwrapDom>
        my-grandparent
        <app-my-parent-comp></app-my-parent-comp>
    </div>
    `
})
export class MyGrandParentComponent {}
        
こうすることで、DOMの親子関係を変更することができました。

合同会社タコスキングダム|蛸壺の技術ブログ


不要なDOMを削除するremoveChild()メソッド

さて、親子関係の順序は入れ替えることができましたが、残った<app-***>は綺麗に消しておきたいところです。

ということで、
Renderer2のAPIメソッドであるremoveChildを利用します。

用法は
insertBeforeと比べるとわかりやすく、removeChild(消したいノードの親ノード, 消したいノード)とします。

上記の
unwrap-dom.directive.tsを以下のように変更します。

            
            import {
    Directive, AfterViewInit, ElementRef, Renderer2
} from '@angular/core';

@Directive({
    selector: '[appUnwrapDom]'
})
export class UnwrapDomDirective implements AfterViewInit {

    constructor(
        private el: ElementRef,
        private renderer: Renderer2
    ) { }

    ngAfterViewInit() {
        const myDiv = this.el.nativeElement; // <app-***>の中身(=この場合指定した<div>)
        const parent = this.renderer.parentNode(myDiv); // 親要素の<app-*****>
        const superparent = this.renderer.parentNode(parent); // 親要素の一つ上
        this.renderer.insertBefore(superparent, myDiv, parent);

        // 残留した<app-***>を消去
        this.renderer.removeChild(superparent, parent);
    }

}
        
こうすることで、

合同会社タコスキングダム|蛸壺の技術ブログ

と綺麗さっぱりなDOM構造を得ました。


まとめ

今回考察した、angular独特のレンダリング後DOMのアンラップ程度なら、<ng-content></ng-content>を適所に使った方がスッキリとコーディングできてよろしいと思います。

ですが、angular組み込みの構造ディレクテブ(
ngIf, ngIf, ngSwitch,...)を含んだコンポネートを多用すればするほど、htmlレンダリング後におびただしい数のムダなラップ要素やハグレ要素が出力されてしまいます。

こういった複雑なDOMになってしまう場合では、あらかじめDOMの出力位置が予想できていればこその
<ng-content>による解法でやろうと思うと、意図せずng-contentを入れ子構造にしてしまった場合など、レンダリングの挙動がおかしくなり、もうわけのわからん結果に陥るかもしれません。

レンダリング後のHTMLにかなり複雑な処理をする際には、今回のように
Renderer2を用いてDOMの最適化を図ると良いと思います。


参考

今回Renderer2を今更ながら取り上げてしまいましたが、Angular8以降は`Ivy`というレンダリングエンジンに取って代わられてしまうことになっております。

実質上の
Renderer3であるIvyが現在の標準レンダラですが、温故知新、今回のRenderer2をよく知ることでIvyへの理解が深まると感じます。

今回の内容も今後Ivy版でお届けできたらいいな、と思います。

また、
Renderer2で出来ることをまとめている海外のブログがありますので、ご参考ください。

Angular 4 Renderer2 Example

この記事の内容はだいたい Angular Cli 4+をターゲットに書いており、さらっと
AfterViewInitを実装していたり、なんともAngular独特の概念や前知識を割愛しております。

もうちらほらと11の話も出てきている時期で何かと参考書籍の鮮度も気になって参りますが、
Ivyの基本的な利用方法の背景を知りたい方や、また基礎的なangular2+の使いこなしからしっかり身につけておきたい方などには、ちょっと一昔前のangular本で基礎固めしていただくのも良いかなーと思ってしまいます。


Angularの学び方

主要なJavascriptのフレームワークの一角であるAngularは、現在Google主導で開発が進められている玄人フロントエンジニア向けのフレームワークです。

Angularを使えばかなり高度なWebアプリも自由に作成できるのですが、やはり初心者が独学で勉強しようとおもうとかなりの知識が必要でAngularは挫折しやすいと言われています。

特にAngularを使う上で注意が必要なのは半年に一回のペースでプログラムのメジャーバージョンが繰り上がるので、他のフレームワークと比べても格段に技術を追っかけるスピードが早いことも躓く人が多い一因になっているとも思います。

Udemyの動画講座では、基礎からじっくり学べるためより実践的なスキルを身につけることが可能です。以下の講座ではAngular初心者向けに簡単なプロジェクトの作成まで学ぶことができます。

より実践的にフロントエンジニアとして重宝されるためにはSPA(シングルページアプリケーション)をある程度作成できるスキルが必要になりますので、さらに学びたい方は以下のような講座も用意されています。

他にもAngularプログラミングの習得度に応じた講座も多数用意されているので、必要に応じて試されてはいかがでしょうか。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。