ducksというデザインパターンを使ってTodoListを作ってみた

ducksというデザインパターンを使用して簡単なTodoアプリを作成しようと思います。

f:id:top_men:20180820105137p:plain

機能としては以下になります。

  • タスクを追加できる
  • タスクが終わったかどうかのラベルをつけることができる

環境構築

今回は自分みたいにブログの更新に時間がない方などが手軽にちょちょいと構築できるツールでcreate-react-appを使用します。

以下のコマンドを実行します。

create-react-app ducks_todo

ducksについて

ducksの公式サイトには以下のようなルールが記載してあります。

  • reducerはexport deaultをしなければならない
  • action creatorは関数してexportしなければならない
  • actionは定数で定義する

などが挙げられます。

早速実際にアプリを作成していきましょう。

まずプロジェクトフォルダに移動してください。

cd ducks_todo

必要なパッケージをインストール

redux周りのモジュールをインストール

yarn add redux react-redux

次にmaterial-uiをインストールします。 reactに特化したマテリアルデザインCSSフレームワークです。webサイトでよく見かけるボタンやフォームなどの部品を爆速で作ることができます。

material-ui.com

yarn add @material-ui/core

moduleの作成

ducksの肝となる部分をまず初めに作っていきます。 src/modules以下にtodo.jsを作成します。

/* State */
let nextTodoId = 0;

/* Action */
const ADD_TODO = "ADD_TODO";
const TOGGLE_TODO = "TOGGLE_TODO";

/* Action Creator */
export const addTodo = text => {
  return {
    type: "ADD_TODO",
    id: nextTodoId++,
    text
  };
};
export const toggleTodo = id => {
  return {
    type: "TOGGLE_TODO",
    id
  };
};
/* Reducers */
export default function reducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case TOGGLE_TODO:
      return state.map(
        todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
}

actionreducerを同じファイルに配置します。 ducksの公式サイトにも書かれていますが、actionとreducerは密結合な関係であるために同じファイル内にあると何かと見つけやすいですし、ファイルを跨いで確認しなくなるので個人的には良いなと思います。

componentsの作成

src直下にcomponents/Form.jsを作成してください。

import React from "react";
import Button from '@material-ui/core/Button';
export default class Form extends React.Component {
  render() {
    return (
    <form className = "addToto"
      onSubmit = {e => {
        e.preventDefault()
        if (!this.refs.inputText.value.trim()) {
          return
        }
        this.props.addTodo(this.refs.inputText.value)
        this.refs.inputText.value = ""
      }}
     >
      <input type = "text" className = "addToto_input" ref = "inputText" />
      <Button variant = "contained"
        color = "primary"
        style = {{marginLeft: "20px"}}
        onClick = {() => this.props.addTodo(this.refs.inputText.value)}
        >
        Submit
        </Button>
      </form>
    );
  }
}

次にTodoList、Todoコンポーネントを作成します。

import React from "react";
import Todo from "./Todo";
import Card from "@material-ui/core/Card";

export default class TodoList extends React.Component {
  render() {
    return (
      <Card className="todoList">
        <h2 className="todoList_title">TODO</h2>
        <div className="notCompleted">
          {this.props.todos.map(todo => (
            <Todo key={todo.id} toggleTodo={this.props.toggleTodo} {...todo} />
          ))}
        </div>
      </Card>
    );
  }
}
import React from "react";
import Card from "@material-ui/core/Card";
import Checkbox from "@material-ui/core/Checkbox";
import Typography from "@material-ui/core/Typography";

export default class Todo extends React.Component {
  render() {
    return (
      <Card className="todo">
        <div className="todo_header">
          <Checkbox
            tabIndex={-1}
            disableRipple
            onClick={() => this.props.toggleTodo(this.props.id)}
          />
          <Typography
            className={
              this.props.completed ? "completed todo_content" : "todo_content"
            }
          >
            {this.props.text}
          </Typography>
        </div>
        <div className="toggleBtn" />
      </Card>
    );
  }
}

こちらふんだんにmaterial-uiが用意してくれているコンポーネントを使用しています。本当に手軽で使いやすいです。

containerの作成

containerはstoreとcomponentを紐づけるための橋渡し役です。 container/AddTodo.jsを追加してください。

import { connect } from "react-redux";
import * as addTodoModule from "../modules/todo";
import Form from "../components/Form";

const mapDispatchToProps = dispatch => {
  return {
    addTodo: (text) => dispatch(addTodoModule.addTodo(text))
  };
};

export default connect(null, mapDispatchToProps)(Form);

FormコンポーネントにはActionしか利用しないのでconnectの第一引数はnullにするところがポイントです。

次にcontainer/VisibleTodo.jsを作成してください。

import { connect } from "react-redux";
import * as todoListModule from "../modules/todo";
import TodoList from "../components/TodoList";

const mapStateToProps = state => {
  return {
    todos: state.Todo
  };
};

