2024-11-24 2024-01-05

目次

はじめに

前々から気にはなっていた svelte だが、stackoverflow 等が強めに推しているようなのでこの機会に基本的な利用方法等を調査する。
https://survey.stackoverflow.co/2024/technology#2-web-frameworks-and-technologies
https://stackoverflow.blog/2023/10/31/why-stack-overflow-is-embracing-svelte/

特徴

フロントエンドフレームワーク Svelte の特徴。

  • 仮想DOMを使用しない。しかし高速。
  • コードの記述量が少なくて済む
  • 軽量(バンドルサイズが小さい)
  • ビルド等は vite ベース(高速)

環境構築、プロジェクト作成

npx sv create

いきなりエラーになる

■  Failed to install dependencies
│  npm error code EBADENGINE
│  npm error engine Unsupported engine
│  npm error engine Not compatible with your version of node/npm: @sveltejs/vite-plugin-svelte@4.0.1
│  npm error notsup Not compatible with your version of node/npm: @sveltejs/vite-plugin-svelte@4.0.1
│  npm error notsup Required: {"node":"^18.0.0 || ^20.0.0 || >=22"}
│  npm error notsup Actual:   {"npm":"10.9.1","node":"v21.5.0"}
    :
│  
└  Operation failed.

環境変数 NODE_VERSION に 16 セットすれば解決するっぽい。(少し古い issue だが)
https://github.com/sveltejs/kit/discussions/8583

■  Failed to install dependencies
│  npm ERR! code EBADENGINE
│  npm ERR! engine Unsupported engine
│  npm ERR! engine Not compatible with your version of node/npm: eslint@9.15.0
│  npm ERR! notsup Not compatible with your version of node/npm: eslint@9.15.0
│  npm ERR! notsup Required: {"node":"^18.18.0 || ^20.9.0 || >=21.1.0"}
│  npm ERR! notsup Actual:   {"npm":"9.8.1","node":"v18.16.0"}
     :
│  
└  Operation failed.

結果は変わらず。
node バージョンが原因である事は間違いないようなので、結局 nodeのバージョンを色々変えてみて、
自分の環境では最終的に node: 20.9.0、npm: 10.1.0 でエラーがなくなった。

┌  Welcome to the Svelte CLI! (v0.6.5)
│
◇  Where would you like your project to be created?
│  ./sample1
│
◇  Which template would you like?
│  SvelteKit minimal
│
◇  Add type checking with Typescript?
│  Yes, using Typescript syntax
│
◆  Project created
│
◇  What would you like to add to your project? (use arrow keys / space bar)
│  prettier, eslint
│
◇  Which package manager do you want to install dependencies with?
│  npm
│
◆  Successfully setup add-ons
│
◆  Successfully installed dependencies
│
◇  Successfully formatted modified files
│
◇  Project next steps ─────────────────────────────────────────────────────╮
│                                                                          │
│  1: cd sample1                                                           │
│  2: git init && git add -A && git commit -m "Initial commit" (optional)  │
│  3: npm run dev -- --open                                                │
│                                                                          │
│  To close the dev server, hit Ctrl-C                                     │
│                                                                          │
│  Stuck? Visit us at https://svelte.dev/chat                              │
│                                                                          │
├──────────────────────────────────────────────────────────────────────────╯
│
└  You're all set!

ファイル構成

.
├── README.md
├── eslint.config.js
├── package-lock.json
├── package.json
├── src
│   ├── app.d.ts
│   ├── app.html
│   ├── lib
│   │   └── index.ts
│   └── routes
│      ├── +page.svelte
│      └── sample1
├── static
│   └── favicon.png
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts

svelteのコンポーネント

svelteのコンポーネントは .svelte ファイルに以下の形式で記述する。

<script>
	// ロジックを記述

	// export する事により利用側から指定可能なプロパティを定義可能
	export let arg1;
</script>

<!-- 0個以上のマークアップを記述 -->

<style>
	/* styleを記述 */
</style>

<script context="module"> による初期化

https://svelte.jp/docs/svelte-components#script-context-module

context="module" 属性をもつ <script> タグはモジュールが最初に読み込まれる時に1回だけ評価される。
また module script で定義された変数はリアクティブではない。
※変数内容自体は更新されるが、再レンダリングの対象にはならない。

<script context="module">
console.log("this is script(module) tag");

