【もりけん塾】JS課題31 – Vanilla JSでドロワーメニューのオプション機能を作成 –

JavaScript

Vanilla JSでドロワーメニューのオプション機能を作成して、左右どちらから出てくるか&スピードを選べるようにしました。

こんにちは。Webコーダーのはるです。

現在、所属している「もりけん塾」でJavaScriptのハンズオン課題 に取り組み、レビューをいただいています。

今日は、課題31 についてアウトプットをしていきます。
(前回までのアウトプットは、こちらです。)

実装する過程で学んだことを、学習ノートとして記録していきます。

認識の間違えている箇所がありましたら、お問い合わせからご指摘いただけるとうれしいです。

JavaScript課題31の仕様

    (もりけんさんのJavaScriptハンズオン課題より)

    JavaScript課題31の学習記録

    主にレビューいただいた箇所の復習として記録を残します。

    制作物

    前回作成した基本のドロワーメニューにオプショナルをつけました。

    drawer-menu.js の 以下のオブジェクトの値を変更すると、実際にメニューの動きを編集できます。

    (メニューは、ログイン画面・会員登録画面のハンバーガーボタンを押すと開閉します)

    const menuOption = { 
      direct : "left", // left or right
      speed : 400 // number
    }; 

    オプション機能 完成版

    前回作成した基本のドロワーメニューに追加したJavaScriptです。

    // JavaScript
    
    const option = {
     direct: "left", //left or right
     speed: 400 // number(ms)
    };
    
    const setDirect = (menu, direct) => {
     switch(direct){
      case "right":
       menu.classList.add("right");
       break;
      case "left":
      default:
       menu.classList.add("left");
       break;
     }
    }
    
    const convertMillisecondsToSeconds = speed => `${speed / 1000}s`;
    const formatSeconds = (speed = 400) => {
     if(typeof speed !== "number"){
      throw new Error(`speed expect number type. but got ${typeof speed}`)
     }
     return convertMillisecondsToSeconds(speed);
    }
    
    const setSpeed = (menu, speed) => {
     menu.style.transitionDuration = formatSeconds(speed);
    }
    
    const initMenu = (menu, option = {}) => {
     setDirect(menu, option?.direct);
     setSpeed(menu, option?.speed);
    }
    
    initMenu(drawerMenu, option);

    オプション① ドロワーメニューの方向を変える

    いただいたレビューも記録しながら、コードを分解していきます。

    まず、オプションとはそれ自体undefinedになる想定であって、あってもなくても良いもの -> あれば設定を行うもの だと教えていただきました。(よく考えれば、swiperとかsplideとか他のライブラリでもそうだった…)

    オプションがセットされている時は、以下のように使用しますが

    initMenu(drawerMenu, option);

    オプションがない時は、以下でも動くコードにする必要があります。

    initMenu(drawerMenu);
    

    レビュー

    はじめにPR提出した時のコードです(ドロワーメニューの方向を変える部分のみ)

    const menuOption = { direct : "left" }; //left or right
    
    const setMenuOption = () => {
     const {direct} = menuOption;
     changeMenuDirect(direct, drawerMenu);
    }
    
    const changeMenuDirect = (direct, menu) => {
     switch(direct){
     case "right":
      menu.classList.add("is-right");
      break;
     case "left":
      menu.classList.add("is-left");
      break;
     default:
      menu.classList.add("is-left");
      break;
    }
    setMenuOption();
    optionは第二引数にすべき(optionがないときもあるので)
    関数名、クラス名の修正
    • optionは任意なので毎回セットするわけではない : setMenuOption -> initMenu()に修正
    • 方向は毎回変えるわけではない :  changeMenuDirect() -> setDirect()に修正
    • is-**クラスはJavaScriptではbooleanを扱うことが多いので、CSSの命名がわかりにくい。-> .left .rightに修正
    initMenu()でoptionをまるっと取得して、任意のoptionが存在すれば引数で渡す
    // 修正前
    const setMenuOption = () => {
     const {direct} = menuOption;
     changeMenuDirect(direct, drawerMenu);
    }
    
    // 修正後
    const initMenu = (menu, option = {}) => {
     setDirect(menu, option?.direct);
    }

    この時、initMenu()に渡す引数は

    • menu -> ドロワーメニューのHTML要素
    • option = {} -> オプションをまるっと取得。デフォルト引数を設定することで、optionがundefinedだったときには空の{ } を渡すことができる。
    optionが undefinedの時とは?
    optionが何も設定されていない時。const option = {// direct: “left”,
    // speed: 400
    };

    setDirect() 関数に option?.direct とすることで、
    optionオブジェクトの中にdirectキーが存在しないときはエラーにならずにundefinedを返します。

    setDirect(menu, option?.direct);
    

    ?. はオプショナルチェーンと呼ばれる演算子です。

    これは、オブジェクトのプロパティやメソッドにアクセスする際に、そのオブジェクトが存在しない場合でもエラーを回避できる機能です。

    switch文のcaseとdefaultをまとめて記述できる
    const setDirect = (menu, direct) => {
     switch(direct){
      case "right":
       menu.classList.add("right");
       break;
      case "left":
      default: //一緒に記述可能
       menu.classList.add("left");
       break;
     }
    }
    

    ケース defaultは、direct引数にundefinedが渡った時 = optionが存在しない時 に実行されます。

    メニューを右表示に設定すると、メニューの移動変更が見えてしまう

    optionのdirectをrightにした時、メニューが移動するのが見えてしまう(デフォルトをleftにしているためと思われる)


    GitHub PR画面より動画引用

    動作確認でご指摘をいただき、メニュー移動が見えないように配慮しました。
    (ちょっと強引感ある…ベストアンサーがあればぜひご教授ください…)

    記述量が多いので、変更した部分だけ載せてます..
    
    .header__nav{ // ドロワーメニュー本体
     opacity: 0;
     top : 0;
    }
    
    .header__nav.left{
     left : -100%; // transformではなくleftで移動
     opacity: 1; // leftの値をセットしてからopacity: 1にする
     will-change: left;
     transition: left ease-in-out;
    }
    
    .header__nav.right{
     right : -100%; // transformではなくrightで移動
     opacity: 1; // rightの値をセットしてからopacity: 1にする
     will-change: right;
     transition: right ease-in-out;
    }
    
    .header__nav.left.is-open{
     left: 0;
    }
    .header__nav.right.is-open{
     right: 0;
    }

    もっといい方法がありそう・・・。

    オプション② スピードを変更する

    swiperやsplideのオプション設定を見ると、スピードの単位にミリ秒を使用していました。

    JavaScriptでは(その他プログラミング言語でも)、標準的な時間管理の単位としてミリ秒が採用されていることを知りました。

    • プログラム間で時間の表現や計算が統一され互換性が保たれる
    • ミリ秒単位で時間を扱うことで、秒単位や分単位などの他の単位へ容易に変換できる

    そこで、オプションではミリ秒を指定して、transition-duration(CSS)を設定するときに秒数に変換するという練習をしてみることにしました。(ミリ秒でも動くらしいが..)

    レビュー

    はじめにPRした時のコードです。

    const option = {
     direct: "left", //left or right
     speed: 400, // number(ミリ秒)
    };
    
    const convertMillisecondsToSeconds = speed => `${speed / 1000}s`;
    
    const changeSpeed = (menu, speed) => {
     const defaultSpeed = "0.4s";
     const judgedSpeed = typeof speed === "number"? convertMillisecondsToSeconds(speed): defaultSpeed;
     menu.style.transitionDuration = judgedSpeed;
    }
    
    const initMenu = (menu, option = {}) => {
     changeDirect(menu, option?.direct);
     changeSpeed(menu, option?.speed);
    }
    
    initMenu(drawerMenu, option);
    formatする関数と、setする関数を分けた方がわかりやすい

    下記のコードを分割することとしました。

    const changeSpeed = (menu, speed) => {
     const defaultSpeed = "0.4s";
     const judgedSpeed = typeof speed === "number"? convertMillisecondsToSeconds(speed): defaultSpeed;
     menu.style.transitionDuration = judgedSpeed;
    }
    

    この関数でやっていたこと

    • number型であれば、引数のスピードを秒数に変換する
    • number型でなければ、デフォルトのスピードを使用
    • これらのスピードをtransition-durationにセットする

    ちょっとやっている内容が多かった..

    以下のように、formatとsetの関数を分けました。

    // formatする
    const formatSeconds = (speed = 400) => {
     if(typeof speed !== "number"){
      throw new Error(`speed expect number type. but got ${typeof speed}`)
     }
     return convertMillisecondsToSeconds(speed);
    }
    
    // setする
    const setSpeed = (menu, speed) => {
     menu.style.transitionDuration = formatSeconds(speed);
    }

    デフォルトのスピードは別途定義するのではなく、デフォルト引数として設定することでundefinedに対応することができます。-> undefinedが渡ってくることが想定できている場合は、デフォルト引数が有用だと知りました。

    また、number型以外が渡ってきた時は、エラーをthrowすることで、使用側にそれを伝えることができるようにしました。

    throw New errorは、console.errorと違い、その後の動きを全てストップさせます。

    そのため、今回の場合はconsoleにエラーが表示されて、transition-durationもセットされません。

    (仕様にはなかったのですが、number型以外の場合も考えたかったので、レビューで教えていただいたこちらの方法で実装することとしました。とても勉強になりました。)

    > 関数のデフォルト引数は、関数に値が渡されない場合や undefined が渡された場合に、デフォルト値で初期化される形式上の引数を指定することができます。

    //

    学習に使用している本は、JavaScript本格入門・独習JavaScriptです。

      

    あとがき

    もりけんさん(@terrace_tec)、さえさん(@sae_prog)、まいさん(@mai2022web) お忙しい中レビューいただきありがとうございました!

    初っ端からオプションの意味をうまく捉えられておらず、出発点からの修正となりました🙇‍♀️

    デフォルト引数やオプショナルチェーンなどを学び、修正を重ねることで、スマート&undefinedにも対応できるコードになりました。

    ありがとうございました!

    教えていただいたことを生かして、次の課題に進みたいと思います!

    今日は以上です。

    //

    【もりけん塾で勉強しています】

    もりけんさん(@terrace_tec)のHPはこちら