const mapDispatchToProps = dispatch => {
  return {
    toggleTodo: (id) => dispatch(todoListModule.toggleTodo(id))
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList);

TodoListには完了したかどうかのフラグを変更するためのtoggleTodoアクションと、追加したTodoのstateを渡します。

containerを配置するだけの役割のsrc/components/App.jsを作成します。

import React, { Component } from "react";
import "../App.css";
import AddTodo from "../containers/AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";

class App extends Component {
  render() {
    return (
      <div className="App">
        <AddTodo />
        <VisibleTodoList />
      </div>
    );
  }
}

export default App;

その後ルートのコンポーネントである`index.jsを作成し、そこにApp.jsを配置します。

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./components/App";
import { Provider } from "react-redux";
import configureStore from "./configureStore";

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

AppコンポーネントをProviderでラップしてあげれば完成です。

まとめ

actionとreducerが同じファイル内にあるだけで探す手間が省け、actionのディレクトリが減るので個人的には使いやすいと感じました。 まだunduxというデザインパターンもあるので他のものを試してみたいと思います。

unduxについて

github.com

今回作成したTodoをgithubにあげたのでよかったらcloneして試してみたください。

github.com

z-indexで躓いてしまうこと

今回はz-indexで思い悩んでしまうことについて取り上げてみます。


この記事を書く前までz-indexを指定した値の大きな要素ほど前面に配置されると思っていました。


しかしこれは間違いでした。。。
z-indexのことを知らなすぎました。


それでは何を知らなかったのか、どのような局面で自分は躓いたのかを説明していきます。




html

<div class="white">
  <div class="red"></div>
</div>


css

.white {
  position:relative;
  z-index:1;
  width:60px;
  height:60px;
  background:#fff;
  border:solid 1px #000;
}

.red {
  position:absolute;
  top:10px;
  left:20px;
  z-index:-1;
  width:60px;
  height:60px;
  background:red;
  border:solid 1px #000;
}


あれれ、思った通りの場所に配置されないな汗
.whiteの方がz-indexの値が大きいから前面にくるでしょ?
と、結構思い悩みました。。


z-indexは値の大きさだけで要素が前面に配置されるわけではないのです!
z-indexプロパティは、positionプロパティの初期値(static)以外の値が指定されている要素には、自動的にautoが適用された状態になります。
要するに今回の場合、.redのpositionがabsoluteと指定されているので親要素である.whiteと同じz-index:auto;が自動的に指定されるのです。
しかしこれだけでは.whiteと.redのz-indexの値は同じだけであって何故.redが前面に配置されるかわかりませんよね。。


では他に何か要因があるのでしょうか??


スタック文脈スタックレベルというものが関係している模様!



まずスタック文脈です。


スタック文脈は階層構造のことをいいます。

・ルート要素(HTML)
・位置指定されていて、z-index 値がauto以外の要素
・z-index 値が "auto" 以外の flex アイテム
・1 未満の opacity 値を持つ要素(不透明度の仕様をご覧ください)
・transform 値が "none" 以外の要素
・mix-blend-mode 値が "normal" 以外の要素


上記のようなスタック文脈を形成する条件を満たした要素は、自身を基準としたローカルスタック文脈を形成します。
スタック文脈を形成しない要素はその親のスタック文脈に内包されることになります。



次にスタックレベルは、同一のスタック文脈内での重なり順です。z-indexの値が、同一スタック文脈内でのスタックレベルになります。同じスタック文脈内に、同じスタックレベルの要素がある場合、その構造内でより後方にある要素が前面に配置されます。


少し例を挙げてみたいと思います。


blue
red
yellow
green
black


html

<div class="white_box">
  <div class="blue"></div>//z-index:5;
  <div class="red"></div>//z-index:3;
  <div class="yellow">//z-index:4;
    <div class="green"></div>//z-index:10;
    <div class="black"></div>//z-index:7;
  </div>
</div>


css

.white_box {
  position:relative;
  width:800px;
  height:300px;
}

.blue {
  position:relative;
  top:50px;
  z-index:5;
  width:500px;
  height:200px;
  background:rgba(0, 0, 255, 0.7);
  color:#fff;
}

.red {
  position:absolute;
  top:100px;
  left:50px;
  z-index:4;
  width:500px;
  height:200px;
  background:red;
  color:#fff;
}

.yellow{
  position:absolute;
  top:30px;
  left:100px;
  z-index:3;
  width:300px;
  height:100px;
  background:rgba(255, 255, 0, 0.8);
  color:#fff;
}

.green {
  position:absolute;
  top:40px;
  left:200px;
  z-index:10;
  width:250px;
  height:80px;
  background:green;
  color:#fff;
}

.black {
  position:absolute;
  top:70px;
  left:100px;
  z-index:7;
  width:150px;
  height:80px;
  background:black;
  color:#fff;
}


まず黒色のボーダーで囲まれた箱の中に色がついた箱が合計5個あり、その箱すべてにz-indexの値をつけました。
これらはスタック文脈が形成されています。
blue,yellow,redの箱は同一のスタック文脈の中にあるのでスタックレベルで前面か、後方に配置されるかが決まります。
z-indexの値が大きいほど前面に配置されます。
z-indexが定められていない要素が複数ある場合は下に記述した要素の方が前面に表示されます。


そして注目してほしいのはyellowの中にある要素.greenと.backです。
どちらも.blue,redの箱よりもz-indexの値が大きいにも関わらずblueより前面に配置されません。


これは先ほど説明した通りスタック文脈の中にある要素はそのスタック文脈の外にでることができません
したがって.blueやredの箱よりも後方に配置されるのです。



このようにz-indexを扱う上ではスタック文脈、スタックレベルまたpositionの知識が必要とされます。
知らなかった人は実践してもう一度確かめてください!!

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で詰まったことやこれは便利だと思ったことを記事にいていきます。