Chapter 10

第7章 ReduxをUIに反映しよう 後編 -フローを表示-

てべすてん
てべすてん
2022.03.11に更新

フローを編集パネルに表示

記号を表示できたので最後にフローを表示します。

フロー表示の例を次のフローチャートを使って解説します。

このフローチャートは3種類のパーツ×6個で構成されています。

  • 記号×3
  • フローの矢印×2
  • フロー×1

このうち記号は先ほど表示することができました。残るはフローの矢印とフローですが、フロー=記号+フローの矢印となっているため、次に作らなければいけないのはフローの矢印です。

フローの矢印はArrow.tsxとして実装します。矢印はcanvasに描画していくことにしていきます。

プロジェクトルート/src/sym/flow/Arrow.tsx
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { baseSetting } from "sym/base/SymBase";

const width = baseSetting.size.width ;
const height = baseSetting.size.height/2 ;

export interface ArrowProps {}

const Arrow: FC<ArrowProps> = () => {
    const ref = useRef<HTMLCanvasElement>(null) ;
    useEffect(()=>{
        const canvas = ref.current ;
        if(canvas){
            const ctx = canvas.getContext("2d");
            if(ctx){
                ctx.fillStyle = baseSetting.color.back ;
                ctx.strokeStyle = baseSetting.color.fore ;
                ctx.lineWidth = baseSetting.size.lineWidth ;

                //線を引く
                ctx.beginPath();
                ctx.moveTo(width/2,0);
                ctx.lineTo(width/2,height);
                ctx.closePath();
                ctx.stroke() ;
            }
        }
    },[])
    return (
        <Box sx={{width,height,}}>
            <canvas
                width={width}
                height={height}
                ref={ref}/>
        </Box>
    );
};
export default Arrow;

どんなふうに表示されるのか確認するためにBuildPanelに表示してみましょう。

プロジェクトルート/src/components/App/BuildPanel.tsx
  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 Arrow from "sym/flow/Arrow";
  import RectSym from "sym/rect/RectSym";


  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>

+       <Arrow />
      </div>
    )
  } 
  export default BuildPanel ;

確認出来たら<Arrow />は消しておきましょう。

今回はただの線ですがもうちょっとctx.〇〇をいじると本格的な矢印にできます。やりたい人はArrow.tsxのctx.beginPath以降をがんばっていじってください。

あとはこのArrowをSymの間に入れるだけです。またスタイルを整えるためにMUIのStackコンポーネントを使用しています。

プロジェクトルート/src/components/App/BuildPanel.tsx
  import ButtonGroup from "@mui/material/ButtonGroup";
  import List from "@mui/material/List";
  import ListItem from "@mui/material/ListItem";
+ import Stack from "@mui/material/Stack";
  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 Arrow from "sym/flow/Arrow";
  import RectSym from "sym/rect/RectSym";


  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>
+               <Stack direction="column">
+                 {flow.childrenSyms.map((symId,idx)=>(
+                   <>
+                     {idx === 0 ? null : <Arrow /> }
+                     <RectSym itemId={symId}/>
+                   </>
+                 ))}
+               </Stack>
              </ListItem>
            ) ;
          })}
        </List>
      </div>
    )
  } 
  export default BuildPanel ;


先頭の時以外はRectSymの直前にArrowを表示するように書きました。

最後にフローを作成します。

プロジェクトルート/src/sym/flow/Flow.tsx
import Stack from "@mui/material/Stack";
import { FC } from "react";
import { useItem } from "redux/items/hooks";
import { ItemId,Flow } from "redux/items/types";
import RectSym from "sym/rect/RectSym";
import Arrow from "./Arrow";

export interface FlowProps {
    flowId:ItemId,
}

const FlowComp: FC<FlowProps> = ({flowId}) => {
    const flow = useItem(flowId) as Flow;
    return (
        <Stack direction="column">
            {flow.childrenSyms.map((symId, idx) => (
                <>
                    {idx === 0 ? null : <Arrow />}
                    <RectSym itemId={symId} />
                </>
            ))}
        </Stack>
    );
};
export default FlowComp;

//※ほぼBuildPanelからコピーしています。

