FullCalendarで簡単なカレンダーを作る

はじめに

fullcalendarには色々とカレンダーの表示方法がある。一番シンプルなのものをメモしておく。

fullcalendar.io

(1)一番シンプルな月単位カレンダーを作る

fullcalendar.io

インストール

まずライブラリをインストール。

npm install @fullcalendar/react @fullcalendar/core @fullcalendar/daygrid

React Component - Docs | FullCalendar

コード

mport React from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from "@fullcalendar/daygrid";

const Calendar = () => {
  return (
    <FullCalendar
      plugins={[dayGridPlugin]}
      initialView="dayGridMonth"
    />
  );
}

export default Calendar;
  • plugins={[dayGridPlugin]}
    このpluginを指定することで、月次カレンダーが表示されるようになる。

  • initialView="dayGridMonth" pluginsでdayGridPluginを指定した時の、初期表示ビューの設定。 dayGridMonthで月単位で表示する標準的なグリッドビューとなる。
    他にも、

    • dayGridWeek
    • dayGridDay

結果

(2)時間単位のカレンダーを作る

fullcalendar.io

インストール

npm install @fullcalendar/timegrid

コード

import React from 'react';
import FullCalendar from '@fullcalendar/react';
import timeGridPlugin from "@fullcalendar/timegrid";

const Calendar = () => {
  return (
    <FullCalendar
      plugins={[timeGridPlugin]}
      initialView="timeGridWeek"
    />
  );
}

export default Calendar;

結果

参考にしたサイト

pluginから理解するFullCalendar with React #UI - Qiita

React + TypeScript + FullCalendar: 簡単なカレンダーをつくる #JavaScript - Qiita

型ガード is演算子について

is演算子とは

正式名称「ユーザー定義型ガード(User-Defined Type Guards)」といい、自分で定義した型に対して型ガードを適用する際に使う。 順番に説明していく。

型ガードとは

TypeScriptの機能であり、特定の型が使われているかを判定することができる。 型ガードを使うことで、型エラーを事前に防ぐことができるため、コードを安全に書ける。
型ガードは大きく分けて2種類あり、 「組み込み型ガード」 、「ユーザー定義型型ガード」がある。

組み込み型の型ガード

TypeScriptの組み込み型に対して使用する型ガードである。

typeof

プリミティブ型(string, number, boolean, symbol, object等)を判定する際に使用される。

function getType(value: string | number) {
  if (typeof value === "string") {
    console.log("This is a string!");
  } else {
    console.log("This is a number!");
  }
}

getType("Hello, TypeScript!"); // This is a string!
getType(42); // This is a number!

instanceof

オブジェクトが特定のクラスのインスタンスであるかどうかを判定する。

class Person {
  constructor(public name: string) {}
}

class Animal {
  constructor(public species: string) {}
}

function printInfo(value: Person | Animal) {
  if (value instanceof Person) {
    console.log(`こんにちは、 ${value.name}!`);
  } else {
    console.log(`これは ${value.species} です`);
  }
}

printInfo(new Person("ジョン")); // こんにちは、ジョン!
printInfo(new Animal("犬")); // これは 犬 です

ユーザー定義型型ガード

こっちが本題。自分で定義した型に対する型ガードはこちらを使う。
自分で定義した型かどうか判定する「関数」を定義することで実現できる。

// 2種類の型を定義
interface User {
  name: string;
  type: "user";
}

interface Admin {
  name: string;
  type: "admin";
  permissions: string[];
}

// ユーザーのデータがどちらの型か判定するカスタム型ガード関数
function isAdmin(user: User | Admin): user is Admin {
  return user.type === "admin";
}

// 実際に使ってみる
const john: User = { name: "John", type: "user" };
const jane: Admin = { name: "Jane", type: "admin", permissions: ["read", "write"] };

// 判定
console.log(isAdmin(john));  // false
console.log(isAdmin(jane));  // true

isAdmin関数は、User型かAdmin型かの引数userを受け取り、もじtypeフィールドが"admin"であればtrueを返す。
user is Adminを関数isAdminの戻り値の型定義として付けることで、このisAdmin関数は型を判定するものなんだとTypeScriptに伝えている(多分)。
このように、is演算子を使用した関数を定義した場合、必ずtrueかfalseを戻り値とする必要がある。そうでないとコンパイルエラーになる。

参考にしたサイト様

型ガードで TypeScript コードを強化!わかりやすい解説と実践例

typeofの使い方

Typeofとは

typeofJavaScriptとTypeScriptで使われる、型を調べるための演算子である。

使い方

