9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rails+Svelteによるフォームの作例

Last updated at Posted at 2020-02-03

Rails+Vue.jsによるフォームの作例Rails+Reactによるフォームの作例Svelte版を作ってみました。

サンプルプログラムはこちら。
https://github.com/kazubon/blog-rails6-svelte

Rails側の作り方や全体的なポイントはVue版と同じですので、Vue版の記事を参照してください。

環境

  • Rails 6.0、Webpacker 4.2、Svelte 3.18。
  • 非SPA、Turbolinksあり。
  • jQueryとBootstrapあり。

Svelteの導入

作成済みのアプリケーションに導入するときは、次のコマンドで。Vueが入っている環境に上書きしたところ、問題なさそうでした。

% bin/rails webpacker:install:svelte

Railsアプリケーションを作成するときに --webpack=svelte の指定もできます。こちらは未確認。

% rails new myapp --webpack=svelte

application.js

applicaiton.jsは、Vue版React版と同じように書けました。HTML要素をid属性で探して、対応するSvelteコンポーネントを new App({ target: HTML要素, props })のようにマウントします(この例では new app.object のところ)。

app/javascript/packs/application.js
import "core-js/stable";
import "regenerator-runtime/runtime";

require("@rails/ujs").start();
require("turbolinks").start();

import EntryIndex from '../entries/index';
import EntryForm from '../entries/form';
import EntryStar from '../entries/star';
import Flash from '../flash';

document.addEventListener('turbolinks:visit', () => {
  if(window.svelteApp) {
    window.svelteApp.$destroy();
    window.svelteApp = null;
  }
});

document.addEventListener('turbolinks:load', () => {
  Flash.show();

  let apps = [
    { elem: '#entry-index', object: EntryIndex },
    { elem: '#entry-form', object: EntryForm },
    { elem: '#entry-star', object: EntryStar }
  ];

  let props = window.jsProps || {};
  apps.forEach((app) => {
    if($(app.elem).length) {
      window.svelteApp = new app.object({ target: $(app.elem)[0], props });
    }
  });
});

Turbolinksとの相性の問題があります。ブラウザーの「戻る」を使ったときに以前のSvelteアプリケーションが残る、というものです。そこで、turbolinks:visitイベントで既存のSvelteアプリケーションを削除します。

app/javascript/packs/application.js
document.addEventListener('turbolinks:visit', () => {
  if(window.svelteApp) {
    window.svelteApp.$destroy();
    window.svelteApp = null;
  }
});

Svelteを埋め込むHTMLのテンプレートでは、propsに渡すためにグローバル変数jsPropsを作っています。

app/views/entries/edit.html.erb
<script>
var jsProps = <%= { entryId: @entry.try(:id) }.to_json.html_safe %>;
</script>
<div id="entry-form"></div>

編集ページのフォーム

記事の編集フォームだけかいつまんで紹介します。

propsとstate

VueやReactのpropsに当たるものは、次のようにexportを使って記述します。

app/javascript/entries/form.svelte
export let entryId;

.svelteファイルがJavaScriptに変換されるときは、ソース中の<script></scirpt> の内容も変換されます。exportは次のように変換されていました。

