投稿日:2023年11月9日

先日、下記のような相談がありました。

申し込みフォームから登録したお客さまから、抽選(ランダム)で何名か抽出したい。

検証してみました。

条件として、

❶申し込み総数を上限値とすること。
❷抽出数は総数以下であること。
❸抽出結果に重複がないこと。
❹抽出結果を昇順でソートすること。

先にお見せします。今回作ったものは以下です。

 仕様

総数と抽出数が空欄だとアラートを出します。
総数より抽出数が多いとアラートを出します。
総数と抽出数に半角数字以外が入力されるとアラートを出します。
出力ボタンをクリックするごとに下部にランダムな抽出結果を表示します。
クリアボタンクリックで総数、抽出数、抽出結果を消去します。
コピーボタンをクリックするとコピー(クリップボード)されます。

Reactで作成しました。
コンポーネントは下図のようになっています。

これらとは別に、ボタンクリックの関数コンポーネント OutPutBtnAction(出力)ClearBtnAction(クリア)CopyBtnAction(コピー)も作成しました。
ソースコードです。

const TotalNum = () => {
  return (
    <div className="numOfAppWrap contWrap">
      <p>総数:</p>
      <input type="text" className="numOfApp inputItem" size="10" />
    </div>
  );
};

export default TotalNum;

▲総数を取得するコンポーネント。input要素でできています。

const GetNum = () => {
  return (
    <div className="numOfExWrap contWrap">
      <p>抽出数:</p>
      <input type="text" className="numOfEx inputItem" size="10" />
    </div>
  );
};

export default GetNum;

▲抽出数を取得するコンポーネント。

import OutPutBtnAction from "./OutPutBtnAction";
import ClearBtnAction from "./ClearBtnAction";
import CopyBtnAction from "./CopyBtnAction";

const Buttons = () => {
  return (
    <div className="btnWrap">
      <p className="outPutBtn btn" onClick={OutPutBtnAction}>
        出力
      </p>
      <p className="clearBtn btn" onClick={ClearBtnAction}>
        クリア
      </p>
      <p className="copyBtn btn" onClick={CopyBtnAction}>
        コピー
      </p>
    </div>
  );
};

export default Buttons;

▲ボタンのコンポーネント。 OutPutBtnAction(出力)ClearBtnAction(クリア)CopyBtnAction(コピー)コンポーネントを読み込みクリックイベントに設定します。

const OutPutBtnAction = () => {
  let numOfAppVal = document.querySelector(".numOfApp").value; // 総数入力欄
  let numOfExVal = document.querySelector(".numOfEx").value; // 抽出数入力欄

  //空欄の場合
  if (numOfAppVal === "" || numOfExVal === "") {
    alert("総数、抽出数に数値を入力してください。");
    return;
  }

  //総数が数値以外の場合
  if (isNaN(numOfAppVal)) {
    alert("総数には半角数値を入力してください。");
    document.querySelector(".numOfApp").value = "";
    return;
  }

  //抽出数が数値以外の場合
  if (isNaN(numOfExVal)) {
    alert("抽出数には半角数値を入力してください。");
    document.querySelector(".numOfEx").value = "";
    return;
  }

  // 総数より抽出数が多い場合
  if (Number(numOfAppVal) <= Number(numOfExVal)) {
    alert("抽出数は総数より少ない数値を入力してください。");
    document.querySelector(".numOfEx").value = "";
    //出力された数値があったら消す
    let outputNumWrap = document.getElementById("outputNumWrap");
    while (outputNumWrap.firstChild) {
      outputNumWrap.removeChild(outputNumWrap.firstChild);
    }
    return;
  }

  let ranNumArr = []; //配列を準備
  let maxNum = numOfAppVal, //最大値
    minNum = 1; //最小値
  let getNumbers = numOfExVal; //取得したい個数

  // ▼▼▼ 最大値からランダムな数値を取得する関数
  let randomNum = (max, min) => {
    return Math.floor(Math.random() * (max - min + 1) + min);
  };

  // ▼▼▼ 重複なし処理
  for (let i = minNum; i <= maxNum; i++) {
    while (true) {
      let ranNums = randomNum(maxNum, minNum);
      if (!ranNumArr.includes(ranNums)) {
        ranNumArr.push(ranNums);
        break;
      }
    }
  }

  // ▼▼▼ ランダムに任意の個数を取得する関数
  // 参照にしたサイト:https://h2ham.net/javascript-random-array/
  let getUniqueranNums = (array, num) => {
    var a = array;
    var t = [];
    var r = [];
    var l = a.length;
    var n = num < l ? num : l;
    while (n-- > 0) {
      var i = (Math.random() * l) | 0;
      r[n] = t[i] || a[i];
      --l;
      t[i] = t[l] || a[l];
    }
    return r;
  };

  // ▼▼▼ 昇順ソートの条件
  let compareFunc = (a, b) => {
    return a - b;
  };

  // ▼▼▼ ソートした結果を変数に格納
  let outPutRanNum = getUniqueranNums(ranNumArr, getNumbers).sort(compareFunc);

  // ▼▼▼ 画面に出力
  let outputNumWrap = document.getElementById("outputNumWrap");

  // ▼▼▼ 出力ボタンクリックの度に表示を消す
  while (outputNumWrap.firstChild) {
    outputNumWrap.removeChild(outputNumWrap.firstChild);
  }

  // ▼▼▼ 出力ボタンクリックで要素を追加
  for (let j = 0; j < numOfExVal; j++) {
    let newElements = document.createElement("p"); //pタグ作成
    outputNumWrap.appendChild(newElements);
    newElements.innerHTML = outPutRanNum[j]; //ランダムな数値を表示
  }
};

