アイテムクリエイターにオプションを設定する
今までは処理の簡略化のためにオプションを1つも設定していませんでした(options:[]
としていしていた)。ここではオプションを設定してみましょう。
長方形記号にテキストオプションを設定します。テキストオプションで指定した文字列は長方形記号の中に表示されるようにしましょう。
プロジェクトルート/src/sym/rect/creator
を編集します。
import { Sym } from "redux/items/types";
import itemCreator from "sym/base/creator";
export default function rectCreator() :Sym{
return {
...itemCreator("rect"),
options:[
+ {name:"テキスト", value:"表示文字列", },
],
} ;
}
これをRectSymで表示してみます。
import { Sym } from "redux/items/types";
import SymBase, { SymChild, SymRender } from "sym/base/SymBase";
+ const Child :SymChild = ({itemId,item : _item})=>{
+ const item = _item as Sym ;
+ return (
+ <>
+ {item.options[0].name}
+ :
+ {item.options[0].value}
+ </>
+ )
} ;
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 ;
これを編集できるようにしましょう。オプションの編集はサイドバーから行うようにします。
しかし通常フローチャート上には複数のアイテムが存在します。今回はどのアイテムのオプションを編集するのかをユーザが選択してから、選択したアイテムのオプションを編集できるように実装することにします。
ユーザが記号をクリックした場合、storeState.app.selectItemId
に選択したアイテムのIDを設定し、storeState.app.selectItemId
がnullでない場合にSideBarはstoreState.app.selectItemId
のアイテムのオプションを一覧で表示するようにしましょう。
ということでapp stateにselectItemId
を追加していきます。
import { ItemId } from "redux/items/types";
import { reducerWithInitialState } from "typescript-fsa-reducers";
export const init = {
+ selectItemId:null as (null|ItemId),
} ;
export const app = reducerWithInitialState(init) ;
なお何も選択されていない場合はselectItemIdにはnullを設定することにします。
つづいてselectorにselectItemIdを取得するセレクタを記述します。
import { StoreState } from "redux/store";
export const getSelectItemId = ()=>{
return (store :StoreState)=>store.app.selectItemId ;
} ;
つづいてselectItemIdを変更するアクションを記述します。
import { ItemId } from "redux/items/types";
import actionCreatorFactory from "typescript-fsa" ;
const actionCreator = actionCreatorFactory() ;
//いつ使うのかわからないけど公式ドキュメントに載っていたのでおいておきます
export const init = actionCreator("app/init");
//選択中アイテムを更新
export const setSelectItemId = actionCreator<{itemId:null|ItemId}>("app/selectItemId/set");
reducerでアクションを処理します。
import { ItemId } from "redux/items/types";
import { reducerWithInitialState } from "typescript-fsa-reducers";
import * as actions from "./actions" ;
export const init = {
selectItemId:null as (null|ItemId),
} ;
export const app = reducerWithInitialState(init)
.case( actions.setSelectItemId, (state,payload)=>{
if(state.selectItemId === payload.itemId){
return state ;
}
return {
...state,
selectItemId:payload.itemId,
} ;
} ) ;
最後にhookに処理をまとめます。
import { useDispatch, useSelector } from "react-redux";
import { ItemId } from "redux/items/types";
import { setSelectItemId } from "./actions";
import { getSelectItemId } from "./selectors";
export function useSelectItem(){
const selectItemId = useSelector(getSelectItemId());
const dispatch = useDispatch() ;
const changeSelectItemId = (itemId:null|ItemId)=>{
dispatch(setSelectItemId({itemId}));
}
return {
selectItemId,
changeSelectItemId,
} ;
}
これでuseSelectItem
を使えば選択中のアイテムの取得・更新ができるようになります。Sidebarに使ってみましょう。
import {FC} from "react" ;
+ import { useSelectItem } from "redux/app/hooks";
+ import { useItem } from "redux/items/hooks";
export interface SidebarProps{
}
const Sidebar :FC<SidebarProps> = ({})=>{
+ const {
+ selectItemId,
+ } = useSelectItem() ;
+ const selectItem = useItem(selectItemId??"") ; //selectItemは後で使います
return (
<div>
+ {selectItemId?`選択中のアイテムは${selectItemId}です`:"アイテムが選択されていません"}
</div>
)
}
export default Sidebar ;
あとはそれぞれの記号をクリックしたときに選択するようにしましょう。
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
+ import { useSelectItem } from "redux/app/hooks";
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) ;
}
}
}, []) ;
//選択処理
+ const { changeSelectItemId, } = useSelectItem() ;
return (
<Box sx={{
position:"relative",width:width,height:baseSetting.size,
textAlign:"center",fontSize:"14px"}}
+ onClick={()=>changeSelectItemId(itemId)}>
<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 ;
選択した記号がサイドバーで取得できました。ただこれではどの記号が選択されたのかが分かりずらいので選択された記号は青色で囲われるようにしましょう。書き込む色はctx.strokeStyle
で指定しているので、その記号が選択中ならctx.strokeStyle
が青色になるように変更します。
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { useSelectItem } from "redux/app/hooks";
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})=>{
//選択処理
const { selectItemId,changeSelectItemId, } = useSelectItem() ;
//アイテムの取得 -> 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 ;
+ if(selectItemId === itemId){
+ ctx.strokeStyle = "blue" ;
+ }
//renderの呼び出し
render(ctx,setting) ;
}
}
}, [itemId,selectItemId,]) ;
return (
<Box sx={{
position:"relative",width:width,height:baseSetting.size,
textAlign:"center",fontSize:"14px"}}
onClick={()=>changeSelectItemId(itemId)}>
<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 ;
これで選択中の記号が分かりやすくなりました。
アイテムのオプションの更新更新機能を実装
あとはサイドバーでオプションを変更する処理を書いていきます。
とりあえずすべてのオプションをMUIのList
,ListItem
を使って表示します。
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import {FC} from "react" ;
import { useSelectItem } from "redux/app/hooks";
import { useItem, } from "redux/items/hooks";
import { Sym } from "redux/items/types";
export interface SidebarProps{
}
const Sidebar :FC<SidebarProps> = ({})=>{
const {
selectItemId,
} = useSelectItem() ;
const selectItem = useItem(selectItemId??"") as Sym ;
return (
<div>
{selectItemId?
<List>
{selectItem.options.map(option=>{
return (
<ListItem>
{option.name}
:
{option.value}
</ListItem>
) ;
})}
</List>
:"アイテムが選択されていません"}
</div>
)
}
export default Sidebar ;
あとはvalueをTextFieldにすればオプションを編集できるでしょう。
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import TextField from "components/util/TextField";
import {ChangeEvent, FC, SyntheticEvent} from "react" ;
import { useSelectItem } from "redux/app/hooks";
import { useItem, } from "redux/items/hooks";
import { Sym } from "redux/items/types";
export interface SidebarProps{
}
const Sidebar :FC<SidebarProps> = ({})=>{
const {
selectItemId,
} = useSelectItem() ;
const selectItem = useItem(selectItemId??"") as Sym ;
+ const handleOptionChange = (idx:number, e :ChangeEvent<HTMLInputElement|HTMLTextAreaElement>)=>{
+ //オプションの更新処理
+ }
return (
<div>
{selectItemId?
<List>
{selectItem.options.map((option,idx)=>{
return (
<ListItem>
{option.name}
:
+ <TextField value={option.value} onChange={e=>handleOptionChange(idx,e)}/>
</ListItem>
) ;
})}
</List>
:"アイテムが選択されていません"}
</div>
)
}
export default Sidebar ;
handleOptionChange
の第1引数idxはどのオプションを更新したいのかを示す数字です。記号オブジェクト.options[idx]
でオプションオブジェクトにアクセスできます。
最後にオプションを更新します。
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import TextField from "components/util/TextField";
import {ChangeEvent, FC, SyntheticEvent} from "react" ;
import { useSelectItem } from "redux/app/hooks";
import { useItem, useItemOperations, } from "redux/items/hooks";
import { Sym } from "redux/items/types";
export interface SidebarProps{
}
const Sidebar :FC<SidebarProps> = ({})=>{
const {
selectItemId,
} = useSelectItem() ;
const selectItem = useItem(selectItemId??"") as Sym ;
const { setItem } = useItemOperations();
const handleOptionChange = (idx:number, e :ChangeEvent<HTMLInputElement|HTMLTextAreaElement>)=>{
//オプションの更新処理
+ const newValue = e.target.value ;
+ const newOptions = [...selectItem.options] ;
+ newOptions[idx] = {
+ ...newOptions[idx],
+ value:newValue,
+ } ;
+ const newItem = {
+ ...selectItem,
+ options:newOptions,
+ } ;
+ setItem(selectItemId??"",newItem);
}
return (
<div>
{selectItemId?
<List>
{selectItem.options.map((option,idx)=>{
return (
<ListItem>
{option.name}
:
<TextField value={option.value} onChange={e=>handleOptionChange(idx,e)}/>
</ListItem>
) ;
})}
</List>
:"アイテムが選択されていません"}
</div>
)
}
export default Sidebar ;
handleOptionChange
では少し複雑ですが、次のような処理を行っています。
- eから入力値を取得
- 更新前のオプションをnewOptions(更新後のオプション)としてコピー
- newOptionsのidx番目のvalueを入力地に上書き
- 更新後のオプションを反映したnewItemを作成
- selectItemId(選択中のアイテムのID)でアイテムを更新。(この時selectItemIdはnullの可能性があるので??を使ってnullならばIDが""のアイテムを更新させる)
これでオプションの更新ができました。
アイテムの削除機能を実装
アイテムの削除はサイドバーから行うことにします。
サイドバーに削除ボタンを作成し,handleRemoveSymで記号を削除します。
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
+ import Button from "components/util/Button";
import TextField from "components/util/TextField";
import {ChangeEvent, FC, SyntheticEvent} from "react" ;
import { useSelectItem } from "redux/app/hooks";
import { useItem, useItemOperations, } from "redux/items/hooks";
import { Sym } from "redux/items/types";
export interface SidebarProps{
}
const Sidebar :FC<SidebarProps> = ({})=>{
const {
selectItemId,
} = useSelectItem() ;
const selectItem = useItem(selectItemId??"") as Sym ;
+ const { setItem,removeItem } = useItemOperations();
const handleOptionChange = (idx:number, e :ChangeEvent<HTMLInputElement|HTMLTextAreaElement>)=>{
//オプションの更新処理
const newValue = e.target.value ;
const newOptions = [...selectItem.options] ;
newOptions[idx] = {
...newOptions[idx],
value:newValue,
} ;
const newItem = {
...selectItem,
options:newOptions,
} ;
setItem(selectItemId??"",newItem);
}
+ const handleRemoveSym = ()=>{
+ if(selectItemId){
+ removeItem(selectItemId);
+ }
+ } ;
return (
<div>
{selectItemId?
<>
<List>
{selectItem.options.map((option,idx)=>{
return (
<ListItem>
{option.name}
:
<TextField value={option.value} onChange={e=>handleOptionChange(idx,e)}/>
</ListItem>
) ;
})}
</List>
+ <Button onClick={handleRemoveSym}>
+ 記号を削除する
+ </Button>
</>
:"アイテムが選択されていません"}
</div>
)
}
export default Sidebar ;
ただしこのコードはエラーを引き起こします。
これはremoveItem関数(が呼び出すreducer)ではアイテム一覧からしかアイテムを削除せず、親フローのchildrenSymsからは削除しないために、親フローの描画時に存在しない(削除された)アイテムIDを参照しようとしてしまうからです。なので子アイテムをフローしたときに同時に親フローのchildrenSymsからも削除されるようにitems のreducerを変更します。
ただ削除するときに取得できるのは子アイテムのIDだけで、その子アイテムの親アイテムがどれなのか判別する方法がありません。そこで記号オブジェクトのプロパティとしてparentFlow(親フローのID)を設定し、記号オブジェクト作成時にそれを登録するように変更します。
export type ItemId = string ;
export type OptionValue =
string | number | boolean |
string[] | number[] | boolean[] ;
export interface Option {
name:string;
value:OptionValue ;
}
export interface Item {
itemType:string;
}
export interface Sym extends Item {
options:Option[];
+ parentFlow:ItemId,
}
export interface Flow extends Item {
childrenSyms:ItemId[] ;
}
export type Items = {
[key:ItemId] :Item ;
} ;
+ import { ItemId, Sym } from "redux/items/types";
import itemCreator from "sym/base/creator";
+ export default function rectCreator(parentFlow:ItemId) :Sym{
return {
...itemCreator("rect"),
+ parentFlow,
options:[
{name:"テキスト", value:"表示文字列", },
],
} ;
}
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Button from "components/util/Button";
import { FC } from "react";
import { useItem, useItemOperations } from "redux/items/hooks";
import { ItemId,Flow, Sym } from "redux/items/types";
import { baseSetting } from "sym/base/SymBase";
import rectCreator from "sym/rect/creator";
import RectSym from "sym/rect/RectSym";
import { createRandomItemId } from "sym/util";
import Arrow from "./Arrow";
export interface FlowProps {
flowId:ItemId,
}
const FlowComp: FC<FlowProps> = ({flowId}) => {
const flow = useItem(flowId) as Flow;
const { setItem } = useItemOperations() ;
//追加処理
const handleAddSym = (idx:number)=>{
//子要素の追加処理
//idxで指定した位置に追加する
+ const sym :Sym = rectCreator(flowId) ;
const symId = createRandomItemId(sym.itemType) ;
setItem(symId,sym);
const newChildrenSyms = [...flow.childrenSyms] ;
newChildrenSyms.splice(idx,0,symId) ;
const newFlow = {
...flow,
childrenSyms:newChildrenSyms,
} ;
setItem(flowId,newFlow)
} ;
return (
<Stack direction="column">
{flow.childrenSyms.map((symId, idx) => (
<>
{idx === 0 ? null : <Arrow />}
<Box sx={{display:"flex",justifyContent:"center",width:baseSetting.size.width}}>
<Button onClick={()=>handleAddSym(idx)} sx={{width:"fit-content"}}>追加</Button>
</Box>
<RectSym itemId={symId} />
<Box sx={{display:"flex",justifyContent:"center",width:baseSetting.size.width}}>
<Button onClick={()=>handleAddSym(idx+1)} sx={{width:"fit-content"}}>追加</Button>
</Box>
</>
))}
{/* 子要素がない */}
{flow.childrenSyms.length === 0 ?
<>
子要素がありません
<Button onClick={()=>handleAddSym(0)}>
記号を追加する
</Button>
</>
: ""}
</Stack>
);
};
export default FlowComp;
これで子アイテム(記号)からその親フローを取得することができます。取得した親フローのchildrenSymsからも該当記号を削除する処理をitemsのreducerに追加します。
import { reducerWithInitialState } from "typescript-fsa-reducers";
import * as actions from "./actions" ;
import { Flow, isSym, Items } from "./types";
export const init :Items = {} ;
export const items = reducerWithInitialState(init)
.case( actions.set ,(state,payload)=>{
const newState = {
...state,
[payload.itemId] : payload.item,
} ;
return newState ;
})
.case( actions.remove ,(state,payload)=>{
const deleteItem = state[payload.itemId] ;
let newState = {...state} as Items ;
let isUpdate = false ;
//アイテム一覧から削除
const keys = Object.keys(state)
if(keys.includes(payload.itemId)){
isUpdate = true ;
delete newState[payload.itemId] ;
}
+ //フローの場合はchildrenSymsからも削除
+ if(isSym(deleteItem)){ //isSym関数は後述のtypes.tsにて定義
+ const parentFlow = (newState[deleteItem.parentFlow]) as Flow ;
+ parentFlow.childrenSyms = parentFlow.childrenSyms.filter(symId=>symId!==payload.itemId) ;
+ }
+ //戻り値
+ if(isUpdate){
+ return newState ;
+ }
+ return state ;
+ })
ここで使用するisSymのような与えられた値がSymオブジェクトなのかどうかを判断する関数をいくつか用意したtypes.tsを示します。
export type ItemId = string ;
export type OptionValue =
string | number | boolean |
string[] | number[] | boolean[] ;
export interface Option {
name:string;
value:OptionValue ;
}
export interface Item {
itemType:string;
}
export interface Sym extends Item {
options:Option[];
parentFlow:ItemId,
}
export interface Flow extends Item {
childrenSyms:ItemId[] ;
}
export type Items = {
[key:ItemId] :Item ;
} ;
+ export function isItem(arg: any):arg is Item{
+ return (
+ arg &&
+ typeof arg === "object" &&
+ typeof arg.itemType === "string"
+ ) ;
+ }
+ export function isSym(arg: any):arg is Sym{
+ return (
+ arg &&
+ arg.options instanceof Array &&
+ isItem(arg)
+ ) ;
+ }
+ export function isFlow(arg: any):arg is Flow{
+ return (
+ arg &&
+ arg.childrenSyms instanceof Array &&
+ isItem(arg)
+ ) ;
+ }
これで削除の処理も実装できました😁
これまでのソースコード
import Box from "@mui/material/Box";
import { FC, useEffect, useRef } from "react";
import { useSelectItem } from "redux/app/hooks";
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})=>{
//選択処理
const { selectItemId,changeSelectItemId, } = useSelectItem() ;
//アイテムの取得 -> 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 ;
if(selectItemId === itemId){
ctx.strokeStyle = "blue" ;
}
//renderの呼び出し
render(ctx,setting) ;
}
}
}, [itemId, selectItemId]) ;
return (
<Box sx={{
position:"relative",width:width,height:baseSetting.size,
textAlign:"center",fontSize:"14px"}}
onClick={()=>changeSelectItemId(itemId)}>
<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 { ItemId, Sym } from "redux/items/types";
import itemCreator from "sym/base/creator";
export default function rectCreator(parentFlow:ItemId) :Sym{
return {
...itemCreator("rect"),
parentFlow,
options:[
{name:"テキスト", value:"表示文字列", },
],
} ;
}
import { Sym } from "redux/items/types";
import SymBase, { SymChild, SymRender } from "sym/base/SymBase";
const Child :SymChild = ({itemId,item : _item})=>{
const item = _item as Sym ;
return (
<>
{item.options[0].name}
:
{item.options[0].value}
</>
)
} ;
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 ;
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Button from "components/util/Button";
import { FC } from "react";
import { useItem, useItemOperations } from "redux/items/hooks";
import { ItemId,Flow, Sym } from "redux/items/types";
import { baseSetting } from "sym/base/SymBase";
import rectCreator from "sym/rect/creator";
import RectSym from "sym/rect/RectSym";
import { createRandomItemId } from "sym/util";
import Arrow from "./Arrow";
export interface FlowProps {
flowId:ItemId,
}
const FlowComp: FC<FlowProps> = ({flowId}) => {
const flow = useItem(flowId) as Flow;
const { setItem } = useItemOperations() ;
//追加処理
const handleAddSym = (idx:number)=>{
//子要素の追加処理
//idxで指定した位置に追加する
const sym :Sym = rectCreator(flowId) ;
const symId = createRandomItemId(sym.itemType) ;
setItem(symId,sym);
const newChildrenSyms = [...flow.childrenSyms] ;
newChildrenSyms.splice(idx,0,symId) ;
const newFlow = {
...flow,
childrenSyms:newChildrenSyms,
} ;
setItem(flowId,newFlow)
} ;
return (
<Stack direction="column">
{flow.childrenSyms.map((symId, idx) => (
<>
{idx === 0 ? null : <Arrow />}
<Box sx={{display:"flex",justifyContent:"center",width:baseSetting.size.width}}>
<Button onClick={()=>handleAddSym(idx)} sx={{width:"fit-content"}}>追加</Button>
</Box>
<RectSym itemId={symId} />
<Box sx={{display:"flex",justifyContent:"center",width:baseSetting.size.width}}>
<Button onClick={()=>handleAddSym(idx+1)} sx={{width:"fit-content"}}>追加</Button>
</Box>
</>
))}
{/* 子要素がない */}
{flow.childrenSyms.length === 0 ?
<>
子要素がありません
<Button onClick={()=>handleAddSym(0)}>
記号を追加する
</Button>
</>
: ""}
</Stack>
);
};
export default FlowComp;
import { ItemId } from "redux/items/types";
import { reducerWithInitialState } from "typescript-fsa-reducers";
import * as actions from "./actions" ;
export const init = {
selectItemId:null as (null|ItemId),
} ;
export const app = reducerWithInitialState(init)
.case( actions.setSelectItemId, (state,payload)=>{
if(state.selectItemId === payload.itemId){
return state ;
}
return {
...state,
selectItemId:payload.itemId,
} ;
} ) ;
import { StoreState } from "redux/store";
export const getSelectItemId = ()=>{
return (store :StoreState)=>store.app.selectItemId ;
} ;
import { ItemId } from "redux/items/types";
import actionCreatorFactory from "typescript-fsa" ;
const actionCreator = actionCreatorFactory() ;
//いつ使うのかわからないけど公式ドキュメントに載っていたのでおいておきます
export const init = actionCreator("app/init");
//選択中アイテムを更新
export const setSelectItemId = actionCreator<{itemId:null|ItemId}>("app/selectItemId/set");
import { useDispatch, useSelector } from "react-redux";
import { ItemId } from "redux/items/types";
import { setSelectItemId } from "./actions";
import { getSelectItemId } from "./selectors";
export function useSelectItem(){
const selectItemId = useSelector(getSelectItemId());
const dispatch = useDispatch() ;
const changeSelectItemId = (itemId:null|ItemId)=>{
dispatch(setSelectItemId({itemId}));
}
return {
selectItemId,
changeSelectItemId,
} ;
}
import { reducerWithInitialState } from "typescript-fsa-reducers";
import * as actions from "./actions" ;
import { Flow, isSym, Items } from "./types";
export const init :Items = {} ;
export const items = reducerWithInitialState(init)
.case( actions.set ,(state,payload)=>{
const newState = {
...state,
[payload.itemId] : payload.item,
} ;
return newState ;
})
.case( actions.remove ,(state,payload)=>{
const deleteItem = state[payload.itemId] ;
let newState = {...state} as Items ;
let isUpdate = false ;
//アイテム一覧から削除
const keys = Object.keys(state)
if(keys.includes(payload.itemId)){
isUpdate = true ;
delete newState[payload.itemId] ;
}
//フローの場合はchildrenSymsからも削除
if(isSym(deleteItem)){
const parentFlow = (newState[deleteItem.parentFlow]) as Flow ;
parentFlow.childrenSyms = parentFlow.childrenSyms.filter(symId=>symId!==payload.itemId) ;
}
//戻り値
if(isUpdate){
return newState ;
}
return state ;
})
export type ItemId = string ;
export type OptionValue =
string | number | boolean |
string[] | number[] | boolean[] ;
export interface Option {
name:string;
value:OptionValue ;
}
export interface Item {
itemType:string;
}
export interface Sym extends Item {
options:Option[];
parentFlow:ItemId,
}
export interface Flow extends Item {
childrenSyms:ItemId[] ;
}
export type Items = {
[key:ItemId] :Item ;
} ;
export function isItem(arg: any):arg is Item{
return (
arg &&
typeof arg === "object" &&
typeof arg.itemType === "string"
) ;
}
export function isSym(arg: any):arg is Sym{
return (
arg &&
arg.options instanceof Array &&
isItem(arg)
) ;
}
export function isFlow(arg: any):arg is Flow{
return (
arg &&
arg.childrenSyms instanceof Array &&
isItem(arg)
) ;
}
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import Button from "components/util/Button";
import TextField from "components/util/TextField";
import {ChangeEvent, FC, SyntheticEvent} from "react" ;
import { useSelectItem } from "redux/app/hooks";
import { useItem, useItemOperations, } from "redux/items/hooks";
import { Sym } from "redux/items/types";
export interface SidebarProps{
}
const Sidebar :FC<SidebarProps> = ({})=>{
const {
selectItemId,
} = useSelectItem() ;
const selectItem = useItem(selectItemId??"") as Sym ;
const { setItem,removeItem } = useItemOperations();
const handleOptionChange = (idx:number, e :ChangeEvent<HTMLInputElement|HTMLTextAreaElement>)=>{
//オプションの更新処理
const newValue = e.target.value ;
const newOptions = [...selectItem.options] ;
newOptions[idx] = {
...newOptions[idx],
value:newValue,
} ;
const newItem = {
...selectItem,
options:newOptions,
} ;
setItem(selectItemId??"",newItem);
}
const handleRemoveSym = ()=>{
if(selectItemId){
//親から削除
//アイテム自体を削除
removeItem(selectItemId);
}
} ;
return (
<div>
{selectItemId && selectItem?
<>
<List>
{selectItem.options.map((option,idx)=>{
return (
<ListItem>
{option.name}
:
<TextField value={option.value} onChange={e=>handleOptionChange(idx,e)}/>
</ListItem>
) ;
})}
</List>
<Button onClick={handleRemoveSym}>
記号を削除する
</Button>
</>
:"アイテムが選択されていません"}
</div>
)
}
export default Sidebar ;