これをBuildPanelに読み込みます。

プロジェクトルート/src/components/App/BuildPanel.tsx
  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, Sym ,Flow as FlowType ,} from "redux/items/types";
  import { useFlows } from "redux/meta/hooks";
+ import FlowComp from "sym/flow/Flow";


  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 FlowType;

    //テスト用イベントハンドラ
    const testAddFlow = ()=>{
      //id:"test-flow-(random)"のフローを追加する
      const flowId :ItemId = "test-flow-"+Math.floor(Math.random()*1000) ;
+     const flow :FlowType = {
        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 FlowType);
    } ;
    return (
      <div>
        {/* テスト用ボタン */}
        <ButtonGroup>
          <Button onClick={testAddFlow}>フローを追加</Button>
          <Button onClick={testAddSym}>記号を追加して先頭のフローのchildrenSymsに追加</Button>
        </ButtonGroup>

        {/* テスト表示用 */}
        <List>
          {Object.entries(flows).map(([flowId,_item])=>{
            return (
              <ListItem>
                <h2>
                  id:{flowId}
                </h2>
+               <FlowComp flowId={flowId}/>
              </ListItem>
            ) ;
          })}
        </List>
      </div>
    )
  } 
  export default BuildPanel ;


フローも作成できました😂長かったこの章もようやく終了です!

最後にフローの見た目を整えてこの章を終わりとしましょう!(いやー長かった)

プロジェクトルート/src/components/App/BuildPanel.tsx
import ButtonGroup from "@mui/material/ButtonGroup";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Stack from "@mui/material/Stack";
import Button from "components/util/Button";
import { FC } from "react" ; 
import { useItem, useItemOperations, useItems } from "redux/items/hooks";
import { ItemId, Sym ,Flow as FlowType ,} from "redux/items/types";
import { useFlows } from "redux/meta/hooks";
import FlowComp from "sym/flow/Flow";


export interface BuildPanelProps{
}

const BuildPanel :FC<BuildPanelProps> = ({})=>{
  const {
    flows:flowIds,
    addFlow,
  } = useFlows();
  const flows = useItems(flowIds) ;
  const {setItem} = useItemOperations();

  //テスト用イベントハンドラ
  const testAddFlow = ()=>{
    //id:"test-flow-(random)"のフローを追加する
    const flowId :ItemId = "test-flow-"+Math.floor(Math.random()*1000) ;
    const flow :FlowType = {
      itemType:"test-flow",
      childrenSyms:[],
    } ;
    
    setItem(flowId,flow);
    addFlow(flowId);
  } ;

  const testAddSym = (targetFlowId:ItemId)=>{
    const targetFlow = flows[targetFlowId] as FlowType ;
    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 FlowType);
  } ;
  return (
    <div>
      {/* テスト用ボタン */}
      <Button onClick={testAddFlow}>フローを追加</Button>

      {/* テスト表示用 */}
      <Stack spacing={2}>
        {Object.entries(flows).map(([flowId,_item])=>{
          return (
            <div>
              <FlowComp flowId={flowId}/>
              <Button onClick={()=>testAddSym(flowId)}>
                記号を追加
              </Button>
            </div>
          ) ;
        })}
      </Stack>
    </div>
  )
} 
export default BuildPanel ;

ここまでのソースコード
プロジェクトルート/src/sym/base/SymBase.tsx
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",fontSize:"14px"}}>
                    
                <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 ;

プロジェクトルート/src/sym/flow/Arrow.tsx
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { baseSetting } from "sym/base/SymBase";

const width = baseSetting.size.width ;
const height = baseSetting.size.height/2 ;

export interface ArrowProps {}

const Arrow: FC<ArrowProps> = () => {
    const ref = useRef<HTMLCanvasElement>(null) ;
    useEffect(()=>{
        const canvas = ref.current ;
        if(canvas){
            const ctx = canvas.getContext("2d");
            if(ctx){
                ctx.fillStyle = baseSetting.color.back ;
                ctx.strokeStyle = baseSetting.color.fore ;
                ctx.lineWidth = baseSetting.size.lineWidth ;

                //線を引く
                ctx.beginPath();
                ctx.moveTo(width/2,0);
                ctx.lineTo(width/2,height);
                ctx.closePath();
                ctx.stroke() ;
            }
        }
    },[])
    return (
        <Box sx={{width,height,}}>
            <canvas
                width={width}
                height={height}
                ref={ref}/>
        </Box>
    );
};
export default Arrow;