let initValue = 10; 
</script>

<script>
let value = initValue;
console.log("this is script tag");
const onClick = () => {
    initValue += 1;
    value += 1;
    console.log(`initValue: ${initValue}`);
    console.log(`value: ${value}`);
};
</script>

<div>initValue: {initValue}</div> <!-- 再レンダリングされない --> 
<div>value    : {value}</div>     <!-- 再レンダリングされる --> 

<input type="button" on:click={onClick} value="button" />

リアクティブな変数

scriptタグ内のトップレベルで宣言された変数は基本的にリアクティブ(※)になる。
※ただし、.js や .ts ファイルは対象外。(.svelte ファイルに限られる)
※変更を自動的に検知して必要に応じて再レンダリングされる。
※後述 ルーン文字の使用 も参照。

<script>
let count = 0;
const countUp = () => {
  count++;
};
</script>

<p>count: {count}</p>
<input type="button" value="Add" on:click={countUp}>

また $: を付与する事により任意のステートメントをリアクティブにする事が出来る。
※React でいう useMemo 的な事が可能。

以下の 関数:multi は count2 が変わった時にだけ呼び出され、リアクティブ変数 count2_2 の内容を更新する。

<script>
let count1 = 0;
const count1Up = () => {
  count1++;
};

let count2 = 0;
const count2Up = () => {
  count2++;
};

const multi = (val) => {
    console.log("do multi");
    return val * 2;
};

$: count2_2 = multi(count2);

</script>

<h2>useMemo</h2>

<p>count1: {count1}</p>
<input type="button" value="Add" on:click={count1Up}>

<hr />
<p>count2: {count2}, count2 * 2 = {count2_2}</p>
<input type="button" value="Add" on:click={count2Up}>

ルーン文字の使用

Svelte5からはルーン文字を使用してリアクティブな変数を明示出来る。
また、.svelte ファイルだけでなく、.svelte.js や .svelte.ts ファイルも対象に出来る。
https://svelte.jp/blog/runes
https://svelte.jp/docs/svelte/what-are-runes

以下、ルーン文字の使用有無による実装の違いについて記載する。(内容はシンプルなカウンタ)

ルーン文字を使用しない場合

<script>
let count = 0;
$: count2 = count * 2;
</script>

count: <input type="button" value="-" on:click={()=>count -= 1} />
{count}
<input type="button" value="+" on:click={()=>count += 1}/>
... count * 2 = {count2}

ルーン文字を使用する場合

<script>
let count = $state(0);                  // ≒ react の useState
let count2 = $derived(count * 2);       // ≒ react の useMemo
</script>

count:
<input type="button" value="-" on:click={()=>count -= 1} />
{count}
<input type="button" value="+" on:click={()=>count += 1}/>
... count * 2 = {count2}

また、ルーン文字が使用される場合、scriptタグ内のトップレベル変数は全てリアクティブではなくなる。
また $: は許可されない(エラー)。

<script>
let count1 = 0; // この変数はリアクティブではなくなる
// $: count1_2 = count1 * 2;  <-- これはエラーになる( `$:` is not allowed in runes mode )
let count2 = $state(0);
let count2_2 = $derived(count2 * 2);
</script>

<!-- こちらは非リアクティブ -->
count1:
<input type="button" value="-" on:click={()=>count1 -= 1} />
{count1}
<input type="button" value="+" on:click={()=>count1 += 1}/>

<hr />

<!-- こちらはリアクティブ -->
count2:
<input type="button" value="-" on:click={()=>count2 -= 1} />
{count2}
<input type="button" value="+" on:click={()=>count2 += 1}/>

マークアップの基本

https://svelte.jp/docs/basic-markup

小文字のタグは通常のHTML要素、大文字で始まるタグはコンポーネントを示す。

<script>
	import Widget from './Widget.svelte';
</script>

<div>
	<Widget />
</div>

値の展開

https://svelte.jp/docs/basic-markup

{}で囲む事により変数の内容を展開する事ができる。
また、Javascript式を書くことも可能。

<script>
    let count = 0;
    function handleClick() {
        count = count + 1;
    }   
</script>

<p>count: {count}</p>             <!-- 変数の内容をそのまま展開 -->
<p>count + 10 = {count + 10}</p>  <!-- JavaScript式を書くことも可能 -->
<button on:click={handleClick}>+</button>

