かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript レスポンシブ時のイベントには MediaQueryListAPI matchMedia が便利だった!

レスポンシブ時の View の変化は基本的に CSS のメディアクエリで行いたいが、 window サイズが大きくなったときにモバイル用のモーダルメニューを閉じるとか、意外とレスポンシブ時に JS でイベントを実行したい時がある

window.resize を使った今までやりかた

const breakpoint = 768 as const;

export const App = () => {
  const timerRef = useRef<number>(0);
  const [isSp, setIsSp] = useState<boolean>(true);

  useEffect(() => {
    const timer = timerRef.current;
    const resizeEvent = () => {
      window.clearTimeout(timer);
      window.setTimeout(() => {
        if (window.innerWidth >= breakpoint) {
          setIsSp(false);
        } else {
          setIsSp(true);
        }
      }, 100);
    };

    // レンダリング時に window サイズをチェックするために呼び出す
    resizeEvent();
    // add addEventListener
    window.addEventListener("resize", resizeEvent);
  
    return () => {
      window.clearTimeout(timer);
      window.removeEventListener("resize", resizeEvent);
    };
  }, []);
  
  return (<div>{isSp? "true" : "false"}</div>)
}

今までは window.resize イベントを監視して特定の window サイズになった際にイベントを発火させていたがイベントを間引く debounde の処理などいろいろと面倒な部分があった
また、debounce させていても window がリサイズされる事にイベントが呼び出されるので処理が多くなってしまう

MediaQueryListAPI matchMedia を使った方法

const breakpoint = 768 as const;

export const App = () => {
  const [isSp, setIsSp] = useState<boolean>(true);

  useEffect(() => {
    const mediaQueryList = window.matchMedia(`(min-width: ${breakpoint}px)`);
    const handleOrientationChange = (
      evt: MediaQueryList | MediaQueryListEvent
    ) => {
      if (evt.matches) {
        setIsSp(false);
      } else {
        setIsSp(true);
      }
    };

    // レンダリング時に window サイズをチェックするために呼び出す
    handleOrientationChange(mediaQueryList);
    // add addEventListener
    mediaQueryList.addEventListener("change", handleOrientationChange);

    return () => {
      mediaQueryList.removeEventListener("change", handleOrientationChange);
    };
  }, []);
  
  return (<div>{isSp? "true" : "false"}</div>)
}

MediaQueryListAPI を使った方法だと設定したブレイクポイントを跨いだタイミングでだけ change イベントが発火される
リスナー関数の引数は初回呼び出し時は MediaQueryList で change イベントから受け取る場合は MediaQueryListEvent になるようなので evt: MediaQueryList | MediaQueryListEvent とした。もっと良い指定方法があるかもしれない。

Sample

次のサンプルでは breakpoint を跨ぐと各エリアのデザインが変更され、発火されたイベントの回数を表示している。
matchMedia を使った方は必要なときだけイベントが発火しているのが分かると思う


[参考]

ウェス・アンダーソンもともと好きだったけど、グランド・ブダペスト・ホテル は本当に良かった!

Node.js path.resolve は末尾の `/` が消える仕様になってる

表題の通り。
DB に入っている文字列から末尾に / のあるパスを作りたかったが、最終的に path.resolve を通すと末尾の / が削除される

import path from 'path';

path.resolve('/', '/foo/bar/');
// -> /foo/bar

The resulting path is normalized and trailing slashes are removed unless the path is resolved to the root directory.
cf. Path | Node.js v22.0.0 Documentation:title]

末尾に / を追加したい場合は path.join を使えば OK

import path from 'path';

path.join('/', 'foo/bar', '/');
// -> /foo/bar

path.join(path.resolve('/', '/foo/bar/'), '/');
// -> /foo/bar/

path.resolve で末尾に / の無いパスに正規化されるので、文字列結合で / を足しても大丈夫だと思う
末尾の / で検索したいときは 「trailing slash」で検索しすれば良い。学び

おわり


[参考]

PHP 配列からいい感じに CSS のクラス名を作りたい

3億年ぶりに古の WordPress 製のサイトを更新する必要があり、PHP のテンプレートで CSS のクラス名を動的に作成する必要があった JavaScript で言うところの classnames のような挙動をさせたい

CSS のクラス

class="foo bar mofu" のような形

  • スペース区切りの文字列
  • クラス名が重複していても特に問題はない

配列から Falsey な値を取り除いて スペースで結合すれば良い

TypeScript での例

export const classnames = (
 ...args: (string | undefined | null | false)[]
): string => {
  if (!args.length) {
    return '';
  }
  return args.filter((value) => 
    value !== null && value !== undefined && value !== false && value !== ""
  ).join(' ').trim();
};

細かいことを抜きにしらこんな感じで実現できる
これに近いことを PHP で実装できれば OK

array_filter + implode で実現する

  1. array_filter で配列から Falsey な値を取り除く
  2. implode で配列をスペース区切りで文字列結合する
<?php declare(strict_types=1);
function classnames(array $arr): string {
  return implode(' ', array_filter($arr));
}

echo classnames(['ichigo', false, 'aoi', 0, 'ran', '', null, 'akari']);
// -> "ichigo aoi ran akari"

このコードには下記の問題があるが、ライブラリとして公開しているわけでもないのでヨシ!とした

  • 0"0" も Falsey な値として除去されてしまうが、そんな class 名を付けることはほぼ無い想定とした
  • 関数に型を設定しているが、PHP の形は array に含まれる要素の型を指定する方法がイマイチわからなかったので、文字列変換できないものが渡されるとクラッシュする可能性がある

array_filter Example #2 array_filter() without callback

<?php
$entry = [
    0 => 'foo',
    1 => false,
    2 => -1,
    3 => null,
    4 => '',
    5 => '0',
    6 => 0,
];
print_r(array_filter($entry));
// Array
// (
//     [0] => foo
//     [2] => -1
// )

cf. PHP: array_filter - Manual

うごいてるからヨシ!

おわり ฅ^•ω•^ฅ


[参考]