初心者コーダーのための
HTML/CSS JavaScript PHP WordPress
難しい言葉や概念より、実践重視の入門ブログ
JavaScript

【JavaScript】1文字ずつspanタグで囲む方法【reduceの使い方】

【JavaScript】reduce テキストを1文字ずつ分割しよう!

今回は、reduceというものを使って、
要素のテキストを1文字ずつspanタグで囲んでいく方法を紹介します。

これを使えるようになると、こんなことができるようになります

ハロー!エブリワン!

これはプレーンのテキストを無限にジャンプさせているダサいアニメーションですが、スクロールで現れるアニメーションの演出や、ロード画面のアニメーションなど、その用途は様々です。

これをマスターすると、ウェブサイトでの演出の幅が大きく広がります!

サンプルコードの使い方

コード右上のアイコンをクリックすることでコピーできます。
全てのサンプルコードは、空のhtmlファイルにコピペするだけで結果を確認することができます。

それでは本文へ、レッツゴー!

1文字ずつspanタグで囲んでいく方法

残念ながら、1行の記述で済ませられる便利なメソッドはありません。

そのため、まずはどのような流れで1文字ずつspanタグで囲んでいくのかを見ていきましょう。

流れ

1文字ずつspanタグで文字を囲んでいくためには、以下のプロセスが必要です。

① 要素内のテキストを取得し、1文字ずつ分割して配列に格納する
② 配列をspanタグで囲み、全てを結合させる(ここでreduceを使う)
③ 要素内のテキストを、②で上書きする

それでは、実際に見てみましょう。

配列について分からない方はこちら

サンプルコード

「こんにちは」を1文字ずつspanタグで囲んでいきます!

<p class="sampleText">こんにちは</p>

<script>

/* 要素の取得 */
const sampleText = document.querySelector('.sampleText')

/* 取得した要素内のテキストを1文字ずつ配列に格納 */
const sampleTextArr = sampleText.textContent.split('');

/* 配列を順にspanタグで囲み、結合していく */
const sampleTextInner = sampleTextArr.reduce(function(acc, cur){
    console.log(`現在の累積値:${acc}\n今回の値:${cur}`);
    return `${acc}<span>${cur}</span>`;
},'');

console.log(sampleTextInner);

/* 要素の中身を上書き */
sampleText.innerHTML = sampleTextInner;

</script>

constについて分からない方はこちら
querySelectorについて分からない方はこちら
console.logについて分からない方はこちら
functionについて分からない方はこちら
textContent、innerHTMLについて分からない方はこちら

pタグだけで囲んであった「こんにちは」が、<span>こ</span><span>ん</span>....と1文字ずつspanタグで囲まれているのが、検証ツールで確認できますね。

ですが、検証ツールで注目すべきはconsoleの方です!

一体...何が起こってるんだ....。

それでは、reduceメソッドの謎に迫っていきましょう。

reduceメソッドとは?その仕様と使い方

reduceは、配列の要素を[0]から1つずつ順番に、指示通りに累積していってくれるメソッドです。

全ての累積値・合計値をピャッと出してくれるのではなく、1つずつ順番に指示通り累積しています。
つまりforeachのような、繰り返し処理が行われています。

先ほどは文字列を1つずつspanで囲んで結合させていっていましたが、数字の計算にも対応しています。

reduceの使い方

以下の記述がreduceの基本的な使い方です。

const 変数名 = 配列.reduce(function(acc, cur){
    return acc + cur;
}, 初期の累積値);

結果として、変数名に累積値が格納されます。

reduceの第一引数には関数第二引数には初期の累積値が入ります。
第二引数は必須ではなく、何も設定しなかった場合は配列の一番最初の値が入ります。
詳しくはサンプルコードを見ていきましょう。

サンプルコード

1000, 200, 30, 4 の4つの数字を、2倍しながら足していくサンプルコードです。

第二引数に0を設置した場合と、何も設定しなかった場合との違いに注目してみましょう。

<script>
const sampleArr = [1000, 200, 30, 4];

/* 第二引数に0を設置した場合 */
const total = sampleArr.reduce(function(acc, cur){
    console.log(`現在の累積値:${acc}\n今回の値:${cur}`);
    return acc + cur * 2;
}, 0);