条件分岐 {#if ...}

https://svelte.jp/docs/logic-blocks

{#if expression}...{/if}
{#if expression}...{:else if expression}...{/if}
{#if expression}...{:else}...{/if}

繰り返し {#each ...}

{#each expression as name}...{/each}
{#each expression as name, index}...{/each}
{#each expression as name (key)}...{/each}
{#each expression as name, index (key)}...{/each}
{#each expression as name}...{:else}...{/each}

サンプル

<ul>
	{#each items as item}
		<li>{item.name} x {item.qty}</li>
	{/each}
</ul>

Proimiseの状態に応じた分岐 {#await ...}

https://svelte.jp/docs/logic-blocks#await

await ブロックを使用すると、Promise が取りうる 3 つの状態(pending(保留中)、fulfilled(成功)、rejected(失敗))に分岐できる。

{#await expression}...{:then name}...{:catch name}...{/await}
{#await expression}...{:then name}...{/await}
{#await expression then name}...{/await}
{#await expression catch name}...{/await}

サンプル

{#await promise}
	<!-- promise is pending -->
	<p>waiting for the promise to resolve...</p>
{:then value}
	<!-- promise was fulfilled or not a Promise -->
	<p>The value is {value}</p>
{:catch error}
	<!-- promise was rejected -->
	<p>Something went wrong: {error.message}</p>
{/await}

要素の再作成 {#key ...}

https://svelte.jp/docs/logic-blocks#key

key ブロックは式(expression)の値が変更されたときに、その中身を破棄して再作成する。

{#key expression}...{/key}

サンプル

{#key value}
	<Component />
{/key}

特殊タグ

https://svelte.jp/docs/special-tags

{@html ...} ... HTMLタグをエスケープしない
{@debug ...} ... 指定した変数の値が変更されるたびログ出力
{@const ...} ... マークアップ内でローカル定数を定義する

イベントの捕捉

https://svelte.jp/docs/element-directives#on-eventname

on:eventname={handler}
on:eventname|modifiers={handler}
<button on:click={handleClick}>
	count: {count}
</button>

| を使ってDOMイベントに修飾子(modifiers)を追加する事も可能。

<form on:submit|preventDefault={handleSubmit}>
	<!-- `submit` イベントのデフォルトの動作が止められているためページはリロードされない -->
</form>

バインディング

https://svelte.jp/docs/element-directives#bind-property

bind:プロパティ名={変数名} でバインド可能。
※プロパティ名と変数名が同じ時は省略形式可。

<input bind:value={value} />
<input bind:value />

value値のバインディング

<script>
    let message = $state('hello');
</script>

<input bind:value={message} />
<p>{message}</p>

<select> 値のバインディング

https://svelte.jp/docs/element-directives#binding-select-value

<script>
    let selected = '';
</script>

{selected || '空値'} が選択されています<br />
<select bind:value={selected}>
    <option value="">(未選択)</option>
    <option value="a">a</option>
    <option value="b">b</option>
    <option value="c">c</option>
</select>

ブロックレベル要素のバインディング

https://svelte.jp/docs/element-directives#block-level-element-bindings

描画結果から clientWidth, clientHeight, offsetWidth, offsetHeight を取得する事が出来る。

<script>
    let width = 0;
    let height = 0;
</script>

<div bind:offsetWidth={width} bind:offsetHeight={height} style="padding: 10px; background: #efe; width: 200px;">
    width: {width}, height: {height}<br />
</div>

バインドのグループ化

https://svelte.jp/docs/element-directives#bind-group

<script>
    let radioChecked  = 'a';
    let multiChecked = [];
</script>

<div>
    {radioChecked} が選択されています<br />
    <input type="radio" bind:group={radioChecked} value="a" />a
    <input type="radio" bind:group={radioChecked} value="b" />b
    <input type="radio" bind:group={radioChecked} value="c" />c
</div>

<div>
    {multiChecked} が選択されています<br />
    <input type="checkbox" bind:group={multiChecked} value="a" />a
    <input type="checkbox" bind:group={multiChecked} value="b" />b
    <input type="checkbox" bind:group={multiChecked} value="c" />c
    <input type="checkbox" bind:group={multiChecked} value="d" />d
</div>

DOM要素の参照

https://svelte.jp/docs/element-directives#bind-this

<script>
function moveBack() {
  history.back();
}
let myInput;
let info_message = ''; 
const checkInput = () => {
    info_message = myInput.value + "が入力されています";
}
</script>

<div>
    {info_message}<br />
    <input bind:this={myInput} type="text" value="" />
    <button on:click={checkInput} >check</button>
</div>

スタイルの指定

https://svelte.jp/docs/element-directives#style-property

<script>
let color = 'red';
let bgColor = 'green';
</script>

<!-- この2つは同等 -->
<div style:color="red">...</div>
<div style="color: red;">...</div>

<div style:color={color}>変数を使用</div>

<div style:color>プロパティと変数の名前が一致する場合の短縮形</div>

<div style:color style:width="12rem" style:background-color={bgColor}>複数のスタイルを含めることが可能</div>

<div style:color|important="blue">important ハック</div>

use:action

https://svelte.jp/docs/element-directives#use-action
https://svelte.jp/docs/svelte-action

要素が作成されるときに呼び出される関数を指定する事が可能。
※要素がアンマウントされるときに呼び出される destroy メソッドをもつオブジェクトを返す事も可能。

<script>

export let arg;

const myElement = (arg) => {
    console.log("created myElement!");
    return {
        destroy: function(){
            console.log("destroy myElement!");
        }   
    };  
};
</script>

<div use:myElement>
    This is My Element. ( arg: {arg} )
</div>

イベント発火/捕捉

https://svelte.jp/docs/component-directives#on-eventname

<script>
const onChange = (e) => {
    console.log(`"${e.target.value}" が入力されました`); 
};
const onClick = () => {
    console.log("ボタンがクリックされました"); 
};
</script>

<input type="text" on:change={onChange} />
<input type="button" on:click={onClick} value="click me" />

createEventDispatcher を使用してカスタムイベントを発火する事も可能。

<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>

<!-- そのまま親に転送する場合 -->
<input type="button" on:click value="forward event" />
<!-- カスタムイベントを発火する場合 -->
<input type="button" on:click={()=>dispatch('notify1', `this is dispatchEvent`)} value="custom event(notify1) " />

この場合、利用側は on:イベント名 でイベントを補足する事が出来る。

<script>
const onClick = (e) => {
  console.log(e);
};
const onNotify1 = (e) => {
  console.log(e);
  console.log(e.detail);
};
</script>

<MyConponent on:click={onClick} on:notify1={onNotify1} />

ライフサイクル関数など

https://svelte.jp/docs/svelte

<script>

import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte';

export let text = ''; 
const onChangeText = (e) => {
    console.log(e);
    text = e.target.value;
};

onMount(()=>{
    console.log("onMount");
});
beforeUpdate(() => {
    console.log("beforeUpdate");
});
afterUpdate(() => {
    console.log("afterUpdate");
});
onDestroy(() => {
    console.log("onDestroy");
});

</script>

<h2>ライフサイクル関数</h2>

<input type="text" on:change={onChangeText} value={text} />

ルーティング等

https://kit.svelte.jp/docs/routing
https://kit.svelte.jp/docs/routing#page-page-js
https://kit.svelte.jp/docs/load

基本的に routes配下にディレクトリ 及び ファイルを配置するだけで良い。
※+page.svelte そのディレクトリ配下のルートにアクセス時に使用される。

 └── routes
   ├── +page.svelte
   ├── sample1
   │   └── +page.svelte
   └── sample2
       └── +page.svelte

Rest的に /items で商品一覧、/items/{商品ID} を商品詳細ページとするような場合、以下のように構成する。

 └── routes
   ├── +page.svelte
   └── items
       └── +page.svelte ... 一覧ページ用
       └── [id]
          └── +page.svelte ... 詳細ページ用

以下にサンプルを記載する。

routes/items/SampleData.svelte

<script context="module">
export const items = [ 
    {"id": "0001", name: "商品0001", price: 1100},
    {"id": "0002", name: "商品0002", price: 2200},
    {"id": "0003", name: "商品0003", price: 3300},
    {"id": "0004", name: "商品0004", price: 4400},
    {"id": "0005", name: "商品0005", price: 5500},
];
</script>

routes/items/+page.svelte

<script>
import { goto } from '$app/navigation';
import { items } from './SampleData.svelte';
const pageMode = (id) => {
    const res = items.filter((x)=>x.id == id);
    if (!res.length) {
        return;
    }   
    const rec = res[0];
    goto(`/items/${id}`)
};
</script>

<h2>商品一覧</h2>
<table class="tbl">
    <thead>
    <tr>
        <th>商品ID</th>
        <th>商品名</th>
        <th>価格</th>
    </tr>
    </thead>
    <tbody>
    {#each items as x}
    <tr>
        <td on:click={()=>pageMode(x.id)}>{x.id}</td>
        <td>{x.name}</td>
        <td>{x.price}</td>
    </tr>
    {/each}
    </tbody>
</table>

<style>
.tbl {
    th,td {
        border: 1px solid #333;
    }
    td:first-child {
        cursor: pointer;
        color: #00f;
        &:hover {
            text-decoration: underline;
        } 
    } 
}
</style>

レンダリング前のデータ読み込み

レンダリングの前にデータを読み込む必要がある場合、+page.js モジュールにて load 関数をエクスポートする事でこれを実現できる。

https://kit.svelte.jp/docs/routing#page-page-js
https://kit.svelte.jp/docs/load

routes/items/[id]/+page.js

import { error } from '@sveltejs/kit'; 
import { items } from '../SampleData.svelte';

/** @type {import('./$types').PageLoad} */
export function load({ params }) {
    const filtered = items.filter((x)=>x.id == params.id);
    if (filtered.length) {     
        return filtered[0];    
    }
    error(404, 'Not found');   
}

load 関数の戻り値は page で data プロパティから取得する事が出来る。

https://kit.svelte.jp/docs/load#page-data

routes/items/[id]/+page.svelte

<script>
export let data;
</script>

<h2>商品詳細</h2>

<div>商品ID: {data.id</div>
<div>商品名: {data.name}</div>
<div>価格  : {data.price}</div>

共通レイアウト

https://kit.svelte.jp/docs/routing#layout

  • 全てのページに適用するレイアウトは src/routes/+layout.svelte で作成出来る。
  • <slot></slot> の部分に各ページの内容が展開される。
  • 同様に特定のPATH配下に +layout.svelte を作成する事で、特定PATH配下でのみ使用するレイアウトの作成も可能。
  • レンダリング前にデータ読み込みが必要な場合は +layout.js を作成する。(考え方は上記 +page.svelte と同じ)
<div id="nav">
  <a>メニュー1</a>
  <a>メニュー2</a>
</div>

<div id="contents">
   <slot></slot>   <!-- ここに各ページの内容が展開される -->
</div>

<div id="footer">
フッター
</div>

環境変数

.env や .env.development、.env.local 等の内容を読み込む事が出来る。

https://kit.svelte.jp/docs/modules#$env-dynamic-private
https://kit.svelte.jp/docs/modules#$env-dynamic-public
https://kit.svelte.jp/docs/modules#$env-static-private
https://kit.svelte.jp/docs/modules#$env-static-public

.env.local

PUBLIC_SAMPLE_VAR1=test123

利用側

<script>

import { PUBLIC_SAMPLE_VAR1 } from '$env/static/public';
console.log(PUBLIC_SAMPLE_VAR1);

</script>

複数コンポーネント間での値の共有

svelte/store の機能を使用してアプリケーション全体で管理する状態などの管理を行う事が出来る。(ReactのRedux的なもの)

機能はいくつかあるが、以下を利用すれば大抵の事は出来る。( https://svelte.jp/docs/svelte-store )

機能説明
writableサブスクリプションによる更新と閲覧の両方が可能なストアを作成する
readonlyストアを受け取り、読み取り可能な古いストアから派生した新しいストアを返す
derived1 つ以上の読み取り可能なストアを同期し、その入力値に集計関数を適用して派生した値ストア
getサブスクライブしてすぐにサブスクライブを解除することで、ストアから現在の値を取得する

サンプル

<script>
import { readonly, writable } from 'svelte/store';

const sampleStore = writable({ num: 1, str: "test1"});
const sampleReadOnly = readonly(sampleStore);

// subscribeにて購読可能
sampleReadOnly.subscribe((data)=>{
    console.log("change data!", data);
});

let inputNum;
let inputStr;
const update = () => {
    // セットメソッドを使用する場合
    sampleStore.set({num: inputNum.value, str: inputStr.value});
    // $参照で直接セットする場合(.svelte ファイルのみで使用可)( Cannot reference store value outside a `.svelte` file ) (svelte 5.0.0 時点)
    //$sampleStore = {num: inputNum.value, str: inputStr.value};
};

</script>

<!-- $によるストア値の参照(.svelte ファイルのみで使用可) --> 
num1: <input type="text" bind:this={inputNum} value={$sampleStore.num} />
str1: <input type="text" bind:this={inputStr} value={$sampleStore.str} />

<input type="button" on:click={update} value="OK" />

もう少し実用的な例として、ログイン状態によってメニュー表示やログインページへの自動遷移を行う場合の例を記載する。
※例では、利用(各ページ)側にはreadonlyなストア、及び 更新用のメソッドのみを提供し、ストア自体の直接更新はさせない形にしたが、writable なストアだけを公開する方法でも可(方針次第)。

ユーザ情報を管理するストア ( src/routes/store.js )

//import { page } from "$app/stores";
import { goto } from '$app/navigation';
import { readonly, writable } from 'svelte/store';

const userProfileStore = writable({ isLoggedIn: false, id: null, name: null, email: null, roles: []});
export const userProfile = readonly(userProfileStore);

userProfile.subscribe((u) => {

  // 未ログイン時はログインページに自動遷移
  try {
    const urlAll = window?.location?.href || ''; 
    const uri = urlAll.replace(/^http(s|):\/\/[^/]+(\/[^\?#]+).*/, "$2")
    //const uri = $page.url.pathname;  // .svelte ファイル以外では使用できない ( Cannot reference store value outside a `.svelte` file )

    // 未ログイン かつ ログインページでない時
    if (!u.isLoggedIn && uri !== "/login") {
      goto(`/login/`);
      return;
    }   
  } catch (e) {
  }
});

export const setUserProfile = (id, name, email, roles) => {
  //$userProfileStore = { isLoggedIn: true, id, name, email, roles };
  userProfileStore.set({ isLoggedIn: true, id, name, email, roles })
};

export const unsetUserProfile = () => {
  //$userProfileStore = { isLoggedIn: false, id: null, name: null, email: null, roles: []};
  userProfileStore.set({ isLoggedIn: false, id: null, name: null, email: null, roles: []});
};

共通レイアウト ( routes/+layout.svelte )

<script>
import { unsetUserProfile, userProfile } from './store.js';
</script>

<!-- ヘッダ(メニュー) -->
{#if ($userProfile.isLoggedIn === true)}
<div id="header">
    <ul class="left">
        <li><a href="/">TOP</a></li>
        <li><a href="/sample1">メニュー1</a></li>
        <li><a href="/sample2">メニュー2</a></li>
        <li><a href="/sample3">メニュー3</a></li>
    </ul>
    <div class="right">
        <a href="#" on:click={unsetUserProfile}>ログアウト</a>
    </div>
</div>
{/if}

<!-- コンテンツ -->
<div id="contents">
    <slot></slot>
</div>

<div id="footer">
    フッター
</div>

ログインページ( routes/login/+page.svelte )

<script>
//import { goto } from '$app/navigation';
//import { PUBLIC_API_URL_PREFIX } from '$env/static/public';
import { setUserProfile, userProfile } from '../store';

let userId;
let password;

const doLogin = () => {

    // ログイン処理
    //const url = PUBLIC_API_URL_PREFIX + "/login";
    //const options = {
    //    method: 'POST',
    //    headers: {"content-type": 'application/json'},
    //    body: JSON.stringify({ user_id: userId.value, password: password.value })
    //};
    //fetch(url, options)
    //.then((response) => {
    //    if (response.ok) {
    //        response.json().then((res)=>{
    //            setUserProfile(res.user_id, res.user_name, res.user_email, []);
    //            console.log(res);
    //            goto(`/`);
    //        }); 
    //    } else {
    //        console.log("error!");
    //    }   
    //})  

    setUserProfile(userId.value, userId.value, "xxxxx@xxxx.com", []);
    goto(`/`);
};

</script>

<div id="login">
    <div id="login_form">
        <div class="row">
            <div>ユーザID:</div>
            <div><input type="text" bind:this={userId} /></div>
        </div>
        <div class="row">
            <div>パスワード:</div>
            <div><input type="password" bind:this={password} /></div>
        </div>
        <div class="row" on:click={doLogin}>
            <button>ログイン</button>
        </div>
    </div>
</div>

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2025-01-05 (日) 19:22:00 (10d)