見出し画像

#76「Webアプリをスマホでもキレイにしよう」

佐賀県唐津市、合同会社パーチのやすけんです。

GAS 100本ノック、第76回は「Webアプリのレスポンシブ対応」です。

前回 #75 で経費申請Webアプリを作ったとき、スマホで開いたら画面が小さくて「これは使えない……」と凹みました。GASのWebアプリは意外とスマホ対応に落とし穴が多く、PCで動いていてもモバイルで見にくいことがよくあります。

写真だと少し大きく見えるが、実際は入力が難しいくらい小さい


今回はその落とし穴をすべて踏み越えて、タスク管理アプリをモバイル・タブレット・PCで崩れない画面にしてみました。

今回のお題

既存のWebアプリにレスポンシブ対応を追加し、モバイル・タブレット・PCで崩れない画面を実現する。

達成条件は4つです。

  • meta viewport タグの正しい付与

  • CSS `@media (max-width: 768px)` でレイアウト切り替え

  • テーブル表示をモバイルではカード型UIに変換

  • ローディングスピナーでUX改善

最初にハマったこと: metaタグをHTMLに書いても効かない

GASのWebアプリをスマホで開いたとき、ページ全体が縮小されて文字が豆粒になる——これが最初にぶつかった壁です。

原因を調べたら、なんと GASのHTMLファイルに直接書いた `<meta>` タグは無視される という仕様でした。

<!-- これはGASでは効かない -->
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

GASはHTML出力時にヘッダーを独自に管理しているため、HTMLファイル内のmetaタグが捨てられてしまうのです。

正しい解決策: addMetaTag() をサーバー側で呼ぶ

`doGet()` の中で `HtmlOutput.addMetaTag()` を使うのが正解です。

function doGet(e) {
  const output = HtmlService.createTemplateFromFile('index').evaluate();
  output
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0, minimum-scale=1.0')
    .setTitle('タスク管理 #76');
  return output;
}

`minimum-scale=1.0` を付けると、ページを最小まで縮小したときに1倍以下にならないよう制限できます。これだけでスマホ縮小問題は解消しました。

テーブル → カード切り替えの実装

GASのWebアプリでよくあるUIパターンは、データをテーブルで表示することです。ところがテーブルはスマホ画面では横スクロールが発生して使いにくい。今回は `@media (max-width: 768px)` を境に、テーブルを非表示にしてカード型UIに切り替えました。

CSSはこんな構造です。

/* PC: テーブルを表示、カードを隠す */
.task-table { /* 通常通り */ }
.task-cards { display: none; }

/* モバイル: テーブルを隠して、カードを表示 */
@media (max-width: 768px) {
  .task-table { display: none; }
  .task-cards { display: block; }
}

HTMLは同じデータを2種類の形でレンダリングしておき、CSSで切り替えるだけです。Vue.jsを使っているので `v-for` を2箇所書くだけで済みました。

<!-- PC: テーブルビュー -->
<table class="task-table">
  <tr v-for="task in filteredTasks" :key="task.id">...</tr>
</table>

<!-- モバイル: カードビュー -->
<div class="task-cards">
  <div v-for="task in filteredTasks" :key="task.id" class="task-card">...</div>
</div>

モバイルのカードUI: 優先度をボーダーカラーで表現

カード型UIで一工夫したのが、優先度を左のボーダーカラーで視覚化したことです。

.task-card.priority-高 { border-left: 4px solid #ef4444; }
.task-card.priority-中 { border-left: 4px solid #f59e0b; }
.task-card.priority-低 { border-left: 4px solid #10b981; }

Vue.jsで `:class="'priority-' + task.priority"` のように動的にクラスを付与するだけで、データから見た目が自動的に変わります。テーブルでは文字バッジで表現していた情報を、カードでは色でも伝えられます。

ローディングスピナーの実装

データ取得中は何も表示されないとユーザーが「壊れた?」と思ってしまいます。Vue.jsの `ref(true)` で `loading` フラグを管理し、取得完了後に `false` にしています。

const loading = ref(true);

function loadTasks() {
  loading.value = true;
  google.script.run
    .withSuccessHandler(result => {
      tasks.value = result;
      loading.value = false;
    })
    .withFailureHandler(() => {
      loading.value = false;
      showToast('読み込みに失敗しました');
    })
    .getTasks();
}

HTMLでは `v-if="loading"` でスピナーを表示します。

<div v-if="loading" class="spinner-wrap">
  <div class="spinner"></div>
</div>

スピナー自体はCSSアニメーションだけで実現しています。外部ライブラリ不要でシンプルに実装できます。

.spinner {
  width: 36px;
  height: 36px;
  border: 3px solid #d1d5db;
  border-top-color: #4f46e5;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }


このくらいのサイズ感であれば実用に耐える

はまった点

フォームのレイアウト崩れ

PCでは1行に並べていた「テキスト入力 + 選択 + ボタン」のフォームが、スマホでは狭すぎてボタンが押せない状態になっていました。

解決は `flex-wrap: wrap` でフォールバックさせることです。テキスト入力に `flex-basis: 100%` を付けて最初の行を占有させ、選択とボタンを2行目に送りました。

@media (max-width: 768px) {
  .form-row { flex-wrap: wrap; }
  .form-row input[type="text"] { flex-basis: 100%; }
  .form-row select, .btn { flex: 1; }
}

CSSのスコープはiframe内

GASのWebアプリはiframe内に展開されます。そのため外部ページのCSSは一切届きません。CSSフレームワークを使う場合は必ずindex.html内でCDNを読み込む必要があります。`Bootstrap` を使おうとして親ページのCSSを当てようとしてしばらく悩みました。

まとめ

今回学んだこと

  • GASはHTMLに直書きのmetaタグを無視する → `doGet()` で `addMetaTag()` を呼ぶのが必須

  • テーブル → カード変換 は `@media` + `display: none/block` の切り替えで実現できる

  • Vue.jsの `v-if` + `loading` フラグ でローディングスピナーが簡単に実装できる

  • GASのWebアプリはiframe内 なので外部CSSは届かない、全スタイルを自己完結させること

モバイル対応はアフターとして後付けするより、最初から設計に組み込んでおくほうが圧倒的に楽です。次回 #77 は「WebアプリにAIチャットを組み込もう」に挑戦します!


GAS 100本ノック #76「Webアプリをスマホでもキレイにしよう」に挑戦!

GASではHTMLに直書きのmetaタグが無視されるという落とし穴を踏んでハマりました。addMetaTag()をdoGet()で呼ぶのが正解。テーブル→カード切り替えとCSSスピナーも実装してモバイル対応完了✨


いいなと思ったら応援しよう!