follmy

JavaScriptで正規表現を使った要素の置き換えと任意の場所への追加

2020-09-01

今作っているWebサービスで、特定のボタンをクリックすると、特定の要素が追加されるという処理を書いてみました。

手順

下記の手順で追加処理が作れました。

1 置換したい一連の要素を取得 2 要素を置換できるようにするために要素を文字列に変換
3 タグ毎に処理を実施したいのでタグ毎に分割して配列にする
4 配列に入ったタグをループで回し特定の処理を実施して新しい配列を作成
 4-1 タグが<input>なら特定の処理を実施して配列にプッシュ
 4-2 タグが<input>以外ならそのまま配列にプッシュ
5 文字列を要素に変換して追加したい場所に追加する

inputの時に実施する処理は、特定の文字列を置換したい文字列に置き換えるという処理と、要素内のvalueの初期化です。

完成形の処理

先に完成形の処理を記載しておきます。

// 複製して追加したい要素
const originTags = `<div>
                      <div>
                        <input type="hidden" value="8" name="form[hoges_attributes][0][id]" id="hoges_attributes_0_hoge">
                        <input type="hidden" value="1" name="form[hoges_attributes][0][hoge_hoge]" id="hoges_attributes_0_hoge_hoge">
                      </div>
                      <div>
                        <input type="text" value="hello" name="form[hoges_attributes][0][hoge_hoge_hoge]" id="form_hoges_attributes_0_hoge_hoge_hoge">
                      </div>
                    </div>`

const tagRegex = /<.*?>/g;
let tags = originTags.match(tagRegex);

// 置換用数字を作成する関数
let c = 0;
const newIndex = function () {
  let num = new Date().getTime() + c++;
  return num;
}

// 置換処理用関数
const replaceIndex = (item, newIndexOne, newIndexTwo) => {
  item = item.replace(/\[\d{1,}\]/, newIndexOne) ? item.replace(/\[\d{1,}\]/, newIndexOne) : item;
  item = item.replace(/_\d{1,}_/, newIndexTwo) ? item.replace(/_\d{1,}_/, newIndexTwo) : item;
  return item
}

// input検出用正規表現
const inputRegex = /<input.*?>/;

// 配列ループと置換処理
const createNewTags = (tags) => {
  // 新しい配列
  let newTags = [];
  let num = newIndex();
  let newIndexOne = "[" + num + "]";
  let newIndexTwo = "_" + num + "_";
  tags.forEach(tag => {
    if (tag.match(inputRegex)) {
      // inputに一致した場合は、処理をしてから配列にプッシュ
      newTags.push(replaceIndex(tag, newIndexOne, newIndexTwo));
    } else {
      // inputに一致しなかったらそのまま配列にプッシュ
      newTags.push(tag);
    };
  });
  return newTags;
}

// divの作成関数
const createDivElem = (className) => {
  let div = document.createElement('div');
  div.className = className;
  return div
}

// 追加したい要素のdivのクラス名
let className = 'hoge';

// 文字列を要素に変換
const createNewElem = (className, tagElem) => {
  let div = createDivElem(className);
  let tags = createNewTags(tagElem);
  let html = "";
  // 配列のタグ文字列をすべて連結して一つの文字列に
  // divにinnerHTMLとして突っ込む時に一つ一つ突っ込むと勝手に閉じタグがつくられておかしくなる
  tags.forEach(tag => {
    html += tag;
  })
  div.innerHTML = html;
  return div
}

置換したい一連の要素を取得

let t = document.querySelector(".hoges");

要素を置換できるようにするために要素を文字列に変換

let s = t.innerHTML;

タグ毎に処理を実施したいのでタグ毎に分割して配列にする

<>のタグ毎に要素を分割したいので、正規表現で<>を取得し配列を作成

// 正規表現のあとにgを指定すると検索範囲の中で一致する要素を全て取得し、
// 一致毎に分割して配列を作成してくれる
const tagRegex = /<.*?>/g;
let tags = s.match(tagRegex);

ポイントとしては、?をつけているところ。