プロジェクトルート/src/sym/base/SymBase.tsx
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { baseSetting } from "sym/base/SymBase";

const width = baseSetting.size.width ;
const height = baseSetting.size.height/2 ;

export interface ArrowProps {}

const Arrow: FC<ArrowProps> = () => {
    const ref = useRef<HTMLCanvasElement>(null) ;
    useEffect(()=>{
        const canvas = ref.current ;
        if(canvas){
            const ctx = canvas.getContext("2d");
            if(ctx){
                ctx.fillStyle = baseSetting.color.back ;
                ctx.strokeStyle = baseSetting.color.fore ;
                ctx.lineWidth = baseSetting.size.lineWidth ;

                //線を引く
                ctx.beginPath();
                ctx.moveTo(width/2,0);
                ctx.lineTo(width/2,height);
                ctx.closePath();
                ctx.stroke() ;
            }
        }
    },[])
    return (
        <Box sx={{width,height,}}>
            <canvas
                width={width}
                height={height}
                ref={ref}/>
        </Box>
    );
};
export default Arrow;

プロジェクトルート/src/sym/flow/Flow.tsx
import Stack from "@mui/material/Stack";
import { FC } from "react";
import { useItem } from "redux/items/hooks";
import { ItemId,Flow } from "redux/items/types";
import RectSym from "sym/rect/RectSym";
import Arrow from "./Arrow";

export interface FlowProps {
    flowId:ItemId,
}

const FlowComp: FC<FlowProps> = ({flowId}) => {
    const flow = useItem(flowId) as Flow;
    return (
        <Stack direction="column">
            {flow.childrenSyms.map((symId, idx) => (
                <>
                    {idx === 0 ? null : <Arrow />}
                    <RectSym itemId={symId} />
                </>
            ))}
        </Stack>
    );
};
export default FlowComp;

プロジェクトルート/src/sym/rect/RectSym.tsx
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 ;

プロジェクトルート/src/components/App/BuildPanel.tsx
import ButtonGroup from "@mui/material/ButtonGroup";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Stack from "@mui/material/Stack";
import Button from "components/util/Button";
import { FC } from "react" ; 
import { useItem, useItemOperations, useItems } from "redux/items/hooks";
import { ItemId, Sym ,Flow as FlowType ,} from "redux/items/types";
import { useFlows } from "redux/meta/hooks";
import FlowComp from "sym/flow/Flow";


export interface BuildPanelProps{
}

const BuildPanel :FC<BuildPanelProps> = ({})=>{
  const {
    flows:flowIds,
    addFlow,
  } = useFlows();
  const flows = useItems(flowIds) ;
  const {setItem} = useItemOperations();

  //テスト用イベントハンドラ
  const testAddFlow = ()=>{
    //id:"test-flow-(random)"のフローを追加する
    const flowId :ItemId = "test-flow-"+Math.floor(Math.random()*1000) ;
    const flow :FlowType = {
      itemType:"test-flow",
      childrenSyms:[],
    } ;
    
    setItem(flowId,flow);
    addFlow(flowId);
  } ;

  const testAddSym = (targetFlowId:ItemId)=>{
    const targetFlow = flows[targetFlowId] as FlowType ;
    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 FlowType);
  } ;
  return (
    <div>
      {/* テスト用ボタン */}
      <Button onClick={testAddFlow}>フローを追加</Button>

      {/* テスト表示用 */}
      <Stack spacing={2}>
        {Object.entries(flows).map(([flowId,_item])=>{
          return (
            <div>
              <FlowComp flowId={flowId}/>
              <Button onClick={()=>testAddSym(flowId)}>
                記号を追加
              </Button>
            </div>
          ) ;
        })}
      </Stack>
    </div>
  )
} 
export default BuildPanel ;


質問・指摘などはこちらから

https://zenn.dev/tbsten/scraps/7123b1257c2097