console.log(typeof "Hello");  // "string"
console.log(typeof 123);      // "number"
console.log(typeof true);     // "boolean"
console.log(typeof {});       // "object"
console.log(typeof []);       // "object"  // 配列も「object」と表示される
console.log(typeof null);     // "object"  // `null` も「object」と表示される
console.log(typeof undefined);  // "undefined"
console.log(typeof function(){});  // "function"

typeofを型定義につかう

こういう使い方も可

const person = { name: "Alice", age: 25 };

type PersonType = typeof person;

const anotherPerson: PersonType = { name: "Bob", age: 30 };

Firestoreからデータを取得してReactで使用する方法

はじめに

Firestoreからデータを取得して、Reactコンポーネントで使用する流れを、簡単な具体例で説明する。
この例では、Firestoreに保存されている「ユーザー」データを取得し、それをReactコンポーネントに表示する方法を解説する。

前提

  • Firestoreに「users」というコレクションがあり、各ユーザードキュメントにnameageのフィールドがある。

ステップ1 Firebase SDKをインストール

ReactプロジェクトにFirebase SDKをインストールし、Firestoreと連携できるように設定を行う。

npm install firebase

ステップ2 Firestore設定ファイル(firebase.ts)を作成

次に、Firestoreの接続設定を行うためのfirebase.ts(typescriptを使っているので.ts)ファイルを作成し、Firebaseプロジェクトと接続する。 プロジェクトのsrcディレクトリにfirebase.tsファイルを作成し、以下のように記述する。
設定情報については、firestoreの「プロジェクトの設定」に書いてある。

// src/firebase.ts
import { initializeApp } from "firebase/app";  // Firebase の初期化
import { getFirestore } from "firebase/firestore";  // Firestore インスタンスの取得

// ここに自分の Firebase プロジェクトの設定情報を記述
const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID"
};

// Firebase を初期化
const app = initializeApp(firebaseConfig);

// Firestore インスタンスを作成してエクスポート
export const db = getFirestore(app);

ステップ3 Firestoreインスタンスを使用する。

firestoreのデータを使用したい箇所で下記を記述

import React, { useEffect, useState } from 'react';
import { collection, getDocs } from "firebase/firestore";  // Firestore の関数をインポート
import { db } from '../firebase';  // `db` インスタンスをインポート


export interface userType {
  id: string,
  name: string,
  age: number,
}