「*、+、?、{} といった量指定子の直後に使用した場合、その量指定子をスキップ優先(最小回数にマッチ)にする。これはデフォルトとは逆であり、デフォルトは繰り返し優先(最大回数にマッチ)。
引用元:https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Writing_a_Regular_Expression_Pattern

要は、<input><input>みたいな文字列があった時に、/<input.*>/とすると下のように取れてしまう。

const string = "<input><input>";
string.match(/<input.*>/g)[0];
// => ["<input><input>"]

最大の部分、つまり最大一致を取得してしまう困った。
これを<input>と<input>で分割して取得するために、最大の部分ではなく最小の一致で取得する必要がある。そこで、

string.match(/<input.*?>/g)
// => (2) ["<input>", "<input>"]

と?をつけることで、最小の一致部分が取得できるようになる。

また、特定の文字を含まない文字の連続という表現で [^]* という正規表現でも取得できた。ここでは”>“の文字を含まない文字の連続という表現をしたいので、 [^!>]* という形でも取得できた。記号を含まないという表現をしたい場合に ^> ではなく ^!> としないといけないらしい。

string.match(/<input[^!>]*>/g)
// => (2) ["<input>", "<input>"]

配列に入ったタグをループで回し特定の処理を実施して新しい配列を作成

配列に入ったタグをループで回す

tags.forEach(tag){

}

タグが<input>なら特定の処理を実施して配列にプッシュ

let newTags = [];
tags.forEach(tag){
  if (tag.match(inputRegex)) {
    // inputだったときの処理
  } else {
    // input以外の処理
  };
};

タグが<input>以外ならそのまま配列にプッシュ

let newTags = [];
tags.forEach(tag){
  if (tag.match(inputRegex)) {
    // inputだったときの処理
  } else {
    newTags.push(tag);
  };
};

文字列を要素に変換して追加したい場所に追加する

作った配列はただの文字列の塊なので、HTMLに追加できるように要素の形にする必要があります。処理としてはシンプルで、作成したタグ文字列の入っている配列をループ処理して連結し、innerHTMLでつっこむという処理です。

こんなHTMLを作りたいとして

<div class="hoges">
  <div>
    <input type="hidden" value="8" name="form[hoges_attributes][0][id]" id="hoges_attributes_0_hoge">
    <input type="hidden" value="1" name="form[hoges_attributes][0][hoge_hoge]" id="hoges_attributes_0_hoge_hoge">
  <div>
    <input type="text" value="hello" name="form[hoges_attributes][0][hoge_hoge_hoge]" id="form_hoges_attributes_0_hoge_hoge_hoge">
  </div>
</div>

まず一番外側のhogesクラスのdivを作るための関数を用意し

// divの作成関数
const createDivElem = (className) => {
  let div = document.createElement('div');
  div.className = className;
  return div
}

配列で

// 追加したい要素のdivのクラス名
let className = 'hoge';

// 文字列を要素に変換
const addTags = (className, "連結したいタグ文字列の入っている配列") => {
  let div = createDivElem(className);
  let tags = "連結したいタグ文字列の入っている配列";
  let html = "";
  // 配列のタグ文字列をすべて連結して一つの文字列に
  // divにinnerHTMLとして突っ込む時に一つ一つ突っ込むと勝手に閉じタグがつくられておかしくなる
  tags.forEach(tag => {
    html += tag;
  })
  div.innerHTML = html;
  return div
}

これで要素ができたから、あとは突っ込みたいところを取得してつっこむ。

// たとえば
// 指定したクラスの最後の要素の次に追加
let hoges = document.querySelectorAll('.hoges');
let hoge = createTask(className, tags);
hoges[hoges.length - 1].append(hoge);

という感じです。

inputだったときの処理

inputだったときの処理としては、特定の文字列を新しい文字列に置き換えるという処理 具体的には、こんな要素があったときに

[0] → [08090]
_0_ → _08090_

みたいな任意の桁数の数字が入った文字列に置き換えたい

<input type="hidden" value="1" name="form_table[hoges_attributes][0][hogehoge]" id="form_table_hoges_attributes_0_hogehoge">

