ducksというデザインパターンを使ってTodoListを作ってみた
ducks
というデザインパターンを使用して簡単なTodoアプリを作成しようと思います。
機能としては以下になります。
- タスクを追加できる
- タスクが終わったかどうかのラベルをつけることができる
環境構築
今回は自分みたいにブログの更新に時間がない方などが手軽にちょちょいと構築できるツールで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サイトでよく見かけるボタンやフォームなどの部品を爆速で作ることができます。
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; } }
actionとreducerを同じファイルに配置します。 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について
今回作成したTodoをgithubにあげたのでよかったらcloneして試してみたください。
z-indexで躓いてしまうこと
今回はz-indexで思い悩んでしまうことについて取り上げてみます。
この記事を書く前までz-indexを指定した値の大きな要素ほど前面に配置されると思っていました。
しかしこれは間違いでした。。。
z-indexのことを知らなすぎました。
それでは何を知らなかったのか、どのような局面で自分は躓いたのかを説明していきます。
html
<div class="white"> <div class="red"></div> </div>
.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の値が、同一スタック文脈内でのスタックレベルになります。同じスタック文脈内に、同じスタックレベルの要素がある場合、その構造内でより後方にある要素が前面に配置されます。
少し例を挙げてみたいと思います。
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>
.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にアクセスすることができます。 こちら公式サイトになります。
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で詰まったことやこれは便利だと思ったことを記事にいていきます。