前章まででReduxのStoreを作ってそれを扱うためのHooksを作りました。がまだ定義しただけで実際に呼び出していないので、それらはUIに反映されていません。
この章ではstoreのデータをUIに反映していきましょう。
(とはいっても各hookを呼び出してJSX内に記述するだけです)
タイトルの反映
まずはタイトルを反映しましょう。
タイトルのstateはuseTitle
を使えば簡単に取得できます。
取得したタイトルはヘッダーに表示させたいのでHeader.tsx
を編集します。
import AppBar, { AppBarProps } from "@mui/material/AppBar";
import IconButton from "@mui/material/IconButton/IconButton";
import Toolbar from "@mui/material/Toolbar/Toolbar";
import MenuIcon from "@mui/icons-material/Menu" ;
import { FC } from "react";
+ import { useTitle } from "redux/meta/hooks";
+ import TextField from "components/util/TextField";
export type HeaderProps = {} & AppBarProps;
const Header: FC<HeaderProps> = (props) => {
+ const {title,setTitle} = useTitle() ;
return (
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
+ <TextField value={title} onChange={e=>setTitle(e.target.value)} style={{flexGrow:1}}/>
</Toolbar>
</AppBar>
);
};
export default Header;
反映できました!
記号を編集パネルに表示
続いて記号を表示させていきます。画面上に表示されるアイテムは次の手順で取得するものとします。
-
storeState.meta.flows
にあるフローを表示する。 - フローを表示するときはそのフローの
childrenSyms
を縦に並べて表示する。
今回は確認用でフローのIDとそのフローの子記号のIDをMUIのList
とListItem
で表示してみます。
import ButtonGroup from "@mui/material/ButtonGroup";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Button from "components/util/Button";
import {FC} from "react" ;
import { useItem, useItemOperations, useItems } from "redux/items/hooks";
import { ItemId, Flow, Sym } from "redux/items/types";
import { useFlows } from "redux/meta/hooks";
export interface BuildPanelProps{
}
const BuildPanel :FC<BuildPanelProps> = ({})=>{
const {
flows:flowIds,
addFlow,
} = useFlows();
const flows = useItems(flowIds) ;
const {setItem} = useItemOperations();
const targetFlowId = flowIds[0] ;
const targetFlow = useItem(targetFlowId) as Flow;
//テスト用イベントハンドラ
const testAddFlow = ()=>{
//id:"test-flow-(random)"のフローを追加する
const flowId :ItemId = "test-flow-"+Math.floor(Math.random()*1000) ;
const flow :Flow = {
itemType:"test-flow",
childrenSyms:[],
} ;
setItem(flowId,flow);
addFlow(flowId);
} ;
const testAddSym = ()=>{
const symId = "test-sym-"+Math.floor(Math.random()*1000) ;
const sym :Sym = {
itemType:"test-item",
options:[],
} ;
setItem(symId,sym);
setItem(targetFlowId,{
...targetFlow,
childrenSyms:[
...targetFlow.childrenSyms,
symId,
]
} as Flow);
} ;
return (
<div>
{/* テスト用ボタン */}
<ButtonGroup>
<Button onClick={testAddFlow}>フローを追加</Button>
<Button onClick={testAddSym}>記号を追加して先頭のフローのchildrenSymsに追加</Button>
</ButtonGroup>
{/* テスト表示用 */}
<List>
{Object.entries(flows).map(([flowId,_item])=>{
const flow = _item as Flow;
return (
<ListItem>
<h2>
id:{flowId}
</h2>
<List>
{flow.childrenSyms.map(symId=>(
<ListItem>
{symId}
</ListItem>
))}
</List>
</ListItem>
) ;
})}
</List>
</div>
)
}
export default BuildPanel ;
あとはList
やListItem
で表示した部分をアイテム表示専用のコンポーネントに置き換えれば完了です。
アイテムを表示するために軽く設計
ディレクトリ設計時にアイテムに関するファイルはsrc/sym内に配置すると決めていました。
流れ図記号には長方形のものや平行四辺形のものなど様々な形があります。
今回は形ごとにコンポーネントを切り出すことにするので次の表のようにファイルを配置していきましょう。
記述対象 | ファイルパス | 備考 |
---|---|---|
コンポーネント | プロジェクトルート/src/sym/形の名前/コンポーネント名.tsx |
|
アイテムオブジェクト作成関数 | プロジェクトルート/src/sym/形の名前/creator.tsx |
記号表示用コンポーネントの土台となるコンポーネントを作る
あらかじめ記号の設定値のデフォルト値を定義します。
const baseSetting = {
size:{
width:180,
height:40,
lineWidth:2,
},
color:{
fore:"black",
back:"white",
},
} ;
いよいよ記号表示用コンポーネントを作成していくわけですが、記号表示用コンポーネントは各形で差があるものの、共通部分が多くあるはずです。なので共通部分を土台コンポーネントに記述し、すべての記号表示用コンポーネントを土台コンポーネント上に作ることで同じようなコードを何度も書く必要がなくなるのでとても良いと思います。
ということで土台コンポーネント(名付けてSymBaseコンポーネント)を作成していきましょう。SymBaseコンポーネントは次のように使用することを理想とします。
const child :SymChild = ({item,itemId})=>{
return <div>{/* 子要素の出力 例えばテキストの表示など */}</div> ;
} ;
const render :SymRenderer = (ctx,setting)=>{ /*settingの情報をもとにctx(キャンバスのコンテキスト)に描画する処理*/ } ;
const RectSym = SymBase(child,render) ;
export default RectSym ;
こうして定義したRectSymを
<RectSym itemId={/*アイテムID*/} />
のようにReact要素として呼び出すことで
<div class="sym-base">
<canvas />{/* これにrenderを適用させる */}
{/* ここにchildを描画する */}
</div>
のようなDOMを出力するものとします。
よってSymBaseの要件は次の3点となります。
- 引数はchildとrender
- childには子コンポーネントを指定する
- renderにはctxとsettingを渡すとctxに図形を描画する処理を記述する
- 戻り値がFC<{itemId:ItemId}>
これをそのまま実装すると次のようになります。
import { FC, useEffect } from "react";
import { Item, ItemId } from "redux/items/types";
export type SymChild = FC<{itemId:ItemId,item:Item}> ;
export type SymRender = (
ctx:CanvasRenderingContext2D,
setting:{
size:{
width:number,
height:number,
lineWidth:number,
},
color:{
fore:string,
back:string,
}
}
)=>void ;
const BaseSym = (Child:SymChild, render:SymRender)=>{
const Sym :FC<{itemId:ItemId}> = ()=>{
return (
<div>
<canvas /> {/* このCanvasにrenderを適用 */}
{/* ここにchildを描画 */}
</div>
) ;
} ;
return Sym ;
} ;
export default BaseSym ;
VSCodeを使っている方はこの時BaseSymをマウスでホバーしてください。SymBaseは引数にChild,renderをとり、戻り値にFC<{itemId: ItemId;}>をとることが確認できると思います。
上のままではせっかく受け取ったchildとrenderをSymに適用させていないのでchild,renderが反映されません。まずはChildを適用させていきましょう。
import { FC } from "react";
import { useItem } from "redux/items/hooks";
import { Item, ItemId } from "redux/items/types";
export const baseSetting = {
size:{
width:180,
height:40,
lineWidth:2,
},
color:{
fore:"black",
back:"white",
},
} ;
export type SymChild = FC<{itemId:ItemId,item:Item}> ;
export type SymRender = (
ctx:CanvasRenderingContext2D,
setting:{
size:{
width:number,
height:number,
lineWidth:number,
},
color:{
fore:string,
back:string,
}
}
)=>void ;
const BaseSym = (Child:SymChild, render:SymRender)=>{
const Sym :FC<{itemId:ItemId}> = ({itemId})=>{
+ const item = useItem(itemId) ;
return (
<div>
<canvas />
+ <Child itemId={itemId} item={item} />
</div>
) ;
} ;
return Sym ;
} ;
export default BaseSym ;
引数をうけとったitemIdをもとにuseItemでアイテムオブジェクト(item)を取得し、itemId,アイテムオブジェクト(item)をpropsとして渡してChildを呼び出しています。
続いてrenderをcanvasに適用していきます。がこれが少々厄介で...
render関数の定義をもう一度示します。
- 引数にctx(キャンバスのコンテキスト)とsetting(サイズや色など描画するための情報)を渡す
- settingの情報をもとにctxに描画する(ctx.strokeRect()などを用いて)
- 戻り値はなし
この定義に従うと、ctxとsettingを取得してからrender関数を呼び出す必要がありますが、このうちctx(キャンバスのコンテキスト)に関してはキャンバスのDOM要素からしか取得することができないのです。そこでDOM要素を取得するためにReact.useRef()
を用います。
useRefの使い方
useRefはDOM要素を取得できるhooksです。
例えばdivタグのDOM要素を取得したいときは次のように記述します。
const ref = useRef<HTMLDivElement>(null) ;
<div ref={ref}>...</div>
ここでrefをconsole.log
すると次のようなオブジェクトが表示されます。
{
current:/*DOM要素*/
}
つまりuseRefをもちいてDOM要素を取得するにはrefをdivに渡し、ref.currentにアクセスすればいいです。
ただしDOM要素はuseEffect内でしか取得できない決まりがあります。よってref.currentでアクセスしたいときは必ずuseEffect内でアクセスします。(この理由は今回は割愛します)
とりあえずcanvasを取得してみます
+ import { FC, useEffect, useRef } from "react";
import { useItem } from "redux/items/hooks";
import { Item, ItemId } from "redux/items/types";
export const baseSetting = {
size:{
width:180,
height:40,
lineWidth:2,
},
color:{
fore:"black",
back:"white",
},
} ;
export type SymChild = FC<{itemId:ItemId,item:Item}> ;
export type SymRender = (
ctx:CanvasRenderingContext2D,
setting:{
size:{
width:number,
height:number,
lineWidth:number,
},
color:{
fore:string,
back:string,
}
}
)=>void ;
const BaseSym = (Child:SymChild, render:SymRender)=>{
const Sym :FC<{itemId:ItemId}> = ({itemId})=>{
//アイテムの取得 -> Childに渡す
const item = useItem(itemId) ;
//render関連の処理
+ const ref = useRef<HTMLCanvasElement>(null) ;
+ useEffect(() => {
+ const canvas = ref.current ;//キャンバスの取得
+ }, []) ;
return (
<div>
+ <canvas
+ ref={ref}
+ width={baseSetting.size.width}
+ height={baseSetting.size.height}/>
<Child itemId={itemId} item={item} />
</div>
) ;
} ;
return Sym ;
} ;
export default BaseSym ;
これでキャンバスを取得できました。あとはここからコンテキストを取得し、renderを実行するだけです。
import { FC, useEffect, useRef } from "react";
import { useItem } from "redux/items/hooks";
import { Item, ItemId } from "redux/items/types";
export const baseSetting = {
size:{
width:180,
height:40,
lineWidth:2,
},
color:{
fore:"black",
back:"white",
},
} ;
export type SymChild = FC<{itemId:ItemId,item:Item}> ;
export type SymRender = (
ctx:CanvasRenderingContext2D,
setting:{
size:{
width:number,
height:number,
lineWidth:number,
},
color:{
fore:string,
back:string,
}
}
)=>void ;
const BaseSym = (Child:SymChild, render:SymRender)=>{
const Sym :FC<{itemId:ItemId}> = ({itemId})=>{
//アイテムの取得 -> Childに渡す
const item = useItem(itemId) ;
//render関連の処理
const ref = useRef<HTMLCanvasElement>(null) ;
useEffect(() => {
const canvas = ref.current ;
+ if(canvas){
+ const ctx = canvas.getContext("2d") ;
+ if(ctx){
+ const setting = baseSetting ;
+ //renderの呼び出し
+ render(ctx,setting) ;
}
}
}, []) ;
return (
<div>
<canvas
ref={ref}
width={baseSetting.size.width}
height={baseSetting.size.height}/>
<Child itemId={itemId} item={item} />
</div>
) ;
} ;
return Sym ;
} ;
export default BaseSym ;
これでrenderも呼び出せました。とりあえずはSymBaseは完成です。(後で付け足しがあるかもしれませんが)
SymBaseを使って長方形の記号を表示したい
土台ができたので長方形の記号を表示していこうと思います。長方形は英語でrectangle
なのでrectangle symbol
略してRectSym
と呼ぶことにします。
RectSymコンポーネントを定義します。
import SymBase, { SymChild, SymRender } from "sym/base/SymBase";
const Child :SymChild = ({itemId})=>{
return <div>RectChild:({itemId})</div>
} ;
const render :SymRender = (ctx,{size,color})=>{
ctx.fillRect(0,0,size.width,size.height)
ctx.strokeRect(0,0,size.width,size.height);
} ;
const RectSym = SymBase(Child,render) ;
export default RectSym ;
ここでRectSymを表示してみましょう。BuildPanelにListItemで表示していたsymIdを<RectSym itemId={symId} />
と表示しましょう。
import ButtonGroup from "@mui/material/ButtonGroup";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Button from "components/util/Button";
import { FC } from "react" ;
import { useItem, useItemOperations, useItems } from "redux/items/hooks";
import { ItemId, Flow, Sym } from "redux/items/types";
import { useFlows } from "redux/meta/hooks";
+ import RectSym from "sym/rect/component";
export interface BuildPanelProps{
}
const BuildPanel :FC<BuildPanelProps> = ({})=>{
const {
flows:flowIds,
addFlow,
} = useFlows();
const flows = useItems(flowIds) ;
const {setItem} = useItemOperations();
const targetFlowId = flowIds[0] ;
const targetFlow = useItem(targetFlowId) as Flow;
//テスト用イベントハンドラ
const testAddFlow = ()=>{
//id:"test-flow-(random)"のフローを追加する
const flowId :ItemId = "test-flow-"+Math.floor(Math.random()*1000) ;
const flow :Flow = {
itemType:"test-flow",
childrenSyms:[],
} ;
setItem(flowId,flow);
addFlow(flowId);
} ;
const testAddSym = ()=>{
const symId = "test-sym-"+Math.floor(Math.random()*1000) ;
const sym :Sym = {
itemType:"test-item",
options:[],
} ;
setItem(symId,sym);
setItem(targetFlowId,{
...targetFlow,
childrenSyms:[
...targetFlow.childrenSyms,
symId,
]
} as Flow);
} ;
return (
<div>
{/* テスト用ボタン */}
<ButtonGroup>
<Button onClick={testAddFlow}>フローを追加</Button>
<Button onClick={testAddSym}>記号を追加して先頭のフローのchildrenSymsに追加</Button>
</ButtonGroup>
{/* テスト表示用 */}
<List>
{Object.entries(flows).map(([flowId,_item])=>{
const flow = _item as Flow;
return (
<ListItem>
<h2>
id:{flowId}
</h2>
<List>
{flow.childrenSyms.map(symId=>(
+ <RectSym itemId={symId}/>
))}
</List>
</ListItem>
) ;
})}
</List>
</div>
)
}
export default BuildPanel ;
スタイルを整えていないので表示の仕方がいびつですが、div内にcanvasと子要素が表示されています。
スタイルを整えましょう。MUIのBoxコンポーネントのsx propsにスタイル情報を渡すことでcssプロパティを指定してスタイルを設定することができます。
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { useItem } from "redux/items/hooks";
import { Item, ItemId } from "redux/items/types";
export const baseSetting = {
size:{
width:180,
height:40,
lineWidth:2,
},
color:{
fore:"black",
back:"white",
},
} ;
export type SymChild = FC<{itemId:ItemId,item:Item}> ;
export type SymRender = (
ctx:CanvasRenderingContext2D,
setting:typeof baseSetting,
)=>void ;
const SymBase = (Child:SymChild, render:SymRender)=>{
const Sym :FC<{itemId:ItemId}> = ({itemId})=>{
//アイテムの取得 -> Childに渡す
const item = useItem(itemId) ;
//render関連の処理
const ref = useRef<HTMLCanvasElement>(null) ;
useEffect(() => {
const canvas = ref.current ;
if(canvas){
const ctx = canvas.getContext("2d") ;
if(ctx){
const setting = baseSetting ;
ctx.fillStyle = setting.color.back ;
ctx.strokeStyle = setting.color.fore ;
ctx.lineWidth = setting.size.lineWidth ;
//renderの呼び出し
render(ctx,setting) ;
}
}
}, []) ;
return (
+ <Box sx={{position:"relative",width:baseSetting.size.width,height:baseSetting.size,}}>
+ <Box sx={{position:"absolute",left:0,top:0,width:"100%",height:"100%"}}>
<canvas
ref={ref}
width={baseSetting.size.width}
height={baseSetting.size.height}/>
+ </Box>
+ <Box sx={{position:"absolute",left:0,top:0,width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",textAlign:"center"}}>
<Child itemId={itemId} item={item} />
+ </Box>
+ </Box>
) ;
} ;
return Sym ;
} ;
export default SymBase ;
いい感じに表示されいます!残すはフローの表示のみです。
ここまでのコード
この章を執筆中、デモのコードがちょくちょく間違っていたことが発覚しましたので、ここでソースコードをすべてコピーしておくことをお勧めします。
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { useItem } from "redux/items/hooks";
import { Item, ItemId } from "redux/items/types";
export const baseSetting = {
size:{
width:180,
height:40,
lineWidth:2,
},
color:{
fore:"black",
back:"white",
},
} ;
const width = baseSetting.size.width ;
const height = baseSetting.size.height ;
export type SymChild = FC<{itemId:ItemId,item:Item}> ;
export type SymRender = (
ctx:CanvasRenderingContext2D,
setting:typeof baseSetting,
)=>void ;
const SymBase = (Child:SymChild, render:SymRender)=>{
const Sym :FC<{itemId:ItemId}> = ({itemId})=>{
//アイテムの取得 -> Childに渡す
const item = useItem(itemId) ;
//render関連の処理
const ref = useRef<HTMLCanvasElement>(null) ;
useEffect(() => {
const canvas = ref.current ;
if(canvas){
const ctx = canvas.getContext("2d") ;
if(ctx){
const setting = baseSetting ;
ctx.fillStyle = setting.color.back ;
ctx.strokeStyle = setting.color.fore ;
ctx.lineWidth = setting.size.lineWidth ;
//renderの呼び出し
render(ctx,setting) ;
}
}
}, []) ;
return (
<Box sx={{
position:"relative",width:width,height:baseSetting.size,
textAlign:"center",}}>
<Box sx={{position:"absolute",left:0,top:0,width,height,}}>
<canvas
ref={ref}
width={width}
height={height}/>
</Box>
<Box sx={{position:"absolute",left:0,top:0,width,height,}}>
<Child itemId={itemId} item={item} />
</Box>
</Box>
) ;
} ;
return Sym ;
} ;
export default SymBase ;
import SymBase, { SymChild, SymRender } from "sym/base/SymBase";
const Child :SymChild = ({itemId})=>{
return <>RectChild:({itemId})</>
} ;
const render :SymRender = (ctx,{size,color})=>{
ctx.fillRect(0,0,size.width,size.height)
ctx.strokeRect(0,0,size.width,size.height);
} ;
const RectSym = SymBase(Child,render) ;
export default RectSym ;