function instance($$self, $$props, $$invalidate) {
    let { entryId } = $$props;

Vueのdata、Reactのstateに当たるものは、letで変数を作ります。

app/javascript/entries/form.svelte
let entry = {
  title: null, body: null, tags: [], published_at: null, draft: false
};
let alert = null;

値を代入するところは、次のように変換されてました。

$$invalidate(1, entry = res.data.entry);

テンプレート

テンプレートはこんな感じです。特に難しいところはありません。いくつかリンクを貼っておきます。

条件分岐 | 繰り返し | イベント | 入力欄のバインディング

app/javascript/entries/form.svelte
<div>
  <form on:submit|preventDefault={submit}>
    {#if alert}
      <div class="alert alert-danger">{alert}</div>
    {/if}
    <div class="form-group">
      <label for="entry-title">タイトル</label>
      <input type="text" bind:value={entry.title} id="entry-title"
        class="form-control" required maxlength="255" pattern=".*[^\s]+.*">
    </div>
    <div class="form-group">
      <label for="entry-body">本文</label>
      <textarea bind:value={entry.body} id="entry-body" cols="80" rows="15"
        class="form-control" required maxlength="40000"></textarea>
    </div>
    <div class="form-group">
      <label for="entry-tag0">タグ</label>
      <div>
        {#each entry.tags as tag}
          <input bind:value={tag.name}
            class="form-control width-auto d-inline-block mr-2" style="width: 17%"
            maxlength="255" >
        {/each}
      </div>
    </div>
    <div class="form-group">
      <label for="entry-published_at">日時</label>
      <input type="text" bind:value={entry.published_at} id="entry-published_at"
        class="form-control"
        pattern={publishedAtPattern}>
    </div>
    <div class="form-group mb-4">
      <input type="checkbox" bind:checked={entry.draft} id="entry-draft" value="1">
      <label for="entry-draft">下書き</label>
    </div>
    <div class="row">
      <div class="col">
        <button type="submit" class="btn btn-outline-primary">
          {#if entryId}更新{:else}作成{/if}
        </button>
      </div>
      {#if entryId}
        <div class="col text-right">
          <button type="button" class="btn btn-outline-danger" on:click={destroy}>削除</button>
        </div>
      {/if}
    </div>
  </form>
</div>

そのほかのコード

そのほかのコードは、<script></scirpt> の間に関数を並べて記述します。コンポーネントの全体はこんな感じです。素晴らしい簡潔さです。これを思い付いたSvelte作者のセンスはすごい。

コンポーネントが初期化されたときに呼び出すものは、<script></scirpt> の直下に書けばいいだけです(この例ではgetEntry())。

app/javascript/entries/form.svelte
<script>
import Axios from 'axios';
import Flash from '../flash';

export let entryId;

let entry = {
  title: null, body: null, tags: [], published_at: null, draft: false
};
let alert = null;

let publishedAtPattern = '\\d{4}(-|\\/)\\d{2}(-|\\/)\\d{2} +\\d{2}:\\d{2}';

function fillTags(srcTags) {
  let tags = [];
  for(let i = 0; i < 5; i++) {
    tags[i] = srcTags[i] || { name: '' };
  }
  return tags;
}

function getEntry() {
  let path = entryId ? `/entries/${entryId}/edit` : '/entries/new';
  Axios.get(path + '.json').then((res) => {
    entry = res.data.entry;
    entry.tags = fillTags(entry.tags);
  });
}

function validate() {
  if(!(entry.body && entry.body.match(/[^\s]+/))) {
    alert = '本文を入力してください。';
    window.scrollTo(0, 0);
    return false;
  }
  return true;
}

function submit() {
  if(!validate()) {
    return;
  }
  let path = entryId ? `/entries/${entryId}` : '/entries';
  Axios({
    method: entryId ? 'patch' : 'post',
    url: path + '.json',
    headers: {
      'X-CSRF-Token' : jQuery('meta[name="csrf-token"]').attr('content')
    },
    data: { entry }
  }).then((res) => {
    Flash.set({ notice: res.data.notice });
    Turbolinks.visit(res.data.location);
  }).catch((error) => {
    if(error.response.status == 422) {
      alert = error.response.data.alert;
    }
    else {
      alert = `${error.response.status} ${error.response.statusText}`;
    }
    window.scrollTo(0, 0);
  });
}

function destroy() {
// 略
}

getEntry();
</script>

<div>
  <form on:submit|preventDefault={submit}>
<!-- 略 -->
  </form>
</div>

イケていないところ

  • Firefoxでは、ページを表示した時に入力欄に赤い枠が出る(inputにrequiredを付けているため) → entry.title などの初期値をnullにすることで解消。
  • <a href="#"> と書くとwarningが出る。
  • 属性の値に { } が書けない(サンプルでは、変数publishedAtPatternで回避)。
  • $ が書けない(サンプルではjQueryで回避)。
  • {#if}...{:else if}...{/if} という書式はダサいと思う。
9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?