配列と{#each}構文で会議の参加者一覧を作成――各参加者をコンポーネントに分割
先程の例では、各参加者を表示するHTML要素を、人数分ベタ書きしていました。これはあまりエレガントではありません。そこで、まずは参加者を表すステートparticipantsを作成して、それを使ってすべての参加者を表示するようにしてみましょう、
<script lang="ts">
let visible = true;
let participants = [
{ name: '参加者猫 (2)', video: true },
{ name: '参加者猫 (3)', video: true },
{ name: '参加者猫 (4)', video: true },
];
</script>
...
{#each participants as participant, index}
<div class="participant">
<span>{participant.name}</span>
<img src="https://placekitten.com/320/240?image={index+2}">
</div>
{/each}
...
各参加者の表示はコンパクトになり、participantsステートに要素を追加すれば自動的に表示が更新されるようにもなりました。{}はHTML本文だけでなく、属性値(imgタグのsrc属性)の中でも使用できることに注目です。
さて、ここで、各参加者の名前をクリックすることで、ビデオをON/OFFできる機能を追加したいと思います。どうすれば良いでしょうか。
最も単純な方法として、{#each}で受け取ったインデックスを使う方法が考えられます。次のようにして、配列のインデックスを使ってparicipants配列の中のvideoフラグを更新するイメージです:
{#each participants as participant, index}
...
<span on:click={() => participants[index].video = !participants[index].video}>
{participant.name}
</span>
...
{/each}
しかしながら、やっていることの単純さのわりに少々複雑に見えがちですし、今回はよりシンプルに書く方法があるので、それを試してみましょう。すなわち、コンポーネント分割です。
ここまでApp.svelteという単一のコンポーネントだけを使ってコードを書いてきました。このコンポーネントはアプリケーション全体を表現しているので、その内部ではアプリケーションに関することすべてが同時に存在しています。何かを変更しようとすれば、別の何かがそれに影響を与える可能性も常に考えなければなりません。これでは、気軽な変更やテストは難しくなります。
そこで、アプリケーションに関する状態のうち、「参加者1人に関すること」だけを管理するコンポーネントを作成してみます。「参加者1人に関すること」については、そのコンポーネントが管轄することにして、App.svelteの中では忘れても良いことにします。
具体的には、App.svelteと同じフォルダにParticipant.svelteというファイルを作成することで行います。これもApp.svelteと同じくSvelteコンポーネントなので、書き方も基本的には同じです。それでは、先程「参加者1人」にあたる部分を、Participant.svelteに移しましょう。
<script lang="ts">
</script>
<div class="participant">
<span>{participant.name}</span>
<img src="https://placekitten.com/320/240?image={index+2}">
</div>
<style>
.participant {
position: relative;
float: left;
width: 320px;
height: 240px;
border: 1px solid black;
margin: 2px;
background: black;
}
.participant span {
background: black;
color: white;
padding: 0 0.5rem;
position: absolute;
top: 0;
}
</style>
このコンポーネントのHTML断片の中ではparticipant変数を参照していますが、この時点ではparticipantはありません。このコードがApp.svelteの中にいた頃、participantは参加者一覧を表すparticipants配列から取得していましたが、このコンポーネントは「参加者1人」を管理するのが目的でした。参加者一覧を管理する役割は、今でもApp.svelteにまかせて良さそうです。
ということは、App.svelteから参加者1人に関する情報を渡してもらう必要があります。こういうときに使うのがプロパティ(properties/props)です。プロパティは、基本的にはステートと同じように内部状態を表しますが、ひとつだけ特別な違いがあります。それは、コンポーネントの外部から値を設定できるということです。その性質がここで役に立つわけですね。
Svelteでは、プロパティは次のように書きます:
export let participant;
合わせて、Participant.svelteは次のようになります:
<script lang="ts">
export let participant;
</script>
<div class="participant">
<span>{participant.name}</span>
<img src="https://placekitten.com/320/240?image=1">
</div>
<style>
.participant {
position: relative;
float: left;
width: 320px;
height: 240px;
border: 1px solid black;
margin: 2px;
background: black;
}
.participant span {
background: black;
color: white;
padding: 0 0.5rem;
position: absolute;
top: 0;
}
</style>
前半で述べた通り、これは通常のJavaScriptにおけるexportとはまったく違った意味を持つので注意が必要です。といっても「外部に露出する」というexportという言葉の意味からはそこまで離れていないので、慣れてしまえばそれほど気になるという人はあまりいないように思います。
このように宣言すると、コンポーネントを使う側からは次のように使うことができます:
<script lang="ts">
import Participant from './Participant.svelte';
let participants = [
{ name: 'Member cat 1', video: true },
{ name: 'Member cat 2', video: true }
]
</script>
{#each participants as participant}
<Participant participant={participant.name} />
{/each}
内部状態であるはずのparticipantですが、プロパティとして宣言されていることで、呼び出し側からその値を設定できるようになっています。
ところで、App.svelteの中では参加者1人に関する情報をまとめて管理するためにparticipantというオブジェクトにname, videoをまとめる必要がありました。が、そもそもコンポーネント自体がオブジェクトのようなものなので、二重にまとめて扱う意味はないように感じます。そこで、コンポーネントを次のように変更してみましょう。
export let name; export let video;
こうしてみると、video はApp.svelteから指定することはないような気がしてきます。なんといっても、App.svelteが「参加者1人に関すること」を考えなくて良くするためにこのコンポーネントを作ったのですから。videoは、プロパティではなく単なる内部状態、すなわちステートにしていましましょう。
そして、これは本筋ではないのですが、これまで{each}構文の渡してくれるindexを猫の種類を指定するために使っていました。これもコンポーネント外部から渡す必要がありますが、良い機会なので、index(配列内の順番)であることに意味はなく、「猫の種類」を指定するためのプロパティなんだ、という意図が明確になるように、catTypeという名前をつけてあげましょう。まとめると、Participant.svelteのプロパティとステートは、次のようになります:
export let name; export let catType; let video;
App.svelte側はこうなります:
<script lang="ts">
import Participant from './Participant.svelte'
let visible = true;
let participantNames = [ '参加者猫 (2)', '参加者猫 (3)', '参加者猫 (4)'];
</script>
<div class="participant p1">
<span>参加者猫 (1) (自分)</span>
{#if visible}
<img src="https://placekitten.com/320/240?image=1">
{:else}
<img src="https://via.placeholder.com/320x240/000000/FFFFFF/?text=Video%20OFF">
{/if}
</div>
{#each participantNames as name, index}
<Participant {name} catType={index} />
{/each}
<div class="list-of-participants">
<ul>
<li>参加者猫 (1) (自分)</li>
{#each participantNames as name}
<li>{name}</li>
{/each}
</ul>
</div>
<div class="control">
<button>退出</button>
<button>音声 OFF</button>
<button on:click={() => visible = !visible}>ビデオ ON/OFF</button>
<button>画面共有</button>
</div>
<style>
.participant {
position: relative;
float: left;
width: 320px;
height: 240px;
border: 1px solid black;
margin: 2px;
background: black;
}
.participant span {
background: black;
color: white;
padding: 0 0.5rem;
position: absolute;
top: 0;
}
.list-of-participants {
clear: both;
}
</style>
とてもシンプルになりました。{name}となっている箇所は、name={name}の省略記法です。Svelteでは、プロパティ名とそれに渡そうとする変数名が同じ場合だけ、このように省略することができます。最初は戸惑うかもしれませんが、慣れると無駄がなくて便利です。
さて、呼び出し側を修正したら、次のは呼び出される側を仕上げましょう。といっても、舞台は整っています。単なる変数であるステートを更新するだけですべて実現できるように準備してきたので、後はとても単純です。
<script lang="ts">
export let name;
export let catType;
let video;
</script>
<div class="participant">
<span>{name}</span>
<img src="https://placekitten.com/320/240?image={catType+2}">
</div>
<style>
.participant {
position: relative;
float: left;
width: 320px;
height: 240px;
border: 1px solid black;
margin: 2px;
background: black;
}
.participant span {
background: black;
color: white;
padding: 0 0.5rem;
position: absolute;
top: 0;
}
</style>
これで、コンポーネント分割が完了しました。
さて、元々の目的であった「各参加者の名前をクリックすると、それぞれのカメラをON/OFFできる機能」を作り込みましょう。
といっても、最初に自分のカメラ画像に対して実装したのと同じ機能です。複数の参加者を同時に扱うと少し複雑そうに感じたものですが、コンポーネントに分割した今となっては、一人のことだけを考えれば良いので簡単そうです。
videoステートの真偽に応じて画像を表示し分けるようにして、span要素にvideoステートをトグルするハンドラを書いてあげましょう。こうなるはずです:
<script lang="ts">
export let name;
export let catType;
let video = true;
</script>
<div class="participant">
<span on:click={() => video = !video}>{name}</span>
{#if video}
<img src="https://placekitten.com/320/240?image={catType+2}">
{:else}
<img src="https://via.placeholder.com/320x240/000000/FFFFFF/?text=Video%20OFF">
{/if}
</div>
<style>
.participant {
position: relative;
float: left;
width: 320px;
height: 240px;
border: 1px solid black;
margin: 2px;
background: black;
}
.participant span {
background: black;
color: white;
padding: 0 0.5rem;
position: absolute;
top: 0;
}
</style>
これで完成です。アプリケーションを表示してみましょう。これまで通り「ビデオON/OFF」ボタンで自分のビデオ表示を切り替えられることに加えて、各参加者の名前をクリックすることでそれぞれの参加者のビデオの表示も同様のことができるようになりました。

1つ1つのコンポーネントを開発しているときは単純に感じていたものが、全体として表示されると、それなり複雑な機能を備えているように感じます。
それも当然で、実際のところ、それなりに複雑な機能を開発していたからです。すべてをApp.svelteに詰め込んでいたときを思い出してみてください。それなりに複雑な機能の関係性を頭の中に配置してコードを書くのは、それなりの負担を感じるものです。
しかし、それを小さく狭い単位に分割することで、1つ1つは単純に感じられるようにできました。コンポーネントを分割してデータの流れをシンプルにすることによる効果を感じていただけたのではないでしょうか。
