今回は、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>