storybook+react+typescriptの環境構築
前回の記事でstorybookの簡単な使い方を学んだので、typescriptを導入すると共にstyled-componentsが提供しているThemeProviderを使ってみようと思います。
ThemeProviderをラップすることでどのコンポーネントからもThemeProviderが保持しているthemeにアクセスすることができます。 こちら公式サイトになります。
react+typescript+webpackの環境構築
まずは環境を構築するところからです。 以前記事を書いたので参考にしてみてください。
初っぱなから投げやりですが、ご了承ください。
storybookを導入
上で作成したプロジェクトフォルダに移動
cd プロジェクトフォルダ
storybookを導入するために下記のコマンドを実行
getstorybook
そうするとstories
フォルダなどstorybookの関するものが生成されます。
一応ブラウザで確認するために以下のコマンドを実行してください。
yarn storybook
下記のように表示されていれば大丈夫です。
storybookにtypescriptを対応させる
こちらは公式サイトをなぞっていきます。
必要なモジュールをインストールします。
yarn add -D typescript awesome-typescript-loader @storybook/addon-info react-docgen-typescript-webpack-plugin jest "@types/jest" ts-jest
その後、storybookでtypesriptが使えるように.stories
の中にあるwebpack.config.js
を以下のように編集します。
const path = require("path"); const TSDocgenPlugin = require("react-docgen-typescript-webpack-plugin"); module.exports = (baseConfig, env, config) => { config.module.rules.push({ test: /\.(ts|tsx)$/, loader: require.resolve("awesome-typescript-loader") }); config.plugins.push(new TSDocgenPlugin()); // optional config.resolve.extensions.push(".ts", ".tsx"); return config; };
またプロジェクトフォルダ直下にtsconfig.json
ファイルを作成します。
{ "compilerOptions": { "outDir": "build/lib", "module": "commonjs", "target": "es5", "lib": ["es5", "es6", "es7", "es2017", "dom"], "sourceMap": true, "allowJs": false, "jsx": "react", "moduleResolution": "node", "rootDirs": ["src", "stories"], "baseUrl": "src", "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "noImplicitThis": true, "noImplicitAny": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "declaration": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true }, "include": ["src/**/*"], "exclude": ["node_modules", "build", "scripts"] }
ここではファイルの中の細かい説明は省きます。
styled-componentsの導入
今回はstyled-componentsを使っていくのでインストールします。
yarn add -D styled-components
テーマの作成
webサイト・サービスでよく使われる色を定義したテーマのファイルを作成します。
src/theme.ts
ファイルを作成します。
export type theme = { primary: string primaryVariant: string secondary: string secondaryVariant: string onPrimary: string onSecondary: string } export const theme = { primary: '#6200EE', primaryVariant: '#3700B3', secondary: '#03DAC6', secondaryVariant: '#018786', onPrimary: '#fff', onSecondary: '#000' }
色の命名などは下記を参考にしています。
その後、src/App.tsx
を開きます。
import * as React from "react"; import { theme } from "../theme"; import { ThemeProvider } from "styled-components"; const App = () => { return <ThemeProvider theme={theme} />; }; export default App;
準備としてはここまでとします。 今度は実際にtypescriptでコンポーネントを作成しながら開発を進めていきたいと思います。
ReactのUI開発を促進するstorybookを触ってみた
storybookの超基本的な使い方について紹介できればと思います。
今回はコマンド一つでreactの環境を構築できるお馴染みのcreate-reacta-app
を使っていこうと思います。
インストールしていない方は事前にしておいてください。
環境構築
ではまずは以下のコマンドを実行してください。
create-react-app storybook_paractice
プロジェクトフォルダに移動します。
cd storybook_paractice
storybookに必要なパッケージをインストールします。
yarn add @storybook/cli
package.jsonの中身はこのようになっています。
{ "name": "storybook-sample", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.4.1", "react-dom": "^16.4.1", "react-scripts": "1.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public" }, "devDependencies": { "@storybook/react": "^3.4.8", "@storybook/addon-actions": "^3.4.8", "@storybook/addon-links": "^3.4.8", "@storybook/addons": "^3.4.8", "babel-core": "^6.26.3", "babel-runtime": "^6.26.0" } }
その後プロジェクトフォルダの直下で以下のコマンドを実行します。
getstorybook
このコマンドを実行するとstories
というフォルダができていると思います。
src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── registerServiceWorker.js └── stories └── index.js
package.json
のscriptの部分にも記述が追加されています。
"scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "storybook": "start-storybook -p 9009 -s public", // 追加されている部分 "build-storybook": "build-storybook -s public" // 追加されている部分 },
では早速ローカルサーバーを起動してstorybookの画面をみてみましょう。
yarn storybook
このような画面が表示されると思います。
stories/index.js
にコンポーネントの設定をしていきます。
import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { linkTo } from '@storybook/addon-links'; import { Button, Welcome } from '@storybook/react/demo'; storiesOf('Welcome', module).add('to Storybook', () => <Welcome showApp={linkTo('Button')} />); storiesOf('Button', module) .add('with text', () => <Button onClick={action('clicked')}>Hello Button</Button>) .add('with some emoji', () => ( <Button onClick={action('clicked')}> <span role="img" aria-label="so cool"> 😀 😎 👍 💯 </span> </Button> ));
storiseOf()
関数の第一引数にタイトル(上のブラウザのサイドバーにあるWelcomeやButton)を、add
関数でサブメニューのような階層を作ることができます。
ストーリーを構成するコンポーネントの配置場所は任意で良いです。
またストーリーを追加するごとにページが増えます。
コンポーネントの作成
実際にコンポーネントを作成してみましょう。
まずLabelコンポーネントを作成します。今回はstyled-componetsを使うのでインストールをしましょう。
yarn add styled-components
その後、Label.jsファイルを作成します。
import React, { Component } from 'react'; import styled from 'styled-components'; class Label extends Component { render(props){ return( <LabelCom {...this.props}>Hello, storybook!</LabelCom> ) } } const LabelCom = styled.div` color: #fff; width: 200px; margin: 0 auto; padding: 10px; text-align:center; background: ${(props) => { return props.type === "alert" ? "red" : "green" }}; `; export default Label;
受け取るprops
によって背景色を変更しているだけです。
その後、stories/index.js
を編集します。
import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { linkTo } from '@storybook/addon-links'; import { Button, Welcome } from '@storybook/react/demo'; import Label from '../Label'; // 追加 storiesOf('Welcome', module).add('to Storybook', () => <Welcome showApp={linkTo('Button')} />); storiesOf('Button', module) .add('with text', () => <Button onClick={action('clicked')}>Hello Button</Button>) .add('with some emoji', () => ( <Button onClick={action('clicked')}> <span role="img" aria-label="so cool"> 😀 😎 👍 💯 </span> </Button> )); storiesOf('Label', module) .add('default', () => <Label type="" />) .add('alert', () => <Label type="alert" />)
props
に文字列alertを渡した時と、何も渡さない時でストーリーを分けました。
またstorybookのプレビュー画面に独自のスタイルやスクリプトを読み込ませたいときは.storybook
以下にpreview-head.html
を作成します。
例えばフォント読み込んだり、bodyにスタイルを付与しています。
<link href="https://fonts.googleapis.com/earlyaccess/roundedmplus1c.css" rel="stylesheet" /> <style> body { height: 100vh; display: flex; align-items: center; justify-content: center; } </style>
こちら公式サイトで説明されています。
https://storybook.js.org/configurations/add-custom-head-tags/
実際に画面を見るとdefault用とalert用のストーリーが確認できると思います。
アドオンの紹介
次にstorybookにはアドオン(拡張機能)があります。その中から一つinfoというものを今回は紹介します。
必要なモジュールをインストールします。
yarn add @storybook/addon-info
以下のコードを追加します。
stories/index.js
でwidthInfo
、Appコンポーネントをインポートします。(省略していますがAppコンポーネントではpropsでtextを渡すようにしています。)
import { withInfo } from '@storybook/addon-info'; import App from '../App'; storiesOf('App', module) .add('info of App', withInfo(` マークダウン形式で記述することができます。 ### タイトル ~~~js <Button>Click Here</Button> ~~~ `)(() => <App text="hello from storybook"/> ) )
widthInfoメソッドの中でマークダウンを記述することができます。 infoという拡張機能は文字の通りコンポーネントの情報について記述することに特化したものになります。
ブラウザでみるとこのようになります。
まとめ
ButtonやSideBarなどパーツごとに分けるのか、それともページごとにコンポーネントを分けるのかなど、どのようなカテゴリー分けが一般的にはあるのか気になりました。コンポーネントがどのような役割をもっているかをこのようにガイドラインのような形で確認することができるので大規模な開発では積極的につかっていきたいです。またデザイナーがコンポーネントを記述できる環境であれば、デザイナーに任せてしまうことでエンジニアがより開発の方に集中できるようになるのではないかと思いました。
Reactのプロジェクトならstyled-components使うのがいいかも
昨日から自分が所属している会社のユニットメンバー5人日替わりで個人ブログを更新していこうという取り組みをしていくことになりました。 第一回の記事です。
ブログ続かないからちょっと始めてみた#はてなブログ
— nan.nan.nanan (@chanan_nan) 2018年7月24日
ブログローテーションはじめてみた - ちゃなんログhttps://t.co/B89QaZacnd
ブログをローテーションで書いていくことの良さが記載してあります。 チームで行う取り組みなので個々人に責任感が生まれ強制力が働き良い取り組みだと思います。
というわけで今回は自分の番ということなので書いていきます!
タイトルにもあるようにstyled-componentについて説明をしていきます。
と、その前にcssのことについて軽く触れておきます。
cssの問題点
- グローバルスコープ
- 誰でも簡単に書けてしまう自由さ
- どこに誰がどのような目的で書いたかわからなくなるときがある
などが挙げられると思います。 「グローバルスコープ」であるからこそクラス名が被らないような命名を考える時間などのコストが発生します。「誰でも簡単に書けてしまう」ということからFLOCSSやSMACSS、 RSCSSなどのフレームワークが登場し、記述するためのルールなどを設けてある程度記述する自由さに制限を与えていると思います。フレームワークの学習コストがそこまで高い訳ではないですが、実際に業務で導入してみないとメリット・デメリットがわからず、プロジェクトメンバー間での理解度にも差が生まれてきてしまいます。
「cssは簡単だわー。」とか言われがちですが規模のあるプロジェクトなど運用していくことを考えると非常に難易度が上がると考えています。
このような問題をより解決に導くツールとしてstyled-componentsがあります。 styled-componentsは簡単にいうとjsの中でcssを記述することができるCSS in JSのライブラリの一つでReactのプロジェクトでは利用されることが多いようです。
styled-componentsの良いところ
- コンポーネント単位でスコープが生成される
- コンポーネント単位でcssが管理されているからスタイルを探すのに時間が掛からない
- autoprefixerが自動で付与される
- cssにjsの値を渡すことができる
大まかにこんな感じです。 実際にコードを交えながら一つ一つ見ていきましょう!
今回はcreate-react-app
を使ってプロジェクトフォルダを作成します。
create-react-app styled-components_practice
styeled-components
をインストールします。
yarn add styled-components
その後、App.jsを編集します。
import React, { Component } from 'react'; import styled from 'styled-components'; import HelloButton from './HelloButton'; import logo from './logo.svg'; class App extends Component { render() { return ( <Wrapper> <Logo><img src={logo} alt="logo"/></Logo> <Title>Hello, styled-components!!!</Title> <HelloButton /> </Wrapper> ); } } const Wrapper = styled.div` background: #323232; padding: 20px; height: 100vh; text-align: center; `; const Logo = styled.p` width : 100px; margin: 0 auto; img { width: 100%; } `; const Title = styled.h1` color: #fff; font-family: "Meiryo"; text-align: center; margin: 0; font-weight: lighter; `; export default App;
新たにHelloButton.jsファイルを作成して下のように記述してください。
import React, { Component } from 'react'; import styled from 'styled-components'; class HelloButton extends Component { render() { return ( <Button>Click</Button> ); } } const Button = styled.button` margin: 0 auto; width: 150px; border: none; padding: 18px; border-radius: 56px; font-size: 18px; font-weight: lighter; margin-top: 30px; cursor: pointer; `; export default HelloButton;
実際にローカルサーバーを立ち上げると、以下のようにブラウザに表示されると思います。
では
コンポーネント単位でスコープが生成される
ちゃんと上記のようにスコープが生成されているか確かめてみます。
styled-componentsが良い感じにクラス名が被らないように、ランダムな文字列を入れてくれています。 よげそうですね!
また
コンポーネント単位でcssが管理されているからスタイルを探すのに時間が掛からない
こちらもファイルを見てわかるようにコンポーネントごとにクラスを定義しているので何がどこに書いてあるのか素早く探し出すことができます!
次に検証するのがこちらです。
autoprefixerが自動で付与される
HelloButton.jsのファイルを編集します。 以下のスタイルを追加してください。
const Button = styled.button` margin: 0 auto; width: 150px; border: none; padding: 18px; border-radius: 56px; font-size: 18px; font-weight: lighter; margin-top: 30px; cursor: pointer; display: flex; // 追加箇所 justify-content: center; // 追加箇所 `;
再度ブラウザの検証ツールを確認します。
なんと自動でベンダープレフィクスが付与されています! autoprefixerを別途インストールする必要もありませんね!
では最後に
cssにjsの値を渡すことができる
こちらを検証してみます!
その前に自分はVue.jsの単一ファイルコンポーネントを使ってjs側でrailsから取得した画像名をcssのbackgroundプロパティのurlに指定したいと思ったときがありました。 単一ファイルコンポーネントの場合、jsの変数などをstyleタグの中に渡すことができません。このようなかゆいところをstyled-componentsのテンプレート文字列が解決してくれます!
見ていきましょう。
ここではボタンの背景色をprops
によって変更するといったことをしてみます。
まずApp.jsでChildコンポーネントにpropsを渡してあげます。
import React, { Component } from 'react'; import styled from 'styled-components'; import HelloButton from './HelloButton'; import logo from './logo.svg'; class App extends Component { render() { return ( <Wrapper> <Logo><img src={logo} alt=""/></Logo> <Title>Hello, styled-components!!!</Title> <HelloButton status="success" /> </Wrapper> ); } } const Wrapper = styled.div` background: #323232; padding: 20px; height: 100vh; text-align: center; `; const Logo = styled.p` width : 100px; margin: 0 auto; img { width: 100%; } `; const Title = styled.h1` color: #fff; font-family: "Meiryo"; text-align: center; margin: 0; font-weight: lighter; `; export default App;
HelloButton.jsの方でpropsを受け取ってpropsに応じて背景色を変更するロジックを書きます。テンプレート文字列の中で${この中でロジック}
を使います。
import React, { Component } from 'react'; import styled from 'styled-components'; class HelloButton extends Component { render() { return ( <Button {...this.props}>Click</Button> ); } } const Button = styled.button` margin: 0 auto; width: 150px; border: none; padding: 18px; border-radius: 56px; background: ${(props) => { return props.status === "success" ? "green" : "red" }}; color: #fff; font-size: 18px; font-weight: lighter; margin-top: 30px; cursor: pointer; display: flex; justify-content: center; `; export default HelloButton;
このように表示されればOKです! propsで受けとった値をcssに渡すことができました。
まとめ
テンプレート文字列で書いていくのは最初違和感がありましたが、それ以上に大きなメリットを感じることができたため積極的に使っていこうと思いました。またFLOCSSなどのフレームワークを全くなくすということはせず、併用していくことは全然ありだと感じました。
Vue+Railsでファイルアップロード機能を作成してみる
今回はRailsとVueを使用してファイルアップロード機能について作成したいと思います。
開発環境
投稿記事を例に説明していきます。
投稿用のテーブル作成
投稿のタイトル、本文、サムネイル画像用のテーブルを用意します。
rails g model Post title:string content:string image_name:string
controllerの設定
アップロードするのに必要な箇所だけを載せます。
class PostsController < ApplicationController def new @post = Post.new end def create if params[:post][:image_name].present? @post = Post.new( title: params[:post][:title], content: params[:post][:content], image_name: params[:post][:image_name].original_filename, user_id: current_manager.id ) output_path = Rails.root.join('public/images', params[:post][:image_name].original_filename) File.open(output_path, 'w+b') do |fp| fp.write(params["post"]["image_name"].read) end else @post = Post.new( title: params[:post][:title], content: params[:post][:content], image_name: "no-image.png", ) end redirect_to root_path end end
DBにはparamsで取得したimage_nameを直接いれようとするとエラーになります。
アップロードされたファイル(params[:image_name)
は以下のようにActionDispatch::Http::UploadedFile
クラスとなっています。
=> #<ActionDispatch::Http::UploadedFile:0x00007fc3222860a8 @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"post[image_name]\"; filename=\"img_04.jpg\"\r\nContent-Type: image/jpeg\r\n", @original_filename="img_04.jpg", @tempfile=#<File:/var/folders/t0/pl2n_7gx5vdbc8w1dww0j8400000gn/T/RackMultipart20180721-5143-1qm64n6.jpg>>
params[:image_name].original_filename
でファイル名を取得します。
その後、画像を保存する場所のパスを作成します。
output_path = Rails.root.join('public/images', params[:post][:image_name].original_filename)
次に上記で取得したパスに画像を保存します。
File.open(output_path, 'w+b') do |fp| fp.write(params["post"]["image_name"].read) end
ファイルの読み込みと書き込み処理にはFileクラスがあります。
ファイル本体のデータを保存するためにread
メソッドを使用します。
これでrails側の設定は終わりです。
vueとrailsの連結部分
今回はrails側でform_tag
等は使用しないでvue側でpostリクエストをするのでrails側で作成したトークンをvueに渡す必要があります。
詳細は以前自分が書いた記事があるので参考にしてみてください。
vue側の設定
<template> <div class='wrapper'> <form action='/posts' method="post" enctype="multipart/form-data"> <input name="authenticity_token" type="hidden" :value="token"> <p class="postTitle"> <input type='text' placeholder="タイトル" class="postTitleField" v-model="post.title" name="post[title]" > </p> <input type="file" name="post[image_name]"> <p class="tag"><input type='text' class="tag_input" placeholder="タグを入力してください" name="tag_name"></p> <button class="releaseBtn">公開する</button> </form> </div> </template> <script lang="ts"> import { Vue, Prop } from "vue-property-decorator"; import Post from "./data/post"; export default class New extends Vue { @Prop() public post: Post; @Prop() public token: string; } </script>
ここでは親からアクセストークンをpropsで受け取りinputタグのvalueに代入しているところがないとinvalid tokenとエラーになります。
まとめ
以上でgemを使用しないで簡単なアップロードの機能ができました。 最初は画像データの中身が文字列として保存されてしまったりと躓きましたが、なんとか完成することができました。
rails+vueの開発環境でフォームのpost通信を行う方法
今回もvue
+rails
のプロジェクトにおいて自分が実際にハマった?体験を記事にしていきます。
開発環境
やりたいこと
railsで新規投稿ページや編集ページを作成する場合、よく利用される方法として標準で用意されれいるform_forやform_tagなどがあります。
タイトルにもあるように今回実現したいこととしては、.vue
の単一ファイルコンポーネントを使用してformの送信をしたいです。
.erb
ファイルの中ではform_tagを使用できますが、.vue
ファイルでは使用することができません。
rails4から標準でCSRFへの対応をしてくれています。
そのためform_tagなどのヘルパーメソッドが実行された時に下記のようにauthenticity_token
が埋め込まれます。
<input name="authenticity_token" type="hidden" value="4aEXK2Ztfe+8fUEz5QrRYw6oETI1OWL0Lkg+EQyz81TixmxF1x7niphP2ROHngj0AY2iu3lZwTfbd4y2Tf93oA==">
今回はrailsが用意てくれているformのヘルパーメソッドは使用しないのでvue側で用意してあげなければいけません。
vueとrailsの連携についての記事はこちらを参考にしてみてください。
解決方法
railsではトークンを作成してくれるform_authenticity_tokenというメソッドがあるみたいです。それを今回は利用します。
新規作成ページを例にします。
rails側のファイル
<div class="postNew"> <new-component :post="post" :token="authenticationToken"></new-component> </div> <script> new PostNew({ el: '.postNew', data:{ post: <%= raw @post.to_json %>, authenticationToken: '<%= form_authenticity_token %>' } }) </script>
上のコードではvueモデルのdataに投稿データとトークンを渡しています。
vuemodelを定義しているファイル
下記はvue-class-componentを使用しています。
import { Vue, Component } from 'vue-property-decorator'; import New from "../../component/post/new.vue"; @Component({ components: { "new-component": New } }) export default class PostNew extends Vue { } (<any>window).PostNew = PostNew;
新規登録ページ用の単一ファイルコンポーネントを登録し、windowのプロパティにクラスを追加しています。
vueファイル
<template> <div class='wrapper'> <form action='/posts' method="post"> <input name="authenticity_token" type="hidden" :value="token"> <!--省略--> </form> </div> </template> <script lang="ts"> import { Component, Vue, Prop } from "vue-property-decorator"; import Post from "../data/post"; export default class New extends Vue { @Prop() public post: Post; @Prop() public token: string; } </script> <style> </style>
ここで大切なのはpropで受け取った値をinput
タグのvalueにいれている箇所です。
こうすることでformにトークンを埋め込むことができました。
ブラウザで表示した結果です。
この処理を記述する前は、postした時にInvalidAuthenticityToken
と下記のエラーが表示されていました。
ですが、上記の処理をすることで埋め込まれたトークンが正しく認証されました!!
まとめ
rails側からvue側へどのようなものを渡してあげるべきかを上手にやってあげればなんとかうまくできるといった感じです。(ざっくりですみません) 今後もvueとrailsで詰まったことやこれは便利だと思ったことを記事にいていきます。
railsのプロジェクトにおけるvueを使った開発手法
今回はrailsのプロジェクトにおいてフロントにvue.jsを導入してみたことを記事にしてみました。具体的には下記のやりたいことに書いてあります。
開発環境
やりたいこと
railsでjavascriptのフレームワークなどを使用せずに開発をする場合コントローラーで作ったインスタンス変数などをerb.html
ファイルに渡すことが多いと思います。
今回は冒頭にも書きましたがvueの単一ファイルコンポーネントを使用して開発することを前提としています。そこで.vue
ファイルにrails側で作成したデータを渡す必要があります。erb.html
ファイルなら直接rubyのコードを記述することができますが、.vue
ファイルはrubyのコードを書くことができません。
そこでどうしたら上記の問題を解決できるのか悩んでいました。
結論を言いますとwindowオブジェクトにviewmodelを追加すれば良いということです。
ここでは実際にユーザー情報が書かれたユーザ一詳細ページを例に解説していきます。(※ディレクトリ構成については説明していないのでご了承ください。)
ユーザー詳細ページの例
windowオブジェクトにviewmodelを追加している部分
import { Vue, Component } from 'vue-property-decorator'; import User from "../../component/users/User.vue"; @Component({ components: { "user-profile-component": User } }) class UserProfile extends Vue { } (<any>window).UserProfile = UserProfile;
前提としてtypescriptを使用して開発をしています。今回はそのことについてはあまり触れないので詳細を知りたい方は以下のリンクを参考にしてみてください。
はじめに、表示する用のUserコンポーネントを登録しています。 その後、windowオブジェクトのプロパティとしてUserProfileクラスを登録しています。
// typescriptを使用した記述方法 (<any>window).UserProfile = UserProfile; // typescriptを使用していない場合 window.UserProfile = UserProfile
viewModelをインスタンス化している部分
<div id="userProfile"> <user-profile-component :user="user"></user-profile-component> </div> <%= javascript_include_tag "/javascripts/viewmodel/users/show" %> <script> new UserProfile({ el: "#userProfile", data: { user: <%= raw @user.to_json %> } }) </script>
ここでviewファイルである.erb.html
ファイルとvueインスタンスをバインドしています。
その中にユーザー詳細ページを表示するコンポーネントを配置してprops
にrailsからのデータを渡しています。
<user-profile-component :user="user"></user-profile-component>
<script> new UserProfile({ el: "#userProfile", data: { user: <%= raw @user.to_json %> // railsのデータをインスタンス変数のuserに代入 } }) </script>
<%= raw @user.to_json %>
このto_json
メソッドは、rubyのHashやArrayをjson形式に変換してくれています。こうすることでフロント側で受け取りやすいデータ構造になります。
to_json
を使った簡単な例
to_json
を使用すると
json形式に変換してくれましたね。
またraw
メソッドも使う必要があります。
使った場合と使わない場合の出力結果を見てみましょう。
(ここではdebuggerを仕込んだ出力結果を見ています。)
- rawメソッドを使わない場合
- rawメソッドを使った場合
ダブルクォーテーション""
が"
と変換されているのがわかります。
つまりrawメソッドを使用しない場合は、自動的にエスケープされています。
エスケープされないためにraw
メソッドを使用する必要があるのです。
railsから受け取ったデータを受け取って描画している部分
<template> <div class='user'> <div class='user_name'>{{user.name}}</div> <div class='user_email'>{{user.email}}</div> </div> </template> <script lang="ts"> import { Vue, Prop } from "vue-property-decorator"; import UserData from "../../data/user"; export default class User extends Vue { @Prop() public user: UserData; } </script>
.vue
ファイルでは親から値を取得するためにpropsを定義してあげます。
あとは目的に合わせて表示するだけです。
まとめ
vueの単一ファイルコンポーネントを使用ってrailsからvueファイルにデータ渡すことができました。今回はvueを例に紹介しましたがreactなど他のフレームワークでも上記で説明した手法で開発できると思います。 簡単ではありますが、以上になります。
vue.jsはじめました@超基礎編
最近業務でvueを触る機会が多いので備忘録として残します。
ここで記述しているvueのバージョンは2.5.16
です。
v-onについて
v-on
ディレクティブはイベントリスナに加え、Vueインスタンスのメソッドを呼び出すことができます。
<button v-on:click="doSomething">Click</button> <!--省略記法--> <button @click="doSomething">Click</button>
v-on
は省略できるのでコードを書く際には@イベントリスナ名
としましょう。
慣れるまではv-on
を書くことをすすめます。
v-modelについて
フォームの入力値をvue側のデータとバインドする「双方向データバインディング」を簡単に実現することができます。これがVue.jsの主要な機能とも言えます。
<div id="app"> <input v-model="messgae"> <p>{{messgae}}</p> </div>
new Vue({ el: "#app", data: { messgae: "Hello" } })
上記のコードを載せたので試してみてください。
inputタグの値が変更される度にバインドしたmessage
が変更されていると思います。
v-model
がしていることは二つあります。
- inputタグにvue側のデータをバインドする
- イベントハンドリングをしてデータを更新している
1について下記のようにまずはHTML側とvueをバインドします。
<input v-bind:value="message">
2では下記のように値を更新しています。
まずinput
イベントハンドラでユーザーが入力した値を取得します。
<input @input="message">
その後、vue側のデータ変数に実際にユーザーが入力した値を代入します。
this.message = e.target.value
v-on
の仕組みは大事なのでここで何をしているかは言語化できるようにしましょう。
v-bindディレクティブについて
bindとは他動詞では〜を結びつける
という意味です。
Vue側のデータをDOMと結びつけることを意味します。
リアクティブにデータを更新したいときに使用します。
最も簡単な例はテキストとしてバインドする例です。
<div id="app"> {{ message }} </div>
new Vue({ el: "#app", data: { message: "Hello Vue" } })
二重のブランケットの場合はプレーンなテキストとして表示されます。 htmlタグとして認識させたい場合は、v-htmlというディレクティブを使用します。
<div id="app"> <p v-html="message"></p> </div>
new Vue({ el: "#app", data: { message: "<h1 style='color:red;'>Hello, Vue!!!</h1>" } })
vueのバージョン1では、下記にように三重のブランケットにしてhtmlとして認識させるようにしていたみたいです。
<div id="app"> {{{ message }}} </div>
またHTML属性にバインドするケースも多いです。 例えばapiから受け取ったデータをテキストリンクとして表示させたいとき。
<a v-bind:href="itemLink">Google</a>
new Vue({ el: '#app', data: { itemLink: "http://google.com" } })
また省略記法としてv-bind
の代わりに:属性名
と書くことが多いので慣れておきましょう。
イベント修飾子
イベント修飾子は、クリックやキー操作などDOMイベントの変更をしたいときに使用します。
今回はその中でも.stop
修飾子について説明したいと思います。
例えばモーダルがあるとします。
- モーダルのコンテンツを覆っているDOM →
modal
- モーダルのコンテンツのDOM →
modal_content
<div class="modal"> <div class="modal_content"></div> </div>
モーダルのコンテンツに何か入力フォームやクリックするものなどあったときに、同じイベントをハンドルした要素がネストしている場合、イベントの発生元であるe.target.value
から親要素へ向かって設定している要素のイベントが連鎖して行われます。
そのようなときにmodal
にはクリックするとモーダルが非表示になるイベント、modal_content
にも入力フォームなどに入力するようなイベントを設定していると入力しようとしたときにモーダルが非表示になってしまいモーダルが閉じてしまいます。
このようなケースの場合など.stop
修飾子を使用します。
まずは.stop
修飾子をつけないで試してみます。
<div id="app"> <div class="modal" @click="getClassinfo('modal')"> modal <div class="modal_content" @click.stop="getClassinfo('modal_content')"> modal_content </div> </div> </div>
new Vue({ el: '#app', methods: { getClassinfo: function(className) { console.log(className) } } })
modal_contentをくりっくした出力結果が以下になります。
modal_content modal
modal_contentがクリックされるたびにmodalのクリックイベントも発火しています。
今度は.stop
修飾子をつけて、modal_contentをクリックしてみてください。
<div id="app"> <div class="modal" @click="getClassinfo('modal')"> modal <div class="modal_content" @click.stop="getClassinfo('modal_content')"> modal_content </div> </div> </div>
javascriptのコードは変わらないので省略します。
出力結果は以下のようになります。
modal_content
.stop
をつけることでイベントの伝搬を止めたい箇所を指定できました。
親→子へ値を渡す方法
<div id="app"> <child-component number="3"></child-component> </div>
Vue.component('child-component', { template: `<div>{{number}}</div>`, props: ['number'] }) new Vue({ el: '#app', })
ここでは子コンポーネントである'child-component
にpropsの値として1を渡しています。=
の右辺の値が親が渡したい値であり左辺のnumber
は子の方でpropsとして定義する必要があります。
子→親へ何かをするとき
子のコンポーネントの状態に応じて親コンポーネントに何かしてほしいとき、また子が持っているデータを親に渡したいときは$emit
を使用します。
<div id="app"> <div class="parents"> <child-component @child-event="getData"></child-component> </div> </div>
Vue.component('child-component', { template: `<div @click='giveData'>クリック</div>`, methods: { giveData(){ this.$emit("child-event") } } }) new Vue({ el: '#app', methods: { getData(){ console.log("受け取りました") } } })
ここで大切になるのがカスタムイベントです。
$emit
を使って任意のタイミング(ここではクリックをしたとき)でカスタムイベントであるchild-event
を発火させます。それを親側で受け取るためには親側でv-on
を使って子のカスタムイベントをハンドリングします。
そしてchild-event
が発火したタイミング(クリックしたとき)で親で定義したgetData
が実行され下記のような出力結果になります。
受け取りました
上記では子のコンポーネントのイベントに応じて親のメソッドを実行してるだけでした。 では、子のコンポーネントから何かしらのデータを送ってみましょう。 htmlファイルは変更箇所がないので記述しません。
Vue.component('child-component', { template: `<div @click='giveData'>クリック</div>`, methods: { giveData(){ this.$emit("child-event", "come from child-component") } } }) new Vue({ el: '#app', methods: { getData(message){ console.log(message) } } })
出力結果です。
come from child-component
$emit
の第一引数にカスタムイベント名、第二引数に渡したいデータを追加してあげると子→親
にデータを与えることができます。
今は決め打ちで値を渡してみましたが、子のコンポーネントで定義したdata属性のpersons
という中身がオブジェクトの集まりのデータを渡してみます。
Vue.component('child-component', { template: `<div @click='giveData'>クリック</div>`, data: function() { return { persons: [ {name: "田中", age: 23}, {name: "佐藤", age: 22}, {name: "武田", age: 17} ] } }, methods: { giveData(){ this.$emit("child-event", this.persons) } } }) new Vue({ el: '#app', methods: { getData(persons){ console.log(persons) } } })
クリックするとconsole.log上にこのように子から親にデータが渡ってきていることがわかると思います。
(3) [{…}, {…}, {…}, __ob__: Observer]
上記のコードはこちらに書いたので試してみてください。
今度はvue-class-components
について記事を書きたいと思います。