storybook+react+typescriptの環境構築

前回の記事でstorybookの簡単な使い方を学んだので、typescriptを導入すると共にstyled-componentsが提供しているThemeProviderを使ってみようと思います。

ThemeProviderをラップすることでどのコンポーネントからもThemeProviderが保持しているthemeにアクセスすることができます。 こちら公式サイトになります。

www.styled-components.com

react+typescript+webpackの環境構築

まずは環境を構築するところからです。 以前記事を書いたので参考にしてみてください。

top-men.hatenablog.com

初っぱなから投げやりですが、ご了承ください。

storybookを導入

上で作成したプロジェクトフォルダに移動

cd プロジェクトフォルダ

storybookを導入するために下記のコマンドを実行

getstorybook

そうするとstoriesフォルダなどstorybookの関するものが生成されます。 一応ブラウザで確認するために以下のコマンドを実行してください。

yarn storybook

下記のように表示されていれば大丈夫です。

f:id:top_men:20180808013742p:plain

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'
}

色の命名などは下記を参考にしています。

material.io

その後、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

このような画面が表示されると思います。

f:id:top_men:20180730195525p:plain

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.jswidthInfo、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という拡張機能は文字の通りコンポーネントの情報について記述することに特化したものになります。

ブラウザでみるとこのようになります。

f:id:top_men:20180801015846p:plain

まとめ

ButtonやSideBarなどパーツごとに分けるのか、それともページごとにコンポーネントを分けるのかなど、どのようなカテゴリー分けが一般的にはあるのか気になりました。コンポーネントがどのような役割をもっているかをこのようにガイドラインのような形で確認することができるので大規模な開発では積極的につかっていきたいです。またデザイナーがコンポーネントを記述できる環境であれば、デザイナーに任せてしまうことでエンジニアがより開発の方に集中できるようになるのではないかと思いました。

Reactのプロジェクトならstyled-components使うのがいいかも

昨日から自分が所属している会社のユニットメンバー5人日替わりで個人ブログを更新していこうという取り組みをしていくことになりました。 第一回の記事です。

ブログをローテーションで書いていくことの良さが記載してあります。 チームで行う取り組みなので個々人に責任感が生まれ強制力が働き良い取り組みだと思います。

というわけで今回は自分の番ということなので書いていきます!

タイトルにもあるように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;

実際にローカルサーバーを立ち上げると、以下のようにブラウザに表示されると思います。

f:id:top_men:20180725014329p:plain

では

コンポーネント単位でスコープが生成される

ちゃんと上記のようにスコープが生成されているか確かめてみます。

f:id:top_men:20180725014833p:plain

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; // 追加箇所
`;

再度ブラウザの検証ツールを確認します。

f:id:top_men:20180725020040p:plain

なんと自動でベンダープレフィクスが付与されています! 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;

f:id:top_men:20180725031512p:plain

このように表示されればOKです! propsで受けとった値をcssに渡すことができました。

まとめ

テンプレート文字列で書いていくのは最初違和感がありましたが、それ以上に大きなメリットを感じることができたため積極的に使っていこうと思いました。またFLOCSSなどのフレームワークを全くなくすということはせず、併用していくことは全然ありだと感じました。

Vue+Railsでファイルアップロード機能を作成してみる

今回はRailsとVueを使用してファイルアップロード機能について作成したいと思います。

開発環境

  • vue:2.3.4
  • typescript:2.2.2
  • ruby2.4.2
  • rails5.2.0

投稿記事を例に説明していきます。

投稿用のテーブル作成

投稿のタイトル、本文、サムネイル画像用のテーブルを用意します。

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に渡す必要があります。 詳細は以前自分が書いた記事があるので参考にしてみてください。

top-men.hatenablog.com

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のプロジェクトにおいて自分が実際にハマった?体験を記事にしていきます。

開発環境

  • vue:2.3.4
  • typescript:2.2.2
  • ruby2.4.2
  • rails5.2.0

やりたいこと

railsで新規投稿ページや編集ページを作成する場合、よく利用される方法として標準で用意されれいるform_forform_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の連携についての記事はこちらを参考にしてみてください。

top-men.hatenablog.com

解決方法

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を使用しています。

github.com

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にトークンを埋め込むことができました。 ブラウザで表示した結果です。

f:id:top_men:20180701214310p:plain

この処理を記述する前は、postした時にInvalidAuthenticityTokenと下記のエラーが表示されていました。

f:id:top_men:20180701215231p:plain

ですが、上記の処理をすることで埋め込まれたトークンが正しく認証されました!!

まとめ

rails側からvue側へどのようなものを渡してあげるべきかを上手にやってあげればなんとかうまくできるといった感じです。(ざっくりですみません) 今後もvueとrailsで詰まったことやこれは便利だと思ったことを記事にいていきます。

railsのプロジェクトにおけるvueを使った開発手法

今回はrailsのプロジェクトにおいてフロントにvue.jsを導入してみたことを記事にしてみました。具体的には下記のやりたいことに書いてあります。

開発環境

  • vue:2.3.4
  • typescript:2.2.2
  • ruby2.4.2
  • rails5.2.0

やりたいこと

railsjavascriptフレームワークなどを使用せずに開発をする場合コントローラーで作ったインスタンス変数などをerb.htmlファイルに渡すことが多いと思います。

f:id:top_men:20180617155702p:plain

今回は冒頭にも書きましたが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を使用して開発をしています。今回はそのことについてはあまり触れないので詳細を知りたい方は以下のリンクを参考にしてみてください。

github.com

はじめに、表示する用の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インスタンスをバインドしています。 その中にユーザー詳細ページを表示するコンポーネントを配置してpropsrailsからのデータを渡しています。

<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を使った簡単な例

f:id:top_men:20180617142202p:plain

to_jsonを使用すると

f:id:top_men:20180617142310p:plain

json形式に変換してくれましたね。

またrawメソッドも使う必要があります。 使った場合と使わない場合の出力結果を見てみましょう。 (ここではdebuggerを仕込んだ出力結果を見ています。)

  • rawメソッドを使わない場合

f:id:top_men:20180617144556p:plain

  • rawメソッドを使った場合

f:id:top_men:20180617144831p:plain

ダブルクォーテーション""&quot;と変換されているのがわかります。 つまり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"
  }
})

上記のコードを載せたので試してみてください。

jsfiddle.net

inputタグの値が変更される度にバインドしたmessageが変更されていると思います。 v-modelがしていることは二つあります。

  1. inputタグにvue側のデータをバインドする
  2. イベントハンドリングをしてデータを更新している

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-componentpropsの値として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]

上記のコードはこちらに書いたので試してみてください。

jsfiddle.net

今度はvue-class-componentsについて記事を書きたいと思います。