Share
Embed
Just Tried Stimulus
Shohei Umemoto
June 27, 2019
Transcript
53 slides
-
Stimulus をちょっと 使ってみました Shohei Umemoto (@cafedomancer) Rakuten Tech Sapporo WEBエンジニア
MeetUp@札幌 #5 (June 27, 2019) -
自己紹介 • Shohei Umemoto (@cafedomancer) • 社会人4 年目です (たぶん…) •
開発では Ruby / Rails をよく使います • fish shell / fzf/ neovim が好きです -
会社紹介 • Enishi Tech Inc. (@enishitech) • いろいろなことをやっています • お客様といっしょに考えます
• Ruby / Rails だけの会社というわけでは 特にありません (でも大切にしています) -
話さないこと • JavaScript の細かい話 • Stimulus のセットアップやインストール • Webpacker (Webpack)
の (以下同文) • Stimulus を使う上での Tips みたいなの (in-depth なので) -
Stimulus とは? • Basecamp (元 37signals) が作っている JavaScript フレームワーク •
すごく雑に言うと他のフレームワーク でいうところの Controller しかない • https://stimulusjs.org もぜひ目を通して みてください -
Stimulus の 3 つの要素 • 出てくるのは 3 つだけ • Controller
/ Action / Target • 実際にコードを読んでみよう! -
Stimulus のコード <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text">
<button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } -
Controller の指定 <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text">
<button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } -
Action の指定 <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text">
<button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } -
Target の指定 <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text">
<button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } <!--HTML from anywhere--> <div data-controller="hello"> <input data-target="hello.name" type="text"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> <!--HTML from anywhere--> <div data-controller="hello"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { greet() { `Hello, ${this.nameTarget.value}!` } } -
Stimulus の 3 つの要素 (再) • Controller • JavaScript で動作させたい単位となるもの
• Action • HTML のイベントが発生したときに実行されるもの • Target • HTML からの入力と HTML への出力の対象となるもの -
サンプルをもう 1 個 <!--HTML from anywhere--> <div data-controller="clipboard"> PIN: <input
data-target="clipboard.source" type="text" value="1234" readonly> <button data-action=“clipboard#copy"> Copy to Clipboard </button> </div> // clipboard_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } -
Controller の指定 // clipboard_controller.js import { Controller } from "stimulus"
export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } // clipboard_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } <!--HTML from anywhere--> <div data-controller="clipboard"> PIN: <input data-target="clipboard.source" type="text" value="1234" readonly> <button data-action="clipboard#copy"> Copy to Clipboard </button> </div> -
Action の指定 // clipboard_controller.js import { Controller } from "stimulus"
export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } <!--HTML from anywhere--> <div data-controller="clipboard"> PIN: <input data-target="clipboard.source" type="text" value="1234" readonly> <button data-action="clipboard#copy"> Copy to Clipboard </button> </div> // clipboard_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } -
Target の指定 // clipboard_controller.js import { Controller } from "stimulus"
export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } <!--HTML from anywhere--> <div data-controller="clipboard"> PIN: <input data-target="clipboard.source" type="text" value="1234" readonly> <button data-action="clipboard#copy"> Copy to Clipboard </button> </div> // clipboard_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "source" ] copy() { this.sourceTarget.select() document.execCommand("copy") } } // clipboard_controller.js import { Controller } from "stimulus" export default class extends Controller { copy() { this.sourceTarget.select() document.execCommand("copy") } } -
Stimulus の pros & cons • とてもシンプルなので分かりやすく覚えることが少ない • Reference を見てみると分かると思います
• HTML と JavaScript との関係がひと目で分かる • このあと説明します • まわりで使っている人が見当たらない • ノウハウがまだ無さそう • ゴリゴリの SPA みたいなのを作るのには向いてなさそう -
jQuery を使ったコードとの比較 $('div.hello button.hello-greet’) .click(function() { const $name = $(this).siblings('input.hello-name')
const $output = $(this).siblings('span.hello-output') $output.text(`Hello, ${$name.val()}!`) }) <div class="hello"> <input class="hello-name" type="text"> <button class="hello-greet"> Greet </button> <span class="hello-output"> </span> </div> <div data-controller="hello"> <input data-target="hello.name" type="text"> <button data-action="click->hello#greet"> Greet </button> <span data-target="hello.output"> </span> </div> import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } -
Stimulus のまとめ • Controller しかない JavaScript フレームワーク • 要素としては Controller/
Action / Target の 3 つだけがある • ちょっとした動きモノを作るのに向いてそう -
HTML に対する Stimulus の考え方 • “Stimulus is a JavaScript framework
with modest ambitions. It doesn’t seek to take over your entire front-end—in fact, it’s not concerned with rendering HTML at all.” • でも HTML を render したいケースも無くはない • でもテンプレートの仕組みは用意されていない -
HTML を render したいケース • API から JSON を受け取ってそれを HTML
で表示したい • ユーザーのアクションに基づいて要素を追加したい -
こんな issue がありました • https://github.com/stimulusjs/stimulus/ issues/41 • “HTML 要素の追加に <template>
タグを 使うのはどうだろうか?” • というわけで <template> タグを使って要 素の追加をやってみました -
<template> タグとは? • https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ template • HTML の断片を <template> タグの要素として記述することで
いわゆるテンプレートとして使用することができる • https://www.html5rocks.com/en/tutorials/webcomponents/template/ も参考にしました -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ --> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ --> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ -—> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ --> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ --> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ --> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll(‘td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ --> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの使い方 <table id="producttable"> <thead> <tr> <td>UPC_Code</td> <td>Product_Name</td> </tr> </thead>
<tbody> <!-- ͜͜ʹੜͨ͠ཁૉΛࠩ͠ࠐΉ —> </tbody> </table> <template id="productrow"> <tr> <td class="record"></td> <td></td> </tr> </template> let t = document.querySelector('#productrow') let tb = document.querySelector('tbody') let clone = document.importNode(t.content, true) let td = clone1.querySelectorAll('td') td[0].textContent = '1235646565' td[1].textContent = 'Stuff' tb.appendChild(clone) -
<template> タグの pros & cons • ただの HTML なので読みやすい •
ノンプログラマーの人がいじる時にもやりやすい • だいたいのブラウザーで使える • https://caniuse.com/#feat=template • Fetch API と組み合わせて良い感じに動かせる • https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API • Internet Explorer でうまく動かないケースがある • このあと説明します -
Stimulus と Turbolinks • “Stimulus pairs beautifully with Turbolinks to
provide a complete solution for fast, compelling applications with a minimal amount of effort.” • Turbolinks と組み合わせても問題なく使用することができます • モバイル向けのページでは Turbolinks を使っておくと良さそう -
Internet Explorer で動かすには? • Stimulus のPolyfill が用意されています • https://www.npmjs.com/package/ @stimulus/polyfills
• <template> も Polyfill が用意されています • https://github.com/webcomponents/ polyfills/tree/master/packages/ template • Fetch API も (以下同文) • https://github.com/github/fetch -
<template> タグのネスト • <template> タグは ネストすることもできる • 要素の外側もテンプレートに しておきたいときに使う •
Internet Explorer でも問題なく 動作させておきたい <template id="outer"> <ul> <template id="inner"> <li></li> </template> </ul> </template> <div id="todos"> </div> et outer = document.querySelector('#outer') let list = document.importNode(outer.content, true) let ul = list.querySelector('ul') let inner = list.querySelector('#inner') for (let i = 1; i <= 3; i++) { let item = document.importNode(inner.content, true) let li = item.querySelector('li') li.textContent = 'List Item ' + i list.querySelector('ul').appendChild(item) } let todos = document.querySelector('#todos') todos.appendChild(list) -
こんな issue がありました • https://github.com/webcomponents/ template/pull/ 39#issuecomment-430617066 • “HTML parser
of IE will drop <template /> directly, you won't find anything of your template…” • まじかよ… -
実際に使ってみて • フレームワークが構造を用意してくれているのは 指針になるのですごく分かりやすくて良い • 人数が少ないチームだとコンポーネントに 分かれているよりもずっと触りやすい • <template> が複雑になってくると大変なので
テンプレートで頑張りたいときはもう少し カンタンなアプローチがあると良いかも -
フロントエンドのパッケージ管理 • Stimulus で使うものは Yarn で入れています • Stimulus で使うもの以外は Rails
Assets で入れています • Webpack (er) に乗せるとハマりそう… (見たことある) • Sprocket 4 でどうにかできるのかな? -
コードスタイルの統一 • Standard JS • No configuration を売りにしている • Standard
RB • Rubocop の config ファイルが同梱されている • コードスタイルをレビューする余力がないので なるべく自動化できるところはそちらに寄せたい • スタイルの指摘をされるのもあんまり嬉しくないので (auto correct でわりと解決できる) -
お話したこと • Stimlus について簡単に紹介しました • Stimulus と <template> タグとを 組み合わせるアプローチについて解説しました
• ベストプラクティスなどは まだ模索しているところです