console.log(`合計値:${total}`);

/* 第二引数を設定しなかった場合 */
const total2 = sampleArr.reduce(function(acc, cur){
    console.log(`現在の累積値:${acc}\n今回の値:${cur}`);
    return acc + cur * 2;
});

console.log(`合計値:${total2}`);
</script>

reduceの仕組み

まずはreduceの第一引数に入る関数について。

その関数の第一引数(acc)には累積値が、第二引数(cur)は処理中の配列の値が、繰り返し格納されていきます。

そしてreturnで、累積値をまるごと返すという仕組みです。

次に第二引数の、初期の累積値について。

第二引数を設定すると、その値が一番最初のaccに入り、配列の一番最初の値から順に処理が行われます。
第二引数を設定しなかった場合、配列の一番最初の値が一番最初のaccに入り、配列の2番目の値から処理が始まってしまいます。
そのため、サンプルコードで第二引数を設定しなかった方では、配列の一番最初の値10000が2倍されることなくaccに入ってしまい、合計値に差が出てしまったのです。

文字を1つずつ飛び跳ねさせる!ハロー!エブリワン!

解説は割愛しますが、せっかくなので一番最初に見た1文字ずつ飛び跳ねる「ハロー!エブリワン!」のコードを紹介します。

サンプルコード

<style>
/* ジャンプするアニメーション*/
@keyframes jumping {
    0%{transform: translateY(0);}
    13.33%{transform: translateY(-.5em);}
    20%{transform: translateY(0);}
}
/* ジャンプするクラス
これを後で生成するspanタグに付与する
*/
.jumping{
    display: inline-block;
    animation: jumping 1.5s ease-in-out infinite;
}
</style>

<!-- 今回飛び跳ねる子たち -->
<p class="target" style="font-size: 1.2em;font-weight: bold;">ハロー!エブリワン!</p>

<script>

/* 要素の取得 */
const target = document.querySelector('.target');

/* 1文字ずつspanタグを付与 */
function SpanSplit(e){
    // 要素のテキスト分割し、1文字ずつ配列に格納する
    const elementTextArr = e.textContent.split('');
    // 1文字ずつspanタグで囲み、全てを結合させる
    const elemetInner = elementTextArr.reduce(function(acc, cur){
        return `${acc}<span>${cur}</span>`;
    },'');
    // 要素の中身を、spanタグで囲んだ文字列で上書きする
    e.innerHTML = elemetInner;
}
SpanSplit(target);

/* 先ほど生成したspanタグの取得 */
const targetSpan = document.querySelectorAll('.target > span');

/* アニメーション */
for(let i = 0; i < targetSpan.length; i++){
    // spanタグに.jumpingを付与
    targetSpan[i].classList.add('jumping');
    // 順番に応じてアニメーションのタイミングをずらしていく
    targetSpan[i].style.animationDelay = `calc(${i} * .1s)`;
}

</script>

forについて分からない方はこちら
classList、styleについて分からない方はこちら

【発展】brタグを無視して1文字ずつspanタグで囲む

pタグ内のbrタグを無視して1文字ずつspanタグで囲みたい時があります。

少し内容がややこしいので解説は割愛します。
サンプルコードを置いておきますので、自由に使いまわしてください。

サンプルコード

<p class="target">ハロー!<br>エブリワン!</p>

<script>

/* 要素の取得 */
const target = document.querySelector('.target');

function SpanSplit(e) {

    /* 取得した要素をnode別に分けて格納 */
    const targetNodes = Array.from(e.childNodes);

    /* spanタグで囲んだ要素の中身を累積していく入れ物を用意 */
    let targetInner = "";

    /* テキスト部分のみspanタグで囲む処理 */
    targetNodes.forEach(node => {
        // テキストの場合
        if (node.nodeType == 3) {
            /* テキストを1文字ずつspanタグで囲む */
            targetInner += node.textContent.split('').reduce((acc, cur) => {
                return `${acc}<span>${cur}</span>`;
            }, "");

        // テキスト以外
        } else {
            // 何も手をつけずに累積
            targetInner += node.outerHTML;
        }
    });

    /* 中身を上書き */
    e.innerHTML = targetInner;
}

SpanSplit(target);

</script>