[任意の桁の数字]を置き換える

ex [91039]

// 一文字の数字に一致
\d

// 直前の文字をn回以上と一致
{n.}

// 任意の桁数の数字と一致
\d{n,}

// []と一致、[]はエスケープが必要な記号なので\(バックスラッシュ)でエスケープ
\[\]

// 完成
\[\d{1,}\]

// 正規表現は//で囲む必要があるので
/\[\d{1,}\]/

これを使って置き換えるには

"置換したい文字列のある要素".replace(/\[\d{1,}\]/, 置き換えたい要素)

でできる。

任意の桁の数字を置き換えたい

これも基本は同じ

ex _91039_

// 一文字の数字に一致
\d

// 直前の文字をn回以上と一致
{n.}

// ただのアンダーバー二つ、エスケープいらない
__

// 完成
_\d{1,}_

// 正規表現は//で囲む必要があるので
/_\d{1,}_/

同じように置き換え

"置換したい文字列のある要素".replace(/_\d{1,}_/, 置き換えたい要素)

置き換えたい要素をつくる

今回は [08090] や _08090_ みたいな文字列を作りたい。 文字列は

// 任意の桁数の数字
let num = 08090;
"[" + num + "]"
"_" + num + "_"

こんな感じで作れるので、あとはnumを作れたらいける。

numはこんな感じで作成したタイミングの日時とカウンターを結合してつくる。

let c = 0;
const newIndex = function () {
  let num = new Date().getTime() + c++;
  return num;
}

これはここのを参考にし(パクリ)ましたすいません。

https://github.com/nathanvda/cocoon/blob/master/app/assets/javascripts/cocoon.js

置き換えたい要素を使って置き換える

作った置き換えたい要素を使って置き換えていく。

置き換えたい対象の要素は

let item = `<input type="hidden" value="1" name="form_table[hoges_attributes][0][hogehoge]" id="form_table_hoges_attributes_0_hogehoge">`

置き換えに使う要素は

let c = 0;
const newIndex = function () {
  let num = new Date().getTime() + c++;
  return num;
}

これを使って置き換えるとこうなる

// 置き換える文字列の存在する要素
let item = `<input type="hidden" value="1" name="[hoges_attributes][0][hogehoge]" id="hoges_attributes_0_hogehoge">`

// 文字列に使う任意の桁数の数字を持った文字列を作成
let num = newIndex();
let newIndexOne =  "[" + num + "]";
let newIndexTwo = "_" + num + "_";
item = item.replace(/\[\d{1,}\]/, newIndexOne);
item = item.replace(/_\d{1,}_/, newIndexTwo);

関数にしてみる

配列に直接置き換えの処理を書くと見辛くなるので、置き換えの処理だけを関数にしてみる。引数は「item: 置き換えたい対象の要素, newIndexOne: 置き換え用文字列, newIndexTwo: 置き換え用文字列」。

const replaceIndex = (item, newIndexOne, newIndexTwo) => {
  item = item.replace(/\[\d{1,}\]/, nameIndex) ? item.replace(/\[\d{1,}\]/, nameIndex) : item;
  item = item.replace(/_\d{1,}_/, idIndex) ? item.replace(/_\d{1,}_/, idIndex) : item;
  return item
}

配列ループで処理にする

今まで作ってきた処理を組み込んで配列ループで処理する。

// 要素群
let tags = `<div>
              <div>
                <input type="hidden" value="8" name="form[hoges_attributes][0][id]" id="hoges_attributes_0_hoge">
                <input type="hidden" value="1" name="form[hoges_attributes][0][hoge_hoge]" id="hoges_attributes_0_hoge_hoge">
              </div>
              <div>
                <input type="text" value="hello" name="form[hoges_attributes][0][hoge_hoge_hoge]" id="form_hoges_attributes_0_hoge_hoge_hoge">
              </div>
            </div>`

