10分で作るSvelteマークダウンエディタ
10分で作るSvelteマークダウンエディタ
参考:
Build a Svelte JS App: Magic Framework (Svelte 3 Tutorial) - Snipcart
Svelteとは
これを見るとSvelteが何なのかだいたい分かる https://youtu.be/AdNJ3fydeao
これを読むとなぜSvelteを作ろうとしたかだいたい分かる https://svelte.dev/blog/virtual-dom-is-pure-overhead
マークダウンエディタを作る
プロジェクトセットアップ
まず雛形を作る:
npx degit sveltejs/template svelte-markdown-editor cd svelte-markdown-editor && npm i
マークダウンをパースするライブラリを入れる
markedをインストールする:
npm i marked
ローカル開発する
必要なライブラリがインストールされていれば、次のコマンドでrollup
が実行される:
npm run dev
App.svelte (1)
App.svelte
の<script>
タグで囲まれた部分を次のように編集する:
<script> export let source; </script>
export let source;
で、App.svelte
コンポーネントで使用するプロパティを定義している。これにより、親コンポーネントからsource
propを渡すことができるようになる。
main.js
main.js
では、App.svelte
コンポーネントをどのように描画するかを設定している。App.svelte
は、source
propを受け取るので、次のように設定できる:
import App from './App.svelte'; const app = new App({ target: document.body, props: { source: '# New document' } }); export default app;
Editor.svelte
テキストエリアを持つ、Editor.svelte
を作成する。まずファイルを作成し、次のように編集する:
<script> export let source; </script> <div class="markdown-editor__left-panel"> <textarea bind:value={source} class="markdown-editor__source"></textarea> </div> <style> .markdown-editor__left-panel { width: 50%; border: solid 1px black; height: 90vh; } .markdown-editor__source { border: none; width: 100%; height: 100%; } .markdown-editor__source:focus { outline: none; } </style>
以下の部分で、bind:value
を使ってsource
propが、このテキストエリアの値と"バインド"していることをSvelteに伝える。これにより、親コンポーネントに"テキストエリアの値"(=source
prop)が更新したことを伝えられるようになる。
<textarea bind:value={source} class="markdown-editor__source"></textarea>
スタイル(style
)に関しては、コンパイルされたファイルを見ると、コンポーネントごとに.svelte-zafbdq
のようなハッシュが与えられており、コンポーネントにスコープが閉じられている。
Preview.svelte (1)
マークダウンの入力を受け取り、HTMLとして出力するPreview.svelte
を作成する。ファイルを作成して、次のように編集する:
<script> import marked from 'marked'; export let source; let markdown = marked(source); </script> <div class="markdown-editor__right-panel"> <div class="markdown-editor__output">{markdown}</div> </div> <style> .markdown-editor__right-panel { width: 50%; border: solid 1px black; height: 90vh; overflow: auto; } .markdown-editor__output { width: 100%; padding: 0 2em; } </style>
App.svelte (2)
作成したコンポーネントをApp.svelte
でひとまとめにする:
<script> import Editor from './Editor.svelte' import Preview from './Preview.svelte' export let source; </script> <header class="header"> <h1 class="header-title">Svelte powered markdown editor</h1> </header> <div class="markdown-editor"> <Editor bind:source={source} /> <Preview source={source} /> </div> <style> .header { height: 10vh; display: flex; align-items: center; justify-content: center; } .header-title { margin: 0; } .markdown-editor { width: 100%; display: flex; align-items:flex-start; justify-content: space-evenly; } </style>
作成したコンポーネントは、そのままインポートし、使用できる。その際、通常のHTMLタグと区別するため、必ず大文字で始める。
import Editor from './Editor.svelte' import Preview from './Preview.svelte'
Editor
のテキストエリアの値とsource
はバインドするので、次のようにする: <Editor bind:source={source} />
一方、Preview
ではsource
は更新されないため、バインドする必要はない。
この状態で、ブラウザを開いてみると、2点バグが確認できるはず。
Preview
でHTMLが正しくレンダリングできていない- テキストエリアの値を変更しても、
Preview
の値が更新されない
Preview.svelte (2)
1つ目のバグを解消するには、次のようにSvelteが用意した特別なタグである@html
タグを変数の前に付けることで、markdown
のHTMLを直接描画させる。
<div class="markdown-editor__output">{@html markdown}</div>
2つ目のバグは、markdown
の値が、source
が更新されても再計算されていないことによる。
下記では、JavaScriptの標準構文であるラベル付き文を使って、markdown
変数が"再計算される値"であることをSvelteに伝える[^1]。ReactのuseEffect
内でstate
を変更するふるまいに近いが、deps
(依存)を自由に決めることはできない。
これにより、source
propが更新された時に、markdown
変数も再計算されるようになる。
$: markdown = marked(source);
もし、次のように通常通り変数を設定してしまうと、markdown
は再計算されず、DOMは更新されない。
let markdown = marked(source);
^1
コンパイルされたコード(下記)を見ると、ダーティーチェックしているだけで、ラベル付き文は使われていないっぽい。構文的にエラーを出さないようにするハック?
$$self.$$.update = () => { if ($$self.$$.dirty & /*source*/ 2) { $$invalidate(0, markdown = marked(source)); } };