React MUI (Material-UI)
前々回に引き続き、Reactの話題です。Webプログラミングはたくさんの要素から構成され、しかもライブラリの流行り廃りが激しいため、その時その時で少しずつ切り出して理解する必要があると思っています。
今回はReactでよく使われるUIライブラリ、MUIを試して見ました。
MUI(旧 Material-UI)とは、Googleのマテリアルデザインの仕様を取り入れているUIコンポーネントライブラリで、 見栄えの良いUIを作成することができます。
UIの中でも比較的複雑なTreeViewを下記のサンプルを利用して実装して見ました。
https://mui.com/x/react-tree-view/
環境
node v20.4.0 / Mac(arm64)
インストール
npx create-react-app uidemo3 –template typescript
npm i @mui/material
npm i @mui/icons-material
npm i @mui/x-tree-view
npm i @react-spring/web
RichObjectTreeView.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
import * as React from 'react'; import Box from '@mui/material/Box'; //import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; //import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { TreeView } from '@mui/x-tree-view/TreeView'; //import { TreeItem } from '@mui/x-tree-view/TreeItem'; import { useSpring, animated } from '@react-spring/web'; import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; import { TransitionProps } from '@mui/material/transitions'; import Collapse from '@mui/material/Collapse'; import { TreeItem, TreeItemProps, treeItemClasses } from '@mui/x-tree-view/TreeItem'; import { alpha, styled } from '@mui/material/styles'; interface RenderTree { id: string; name: string; children?: readonly RenderTree[]; } const data: RenderTree = { id: 'root', name: 'Parent', children: [ { id: '1', name: 'Child - 1', }, { id: '2', name: 'Child - 2', children: [ { id: '21', name: 'Child - 21', children: [ { id: '211', name: 'Child - 211', }, ] }, { id: '22', name: 'Child - 22', }, ], }, { id: '3', name: 'Child - 3', }, ], }; function MinusSquare(props: SvgIconProps) { return ( <SvgIcon fontSize="inherit" style={{ width: 14, height: 14 }} {...props}> {/* tslint:disable-next-line: max-line-length */} <path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z" /> </SvgIcon> ); } function PlusSquare(props: SvgIconProps) { return ( <SvgIcon fontSize="inherit" style={{ width: 14, height: 14 }} {...props}> {/* tslint:disable-next-line: max-line-length */} <path d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z" /> </SvgIcon> ); } function CloseSquare(props: SvgIconProps) { return ( <SvgIcon className="close" fontSize="inherit" style={{ width: 14, height: 14 }} {...props} > {/* tslint:disable-next-line: max-line-length */} <path d="M17.485 17.512q-.281.281-.682.281t-.696-.268l-4.12-4.147-4.12 4.147q-.294.268-.696.268t-.682-.281-.281-.682.294-.669l4.12-4.147-4.12-4.147q-.294-.268-.294-.669t.281-.682.682-.281.696 .268l4.12 4.147 4.12-4.147q.294-.268.696-.268t.682.281 .281.669-.294.682l-4.12 4.147 4.12 4.147q.294.268 .294.669t-.281.682zM22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0z" /> </SvgIcon> ); } function TransitionComponent(props: TransitionProps) { const style = useSpring({ to: { opacity: props.in ? 1 : 0, transform: `translate3d(${props.in ? 0 : 20}px,0,0)`, }, }); return ( <animated.div style={style}> <Collapse {...props} /> </animated.div> ); } const CustomTreeItem = React.forwardRef( (props: TreeItemProps, ref: React.Ref<HTMLLIElement>) => ( <TreeItem {...props} TransitionComponent={TransitionComponent} ref={ref} /> ), ); const StyledTreeItem = styled(CustomTreeItem)(({ theme }) => ({ [`& .${treeItemClasses.iconContainer}`]: { '& .close': { opacity: 0.3, }, }, [`& .${treeItemClasses.group}`]: { marginLeft: 15, paddingLeft: 18, borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`, }, })); export default function RichObjectTreeView() { const renderTree = (nodes: RenderTree) => ( <StyledTreeItem key={nodes.id} nodeId={nodes.id} label={nodes.name}> {Array.isArray(nodes.children) ? nodes.children.map((node) => renderTree(node)) : null} item:{nodes.name} <button>Push</button> </StyledTreeItem> ); return ( <Box sx={{ minHeight: 270, flexGrow: 1, maxWidth: 300 }}> <TreeView aria-label="rich object" //defaultCollapseIcon={<ExpandMoreIcon />} defaultCollapseIcon={<MinusSquare />} defaultExpanded={['root']} //defaultExpandIcon={<ChevronRightIcon />} defaultExpandIcon={<PlusSquare />} defaultEndIcon={<CloseSquare />} sx={{ overflowX: 'hidden' }} > {renderTree(data)} </TreeView> </Box> ); } |
上記、データマッピングをしているRichObjectTreeViewサンプルに、デザインをカスタマイズしているCustomizedTreeViewを適用しました。
index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import RichObjectTreeView from './RichObjectTreeView'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <React.StrictMode> <RichObjectTreeView /> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); |
実行
npm start
ビルドも一応確認
npm run-script build
npx serve build
実行画面は同じ
ここで基本に返って、 tsx(typescript & jsx) が、どのように変換されるか確認して見ました。というのはブラウザもnodejsも、JavaScriptしか実行できないにも関わらず、拡張機能があたかも標準のように見えてしまうことをあらためる意味もあります。
sample.tsx
1 2 3 |
function view() { const element = <h1 class="greeting">Hello, world!</h1> } |
tsconfig.json
1 2 3 4 5 |
{ "compilerOptions": { "jsx": "react", } } |
インストール
npm install typescript
npm install react
tsx -> js変換
npx tsc –jsx react-jsx sample.tsx
sample.js
1 2 3 4 5 6 |
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var jsx_runtime_1 = require("react/jsx-runtime"); function view() { var element = (0, jsx_runtime_1.jsx)("h1", { class: "greeting", children: "Hello, world!" }); } |
下記のように編集して実行
sample2.js
1 2 3 4 5 6 7 8 |
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var jsx_runtime_1 = require("react/jsx-runtime"); function view() { var element = (0, jsx_runtime_1.jsx)("h1", { class: "greeting", children: "Hello, world!" }); console.log(element); } view(); |
% node sample2.js
{
‘$$typeof’: Symbol(react.element),
type: ‘h1’,
key: null,
ref: null,
props: { class: ‘greeting’, children: ‘Hello, world!’ },
_owner: null,
_store: {}
}
クライアントサイド、サーバサイド両方で同じ言語が使えることから、nodejsがカバーする機能もどんどん複雑化しつつあります。単純な上記仕組みを頭に入れておくことは必要と考えます。
参考
Category: 未分類