// 要素群を配列で処理
let newTags = [];
let num = newIndex();
let newIndexOne =  "[" + num + "]";
let newIndexTwo = "_" + num + "_";
tags.forEach(tag){
  if (tag.match(inputRegex)) {
    // inputに一致した場合は、処理をしてから配列にプッシュ
    newTags.push(replaceIndex(tag, newNameIndex, newIdIndex));
  } else {
    // inputに一致しなかったらそのまま配列にプッシュ
    newTags.push(tag);
  };
};

ループ処理と置換処理を関数化したバージョンがこれ。

const createNewTags = (tags) => {
  // 新しい配列
  let newTags = [];
  let num = newIndex();
  let newIndexOne = "[" + num + "]";
  let newIndexTwo = "_" + num + "_";
  tags.forEach(tag) {
    if (tag.match(inputRegex)) {
      // inputに一致した場合は、処理をしてから配列にプッシュ
      newTags.push(replaceIndex(tag, newNameIndex, newIdIndex));
    } else {
      // inputに一致しなかったらそのまま配列にプッシュ
      newTags.push(tag);
    };
  };
  return newTags;
};

完成

これを全部結合させると、冒頭に記載した完成形になります。あとはonclickでaddTags()を呼べば要素が追加されます。

// 複製して追加したい要素
const originTags = `<div>
                      <div>
                        <input type="hidden" value="8" name="form[hoges_attributes][0][id]" id="hoges_attributes_0_hoge">
                        <input type="hidden" value="1" name="form[hoges_attributes][0][hoge_hoge]" id="hoges_attributes_0_hoge_hoge">
                      </div>
                      <div>
                        <input type="text" value="hello" name="form[hoges_attributes][0][hoge_hoge_hoge]" id="form_hoges_attributes_0_hoge_hoge_hoge">
                      </div>
                    </div>`

const tagRegex = /<.*?>/g;
let tags = originTags.match(tagRegex);

// 置換用数字を作成する関数
let c = 0;
const newIndex = function () {
  let num = new Date().getTime() + c++;
  return num;
}

// 置換処理用関数
const replaceIndex = (item, newIndexOne, newIndexTwo) => {
  item = item.replace(/\[\d{1,}\]/, newIndexOne) ? item.replace(/\[\d{1,}\]/, newIndexOne) : item;
  item = item.replace(/_\d{1,}_/, newIndexTwo) ? item.replace(/_\d{1,}_/, newIndexTwo) : item;
  return item
}

// input検出用正規表現
const inputRegex = /<input.*?>/;

// 配列ループと置換処理
const createNewTags = (tags) => {
  // 新しい配列
  let newTags = [];
  let num = newIndex();
  let newIndexOne = "[" + num + "]";
  let newIndexTwo = "_" + num + "_";
  tags.forEach(tag => {
    if (tag.match(inputRegex)) {
      // inputに一致した場合は、処理をしてから配列にプッシュ
      newTags.push(replaceIndex(tag, newIndexOne, newIndexTwo));
    } else {
      // inputに一致しなかったらそのまま配列にプッシュ
      newTags.push(tag);
    };
  });
  return newTags;
}

// divの作成関数
const createDivElem = (className) => {
  let div = document.createElement('div');
  div.className = className;
  return div
}

// 追加したい要素のdivのクラス名
let className = 'hoge';

// 文字列を要素に変換
const createNewElem = (className, tagElem) => {
  let div = createDivElem(className);
  let tags = createNewTags(tagElem);
  let html = "";
  // 配列のタグ文字列をすべて連結して一つの文字列に
  // divにinnerHTMLとして突っ込む時に一つ一つ突っ込むと勝手に閉じタグがつくられておかしくなる
  tags.forEach(tag => {
    html += tag;
  })
  div.innerHTML = html;
  return div
}

参考

https://qiita.com/iLLviA/items/b6bf680cd2408edd050f https://ics.media/entry/200825/ https://qiita.com/raccy/items/aac3b8e3981564bbd1fa https://github.com/nathanvda/cocoon/blob/master/app/assets/javascripts/cocoon.js https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Writing_a_Regular_Expression_Pattern


プロフィール

koyamaaa2です。

プライバシーポリシー