const UserList = () => {
  const [users, setUsers] = useState<userType[]>([]);  // 初期状態は空の配列

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        // Firestore から "users" コレクションを取得
        const querySnapshot = await getDocs(collection(db, "users"));
        // データを配列形式にマッピング
        const usersData = querySnapshot.docs.map((doc) => {
          return {
            ...doc.data(),  // ドキュメントのデータを展開
            id: doc.id,      // ドキュメント ID を追加
          } as userType; 
        });
        setUsers(usersData);  // 状態を更新
      } catch (error) {
        console.error("Error fetching users: ", error);
      }
    };

    fetchUsers();  // データ取得関数を呼び出し
  }, []);

  return (
    <div>
      <h1>ユーザーリスト</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            名前: {user.name}, 年齢: {user.age}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;
  • getDocs(collection(db, "users"));の所がデータ取得のキモ。dbはfirebase.tsで定義したdb(firestoreインスタンス)であり、"users"はfirestore上のコレクション名。
  • querySnapshotのデータ構造を下記で説明する。

querySnapshotの構造

QuerySnapshot | JavaScript SDK  |  Firebase JavaScript API reference

代表的なプロパティ

  • docs:クエリで取得したドキュメントのリスト 。各要素はqueryDocumentSnapshotオブジェクト。

  • empty:クエリ結果が空かどうか

  • metadata:スナップショットのメタデータ情報

  • size:取得されたドキュメント数

querySnapshot.docsの構造

querySnapshot.docsの構造は以下のとおりである。

[
  queryDocumentSnapshot, // ドキュメント 1
  queryDocumentSnapshot, // ドキュメント 2
  queryDocumentSnapshot, // ドキュメント 3
  ...               // 取得したドキュメント数に応じて繰り返し
]

QueryDocumentSnapshot | JavaScript SDK  |  Firebase JavaScript API reference

代表的なプロパティとメソッド

  • id:ドキュメントの一意の ID。Firestore コレクション内でドキュメントを特定するための識別子。
querySnapshot.docs.forEach((doc) => {
  console.log("ドキュメント ID:", doc.id);
});
  • data():ドキュメントのデータをオブジェクト形式で取得。
querySnapshot.docs.forEach((doc) => {
  console.log("ドキュメントデータ:", doc.data());
});
  • exists():ドキュメントが存在するかどうかをboolwan型で返す。削除されたり、存在しない場合はfalse。
querySnapshot.docs.forEach((doc) => {
  if (doc.exists) {
    console.log(`ドキュメント ${doc.id} は存在します。`);
  } else {
    console.log(`ドキュメント ${doc.id} は存在しません。`);
  }
});
  • metadata:ドキュメントのメタデータ情報(キャッシュから取得されたかどうか、サーバーとの同期状態など)。

  • ref:そのドキュメントの DocumentReference(参照)。collection メソッドを使ってそのドキュメントのサブコレクションにアクセス可能。

querySnapshot.docs.forEach((doc) => {
  console.log("ドキュメント参照:", doc.ref.path);
});
  • get(fieldPath):特定のフィールドの値を取得。data() とは異なり、フィールドパスを指定して部分的にデータを取り出す。
querySnapshot.docs.forEach((doc) => {
  console.log("指定されたフィールドの値:", doc.get("fieldName"));
});

querySnapshot.docsの具体例

実際のquerySnapshot.docsの構造を以下の例で確認してみる。

例えば、FirestoreのUsersコレクションが以下のようなデータを持っているとする。

ドキュメントID 名前(name) 年齢(age)
user1 Alice 30
user2 Bob 25
user3 Charlie 35

この時、次のようなコードでUsersコレクションからデータを取得する。

const querySnapshot = await getDocs(collection(db, "Users"));
console.log(querySnapshot.docs);

これによって、次のようなquerySnapshot.docsの配列が取得される。

[
  {
    "id": "user1",
    "exists": true,
    "ref": { /* ドキュメント参照情報 */ },
    "data": { "name": "Alice", "age": 30 },
    "metadata": { /* メタデータ情報 */ }
  },
  {
    "id": "user2",
    "exists": true,
    "ref": { /* ドキュメント参照情報 */ },
    "data": { "name": "Bob", "age": 25 },
    "metadata": { /* メタデータ情報 */ }
  },
  {
    "id": "user3",
    "exists": true,
    "ref": { /* ドキュメント参照情報 */ },
    "data": { "name": "Charlie", "age": 35 },
    "metadata": { /* メタデータ情報 */ }
  }
]

<Grid container> と <Grid item> の基本

はじめに

<Grid>コンポーネントはMUIが提供する、グリッドレイアウトを実現するためのコンポーネントである。 <Grid container></Grid>を親要素、<Grid item></Grid>を子要素として指定することで、グリッドレイアウトを使用できる。

基本ルール

  • <Grid container></Grid>の中に<Grid item></Grid>を入れる。
  • アイテムの幅はxs, sm, md, lg, xlのようなbreakpointによって指定する。

具体例

<Grid container spacing={2} sx={{ padding: 2 }}>
    <Grid item xs={12} sm={6} md={4}>~</Grid>
    <Grid item xs={12} sm={6} md={4}>~</Grid>
    <Grid item xs={12} sm={12} md={4}>~</Grid>
</Grid>
  • spacing<Grid container>につけることで、子要素の間隔を調整できる。spacing={2}により、子要素間に2の間隔(8px×2=16px)を設定している。

  • sx={{ padding: 2 }}は親要素全体に16pxの内側余白を設定している。

  • xs={12}は、画面がxs(最小サイズ)以上であった時、1行全体の幅を取るよう指定。
  • sm={6}は、画面がsm(smallサイズ)以上であった時、1行の半分の幅を取るよう指定。
  • md={4}は、画面がmd(Mediumサイズ)以上であった時、1行の1/3の幅を取るよう指定。

display: flexについて

はじめに

以前の記事でdisplay :について書いたが、そこで説明しなかったflexについて調べた。

progmemo.hatenablog.jp

display : flexとは

displayプロパティはCSSプロパティの一つで、要素の表示方法(レイアウトの仕方)を指定するために使われる。 この辺りは以前の記事で書いたとおりである。
display : "flex"と設定すると、その要素はフレックスコンテナといって、子要素を柔軟に配置できるようになる。 blockinlineのように子要素側に設定するのではなく、親要素側に設定する。

具体例

前回同様具体例で説明する。まず適当なフォルダに「page.html」、「sample.css」を作成し、それぞれこのように記載する。

  • page.html
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="sample.css">
    <title>testPage</title>
  </head>
  <body>
    <div class="parent">
      <div class="child" style=~~>要素1</div>
      <div class="child" style=~~>要素2</div>
      <div class="child" style=~~>要素3</div>
    </div>
    
  </body>
</html>
.parent {
    border: 1px solid black; /* 親要素の境界線を表示 */
    padding: 10px; /* 親要素に余白を追加 */
    background-color: green;  /*背景色を追加*/
    display: flex;
}

.child {
    border: 1px solid black; /* 子要素の境界線を表示 */
    padding: 5px; /* 子要素に余白を追加 */
    background-color: yellowgreen; /*背景色を追加*/
    text-decoration: none;
    color: inherit;
}

「style=~~」の部分にを色々変えて挙動の違いを確認する。

何も書かない

何も書かないと子要素はただの横並びになる。

flex-grow : ~

子要素がどれだけ伸びることができるかを指定する。
どれだけの比率にするかをそれぞれ決定できる。

  • 子要素を1: 1: 1で並べたい場合、下記のように書く。
<div class="parent">
    <div class="child" style="flex-grow:1">要素1</div>
    <div class="child" style="flex-grow:1">要素2</div>
    <div class="child" style="flex-grow:1">要素3</div>
</div>

  • 3: 1 : 2で並べたい場合、下記のように書く。
<div class="parent">
    <div class="child" style="flex-grow:3">要素1</div>
    <div class="child" style="flex-grow:1">要素2</div>
    <div class="child" style="flex-grow:2">要素3</div>
</div>

  • 要素2と3は固定長で、要素1だけが画面幅に合わせて伸びるようにする場合、下記のように書く
<div class="parent">
    <div class="child" style="flex-grow:1">要素1</div>
    <div class="child">要素2</div>
    <div class="child">要素3</div>
</div>

参考にしたサイト

【CSS】flex-growを使おう #CSS - Qiita

MUIのpaletteでカスタムプロパティを使用する方法

はじめに

前回の記事で別に書くと宣言していた件

progmemo.hatenablog.jp

今回はTypeScriptを使用する場合に、MUIのcreateTheme関数でカスタムプロパティを使用する方法を調べた。

説明

下記の例において、primaryとsecondaryはMUIで定義されるデフォルトプロパティであるが、hogeColorは定義されていない。

const theme = createTheme({
  palette: {
    primary: { main: '#1976d2' },  // メインカラーを青色に設定
    secondary: { main: '#d32f2f' } // サブカラーを赤色に設定
    hogeColor: {main: '#e0e0e0'}  // ここが今回の焦点
  },
});

この状態で実行すると「'hogeColor'は型 'PaletteOptions' に存在しません。」とエラーになる。
これ実はpaletteで使えるプロパティは node_modules/@mui/material/styles/createPalette.d.ts
に下記のように型定義されている。

...

export interface Palette {
  common: CommonColors;
  mode: PaletteMode;
  contrastThreshold: number;
  tonalOffset: PaletteTonalOffset;
  primary: PaletteColor;
  secondary: PaletteColor;
  error: PaletteColor;
  warning: PaletteColor;
  info: PaletteColor;
  success: PaletteColor;
  grey: Color;
  text: TypeText;
  divider: TypeDivider;
  action: TypeAction;
  background: TypeBackground;
  getContrastText: (background: string) => string;
  augmentColor: (options: PaletteAugmentColorOptions) => PaletteColor;
}

...

export interface PaletteOptions {
  primary?: PaletteColorOptions;
  secondary?: PaletteColorOptions;
  error?: PaletteColorOptions;
  warning?: PaletteColorOptions;
  info?: PaletteColorOptions;
  success?: PaletteColorOptions;
  mode?: PaletteMode;
  tonalOffset?: PaletteTonalOffset;
  contrastThreshold?: number;
  common?: Partial<CommonColors>;
  grey?: ColorPartial;
  text?: Partial<TypeText>;
  divider?: string;
  action?: Partial<TypeAction>;
  background?: Partial<TypeBackground>;
  getContrastText?: (background: string) => string;
}
...

Paletteは実際のテーマの色情報を表すインターフェイスである。実際に色設定として使われる。
PaletteOptionsはcreateTheme関数でテーマを作成する際に使用するインターフェイスである。
このそれぞれにhogeColorが記載されていなければ使用することができない。

カスタムプロパティを追加

上で定義されているPaletteとPaletteOptionsに直接hogeColorを追記するのではなく、declare moduleを使用して型定義を拡張する。

declare module '@mui/material/styles' {
  interface Palette {
    hogeColor: {
      main: string;
    };
  }
  interface PaletteOptions {
    hogeColor?: {
      main?: string;
    };
  }
}

const theme = createTheme({
  palette: {
    primary: { main: '#1976d2' },  // メインカラーを青色に設定
    secondary: { main: '#d32f2f' } // サブカラーを赤色に設定
    hogeColor: {main: '#e0e0e0'}  // ここが今回の焦点
  },
});

参考にしたサイト

Material-UIのPaletteをカスタマイズした時の型定義について - TypeScript #React - Qiita