Svelteを使ってメモアプリを作ってみた(2)

  • プロダクション
Svelteを使ってメモアプリを作ってみた(2)

目次

前回のコラムについて

前回のコラムはこちら
Svelteを使ってメモアプリを作ってみた(1)

前回のコラムでは、Svelteの特徴、作成したアプリの概要、Svelteの導入方法について解説しました。
今回のコラムでは、Svelteの基本的な書き方の説明と、実際に作成したファイルについての説明をしていきます。

Svelteの基礎文法について

Svelteは拡張子が".svelte"のファイルの中に、HTML、CSS、JSをまとめて記述します。
Vueの書き方に似ていますが、1つの違いとしてはHtml部分では"template"タグが不要となっています。

下記に今回アプリの作成で必要な基礎文法を公式サイトのサンプルページと共に紹介していきます。

■変数をHTML内で使用
<p>{変数名}</p>
https://svelte.jp/examples/hello-world

■HTML内で条件分岐
{#if 条件式}
{/if}
https://svelte.jp/examples/if-blocks

■HTML内でループ処理
{#each 配列 as 変数名, i}
{/each}
https://svelte.jp/examples/each-blocks

■対象の要素にイベントの付与
<div on:event={関数}></div>
https://svelte.jp/examples/dom-event

作成したファイル

今回作成したファイルは9つとなります。
・/src/App.svelte(メインファイル。ルート直下で表示しているのはこのファイル)
・/src/pages/List.svelte(メモ一覧画面のコンポーネント)
・/src/pages/Detail.svelte(メモ詳細画面のコンポーネント)
・/src/component/MemoList.svelte(メモ一覧のコンポーネント)
・/src/component/modal/DeleteMemoModal.svelte(メモ削除モーダル)
・/src/component/modal/RegistMemoModal.svelte(メモ登録モーダル)
・/src/store/common.ts(コンポーネント間で値の共有をするためのストアファイル)
・/src/assets/css/reset.css(リセットCSS)
・/src/assets/css/common.css(共通CSS)

これらのファイルの役割と実際のソースコードを記載します。(CSSファイルは除く)

■/src/App.svelte(メインファイル。ルート直下で表示しているのはこのファイル)

アプリケーションのメインコンポーネント。
ストアに定義しているscreenTypeを使用して条件文を定義し、表示する画面の切り替えを行っています。
コンポーネントを呼び出したいときには、scriptタグ内で呼び出したいコンポーネントをimportさせ、
「<コンポーネント名 />」をHTML内に定義させます。
screenTypeが0の場合は一覧画面を表示し、1の場合は詳細画面を表示させています。


<script lang="ts">
  import { screenType } from "./store/common";
  import List from "./pages/List.svelte";
  import Detail from "./pages/Detail.svelte";
  import "./assets/css/reset.css";
  import "./assets/css/common.css";
</script>
<main>   <h1>メモアプリ</h1>   {#if $screenType === 0}     <List />   {:else if $screenType === 1}     <Detail />   {/if} </main>
<style lang="scss">   main {     width: 544px;     padding: 20px;   } </style>

■/src/pages/List.svelte(メモ一覧画面のコンポーネント)

メモ登録モーダルを表示するボタンとメモ一覧コンポーネントの呼び出しを行っています。
メモの一覧はローカルストレージから読み込んでいます。


<script lang="ts">
  import MemoListComponent from "../component/MemoList.svelte";
  import RegistMemoModal from "../component/modal/RegistMemoModal.svelte";
  import DeleteMemoModal from "../component/modal/DeleteMemoModal.svelte";
  import { memoList, showDeleteMemoModalFlg } from "../store/common";
  import { onMount } from "svelte";
  let showRegistMemoModalFlg: boolean = false; // メモ登録モーダルの表示フラグ
  onMount(() => {     loadMemoList(); // ローカルストレージからメモ一覧を取得   });
  /**    * ローカルストレージからメモ一覧を取得    */   function loadMemoList() {     const storageMemoList: any[] =       JSON.parse(localStorage.getItem("memoList")) || [];
    memoList.set(storageMemoList);   }
  /**    * メモ登録モーダルを表示    */   function showRegistMemoModal() {     showRegistMemoModalFlg = true;   }
  /**    * メモ登録モーダルを非表示    */   function closeRegistMemoModal() {     showRegistMemoModalFlg = false;   } </script>
<div class="home">   <div class="contents">     <button type="button" class="btn" on:click={showRegistMemoModal}       >メモを登録</button     >     <div class="memo-list-area">       <MemoListComponent />     </div>   </div> </div>
{#if showRegistMemoModalFlg}   <RegistMemoModal on:close={closeRegistMemoModal} /> {/if} {#if $showDeleteMemoModalFlg}   <DeleteMemoModal /> {/if}
<style lang="scss">   .home {     .contents {       margin-top: 12px;       .btn {         width: 120px;       }       .memo-list-area {         margin-top: 20px;       }     }   } </style>

■/src/pages/Detail.svelte(メモ詳細画面のコンポーネント)

登録したメモの参照・更新を行っています。
更新ボタン押下時には、ローカルストレージに保持しているメモ一覧の内容を更新させています。


<script lang="ts">
  import { screenType, memoList, selectIndex } from "../store/common";
  let memoTitle: string = $memoList[$selectIndex].title;   let memoContext: string = $memoList[$selectIndex].context;
  /**    * メモ一覧画面に戻る    */   function backListPage() {     screenType.set(0);   }
  /**    * メモを更新する    */   function updateMemo() {     const localStorageMemoList: string = localStorage.getItem("memoList");     const newMemoList: any[] = localStorageMemoList       ? JSON.parse(localStorageMemoList)       : [];
    newMemoList[$selectIndex].title = memoTitle;     newMemoList[$selectIndex].context = memoContext;
    localStorage.setItem("memoList", JSON.stringify(newMemoList));
    memoList.set(newMemoList);     backListPage(); // メモ一覧画面に戻る   } </script>
<div class="detail">   <p class="link-text" on:click={() => backListPage()}>一覧画面に戻る</p>   <div class="contents">     <label class="input-title">       <p>タイトル</p>       <input bind:value={memoTitle} type="text" />     </label>     <label class="input-context">       <p>内容</p>       <textarea bind:value={memoContext} />     </label>   </div>   <div class="buttons">     <button class="btn" on:click={updateMemo}>更新</button>   </div> </div>
<style lang="scss">   .detail {     margin-top: 12px;     .contents {       margin-top: 24px;       label {         display: block;         &:not(:first-child) {           margin-top: 12px;         }         input,         textarea {           width: 100%;           margin-top: 8px;           font-size: 20px;         }         input {           height: 32px;         }         textarea {           height: 150px;         }       }     }     .buttons {       display: flex;       align-items: center;       justify-content: center;       margin-top: 24px;       .btn {         width: 120px;       }     }   } </style>

■/src/component/MemoList.svelte(メモ一覧のコンポーネント)

登録しているメモの一覧を表示しています。
1件も登録していない場合は、登録されていないことを促すメッセージを表示させています。
メモクリック時に選択したメモのインデックスをstoreに保持し、メモ詳細画面に遷移させています。


<script lang="ts">
  import {
    screenType,
    memoList,
    showDeleteMemoModalFlg,
    selectIndex,
  } from "../store/common";
  /**    * メモ削除モーダルを表示    *    * @param {number} index 選択したメモのインデックス    */   function showDeleteMemoModal(index: number) {     selectIndex.set(index);     showDeleteMemoModalFlg.set(true);   }
  /**    * 詳細ページを非表示    *    * @param {number} index 選択したメモのインデックス    */   function showMemoDetailPage(index: number) {     selectIndex.set(index);     screenType.set(1);   } </script>
{#if $memoList.length > 0}   <ul class="memo-list">     {#each $memoList as { title, date }, i}       <li on:click={() => showMemoDetailPage(i)}>         <p class="title">{title}</p>         <p class="date">登録日:{date}</p>         <span           class="delete"           on:click|stopPropagation={() => showDeleteMemoModal(i)}>×</span         >       </li>     {/each}   </ul> {:else}   <p>メモは1件も登録されていません</p> {/if}
<style lang="scss">   .memo-list {     li {       border: 1px solid;       padding: 20px;       position: relative;       cursor: pointer;       &:not(:first-child) {         border-top: none;       }       &:hover {         background: #dcfaff;       }       .title {         font-size: 24px;       }       .delete {         position: absolute;         top: 50%;         right: 20px;         transform: translateY(-50%);         font-size: 32px;         &:hover {           color: #cbb5b5;         }       }     }   } </style>

■/src/component/modal/DeleteMemoModal.svelte(メモ削除モーダル)

選択したメモを削除します。
メモ削除時にはローカルストレージから選択したメモを削除します。


<script lang="ts">
  import {
    memoList,
    showDeleteMemoModalFlg,
    selectIndex,
  } from "../../store/common";
  let memoTitle = $memoList[$selectIndex].title;
  /**    * メモを削除    */   function deleteMemo() {     const localStorageMemoList: string = localStorage.getItem("memoList");     const newMemoList: any[] = localStorageMemoList       ? JSON.parse(localStorageMemoList)       : [];
    newMemoList.splice($selectIndex, 1);
    localStorage.setItem("memoList", JSON.stringify(newMemoList));
    memoList.set(newMemoList);     closeModal(); // 本モーダルを閉じる   }
  /**    * 本モーダルを閉じる    */   function closeModal() {     showDeleteMemoModalFlg.set(false);   } </script>
<div class="modal">   <div class="modal-background">     <div class="modal-contents">       <span class="close" on:click={closeModal}>×</span>       <h2 class="title">メモを削除</h2>       <div class="contents">         <p>『{memoTitle}』のメモを削除してもよろしいですか</p>       </div>       <div class="buttons">         <button class="btn" on:click={closeModal}>キャンセル</button>         <button class="btn" on:click={deleteMemo}>削除</button>       </div>     </div>   </div> </div>
<style lang="scss">   .modal {     position: absolute;     top: 0;     bottom: 0;     left: 0;     right: 0;     &-background {       width: 100%;       height: 100%;       background: rgba(232, 222, 220, 0.7);     }     &-contents {       position: absolute;       top: 50px;       left: 0;       right: 0;       margin: auto;       width: 400px;       padding: 20px;       background: #ffffff;       border-radius: 10px;       opacity: 1;       .close {         position: absolute;         top: 10px;         right: 10px;         font-size: 24px;         cursor: pointer;         &:hover {           color: #cbb5b5;         }       }       .title {         text-align: center;       }       .contents {         margin-top: 24px;       }       .buttons {         display: flex;         align-items: center;         justify-content: center;         margin-top: 24px;         .btn {           width: 120px;           &:not(:first-child) {             margin-left: 12px;           }         }       }     }   } </style>

■/src/component/modal/RegistMemoModal.svelte(メモ登録モーダル)

入力したタイトルとテキストの内容をメモとして登録します。
メモ登録時にはローカルストレージに値を追加します。


<script lang="ts">
  import { createEventDispatcher } from "svelte";
  import { memoList } from "../../store/common";
  const dispatch = createEventDispatcher();   let memoTitle: string = "";   let memoContext: string = "";
  /**    * メモを登録    */   function registMemo() {     const localStorageMemoList: string = localStorage.getItem("memoList");     const newMemoList: any[] = localStorageMemoList       ? JSON.parse(localStorageMemoList)       : [];     const now: Date = new Date();
    newMemoList.push({       title: memoTitle,       context: memoContext,       date: `${String(now.getFullYear())}年${String(         now.getMonth() + 1       )}月${String(now.getDate())}日`,     });
    localStorage.setItem("memoList", JSON.stringify(newMemoList));
    memoList.set(newMemoList);     closeModal(); // 本モーダルを閉じる   }
  /**    * 本モーダルを閉じる    */   function closeModal() {     dispatch("close");   } </script>
<div class="modal">   <div class="modal-background">     <div class="modal-contents">       <span class="close" on:click={closeModal}>×</span>       <h2 class="title">メモを登録</h2>       <div class="contents">         <label class="input-title">           <p>タイトル</p>           <input bind:value={memoTitle} type="text" />         </label>         <label class="input-context">           <p>内容</p>           <textarea bind:value={memoContext} />         </label>       </div>       <div class="buttons">         <button class="btn" on:click={closeModal}>キャンセル</button>         <button class="btn" on:click={registMemo}>登録</button>       </div>     </div>   </div> </div>
<style lang="scss">   .modal {     position: absolute;     top: 0;     bottom: 0;     left: 0;     right: 0;     &-background {       width: 100%;       height: 100%;       background: rgba(232, 222, 220, 0.7);     }     &-contents {       position: absolute;       top: 50px;       left: 0;       right: 0;       margin: auto;       width: 500px;       padding: 20px;       background: #ffffff;       border-radius: 10px;       opacity: 1;       .close {         position: absolute;         top: 10px;         right: 10px;         font-size: 24px;         cursor: pointer;         &:hover {           color: #cbb5b5;         }       }       .title {         text-align: center;       }       .contents {         margin-top: 24px;         label {           display: block;           &:not(:first-child) {             margin-top: 12px;           }           input,           textarea {             width: 100%;             margin-top: 8px;             font-size: 20px;           }           input {             height: 32px;           }           textarea {             height: 150px;           }         }       }       .buttons {         display: flex;         align-items: center;         justify-content: center;         margin-top: 24px;         .btn {           width: 120px;           height: 32px;           &:not(:first-child) {             margin-left: 12px;           }         }       }     }   } </style>

■/src/store/common.ts(コンポーネント間で値の共有をするためのストアファイル)

異なるコンポーネント間で値を使いまわしたい場合に、ストアファイルに定義します。
ここでは、screenType(画面タイプ)、showDeleteMemoModalFlg(メモ削除モーダル表示フラグ)、selectIndex(選択したメモのインデックス)、memoList(メモ一覧)の4つを定義しています。

import { writable } from "svelte/store";
export const screenType = writable<number>(0); // 0:一覧画面 1:詳細画面 export const showDeleteMemoModalFlg = writable<boolean>(false); export const selectIndex = writable<number>(null);
export type memo = {   title: string;   context: string;   date: string; };
export const memoList = writable<memo[]>([]);

まとめ

Sveleteを実際に触ってみて一番に思ったことは、ネイティブのJavaScriptの書き方に近いのでJSのフレームワークの中でもかなりとっつきやすいということでした。
情報量はReactやVueなどと比べると少ないですが、公式サイトが非常に親切に書かれているので基礎的な書き方については十分学習できると思いました。
大規模アプリを作成するのはまだ向いていないかもしれないですが、小規模のアプリをJSのフレームワークを使って作ってみたいという方にはかなりオススメのフレームワークとなっております。

この記事の執筆者

Y.M

テクノロジーソリューション事業部

この記事に関するご相談やご質問など、お気軽にお問い合わせください。

お問い合わせ

タグ一覧