export default OutPutBtnAction;

▲要の出力ボタンのコンポーネントです。
昇順ソート処理、重複処理など苦労しました。詳細はコメントに記してある通りです。
84行目、下で説明しているApp.jsで出力されたidセレクタを取得し、92行目で抽出数分のp要素を出力します。
86〜89行目は、出力ボタンクリックの度に出力結果を消去するロジックです。これがないと下に結果が追記される形になります。

const ClearBtnAction = () => {
  document.querySelector(".numOfApp").value = ""; // 総数入力欄を空に
  document.querySelector(".numOfEx").value = ""; // 抽出数入力欄を空に

  let outputNumWrap = document.getElementById("outputNumWrap");

  // ▼▼▼ 抽出結果を消す
  while (outputNumWrap.firstChild) {
    outputNumWrap.removeChild(outputNumWrap.firstChild);
  }
};

export default ClearBtnAction;

クリアボタンのコンポーネント。

const CopyBtnAction = () => {
  let copyTargetTxt = document.getElementById("outputNumWrap").innerText;
  let copyTargetTxtResult = copyTargetTxt.replace(/\n\n/g, "\n");
  navigator.clipboard
    .writeText(copyTargetTxtResult)
    .then(() => alert("コピーしました。"));
};
export default CopyBtnAction;

コピーボタンのコンポーネント。
3行目の正規表現 /\n\n/ は、なぜか1行ごとに空きが入ってしまうので、その空きを削除するために記述しています。
5行目のnavigator.clipboard.writeText() 関数でクリップボードに飛ばします。
処理後にアラートを表示させるには then() 関数を使用し、上のように書かなければいけません。

import "./App.scss";
import React from "react";
import TotalNum from "./TotalNum";
import GetNum from "./GetNum";
import Buttons from "./Buttons";

class App extends React.Component {
  render() {
    return (
      <div id="contentsWrap">
        <TotalNum />
        <GetNum />
        <Buttons />
        {/* ▼ 結果出力用 */}
        <div id="outputNumWrap"></div>
      </div>
    );
  }
}

export default App;

▲親のコンポーネントです。classコンポーネントにしています。

まとめ

最初はjQueryで作りました。
せっかくだからReactで書いてみようと思いましたが、しばらく使ってなかったので忘れかけていました。

少し前に、Reactの書籍を購入して読んでいましたが、7年前のものなので書いたものが動かなかったりしました。

CDNhtmlファイルに書いたりもしまいたが、ver18だとエラーが出て対応が分かりませんでした。
なのでプロジェクトで作ってみました。

勉強不足を感じつつ、精進したいと思います。

最後までお読みいただき、ありがとうございます。

関連記事
Pocket