Svelte と Riot.js v4 のシンタックスの比較

本記事は [Riot.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/riotjs) の第24日目の記事になります！(遅刻組です…)

`Riot.js`（以下、riot）は非常にシンプルかつ軽量で入門の敷居も低く、とても書きやすいコンポーネント指向のUIライブラリです。今回は, [Svelte](https://svelte.dev/) というライブラリの [公式チュートリアル](https://svelte.dev/tutorial/basics) を参考に, riot との書き方の比較を見ていきたいと思います.

結構長いので, 適当に流しながら見ていくと良いかもしれません.

※1 数が多いので, チュートリアルの全てを書いているわけではありません
※2 随時更新するかもしれません :bow: 

# 前提

本記事に対する riot に関する前提を書いていきます.

## 1点目: html の設定

ベースとなる index.html は以下のように

*  `app` タグを設定していること(必要な子コンポーネントも同様とする)
*  `mount` メソッドで初期パラメータは渡さないこと

とします.

```html:index.html
  <body>
    <app></app>

    <script data-src="src/app.riot" type="riot"></script>
    <script>
      riot
        .compile()
        .then(() => {
          riot.mount("app")
        })
        .catch(e => {
          console.error(e)
        })
    </script>
  </body>
```

## 2点目: カスタムコンポーネント

カスタムコンポーネントの書き方は基本的に

* `export default{}` に集約する
* 開始と終了の `app` タグは省略

するものとします.
一応以下のような書き方もできますが, 本記事では割愛します.

```html:appタグ
<h1>Hello { title }!</h1>

<script>
  const title = 'Riot.js'
</script>
```

## 3点目: 書き方

svelte はバンドラを使った書き方をしているようですので, 基本的に riot もそれに則って行こうと思いますが, 一部インブラウザコンパイルの書き方をしています.(1点目の書き方がまさにそれ)

# 基本文法
ではここから実際に書いていきます. 基本的には svelte 側の書き方をベースに, それを riot4 で書いていきますのでご注意ください.

## 拡張子

* svelte → `.svelte`
* riot4 → `.riot` or `.riot.html`

## Hello world!

`svelte`

```html:svelte
<script>
	let name = 'world';
</script>

<h1>Hello {name.toUpperCase()}!</h1>
```

`riot`

```html:riot
<h1>Hello { title }!</h1>

<script>
  export default {
    title: 'Riot.js'
  }
</script>
```

## Dynamic attributes

`svelte`

```html:svelte
<script>
  let src = 'tutorial/image.gif';
  let name = 'Rick Astley';
</script>

<img {src} alt="{name} dances.">
```

`riot`

```html:riot
<img {src} alt="{title} dances.">

<script>
  // name は予約語のため title に変更
  export default {
     src: 'tutorial/image.gif',
     title: 'Rick Astley'
  }
</script>
```

## Styling

```html:共通
<p>This is a paragraph.</p>

<style>
  p {
    color: purple;
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
  }
</style>
```

もし riot4 で `Scoped CSS` を指定したい場合は `:host {}` を使ってください.

参考: [Scoped CSS](https://riot.js.org/documentation/#scoped-css)

## Nested components

`svelte`


```html:svelte
<script>
  // 読み込むだけで良い
  import Nested from './Nested.svelte';
</script>

<style>
  p {
    color: purple;
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
  }
</style>

<p>This is a paragraph.</p>
<Nested/>
```

`riot`

```html:riot
<Nested/>

<script>
  import Nested from './nested.riot'
  export default {
    // components オブジェクトとして設定
    components: {
      Nested
    }
  }
</script>

<style>
  p {
    color: purple;
    font-family: 'Comic Sans MS', cursive;
    font-size: 2em;
  }
</style>
```

```html:Nested(共通子コンポーネント)
<p>This is another paragraph.</p>
```

## HTML tags

`svelte`

```html:svelte
<script>
  let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

<p>{@html string}</p>
```

`riot`

```html:riot
<p>{string}</p>

<script>
  export default {
    string: 'this string contains some <strong>HTML!!!</strong>',
    onMounted() {
      this.$('p').innerHTML = this.string
    }
  }
</script>
```

riot4 には HTML のバインディング機能はありません…

参考: [Render unescaped HTML](https://riot.js.org/documentation/#render-unescaped-html)

# Reactivity

ここからは少しアプリケーションっぽいものを見ていきます.

## Assignments

`svelte`

```html:svelte
<script>
  let count = 0;

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

<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
```

`riot`

```html:riot
<button onclick={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<script>
  export default {
    count: 0,
    handleClick() {
      this.count += 1
      this.update()
    }
  }
</script>
```

やっぱり riot は明示的に `update()` メソッドを実行しないといけないのが面倒に見えるかもしれませんね :sweat_smile: 

## Declarations

`svelte`

```html:svelte
<script>
  let count = 0;
  $: doubled = count * 2;

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

<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<p>{count} doubled is {doubled}</p>
```

`riot`

```html:riot
<button onclick={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>

<script>
  export default {
    count: 0,
    onBeforeMount() {
      this.doubled = this.count * 2
    },
    handleClick() {
      this.count += 1
      this.doubled = this.count * 2
      this.update()
    }
  }
</script>
```

`$: hoge` で自動で監視する変数を増やせるのは便利ですねー. 実は riot ですと, もう一つ `doubled` という変数を用意しなくても, `this.doubled` と書けば内部的に `doubled` という変数を持ってくれます.

また svelte のチュートリアルにも記載があるように, `{count * 2}` と書いても同じことができますね.

## Statements

`svelte`

```html:svelte
<script>
  let count = 0;

  $: if (count >= 10) {
    alert(`count is dangerously high!`);
    count = 9;
  }

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

<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
```

`riot`

```html:riot
<button onclick={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<script>
  export default {
    count: 0,
    handleClick() {
      this.count += 1
      if (this.count >= 10) {
        alert('count is dangerously high!');
      }
      this.update()
    }
  }
</script>
```

`$: ***` と書いても, `***` の中で `count` 変数をコントロールできるというのが本題ですが, 今回の題材ですと, `if` 文を `handleclick()` メソッドの中で書いても動作すれば良いのでちょっと例が微妙でしたね.

## Updated arrays and objects

`svelte`

```html:svelte
<script>
  let numbers = [1, 2, 3, 4];

  function addNumber() {
    numbers = [...numbers, numbers.length + 1];
  }

  $: sum = numbers.reduce((t, n) => t + n, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>
  Add a number
</button>
```

`riot`

```html:riot
<button onclick={addNumber}>
  Add a number
</button>
<p>{numbers.join(' + ')} = {sum}</p>

<script>
  export default {
    numbers: [1, 2, 3, 4],
    onBeforeMount() {
      this.sum = this.numbers.reduce((t, n) => t + n, 0)
    },
    addNumber() {
      this.numbers = [...this.numbers, this.numbers.length + 1]
      this.sum = this.numbers.reduce((t, n) => t + n, 0)
      this.update()
    }
  }
</script>
```

何度か使ってきていますが, riot ですと初期化時点で何かしらの処理をしたい場合, `onBeforeMount()` メソッド内で明示的に処理を書かないといけません. 本当はこのメソッド内の処理を省略したいですね💦重複して書いていますし.

# Props

ここからはコンポーネントへの値の渡し方, いわゆる `props` について見ていきます.

## Declaring props

`svelte`

```html:svelte(main)
<script>
  import Nested from './Nested.svelte';
</script>

<Nested answer={42}/>
```

```html:svelte(Nested)
<script>
  export let answer;
</script>

<p>The answer is {answer}</p>
```

`riot`

```html:riot(main)
<Nested answer={42}/>
<script>
  import Nested from './nested.riot'
  export default {
    // components オブジェクトとして設定
    components: {
      Nested
    }
  }
</script>
```

```html:riot(Nested)
<p>The answer is {props.answer}</p>

<script>
  // インブラウザコンパイルでは, この script が丸っと不要
  import Nested from './Nested.riot'
  export default {
    components: {
      Nested
    }
  }
</script>

<!-- or -->

<p>The answer is {answer}</p>

<script>
  import Nested from './nested.riot'
  export default {
    components: {
      Nested
    },
    // 本当は answer: props.answer と書きたい
    answer: '',
    onBeforeMount(props) {
      this.answer = props.answer
    }
  }
</script>
```

riot の子コンポーネントでは, `props.answer` でも扱えますが, 上記のコメントにあるように export default の変数定義では使えないのが残念です.

## Default values

親(main)コンポーネントは先程の `Declaring props` と同じなので省略します.

`svelte`

```html:svelte(Nested)
<script>
  // 初期値を設定
  export let answer = 'a mystery';
</script>

<p>The answer is {answer}</p>
```

`riot`

```html:riot(Nested)
<p>The answer is {props.answer || answer}</p>

<script>
  export default {
    answer: 'a mystery'
  }
</script>

<!-- or -->

<p>The answer is {answer}</p>

<script>
  export default {
    answer: 'a mystery',
    onBeforeMount(props) {
      this.answer = props.answer || this.answer
    }
  }
</script>
```

こちらも riot は明示的に props の値かコンポーネント内の変数かを提示しないとダメなようです(ここは svelte 楽ですね)

## Spread props

`svelte`

```html:svelte(main)
<script>
  import Info from './Info.svelte';

  const pkg = {
    name: 'svelte',
    version: 3,
    speed: 'blazing',
    website: 'https://svelte.dev'
  };
</script>

<Info {...pkg}/>
```

```html:svelte(Info)
<script>
  export let name;
  export let version;
  export let speed;
  export let website;
</script>

<p>
  The <code>{name}</code> package is {speed} fast.
  Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
  and <a href={website}>learn more here</a>
</p>
```

`riot`

```html:riot(main)
<Info {...pkg} />

<script>
  import Info from './info.riot';

  export default {
    components: {
      Info
    },
    pkg: {
      name: 'riot',
      version: 4,
      speed: 'blazing',
      website: 'https://riot.js.org'
    }
  }
</script>
```

```html:riot(Info)
<p>
  The <code>{props.name}</code> package is {props.speed} fast.
  Download version {props.version} from <a href="https://www.npmjs.com/package/{props.name}">npm</a>
  and <a href={props.website}>learn more here</a>
</p>

<!-- or -->

<p>
  The <code>{title}</code> package is {speed} fast.
  Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
  and <a href={website}>learn more here</a>
</p>

<script>
  export default {
    onBeforeMount(props) {
      this.title = props.name
      this.version = props.version
      this.speed = props.speed
      this.website = props.website
    }
  }
</script>
```

何度か書いて思いましたが, 直接 `{props.name}` と HTML に書いたほうが楽ですね.

# Logic

ここからは条件分岐などのロジックについて書いていきます.

## IF-Else blocks

`svelte`

```html:svelte
<script>
  let user = { loggedIn: false };

  function toggle() {
    user.loggedIn = !user.loggedIn;
  }
</script>

{#if user.loggedIn}
  <button on:click={toggle}>
    Log out
  </button>
{:else}
  <button on:click={toggle}>
    Log in
  </button>
{/if}
```

`riot`

```html:riot
<button if={user.loggedIn} onclick={toggle}>
  Log out
</button>
<button if={!user.loggedIn} onclick={toggle}>
  Log in
</button>

<script>
  export default {
    // ここは必須
    user: {},
    onBeforeMopunt() {
      this.user = { loggedIn: false }
    },
    toggle() {
      this.user.loggedIn = !this.user.loggedIn
      this.update()
    }
  }
</script>
```

2点あります. 

* `else` の機能がない(`else-if` も)
* `user: {}` が必要

特に二つ目が意外で, object ではなくプリミティブな変数であればこれが不要なんですよね…

## Each blocks

`svelte`

```html:svelte
<script>
  let cats = [
    { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
    { id: 'z_AbfPXTKms', name: 'Maru' },
    { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
  ];
</script>

<h1>The Famous Cats of YouTube</h1>

<ul>
  {#each cats as { id, name }, i}
    <li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
      {i + 1}: {name}
    </a></li>
  {/each}
</ul>
```

`riot`

```html:riot
<h1>The Famous Cats of YouTube</h1>
<ul each={(cat, i) in cats}>
  <li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
    {i + 1}: {cat.name}
  </a></li>
</ul>

<script>
  export default {
    cats: [
      { id: 'J---aiyznGQ', name: 'Keyboard Cat' },
      { id: 'z_AbfPXTKms', name: 'Maru' },
      { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
    ]
  }
</script>
```

svelte は `{#each cats as { id, name }, i}` のように, each ループ内で object のそれぞれの key 毎に指定して取得できるのが良いですね :thumbsup: 

## Keyed each blocks

`svelte`

```html:svelte(main)
<script>
  import Thing from './Thing.svelte';

  let things = [
    { id: 1, color: '#0d0887' },
    { id: 2, color: '#6a00a8' },
    { id: 3, color: '#b12a90' },
    { id: 4, color: '#e16462' },
    { id: 5, color: '#fca636' }
   ];

  function handleClick() {
    things = things.slice(1);
  }
</script>

<button on:click={handleClick}>
  Remove first thing
</button>

{#each things as thing}
  <Thing current={thing.color}/>
{/each}
```

```html:svelte(Thing)
<script>
  // `current` is updated whenever the prop value changes...
  export let current;

  // ...but `initial` is fixed upon initialisation
  const initial = current;
</script>

<p>
  <span style="background-color: {initial}">initial</span>
  <span style="background-color: {current}">current</span>
</p>

<style>
  span {
    display: inline-block;
    padding: 0.2em 0.5em;
    margin: 0 0.2em 0.2em 0;
    width: 4em;
    text-align: center;
    border-radius: 0.2em;
    color: white;
  }
</style>
```

`riot`

```html:riot(main)
<button onclick={handleClick}>
  Remove first thing
</button>

<Thing each={thing in things} current={thing.color} />

<script>
  import Thing from './thing.riot'

  export default {
    components: {
      Thing
    },
    things: [
      { id: 1, color: '#0d0887' },
      { id: 2, color: '#6a00a8' },
      { id: 3, color: '#b12a90' },
      { id: 4, color: '#e16462' },
      { id: 5, color: '#fca636' }
    ],
    handleClick() {
      this.things = this.things.slice(1);
      this.update()
    }
  }
</script>
```

```html:riot(Thing)
<p>
  <span style="background-color: {initial}">initial</span>
  <span style="background-color: {current}">current</span>
</p>

<script>
  
  export default {
    onBeforeMount(props) {
      // `current` is updated whenever the prop value changes...
      this.current = props.current
      // ...but `initial` is fixed upon initialisation
      this.initial = this.current
    }
  }
</script>

<style>
  span {
    display: inline-block;
    padding: 0.2em 0.5em;
    margin: 0 0.2em 0.2em 0;
    width: 4em;
    text-align: center;
    border-radius: 0.2em;
    color: white;
  }
</style>
```

riot でもカスタムコンポーネントに直接 `each` ディレクティブを使って複数の子コンポーネントをレンダリングすることができます.

参考: [Looping custom components](https://riot.js.org/documentation/#looping-custom-components)

## Await blocks

`svelte`

```html:svelte
<script>
  let promise = getRandomNumber();

  async function getRandomNumber() {
    const res = await fetch(`tutorial/random-number`);
    const text = await res.text();

    if (res.ok) {
      return text;
    } else {
      throw new Error(text);
    }
  }

  function handleClick() {
    promise = getRandomNumber();
  }
</script>

<button on:click={handleClick}>
  generate random number
</button>

{#await promise}
  <p>...waiting</p>
{:then number}
  <p>The number is {number}</p>
{:catch error}
  <p style="color: red">{error.message}</p>
{/await}
```

`riot`

riot には現在 async/await に対応する機能が存在しないため, 自前で `Promise` を使って実装するしかないです…これも今後実装されてほしいですね :sweat: 

# おわりに

さすがに全部は書いていないですが, svelte のチュートリアル見た感じ, かなりライトなライブラリでとても書きやすい印象を受けました. riot の新しいライバル感が…w npm でのダウンロード数も伸びてきており, 世界的にも注目され始めているようです.

軽量な分, やはり小規模向けでサクッと作るときの選択肢としてはかなり良さげなのではないかなと思います！もしご興味ある方は触ってみてください！__もちろん riot も👍__

ではでは(=ﾟωﾟ)ﾉ