はじめに
この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。
前回まで講座で、Products の CRUD 機能を一通り実装しました。
今回の講座では、
-
React-swiper
を用いた商品情報詳細ページ(CRUD の R) -
MuiThemeProvider
による Material-UI のテーマカラー設定
について実装します。
※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #7 「商品を一覧表示しよう」
要点
-
<MuiThemeProvider>
で、Material-UI のテーマカラーを設定できる。 -
react-swiper
で画像スワイプ機能を実装できる。 - Material-UIの
<Table>
コンポーネントでテーブルを作成できる。
完成系イメージ
http://localhost:3000/product/jJV3RsxCgFllZeo771wR
商品情報の詳細ページを作成します。
左側が画像スライダー、右側がサイズ表示用のテーブルになっています。
サイズ表示用テーブルは、Material-UIの<Table>
コンポーネントを使うことで比較的容易に作成できます。
画像スライダーは、react-id-swiper
というnpmパッケージをインストールすることで、非常に簡単に実装することができます。
メイン
MuiThemeProvider によるテーマカラー設定
Material-UI において、アプリ内で使用するテーマカラーを設定する機能としてMuiThemeProvider
というものがあります。
これを用いることで、例えばコンポーネント内のフォント一つ一つにカラーコードを定義するような必要がなくなります。
また、「primaryが赤、secondaryが青」のように限られた数の色をテーマカラーを設定して運用するため、無駄な色の使いすぎを自然と防ぐことにも繋がります。
今回は既存のthemeファイルを読み込むだけですが、一連の流れが理解できれば、テーマカラーを簡単に変更することができます。
1.src/assets/theme.js ## themeカラーが定義されたファイル
2.src/index.js ## theme.jsを読み込み、アプリ全体に適用
import { createMuiTheme } from '@material-ui/core/styles';
// Pick colors on https://material.io/resources/color/#!/
export const theme = createMuiTheme({
palette: {
primary: {
light: '#88ffff',
main: '#4dd0e1',
dark: '#009faf',
contrastText: '#000',
},
secondary: {
light: '#ffff81',
main: '#ffd54f',
dark: '#c8a415',
contrastText: '#000',
},
grey: {
50: "#fafafa",
100: "#f5f5f5",
200: "#eeeeee",
300: "#e0e0e0",
400: "#bdbdbd",
500: "#9e9e9e",
600: "#757575",
700: "#616161",
800: "#424242",
900: "#212121",
A100: "#d5d5d5",
A200: "#aaaaaa",
A400: "#303030",
A700: "#616161"
}
},
});
themeカラーをカラーコードとして定義します。これを、アプリ全体に適用させます。
import React from 'react';
.
.
.
import {MuiThemeProvider} from "@material-ui/core";
import {theme} from "./assets/theme"
.
.
.
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<MuiThemeProvider theme={theme}>
<App />
</MuiThemeProvider>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
);
.
.
.
<MuiThemeProvider>
で<App />
を丸ごとラッピングして、設定完了です。
これにより、各コンポーネントのmakeStyle()
内において、
const useStyles = makeStyles((theme) => ({
root: {
color: theme.palette.primary.main
}
.
.
.
}));
theme.js
で定義した色を適用することができます。
今回のtheme.js
では、primaryが水色系、secondaryが黄色系で配色されています。
詳細ページの大枠作成
商品情報詳細ページの大枠を作成します。
ひとまずは、URLに含まれるid情報を元に正しいproductsをCloud Firstoreから取り出すところまで進めます。テンプレート、及びパスは、
- テンプレート:
ProductDetail.jsx
- path:
/product/:id
として実装します。
1.src/Router.jsx
2.src/templates/ProductDetail.jsx
3.src/templates/index.js
import React from 'react';
import {Route, Switch} from "react-router";
import {ProductDetail,ProductEdit,ProductList,Reset,SignIn,SignUp} from "./templates";
import Auth from "./Auth"
const Router = () => {
return (
<Switch>
<Route exact path={"/signup"} component={SignUp} />
<Route exact path={"/signin"} component={SignIn} />
<Route exact path={"/signin/reset"} component={Reset} />
<Auth>
<Route exact path={"(/)?"} component={ProductList} />
<Route exact path={"/product/:id"} component={ProductDetail} /> {/*追記*/}
<Route path={"/product/edit(/:id)?"} component={ProductEdit} />
</Auth>
</Switch>
);
};
export default Router
import React,{ useState,useEffect,useCallback } from "react";
import {useSelector} from "react-redux"
import {db} from "../firebase"
import { makeStyles } from "@material-ui/core";
import HTMLReactParser from "html-react-parser"
const useStyles = makeStyles((theme)=>({
sliderBox: {
[theme.breakpoints.down("sm")]: {
margin: "0 auto 24px auto",
height: 320,
width: 320
},
[theme.breakpoints.up("sm")]:{
margin: "0 auto",
height: 400,
width: 400
}
},
detail: {
[theme.breakpoints.down("sm")]: {
margin: "0 auto 16px auto",
height: "auto",
width: 320
},
[theme.breakpoints.up("sm")]:{
margin: "0 auto",
height: "auth",
width: 400
}
},
price: {
fontSize: 36
}
}));
const returnCodeToBr = (text) => {
if (text === "") {
return text
} else {
return HTMLReactParser(text.replace(/\r?\n/g, '<br/>'))
}
};
const ProductDetail = () => {
const classes = useStyles();
const selector = useSelector((state)=>state);
const path = selector.router.location.pathname;
const id = path.split("/product/")[1];
const [product,setProduct] = useState();
useEffect(()=>{
db.collection("products").doc(id).get()
.then(doc => {
const data = doc.data();
setProduct(data)
})
},[]);
return (
<section className="c-sention-wrapin">
{product && (
<div className="p-grid__row" >
<div className={classes.sliderBox}>
</div>
<div className={classes.detail}>
<h2 className="u-text__headline">{product.name}</h2>
<p className={classes.price}>{product.price.toLocaleString()}</p>
<div className="module-spacer--small"></div>
<div className="module-spacer--small"></div>
<p>{returnCodeToBr(product.description)}</p>
</div>
</div>
)}
</section>
)
};
export default ProductDetail
-
[theme.breakpoints.down("sm")]: {...}
とすることで、smサイズの幅をブレイクポイントとして、適用するスタイルを変更しています。 -
useEffect()
を用いて、初期レンダー時に、DBから該当するidの情報を取得します。 -
returnCodeToBr()
で、改行コード\n
を、改行のHTMLタグ<br />
に変換しています。この関数に用いているhtml-react-parser
は、この時点ではインストールしていないはずなので、npm からインストールします。
$ npm install --save html-react-parser
export {default as Home} from './Home'
export {default as ProductDetail} from './ProductDetail'
export {default as ProductEdit} from './ProductEdit'
export {default as ProductList} from './ProductList'
export {default as Reset} from './Reset'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'
いったん動作確認をします。すでにProductCard.jsx
において、リストアイテムをクリックすることで/product/:id
へ遷移されるよう定義されているため、そこからアクセスします。
http://localhost:3000
スカートをクリックすると、
http://localhost:3000/product/ewzuLXPK2Dr0owCG3dt7
スカートの商品情報が取得できていることが分かります!
画像スライダーの設置
このProductDetail
テンプレートに対して、画像スライダーを設置します。
画像スライダーの実装には、react-id-swiper
というnpmパッケージを利用します。依存関係にあるswiper
と合わせて、インストールしておきます。
$ npm install -S swiper@5.4.2 react-id-swiper@3.0.0
画像スライダー用のコンポーネントしてImageSwiper.jsx
を定義し、それをProductDetail.jsx
で読み込みます。
1.src/components/Products/ImageSwiper.jsx
2.src/components/Products/index.js
3.src/templates/ProductDetail.jsx
import React, { useState } from "react";
import Swiper from "react-id-swiper";
import NoImage from "../../assets/img/src/no_image.png"
import 'swiper/css/swiper.css'
const ImageSwiper = (props) => {
const [params] = useState({
pagination: {
el: ".swiper-pagination",
type: "bullets",
clickable: true,
dynamicBullets: true
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev"
},
loop: true
})
const images = props.images
return (
<Swiper {...params}>
{images.length === 0 ? (
<div className="p-media__thumb">
<img src={NoImage} alt="no images"/>
</div>
) : (
images.map(image => (
<div className="p-media__thumb" key={image.id}>
<img src={image.path} alt="商品情報"/>
</div>
))
)}
</Swiper>
);
};
export default ImageSwiper;
react-id-swiper
は、
1.import 'swiper/css/swiper.css'
でスライダー用のcssを読み込む。
- スライダーの性質を
params
として定義 -
<Swiper {...params}>
としてJSXに展開
という流れで実装をします。
'swiper/css/swiper.css'
は、npmインストールすることによって、node_modules
下に保存されるcssファイルです。この中で、スライダーとしての動的な挙動が実装されているイメージです。
また、params
の中で定義するパラメータを変えることで、様々な形式のスライダーを実装できるようです(公式リファレンス参照)
export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ImageSwiper} from "./ImageSwiper" //追記
export {default as ProductCard} from "./ProductCard"
export {default as SetSizeArea} from "./SetSizeArea"
.
.
.
import {ImageSwiper} from "../components/Products" {*追記*}
.
.
.
const ProductDetail = () => {
.
.
.
return (
<section className="c-sention-wrapin">
{product && (
<div className="p-grid__row" >
<div className={classes.sliderBox}>
<ImageSwiper images={product.images} /> {*追記*}
</div>
<div className={classes.detail}>
<h2 className="u-text__headline">{product.name}</h2>
<p className={classes.price}>{product.price.toLocaleString()}</p>
<div className="module-spacer--small"></div>
<div className="module-spacer--small"></div>
<p>{returnCodeToBr(product.description)}</p>
</div>
</div>
)}
</section>
)
};
export default ProductDetail
画像スライダー動作確認
http://localhost:3000/product/jJV3RsxCgFllZeo771wR
コード量も少なく簡単に画像スライダーが実装できるのはすばらしいですね!
サイズ表示テーブル
最後に、商品情報のサイズを表示するテーブルを作成します。
<SizeTable>
コンポーネントを作成し、<ProductDetail>
で読み込みます。
1.src/components/Products/SizeTable.jsx
2.src/components/Products/index.js
3.src/templates/ProductDetail.jsx
import React from 'react';
import Table from "@material-ui/core/Table";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import IconButton from "@material-ui/core/IconButton";
import TableContainer from "@material-ui/core/TableContainer";
import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import {makeStyles} from "@material-ui/styles";
const useStyles = makeStyles({
iconCell: {
padding: 0,
height: 48,
width: 48
}
})
const SizeTable = (props) => {
const classes = useStyles();
const sizes = props.sizes;
return (
<TableContainer>
<Table>
<TableBody>
{sizes.length > 0 && (
sizes.map(size => (
<TableRow key={size.size}>
<TableCell component="th" scope="row">{size.size}</TableCell>
<TableCell>残り{size.quantity}点</TableCell>
<TableCell className={classes.iconCell}>
{size.quantity > 0 ? (
<IconButton>
<ShoppingCartIcon />
</IconButton>
) : (
<div>売切</div>
)}
</TableCell>
<TableCell className={classes.iconCell}>
<IconButton>
<FavoriteBorderIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
);
};
export default SizeTable;
Material-UIの
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>
で、テーブルを実装しています。
export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ImageSwiper} from "./ImageSwiper"
export {default as ProductCard} from "./ProductCard"
export {default as SetSizeArea} from "./SetSizeArea"
export {default as SizeTable} from "./SizeTable" //追記
.
.
.
import {SizeTable} from "../components/Products" {*追記*}
.
.
.
const ProductDetail = () => {
.
.
.
return (
<section className="c-sention-wrapin">
{product && (
<div className="p-grid__row" >
<div className={classes.sliderBox}>
<ImageSwiper images={product.images} />
</div>
<div className={classes.detail}>
<h2 className="u-text__headline">{product.name}</h2>
<p className={classes.price}>{product.price.toLocaleString()}</p>
<div className="module-spacer--small"></div>
<SizeTable sizes={product.sizes}/> {*追記*}
<div className="module-spacer--small"></div>
<p>{returnCodeToBr(product.description)}</p>
</div>
</div>
)}
</section>
)
};
export default ProductDetail
<SizeTable>
コンポーネントをインポートします。
### 動作確認
http://localhost:3000/product/jJV3RsxCgFllZeo771wR
左に画像スライダー、右にサイズテーブルが設置されています!
さいごに
今回の要点をおさらいすると、
-
<MuiThemeProvider>
で、Material-UI のテーマカラーを設定できる。 -
react-swiper
で画像スワイプ機能を実装できる。 - Material-UIの
<Table>
コンポーネントでテーブルを作成できる。
以上です!
このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。