仁和歌ブログ

情報のインプットしかしてこなかった自分が、情報のアウトプットに挑戦していくブログ。

document.createElementの初期化をワンライナーで書きたい

TL;DR

let textInput;
document.body.append(
  ((elm=document.createElement('div'))=>(e=>e)(elm
    ,elm.id='box'
    ,elm.class...

let textInput;
document.body.append(
  ((elm=document.createElement('div'))=>(e=>e)(elm
    ,elm.id='box'
    ,elm.classList.add('box')
    ,elm.append(
      //左ボタン
      ((lBtn=document.createElement('button'))=>(e=>e)(lBtn
        ,lBtn.id="lBtn"
        ,lBtn.classList.add('item')
        ,lBtn.classList.add('btn')
        ,lBtn.innerText="左"
        ,lBtn.addEventListener('click',(ev)=>{
          textInput&&(textInput.value="←")
        })
      ))()
    )
    ,elm.append(
      //値表示
      textInput=((elm=document.createElement('input'))=>(e=>e)(elm
        ,elm.id="textInput"
        ,elm.classList.add('item')
        ,elm.value=""
      ))()
    )
    ,elm.append(
      //右ボタン
      ((rBtn=document.createElement('button'))=>(e=>e)(rBtn
        ,rBtn.id="rBtn"
        ,rBtn.classList.add('item')
        ,rBtn.classList.add('btn')
        ,rBtn.innerText="右"
        ,rBtn.addEventListener('click',(ev)=>{
          textInput&&(textInput.value="→")
        })
      ))()
    )
    ,elm.append(
      //style
      ((style=document.createElement('style'))=>(e=>e)(style
        ,style.textContent=`/*injection style*/
.box {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: stretch;
  justify-content: space-between;
}
.item {
  flex-grow: 1;
}
#textInput {
  text-align: center;
}
`
      ))()
    )
  ))()
);

背景

私はよく、既存サイトを使いやすくするために、プレーンなJS(ES6)でサッと改造したりします。(例えば、 Gmailのツールチップを消したり )

その時、document.createElementで新規要素を作成して、画面に追加することがしばしばあります。

//div要素の作成
let elm = document.createElement('div');
//各種プロパティの設定
elm.id='elm1';
elm.classList.add('elm');
elm.style.color="red";
elm.innerText=1;
//bodyに追加
document.body.append(elm);

上記のように要素の作成とプロパティの初期化を行い、適当にbodyなどに追加します。

要素が1つだけなら良いのですが、大体は複数の要素をネストして目的のパーツを作成します。

let box = document.createElement('div');
box.id='box';
box.classList.add('flexbox');
let item1 = document.createElement('div');
item1.id='item1';
item1.classList.add('flexitem');
box.append(item1);
let item2 = document.createElement('div');
item2.id='item2';
item2.classList.add('flexitem');
box.append(item2);
document.body.append(box);

「要素の生成」と「プロパティの初期化」と「要素の追加」がグッチャグチャに入り混じってますね。

サッと改造するだけなので、記述順は毎回マチマチです。 先に要素の生成と追加を済ませて、後からプロパティを設定することもあります。

let box = document.createElement('div');
let item1 = document.createElement('div');
let item2 = document.createElement('div');
document.body.append(box);
box.append(item1);
box.append(item2);
box.id='box';
box.classList.add('flexbox');
item1.id='item1';
item1.classList.add('flexitem');
item2.id='item2';
item2.classList.add('flexitem');

そして毎回途中で「なんか見づらい…」となり、そして毎回「本当はメソッドチェインで書きたいなぁ」と思います。

例えばこんな風に。

document.body.append(
  document.createElement('div')
  .id='box';
  .append(
    document.createElement('div')
    .id='item1'
    .classList.add('flexitem');
  )
  .append(
    document.createElement('div');
    .id='item2'
    .classList.add('flexitem');
  )
)

jQueryとかではメソッドチェインで実装可能そうですね。

Dartとかではカスケード記法(ダブルドット記法)というのが作られているらしく、似たような書き方ができるようです。

他にも同じような悩みを持っている人がいたのでしょうか。

ただ、私はES6で書きたい…

F12で既存サイトを改造するのに、わざわざライブラリ入れたりトランスパイラ噛ませたりしたくない…

というわけで、ES6だけで出来る方法を編み出しました。

本題

まず、ワンライナーでdocument.createElement&初期化してみる。

let elm = ((elm=document.createElement("div"))=>(e=>e)(elm/*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/))();

コピペで使いやすいように、プロパティの初期化部分はコメントアウトしておきました。

改行や中括弧を足して、構造を見ていきましょう。

let elm = (
  (elm=document.createElement("div")) => {//引数elmの初期値で要素の作成
    return (//「第一引数をそのまま返すアロー関数」の戻り値をそのまま返す
      e=>e//第一引数をそのまま返すアロー関数
    )(elm//「第一引数をそのまま返すアロー関数」の第一引数に要素を渡して実行
/*第二引数以降はプロパティの初期化
     ,elm.id='hoge'
     ,elm.classList.add('hoge')
     ,elm.innerText='hoge'
*/)
  }
)();//これらを即実行して、作成した要素を変数に挿入

アロー関数無しで書いた方が伝わる部分もあると思うので、functionでの書き方でも書いておきます。

let elm = (
  function(elm=document.createElement("div")) {
    return (
      function(e){return e}
    )(elm/*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/)
  }
)();

2つの無名関数を即実行し、内側の関数実行の余剰引数でプロパティ設定を済ませてしまっているわけです。

ポイントは3点。

  • 第一引数をそのまま返すアロー関数
  • 「第一引数をそのまま返すアロー関数」を実行する際の第二引数以降でプロパティを設定
  • 引数elmの初期値で要素の作成

順に説明していきます。

第一引数をそのまま返すアロー関数

まずは第一引数をそのまま返すアロー関数について。

ワンライナーで書くと、何処がそれに該当するのかわかりづらいので、該当の関数だけ抜き出します。

//アロー関数
e=>e
//function
function(e){return e}

これを実行すると、第一引数をそのまま返します。そのまんまです。

この関数の役割は、「document.createElementした要素を呼び出し元へ返す」と「第二引数以降を無視する」の2点になります。

「第二引数以降を無視する」については、次の節で解説します。

「第一引数をそのまま返すアロー関数」を実行する際の第二引数以降でプロパティを設定

「第一引数をそのまま返すアロー関数」は、引数を一つしか取りません。

そのため、第二引数以降(余剰引数)は「実行はされるが無視される」ということになります。(参照することも可能ですが今回は使用しません)

(e=>e)(
  elm//第一引数は呼び出し元に返される
  ,elm.id='hoge'//余剰引数
  ,elm.classList.add('hoge')//余剰引数
  ,elm.innerText='hoge'//余剰引数
)

余剰引数に指定した式は実行されるので、プロパティの設定は実行され、第一引数に指定した要素のみが呼び出し元に返されることになります。

これによりメソッドチェインのように「処理実行後に自身を返却する」という動きになります。

引数elmの初期値で要素の作成

無名関数が2つあるのでわかりづらいですが、外側の関数だけを抜き出せば何をしているのかわかりやすいと思います。

//アロー関数
((elm=document.createElement("div"))=>elm)()
//function
(function(elm=document.createElement("div")) {
  return elm
})()

内側の無名関数をelmに置き換えると、「引数elmの初期値で要素を作成し、それをそのまま返却しているだけ」になります。

なので、これと先程までの関数を組み合わせて「要素の生成」と「プロパティの初期化」を実現しているわけです。

他の方法

余談ですが、要素の作成は引数elmの初期値で作成しなくても実現可能です。

let elm = (
  function(elm) {
    return (
      function(e){return e}
    )(elm/*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/)
  }
)(document.createElement("div"));//これでも可能だが、div要素を生成したことが後ろを見ないとわからないのは使い勝手が悪い

これでも問題ないのですが、document.createElement("div")が最後にくると読みづらいため、引数elmの初期化で要素の生成をおこなっています。

使用例

このワンライナーを使えば、前章の例は以下のように書けます。

let box = ((elm=document.createElement('div'))=>(e=>e)(elm,elm.id='box',elm.classList.add('flexbox')))();
let item1 = ((elm=document.createElement('div'))=>(e=>e)(elm,elm.id='item1',elm.classList.add('flexitem')))();
let item2 = ((elm=document.createElement('div'))=>(e=>e)(elm,elm.id='item2',elm.classList.add('flexitem')));
document.body.append(box);
box.append(item1);
box.append(item2);

これで綺麗に「要素の生成と初期化」と「要素の追加」に分けることができました。

ただ、こままだと初期化するプロパティが増えたとき、追加する場所が探しずらいです。 実用の際には以下のように改行して可読性を上げるのが良いでしょう。

let box = ((elm=document.createElement('div'))=>(e=>e)(
  elm
  ,elm.id='box'
  ,elm.classList.add('flexbox')
))();
let item1 = ((elm=document.createElement('div'))=>(e=>e)(
  elm
  ,elm.id='item1'
  ,elm.classList.add('flexitem')
))();
let item2 = ((elm=document.createElement('div'))=>(e=>e)(
  elm
  ,elm.id='item2'
  ,elm.classList.add('flexitem')
))();
document.body.append(box);
box.append(item1);
box.append(item2);

要素の追加もワンライナーで行う

ここまできたら全てワンライナーにしてしまいましょう。

document.body.append(
  ((elm=document.createElement('div'))=>(e=>e)(
    elm
    ,elm.id='box'
    ,elm.classList.add('flexbox')
    ,elm.append(
      ((elm=document.createElement('div'))=>(e=>e)(
        elm
        ,elm.id='item1'
        ,elm.classList.add('flexitem')
      ))()
    )
    ,elm.append(
      ((elm=document.createElement('div'))=>(e=>e)(
        elm
        ,elm.id='item2'
        ,elm.classList.add('flexitem')
      ))()
    )
  ))()
);

ほぼ「メソッドチェイン的に書きたい」と思っていたのに近い書き方になりました。

まとめ

ワンライナーで要素の生成と初期化ができるようになりました。

let elm = ((elm=document.createElement("div"))=>(e=>e)(elm/*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/))();

要素の追加も行うなら、改行とインデントはしっかりした方が良いでしょう。

document.body.append(
  ((elm=document.createElement('div'))=>(e=>e)(
    elm
    /*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/
    ,elm.append(
      ((elm=document.createElement('div'))=>(e=>e)(
        elm
        /*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/
      ))()
    )
    ,elm.append(
      ((elm=document.createElement('div'))=>(e=>e)(
        elm
        /*,elm.id='hoge',elm.classList.add('hoge'),elm.innerText='hoge'*/
      ))()
    )
  ))()
);

以上。