├── bin ├── _shim.js ├── build.js └── dev.js ├── prettier.config.js ├── src ├── index.tsx ├── App.module.css ├── components │ ├── Text │ │ └── index.tsx │ ├── DefaultConfigure │ │ ├── index.module.css │ │ └── index.tsx │ ├── FlexContainerConfigure │ │ ├── index.module.css │ │ └── index.tsx │ ├── MaterialPanel │ │ ├── index.module.css │ │ ├── presets.ts │ │ └── index.tsx │ ├── RootContainer │ │ ├── index.module.css │ │ └── index.tsx │ ├── FlexContainer │ │ ├── index.module.css │ │ └── index.tsx │ ├── NodeWrapper │ │ ├── index.module.css │ │ └── index.tsx │ └── core │ │ ├── DynamicRoot │ │ └── index.tsx │ │ └── DynamicNode │ │ └── index.tsx ├── types │ └── index.ts ├── constants │ └── index.ts ├── App.tsx ├── utils │ └── index.ts └── mockData.ts ├── .eslintrc.js ├── public └── index.html ├── tsconfig.json ├── .editorconfig ├── .github └── workflows │ └── pipeline.yml ├── README.md ├── package.json ├── .gitignore └── CHANGELOG.md /bin/_shim.js: -------------------------------------------------------------------------------- 1 | export * as React from 'react' 2 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | } 5 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import App from './App' 3 | 4 | ReactDOM.render(, document.querySelector('#app')) 5 | -------------------------------------------------------------------------------- /src/App.module.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | width: 100%; 4 | } 5 | 6 | .content { 7 | flex: 1; 8 | } 9 | 10 | .panel { 11 | width: 210px; 12 | margin-right: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Text/index.tsx: -------------------------------------------------------------------------------- 1 | export interface TextProps { 2 | content?: string 3 | } 4 | 5 | const Text: React.FC = ({ content }) => { 6 | return
{content || 'Text'}
7 | } 8 | 9 | export default Text 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app', 'react-app/jest'], 3 | plugins: ['prettier'], 4 | rules: { 5 | 'prettier/prettier': 'warn', 6 | 'react/react-in-jsx-scope': 'off', 7 | }, 8 | ignorePatterns: ['public/', 'dist/', 'storybook-static/'], 9 | } 10 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | rc-dynamic 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "jsx": "react-jsx", 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "moduleResolution": "node", 16 | "module": "esnext", 17 | "strict": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/DefaultConfigure/index.module.css: -------------------------------------------------------------------------------- 1 | .configure { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | height: 16px; 6 | min-width: 16px; 7 | } 8 | 9 | .action { 10 | height: 16px; 11 | overflow: hidden; 12 | color: white; 13 | background-color: #ab47bc; 14 | cursor: pointer; 15 | user-select: none; 16 | padding: 0 4px; 17 | text-align: center; 18 | font-size: 12px; 19 | float: right; 20 | margin-left: 1px; 21 | line-height: 16px; 22 | text-transform: uppercase; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/FlexContainerConfigure/index.module.css: -------------------------------------------------------------------------------- 1 | .configure { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | height: 16px; 6 | min-width: 16px; 7 | } 8 | 9 | .action { 10 | height: 16px; 11 | overflow: hidden; 12 | color: white; 13 | background-color: #ab47bc; 14 | cursor: pointer; 15 | user-select: none; 16 | padding: 0 4px; 17 | text-align: center; 18 | font-size: 12px; 19 | float: right; 20 | margin-left: 1px; 21 | line-height: 16px; 22 | text-transform: uppercase; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/MaterialPanel/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | /* only edit */ 6 | padding: 5px; 7 | } 8 | 9 | .item { 10 | padding: 5px; 11 | 12 | background-color: #e6f4fa; 13 | margin: 5px; 14 | outline-offset: -1px; 15 | outline: 1px solid #2196f3; 16 | } 17 | 18 | .handle { 19 | /* NOTHING */ 20 | } 21 | 22 | /* Other */ 23 | 24 | .divider { 25 | height: 1px; 26 | background-color: #2196f3; 27 | } 28 | 29 | .controls { 30 | margin: 5px 0; 31 | padding: 5px 10px; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/RootContainer/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | } 4 | 5 | .empty { 6 | /* NOTHING */ 7 | } 8 | 9 | .creative { 10 | /* NOTHING */ 11 | } 12 | 13 | .rootContainer { 14 | position: relative; 15 | } 16 | 17 | .rootItemHandle { 18 | /* NOTHING */ 19 | } 20 | 21 | /* compose */ 22 | 23 | .container.creative > .rootContainer { 24 | display: flex; 25 | flex-direction: column; 26 | overflow: hidden; 27 | } 28 | 29 | .container.creative.empty > .rootContainer { 30 | outline: 1px dashed #2196f355; 31 | min-height: 400px; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/FlexContainer/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | /* NOTHING */ 3 | } 4 | 5 | .empty { 6 | /* NOTHING */ 7 | } 8 | 9 | .creative { 10 | /* NOTHING */ 11 | } 12 | 13 | .fixed { 14 | /* NOTHING */ 15 | } 16 | 17 | .flexContainer { 18 | display: flex; 19 | } 20 | .horizontal > .flexContainer { flex-direction: row; } 21 | .vertical > .flexContainer { flex-direction: column; } 22 | 23 | .flexItem { 24 | flex: 1; 25 | } 26 | 27 | .flexItemHandle { 28 | /* NOTHING */ 29 | } 30 | 31 | /* compose */ 32 | 33 | .container.creative > .flexContainer { 34 | min-height: 50px; 35 | } 36 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { componentMap } from '../constants' 3 | 4 | export type ComponentName = keyof typeof componentMap 5 | 6 | export interface DynamicNodeMeta { 7 | component: ComponentName 8 | config?: Config 9 | children?: DynamicNodeMeta[] 10 | __uid?: string 11 | } 12 | 13 | export interface DynamicRootMeta { 14 | version: string 15 | children?: DynamicNodeMeta[] 16 | } 17 | 18 | export interface ConfigureProps { 19 | children: (props: ChildrenProps, configureNode: ReactNode) => ReactNode 20 | } 21 | 22 | export interface SortableNode { 23 | id: string 24 | meta: DynamicNodeMeta 25 | clone?: () => SortableNode 26 | } 27 | -------------------------------------------------------------------------------- /src/components/MaterialPanel/presets.ts: -------------------------------------------------------------------------------- 1 | import { DynamicNodeMeta } from '../../types' 2 | 3 | const presets: { label: string; clone: () => DynamicNodeMeta }[] = [ 4 | { 5 | label: 'FlexContainer | Vertical', 6 | clone: () => ({ 7 | component: 'FlexContainer', 8 | config: { 9 | direction: 'vertical', 10 | }, 11 | }), 12 | }, 13 | { 14 | label: 'FlexContainer | Horizontal', 15 | clone: () => ({ 16 | component: 'FlexContainer', 17 | config: { 18 | direction: 'horizontal', 19 | }, 20 | }), 21 | }, 22 | { 23 | label: 'Text | Hello', 24 | clone: () => ({ component: 'Text', config: { content: 'Hello' } }), 25 | }, 26 | { 27 | label: 'Text | 中文', 28 | clone: () => ({ component: 'Text', config: { content: '中文' } }), 29 | }, 30 | ] 31 | 32 | export default presets 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | max_line_length = 120 12 | 13 | [*.{ts, tsx}] 14 | ij_typescript_enforce_trailing_comma = keep 15 | ij_typescript_use_double_quotes = false 16 | ij_typescript_force_quote_style = true 17 | ij_typescript_align_imports = false 18 | ij_typescript_align_multiline_ternary_operation = false 19 | ij_typescript_align_multiline_parameters_in_calls = false 20 | ij_typescript_align_multiline_parameters = false 21 | ij_typescript_align_multiline_chained_methods = false 22 | ij_typescript_else_on_new_line = false 23 | ij_typescript_catch_on_new_line = false 24 | ij_typescript_spaces_within_interpolation_expressions = false 25 | 26 | [*.md] 27 | max_line_length = 0 28 | trim_trailing_whitespace = false 29 | 30 | [COMMIT_EDITMSG] 31 | max_line_length = 0 32 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ConfigureProps } from '../types' 3 | 4 | import RootContainer from '../components/RootContainer' 5 | import FlexContainer from '../components/FlexContainer' 6 | import Text from '../components/Text' 7 | 8 | import DefaultConfigure from '../components/DefaultConfigure' 9 | import FlexContainerConfigure from '../components/FlexContainerConfigure' 10 | 11 | export enum DynamicMode { 12 | SURVIVAL = 'SURVIVAL', 13 | CREATIVE = 'CREATIVE', 14 | } 15 | 16 | export enum SortableGroup { 17 | MaterialPanel = 'MaterialPanel', 18 | FlexContainer = 'FlexContainer', 19 | RootContainer = 'RootContainer', 20 | } 21 | 22 | export const componentMap = { 23 | RootContainer, 24 | FlexContainer, 25 | Text, 26 | } 27 | 28 | export const defaultConfigure = DefaultConfigure 29 | export const configureMap: Record>> = { 30 | FlexContainer: FlexContainerConfigure, 31 | } 32 | -------------------------------------------------------------------------------- /src/components/DefaultConfigure/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import { ConfigureProps } from '../../types' 3 | import { DynamicNodeContext } from '../core/DynamicNode' 4 | 5 | import styles from './index.module.css' 6 | 7 | const DefaultConfigure: React.FC> = ({ children }) => { 8 | const nodeContext = useContext(DynamicNodeContext) 9 | 10 | const props = useMemo(() => nodeContext.meta?.config ?? {}, [nodeContext.meta?.config]) 11 | 12 | return ( 13 | <> 14 | {children( 15 | props, 16 | <> 17 | {nodeContext.isActive && ( 18 |
{ 21 | event.stopPropagation() 22 | }} 23 | > 24 |
nodeContext.remove()}> 25 | x 26 |
27 |
28 | )} 29 | 30 | )} 31 | 32 | ) 33 | } 34 | 35 | export default DefaultConfigure 36 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react' 2 | import DynamicRoot from './components/core/DynamicRoot' 3 | import MaterialPanel from './components/MaterialPanel' 4 | import RootContainer from './components/RootContainer' 5 | import { DynamicMode } from './constants' 6 | import { mockMetaTree } from './mockData' 7 | import { DynamicRootMeta } from './types' 8 | import { indexRootMeta } from './utils' 9 | 10 | import styles from './App.module.css' 11 | 12 | const App = () => { 13 | const indexedMetaTree = useMemo(() => indexRootMeta(mockMetaTree), []) 14 | const [metaTree, setMetaTree] = useState(indexedMetaTree) 15 | const [mode, setMode] = useState(DynamicMode.CREATIVE) 16 | 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | ) 29 | } 30 | 31 | export default App 32 | -------------------------------------------------------------------------------- /src/components/NodeWrapper/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | 4 | /* effect */ 5 | outline-offset: -1px; 6 | outline: 0 dashed transparent; 7 | transition-property: outline, padding, margin; 8 | transition-duration: .28s; 9 | } 10 | 11 | .creative { 12 | /* NOTHING */ 13 | } 14 | 15 | .active { 16 | /* NOTHING */ 17 | } 18 | 19 | .ghost { 20 | /* NOTHING */ 21 | } 22 | 23 | .nested { 24 | /* NOTHING */ 25 | } 26 | 27 | .fixed { 28 | /* NOTHING */ 29 | } 30 | 31 | /* compose */ 32 | 33 | .ghost, 34 | :global(.sortable-chosen) { 35 | user-select: none; 36 | background-color: #e6f4fa; 37 | 38 | /* effect */ 39 | transition: background-color .28s; 40 | } 41 | 42 | .container.creative { 43 | padding: 5px; 44 | margin: 5px; 45 | outline: 1px solid #2196f3; 46 | } 47 | 48 | .container.creative.nested { 49 | outline-style: dashed; 50 | } 51 | 52 | .container.creative.active { 53 | background-color: #e6e6fa; 54 | outline-color: #ab47bc; 55 | } 56 | 57 | .container.creative.fixed { 58 | padding: 0; 59 | margin: 0; 60 | outline-style: none; 61 | } 62 | 63 | .container.creative.parentFixed { 64 | outline-color: #2196f355; 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | check-quality-and-build: 10 | runs-on: ubuntu-latest 11 | name: Check quality and build 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: bahmutov/npm-install@v1 15 | - run: yarn --frozen-lockfile 16 | 17 | - name: Check type 18 | run: yarn type 19 | 20 | - name: Check code 21 | run: yarn lint:script 22 | 23 | - name: Build 24 | run: yarn build 25 | 26 | - uses: actions/upload-artifact@v2 27 | with: 28 | name: dist 29 | path: dist/ 30 | 31 | deploy-dist-to-ftp: 32 | needs: check-quality-and-build 33 | runs-on: ubuntu-latest 34 | if: contains(github.event.head_commit.message, 'chore(release)') 35 | name: Deploy dist to FTP 36 | steps: 37 | - uses: actions/download-artifact@v2 38 | with: 39 | name: dist 40 | path: dist/ 41 | 42 | - name: Deploy to ftp 43 | uses: SamKirkland/FTP-Deploy-Action@2.0.0 44 | env: 45 | FTP_SERVER: ${{ secrets.FTP_SERVER }} 46 | FTP_USERNAME: ${{ secrets.FTP_USERNAME }} 47 | FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} 48 | METHOD: sftp 49 | PORT: ${{ secrets.FTP_PORT }} 50 | LOCAL_DIR: dist 51 | REMOTE_DIR: /www/rc-dynamic/preview 52 | ARGS: --delete --verbose --parallel=100 53 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const gzipme = require('gzipme') 4 | const { build } = require('esbuild') 5 | const cssModulesPlugin = require('esbuild-css-modules-plugin') 6 | 7 | process.on('unhandledRejection', (error) => { 8 | console.error('unhandledRejection:', error) 9 | process.exit(1) 10 | }) 11 | ;(async () => { 12 | // Copy public assets 13 | fs.rmSync('dist', { recursive: true, force: true }) 14 | fs.mkdirSync('dist') 15 | fs.copyFileSync('public/index.html', 'dist/index.html') // TODO support multiple files 16 | 17 | // `esbuild` bundler for JavaScript / TypeScript. 18 | await build({ 19 | // Bundles JavaScript. 20 | bundle: true, 21 | // Defines env variables for bundled JavaScript; here `process.env.NODE_ENV` 22 | // is propagated with a fallback. 23 | define: { 'process.env.NODE_ENV': JSON.stringify('production') }, // TODO use dot env ? 24 | // Bundles JavaScript from (see `outfile`). 25 | entryPoints: ['src/index.tsx'], 26 | // Removes whitespace. 27 | minify: true, 28 | // Bundles JavaScript to (see `entryPoints`). 29 | outdir: 'dist', 30 | // React jsx runtime 31 | inject: [path.join(__dirname, './_shim.js')], 32 | // Plugins 33 | plugins: [ 34 | cssModulesPlugin({ 35 | inject: false, 36 | v2: true, 37 | }), 38 | ], 39 | }) 40 | 41 | // create gz file 42 | gzipme('dist/index.js', { mode: 'best' }) 43 | })() 44 | -------------------------------------------------------------------------------- /src/components/core/DynamicRoot/index.tsx: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import React, { createContext, ReactNode, useEffect, useState } from 'react' 3 | import { DynamicRootMeta } from '../../../types' 4 | import { DynamicMode } from '../../../constants' 5 | 6 | export interface DynamicRootProps { 7 | value: DynamicRootMeta 8 | onChange: (value: DynamicRootMeta) => void 9 | mode: DynamicMode 10 | children: ReactNode 11 | } 12 | 13 | export const DynamicRootContext = createContext<{ 14 | meta?: DynamicRootMeta 15 | updateMeta?: (callback: (mete: DynamicRootMeta) => void) => void 16 | mode: DynamicMode 17 | activeId: string | null 18 | setActiveId: React.Dispatch> 19 | }>({ 20 | mode: DynamicMode.SURVIVAL, 21 | activeId: null, 22 | setActiveId: () => {}, 23 | }) 24 | 25 | const DynamicRoot: React.FC = ({ value, onChange, mode, children }) => { 26 | const meta = value 27 | 28 | const [activeId, setActiveId] = useState(null) 29 | 30 | useEffect(() => { 31 | if (mode === DynamicMode.SURVIVAL) { 32 | setActiveId(null) 33 | } 34 | }, [mode]) 35 | 36 | const updateMeta = (callback: (mete: DynamicRootMeta) => void) => { 37 | onChange( 38 | produce(meta, (draft) => { 39 | callback(draft) 40 | }) 41 | ) 42 | } 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ) 49 | } 50 | 51 | export default DynamicRoot 52 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const { build } = require('esbuild') 4 | const liveServer = require('live-server') 5 | const cssModulesPlugin = require('esbuild-css-modules-plugin') 6 | 7 | process.on('unhandledRejection', (error) => { 8 | console.error('unhandledRejection:', error) 9 | process.exit(1) 10 | }) 11 | ;(async () => { 12 | // Copy public assets 13 | fs.rmSync('dist', { recursive: true, force: true }) 14 | fs.mkdirSync('dist') 15 | fs.copyFileSync('public/index.html', 'dist/index.html') // TODO support multiple files 16 | 17 | liveServer.start({ 18 | // Opens the local server on start. 19 | open: true, 20 | // Uses `PORT=...` or 8080 as a fallback. 21 | port: +process.env.PORT || 8080, 22 | // Uses `public` as the local server folder. 23 | root: 'dist', 24 | }) 25 | 26 | // `esbuild` bundler for JavaScript / TypeScript. 27 | await build({ 28 | // Bundles JavaScript. 29 | bundle: true, 30 | // Defines env variables for bundled JavaScript; here `process.env.NODE_ENV` 31 | // is propagated with a fallback. 32 | define: { 'process.env.NODE_ENV': JSON.stringify('development') }, 33 | // Bundles JavaScript from (see `outfile`). 34 | entryPoints: ['src/index.tsx'], 35 | // Uses incremental compilation (see `chokidar.on`). 36 | incremental: true, 37 | // Removes whitespace, etc. depending on `NODE_ENV=...`. 38 | minify: false, 39 | // Bundles JavaScript to (see `entryPoints`). 40 | outdir: 'dist', 41 | // React jsx runtime 42 | inject: [path.join(__dirname, './_shim.js')], 43 | // Watch mode 44 | watch: true, 45 | // Plugins 46 | plugins: [ 47 | cssModulesPlugin({ 48 | inject: false, 49 | v2: true, 50 | }), 51 | ], 52 | }) 53 | })() 54 | -------------------------------------------------------------------------------- /src/components/NodeWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import { useContext } from 'react' 3 | import { DynamicMode } from '../../constants' 4 | import { DynamicNodeContext } from '../core/DynamicNode' 5 | import { DynamicRootContext } from '../core/DynamicRoot' 6 | 7 | import styles from './index.module.css' 8 | 9 | interface Props { 10 | className?: string 11 | handleClassName: string 12 | } 13 | 14 | const CreativeWrapper: React.FC = ({ className, handleClassName }) => { 15 | const nodeContext = useContext(DynamicNodeContext) 16 | const rootContext = useContext(DynamicRootContext) 17 | 18 | // TODO to global 19 | const isNested = nodeContext.meta?.component === 'FlexContainer' 20 | const isParentFixed = nodeContext.parentMeta?.config?.fixedChildren 21 | const isFixed = nodeContext.meta?.config?.fixedChildren 22 | 23 | const creative = rootContext.mode === DynamicMode.CREATIVE 24 | 25 | const { isActive, Configure, Component } = nodeContext 26 | 27 | return ( 28 | 29 | {(props, configureNode) => ( 30 |
{ 41 | if (!creative || isParentFixed) return 42 | 43 | event.stopPropagation() 44 | 45 | nodeContext.setActive(!isActive) 46 | }} 47 | > 48 | 49 | {configureNode} 50 |
51 | )} 52 |
53 | ) 54 | } 55 | 56 | export const ghostClass = styles.ghost 57 | 58 | export default CreativeWrapper 59 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { DynamicNodeMeta, DynamicRootMeta } from '../types' 2 | 3 | export const uid = (() => { 4 | let i = 0 5 | return () => String(i++) 6 | })() 7 | 8 | export const indexChildrenMeta = (children?: DynamicNodeMeta[]): DynamicNodeMeta[] | undefined => 9 | children?.map((child) => ({ 10 | ...child, 11 | __uid: uid(), 12 | children: indexChildrenMeta(child.children), 13 | })) 14 | 15 | export const indexNodeMeta = (nodeMeta: DynamicNodeMeta): DynamicNodeMeta => 16 | [nodeMeta].map((child) => ({ 17 | ...child, 18 | __uid: uid(), 19 | children: indexChildrenMeta(child.children), 20 | }))[0] 21 | 22 | export const cloneNodeMeta = (nodeMeta: DynamicNodeMeta): DynamicNodeMeta => 23 | [JSON.parse(JSON.stringify(nodeMeta))].map((child) => ({ 24 | ...child, 25 | __uid: uid(), 26 | children: indexChildrenMeta(child.children), 27 | }))[0] 28 | 29 | export const indexRootMeta = (meta: DynamicRootMeta): DynamicRootMeta => { 30 | return { 31 | ...meta, 32 | children: indexChildrenMeta(meta.children), 33 | } 34 | } 35 | 36 | export const findCurrentMeta = (rootMeta: DynamicRootMeta, indexPath: number[]): DynamicNodeMeta | undefined => { 37 | const { last } = indexPath.reduce( 38 | ({ next = [] }, index) => ({ 39 | last: next[index], 40 | next: next[index]?.children ?? [], 41 | }), 42 | { 43 | next: rootMeta.children, 44 | last: undefined, 45 | } as { next?: DynamicNodeMeta[]; last?: DynamicNodeMeta } 46 | ) 47 | 48 | return last 49 | } 50 | 51 | export const findParentMeta = (rootMeta: DynamicRootMeta, indexPath: number[]): DynamicNodeMeta | undefined => { 52 | const last = findCurrentMeta(rootMeta, indexPath.slice(0, -1)) 53 | return last 54 | } 55 | 56 | export const hashChildrenIds = (children: DynamicNodeMeta[] = []) => { 57 | return children.map((i) => i.__uid).join('__') 58 | } 59 | 60 | export const equalChildrenIds = (A: DynamicNodeMeta[] = [], B: DynamicNodeMeta[] = []) => { 61 | return hashChildrenIds(A) === hashChildrenIds(B) 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-dynamic 2 | 3 | React component Drag and Drop and Config Driven UI demo. 4 | 5 | Preview here 👉 https://rc-dynamic.pages.dev 6 | 7 | ## Main integrations 8 | 9 | - [esbuild](https://esbuild.github.io/) 10 | - [React v17](https://reactjs.org/blog/2020/10/20/react-v17.html) 11 | - [TypeScript 4.1+](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#jsx-factories) 12 | 13 | ## Getting Started 14 | 15 | ``` bash 16 | # install dependencies 17 | yarn install 18 | 19 | # development mode 20 | yarn serve 21 | 22 | # build production 23 | yarn build 24 | 25 | # check code styles 26 | yarn lint:script 27 | 28 | # check types 29 | yarn type 30 | ``` 31 | 32 | ## Components 33 | 34 | - [Container](./src/components/basic/Container/index.tsx) 35 | - [Text](./src/components/basic/Text/index.tsx) 36 | - [LineChart](./src/components/basic/LineChart/index.tsx) 37 | - [GaugeChart](./src/components/basic/GaugeChart/index.tsx) 38 | - [SunburstChart](./src/components/basic/SunburstChart/index.tsx) 39 | 40 | ## DnD & Sortable library 41 | 42 | - 😅 [atlassian/react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd) 43 | - [#1001](https://github.com/atlassian/react-beautiful-dnd/issues/1001 ) 44 | - ⌛️ [SortableJS/react-sortablejs](https://github.com/SortableJS/react-sortablejs) 45 | - 🤔 [clauderic/dnd-kit](https://github.com/clauderic/dnd-kit) 46 | - 🤔 [clauderic/react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) 47 | - 🤔 [frontend-collective/react-sortable-tree](https://github.com/frontend-collective/react-sortable-tree) 48 | 49 | ## Refer 50 | 51 | - 🌟 https://codesandbox.io/s/react-sortable-js-nested-tu8nn 52 | - 🌟 https://codesandbox.io/s/hook-compose-component-hrglp?file=/src/App.tsx 53 | - https://www.storyblok.com/tp/react-dynamic-component-from-json 54 | - https://github.com/zaydek/esbuild-hot-reload 55 | - https://techmusings.dev/ 56 | - https://github.com/chriskitson/react-drag-drop-layout-builder 57 | - https://medium.com/javascript-in-plain-english/build-a-drag-and-drop-dnd-layout-builder-with-react-and-immutablejs-78a0797259a6 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-dynamic", 3 | "version": "1.0.0-0", 4 | "description": "react component dynamic build and render", 5 | "license": "MIT", 6 | "private": false, 7 | "scripts": { 8 | "serve": "node bin/dev.js", 9 | "build": "node bin/build.js", 10 | "lint:script": "eslint . --fix", 11 | "type": "tsc", 12 | "release": "standard-version" 13 | }, 14 | "devDependencies": { 15 | "@types/classnames": "^2.3.1", 16 | "@types/css-modules": "^1.0.2", 17 | "@types/react": "^17.0.26", 18 | "@types/react-dom": "^17.0.9", 19 | "@types/sortablejs": "^1.10.7", 20 | "@typescript-eslint/eslint-plugin": "^4.32.0", 21 | "@typescript-eslint/parser": "^4.32.0", 22 | "babel-eslint": "^10.1.0", 23 | "cz-conventional-changelog": "3.3.0", 24 | "esbuild": "^0.13.3", 25 | "esbuild-css-modules-plugin": "^2.0.9", 26 | "eslint": "^7.32.0", 27 | "eslint-config-react-app": "^6.0.0", 28 | "eslint-config-standard": "^16.0.3", 29 | "eslint-plugin-flowtype": "^6.1.0", 30 | "eslint-plugin-import": "^2.24.2", 31 | "eslint-plugin-jest": "^24.5.0", 32 | "eslint-plugin-jsx-a11y": "^6.4.1", 33 | "eslint-plugin-node": "^11.1.0", 34 | "eslint-plugin-prettier": "^4.0.0", 35 | "eslint-plugin-promise": "^5.1.0", 36 | "eslint-plugin-react": "^7.26.1", 37 | "eslint-plugin-react-hooks": "^4.2.0", 38 | "eslint-plugin-testing-library": "^4.12.4", 39 | "gzipme": "^1.0.0", 40 | "jest": "^27.2.4", 41 | "live-server": "^1.2.1", 42 | "prettier": "^2.4.1", 43 | "standard-version": "^9.3.1", 44 | "typescript": "^4.4.3" 45 | }, 46 | "dependencies": { 47 | "classnames": "^2.3.1", 48 | "echarts": "^5.2.1", 49 | "immer": "^9.0.6", 50 | "react": "^17.0.2", 51 | "react-dom": "^17.0.2", 52 | "react-sortablejs": "^6.0.0", 53 | "react-use": "^17.3.1", 54 | "sortablejs": "^1.14.0" 55 | }, 56 | "config": { 57 | "commitizen": { 58 | "path": "./node_modules/cz-conventional-changelog" 59 | } 60 | }, 61 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 62 | } 63 | -------------------------------------------------------------------------------- /src/components/MaterialPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import classNames from 'classnames' 3 | import { ReactSortable } from 'react-sortablejs' 4 | import { DynamicMode, SortableGroup } from '../../constants' 5 | import { SortableNode } from '../../types' 6 | import { indexNodeMeta } from '../../utils' 7 | import presets from './presets' 8 | 9 | import styles from './index.module.css' 10 | 11 | interface MaterialPanelProps { 12 | mode: DynamicMode 13 | setMode: (mode: DynamicMode) => void 14 | } 15 | 16 | const MaterialPanel: React.FC = ({ mode, setMode }) => { 17 | // * for control draggable 18 | const handleClassName = useMemo(() => (mode === DynamicMode.CREATIVE ? styles.handle : ''), [mode]) 19 | 20 | const list: SortableNode[] = useMemo( 21 | () => 22 | presets.map((i) => { 23 | const meta = indexNodeMeta(i.clone()) 24 | return { 25 | id: meta.__uid!, 26 | meta, 27 | clone: (): SortableNode => { 28 | const nestedMeta = indexNodeMeta(i.clone()) 29 | return { id: nestedMeta.__uid!, meta: indexNodeMeta(i.clone()) } 30 | }, 31 | } 32 | }), 33 | [] 34 | ) 35 | 36 | return ( 37 |
38 |
39 | {mode === DynamicMode.SURVIVAL && ( 40 | 43 | )} 44 | {mode === DynamicMode.CREATIVE && ( 45 | 48 | )} 49 |
50 |
51 | 52 | list={list} 53 | setList={() => {}} 54 | animation={150} 55 | sort={false} 56 | swapThreshold={1} 57 | group={{ name: SortableGroup.MaterialPanel, pull: 'clone', put: false }} 58 | className={styles.container} 59 | handle={'.' + handleClassName} 60 | > 61 | {presets.map((i) => ( 62 |
63 | {i.label} 64 |
65 | ))} 66 | 67 |
68 | ) 69 | } 70 | 71 | export default MaterialPanel 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # dev file 119 | public/index.js 120 | 121 | # storybook build result 122 | storybook-static 123 | -------------------------------------------------------------------------------- /src/components/RootContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import classNames from 'classnames' 3 | import { ReactSortable, SortableEvent } from 'react-sortablejs' 4 | import { DynamicMode, SortableGroup } from '../../constants' 5 | import { DynamicRootContext } from '../core/DynamicRoot' 6 | import NodeWrapper, { ghostClass } from '../NodeWrapper' 7 | import { equalChildrenIds } from '../../utils' 8 | import DynamicNode from '../core/DynamicNode' 9 | import { SortableNode } from '../../types' 10 | 11 | import styles from './index.module.css' 12 | 13 | const RootContainer: React.FC = () => { 14 | const rootContext = useContext(DynamicRootContext) 15 | 16 | const creative = rootContext.mode === DynamicMode.CREATIVE 17 | const children = useMemo(() => rootContext.meta?.children ?? [], [rootContext.meta?.children]) 18 | const empty = !children.length 19 | 20 | const list: SortableNode[] = useMemo(() => children.map((i) => ({ id: i.__uid!, meta: i })), [children]) 21 | 22 | const setList = (newList: SortableNode[], event?: SortableEvent) => { 23 | // ! Avoid frequent trigger 24 | if (event) { 25 | const previousChildren = children 26 | 27 | const currentChildren = newList 28 | // ! Avoid dirty item. eg: [undefined, { selected: true }] 29 | .filter(Boolean) 30 | .filter((i) => i.id) 31 | .map((i) => i.meta) 32 | 33 | // ! Avoid noisy item prop change. eg: [{ id: ..., raw: ..., chosen: false }] 34 | if (!equalChildrenIds(previousChildren, currentChildren)) { 35 | rootContext.updateMeta?.((rootMeta) => { 36 | rootMeta.children = currentChildren 37 | }) 38 | } 39 | } 40 | } 41 | 42 | return ( 43 |
49 | 50 | handle={'.' + styles.rootItemHandle} 51 | list={list} 52 | setList={setList} 53 | animation={150} 54 | swapThreshold={0.5} 55 | // ! ReactSortable.group not use current value 56 | group={{ name: SortableGroup.RootContainer, put: true }} 57 | className={styles.rootContainer} 58 | ghostClass={ghostClass} 59 | clone={(item) => (item.clone ? item.clone() : item)} 60 | > 61 | {children.map((meta, index) => ( 62 | 63 | 64 | 65 | ))} 66 | 67 |
68 | ) 69 | } 70 | 71 | export default RootContainer 72 | -------------------------------------------------------------------------------- /src/mockData.ts: -------------------------------------------------------------------------------- 1 | import { DynamicRootMeta } from './types' 2 | 3 | export const mockMetaTree: DynamicRootMeta = { 4 | version: '0', 5 | children: [ 6 | { 7 | component: 'FlexContainer', 8 | config: { root: true }, 9 | children: [ 10 | { 11 | component: 'FlexContainer', 12 | config: { direction: 'vertical', fixedChildren: true }, 13 | __uid: '37', 14 | children: [ 15 | { 16 | component: 'FlexContainer', 17 | config: { direction: 'horizontal' }, 18 | __uid: '38', 19 | children: [{ component: 'Text', config: { content: 'Hello' }, __uid: '41' }], 20 | }, 21 | { 22 | component: 'FlexContainer', 23 | config: { direction: 'horizontal' }, 24 | __uid: '39', 25 | children: [ 26 | { component: 'Text', config: { content: 'Hello' }, __uid: '43' }, 27 | { component: 'Text', config: { content: 'Hello' }, __uid: '42' }, 28 | ], 29 | }, 30 | { 31 | component: 'FlexContainer', 32 | config: { direction: 'horizontal', fixedChildren: true }, 33 | __uid: '40', 34 | children: [ 35 | { 36 | component: 'FlexContainer', 37 | config: { direction: 'vertical' }, 38 | __uid: '52', 39 | children: [{ component: 'Text', config: { content: '中文' }, __uid: '46' }], 40 | }, 41 | { 42 | component: 'FlexContainer', 43 | config: { direction: 'vertical' }, 44 | __uid: '55', 45 | children: [{ component: 'Text', config: { content: '中文' }, __uid: '56' }], 46 | }, 47 | { 48 | component: 'FlexContainer', 49 | config: { direction: 'vertical' }, 50 | __uid: '53', 51 | children: [{ component: 'Text', config: { content: '中文' }, __uid: '54' }], 52 | }, 53 | ], 54 | }, 55 | ], 56 | }, 57 | { 58 | component: 'FlexContainer', 59 | config: { direction: 'horizontal' }, 60 | __uid: '47', 61 | children: [ 62 | { component: 'Text', config: { content: '中文' }, __uid: '57' }, 63 | { component: 'Text', config: { content: 'Hello' }, __uid: '51' }, 64 | { component: 'Text', config: { content: '中文' }, __uid: '49' }, 65 | { component: 'Text', config: { content: '中文' }, __uid: '50' }, 66 | ], 67 | }, 68 | ], 69 | __uid: '0', 70 | }, 71 | ], 72 | } 73 | -------------------------------------------------------------------------------- /src/components/FlexContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import classNames from 'classnames' 3 | import { ReactSortable, SortableEvent } from 'react-sortablejs' 4 | import { SortableNode } from '../../types' 5 | import { DynamicMode, SortableGroup } from '../../constants' 6 | import { equalChildrenIds, findCurrentMeta } from '../../utils' 7 | import { DynamicRootContext } from '../core/DynamicRoot' 8 | import DynamicNode, { DynamicNodeContext } from '../core/DynamicNode' 9 | import NodeWrapper, { ghostClass } from '../NodeWrapper' 10 | 11 | import styles from './index.module.css' 12 | 13 | export interface FlexContainerProps { 14 | direction?: 'vertical' | 'horizontal' 15 | fixedChildren?: boolean 16 | } 17 | 18 | const FlexContainer: React.FC = (p) => { 19 | const { direction = 'vertical', fixedChildren = false } = p 20 | const rootContext = useContext(DynamicRootContext) 21 | const nodeContext = useContext(DynamicNodeContext) 22 | 23 | const creative = rootContext.mode === DynamicMode.CREATIVE 24 | const children = useMemo(() => nodeContext.meta?.children ?? [], [nodeContext.meta?.children]) 25 | const empty = !children.length 26 | 27 | const list: SortableNode[] = useMemo(() => children.map((i) => ({ id: i.__uid!, meta: i })), [children]) 28 | 29 | const setList = (newList: SortableNode[], event?: SortableEvent) => { 30 | // ! Avoid frequent trigger 31 | if (event) { 32 | const previousChildren = children 33 | 34 | const currentChildren = newList 35 | // ! Avoid dirty item. eg: [undefined, { selected: true }] 36 | .filter(Boolean) 37 | .filter((i) => i.id) 38 | .map((i) => i.meta) 39 | 40 | // ! Avoid noisy item prop change. eg: [{ id: ..., raw: ..., chosen: false }] 41 | if (!equalChildrenIds(previousChildren, currentChildren)) { 42 | rootContext.updateMeta?.((rootMeta) => { 43 | const nodeMeta = findCurrentMeta(rootMeta, nodeContext.indexPath) 44 | if (nodeMeta) { 45 | nodeMeta.children = currentChildren 46 | } 47 | }) 48 | } 49 | } 50 | } 51 | 52 | return ( 53 |
60 | 61 | handle={'.' + styles.flexItemHandle} 62 | list={list} 63 | setList={setList} 64 | animation={150} 65 | swapThreshold={0.5} 66 | // ! Force refresh ReactSortable.group 67 | key={String(fixedChildren)} 68 | // ! ReactSortable.group not use current value 69 | group={{ 70 | name: SortableGroup.FlexContainer, 71 | put: !fixedChildren, 72 | }} 73 | className={styles.flexContainer} 74 | ghostClass={ghostClass} 75 | clone={(item) => (item.clone ? item.clone() : item)} 76 | > 77 | {children.map((meta, index) => ( 78 | 79 | 80 | 81 | ))} 82 | 83 |
84 | ) 85 | } 86 | 87 | export default FlexContainer 88 | -------------------------------------------------------------------------------- /src/components/FlexContainerConfigure/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import { ConfigureProps } from '../../types' 3 | import { DynamicNodeContext } from '../core/DynamicNode' 4 | import { FlexContainerProps } from '../FlexContainer' 5 | 6 | import styles from './index.module.css' 7 | 8 | export interface FlexContainerConfig { 9 | direction?: 'vertical' | 'horizontal' 10 | root?: boolean 11 | fixedChildren?: boolean 12 | } 13 | 14 | const FlexContainerConfigure: React.FC> = ({ children }) => { 15 | const nodeContext = useContext(DynamicNodeContext) 16 | 17 | const props = useMemo(() => { 18 | const config = nodeContext.meta?.config as FlexContainerConfig | undefined 19 | const { direction = 'vertical', root = false, fixedChildren = false } = config || {} 20 | 21 | return { direction, root, fixedChildren } 22 | }, [nodeContext.meta?.config]) 23 | 24 | const hasChildren = useMemo(() => nodeContext.meta?.children?.length, [nodeContext?.meta?.children]) 25 | 26 | const canBeNested = useMemo( 27 | () => 28 | nodeContext.meta?.children?.length && 29 | nodeContext.meta.children.length >= 2 && 30 | nodeContext.meta.children.every((i) => i.component === 'FlexContainer'), 31 | [nodeContext?.meta?.children] 32 | ) 33 | 34 | const parentFixed = useMemo(() => { 35 | const parent = nodeContext.parentMeta 36 | return parent?.component === 'FlexContainer' && parent?.config?.fixedChildren 37 | }, [nodeContext]) 38 | 39 | return ( 40 | <> 41 | {children( 42 | props, 43 | <> 44 | {nodeContext.isActive && ( 45 |
{ 48 | event.stopPropagation() 49 | }} 50 | > 51 | {!parentFixed ? ( 52 |
nodeContext.remove()}> 53 | x 54 |
55 | ) : null} 56 | {hasChildren ? ( 57 |
nodeContext.repeat()}> 58 | rep 59 |
60 | ) : null} 61 | {hasChildren ? ( 62 | <> 63 | {canBeNested ? ( 64 |
{ 67 | nodeContext.updateConfig((config) => { 68 | config.fixedChildren = !props.fixedChildren 69 | }) 70 | }} 71 | > 72 | fx 73 |
74 | ) : null} 75 | {!props.fixedChildren ? ( 76 |
{ 79 | nodeContext.updateConfig((config) => { 80 | config.direction = props.direction === 'horizontal' ? 'vertical' : 'horizontal' 81 | }) 82 | }} 83 | > 84 | dir:{props.direction!.slice(0, 1)} 85 |
86 | ) : null} 87 | 88 | ) : null} 89 |
90 | )} 91 | 92 | )} 93 | 94 | ) 95 | } 96 | 97 | export default FlexContainerConfigure 98 | -------------------------------------------------------------------------------- /src/components/core/DynamicNode/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useMemo } from 'react' 2 | import { cloneNodeMeta, findCurrentMeta, findParentMeta } from '../../../utils' 3 | import { componentMap, configureMap, defaultConfigure } from '../../../constants' 4 | import { ConfigureProps, DynamicNodeMeta } from '../../../types' 5 | import { DynamicRootContext } from '../DynamicRoot' 6 | 7 | export interface DynamicNodeProps { 8 | meta: DynamicNodeMeta 9 | index: number 10 | } 11 | 12 | export const DynamicNodeContext = createContext<{ 13 | meta?: DynamicNodeMeta 14 | indexPath: number[] 15 | isActive: boolean 16 | setActive: (value: boolean) => void 17 | remove: () => void 18 | repeat: () => void 19 | updateConfig: (callback: (config: Config) => void) => void 20 | parentMeta: DynamicNodeMeta | undefined 21 | Component: React.FC 22 | Configure: React.FC> 23 | }>({ 24 | indexPath: [], 25 | isActive: false, 26 | setActive: () => {}, 27 | remove: () => {}, 28 | repeat: () => {}, 29 | updateConfig: () => {}, 30 | parentMeta: undefined, 31 | Component: () => null, 32 | Configure: () => null, 33 | }) 34 | 35 | const DynamicNode: React.FC = ({ meta, index, children }) => { 36 | const rootContext = useContext(DynamicRootContext) 37 | const parentNodeContext = useContext(DynamicNodeContext) 38 | 39 | const { component } = meta 40 | const { meta: parentMeta } = parentNodeContext 41 | 42 | const Component = componentMap[component] as any 43 | const Configure = configureMap[component] ?? defaultConfigure 44 | 45 | const indexPath = useMemo(() => [...parentNodeContext.indexPath, index], [index, parentNodeContext.indexPath]) 46 | 47 | const isActive = rootContext.activeId === meta.__uid 48 | const setActive = (value: boolean) => rootContext.setActiveId(value ? meta.__uid! : null) 49 | 50 | const remove = () => { 51 | rootContext.updateMeta?.((rootMeta) => { 52 | const parentMeta = findParentMeta(rootMeta, indexPath) || rootMeta 53 | 54 | if (parentMeta?.children) { 55 | parentMeta.children = parentMeta.children.filter((i) => i.__uid !== meta.__uid) 56 | } 57 | }) 58 | } 59 | 60 | const repeat = () => { 61 | rootContext.updateMeta?.((rootMeta) => { 62 | const parentMeta = findParentMeta(rootMeta, indexPath) || rootMeta 63 | if (parentMeta?.children) { 64 | const currentIndex = parentMeta?.children.findIndex((i) => i.__uid === meta.__uid) 65 | const currentNodeMeta = parentMeta.children[currentIndex] 66 | if (currentIndex > -1) { 67 | parentMeta?.children.splice(currentIndex + 1, 0, cloneNodeMeta(currentNodeMeta)) 68 | } 69 | } 70 | }) 71 | } 72 | 73 | const updateConfig = (callback: (config: Config) => void) => { 74 | rootContext.updateMeta?.((rootMeta) => { 75 | const currentMeta = findCurrentMeta(rootMeta, indexPath) 76 | if (currentMeta) { 77 | if (!currentMeta.config) { 78 | currentMeta.config = {} 79 | } 80 | callback(currentMeta.config) 81 | } 82 | }) 83 | } 84 | 85 | return ( 86 | 100 | {children} 101 | 102 | ) 103 | } 104 | 105 | export default DynamicNode 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.0.0-0](https://github.com/Aysnine/rc-dynamic/compare/v0.0.7...v1.0.0-0) (2021-10-07) 6 | 7 | 8 | ### Features 9 | 10 | * click to active ([e1b89dd](https://github.com/Aysnine/rc-dynamic/commit/e1b89dde7aef90540bf3a15f0b993adc49ece243)) 11 | * fixed mode ([8f47d8a](https://github.com/Aysnine/rc-dynamic/commit/8f47d8af04608a6143b5bae55f9323aa4db6a013)) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * config FlexContainer ([2246152](https://github.com/Aysnine/rc-dynamic/commit/2246152718e7c31eea8545cd1fcb0cb06890346d)) 17 | * props bug ([fa7b9d1](https://github.com/Aysnine/rc-dynamic/commit/fa7b9d109f6acb4bf214d7ada981b4803a119bda)) 18 | 19 | ### [0.0.7](https://github.com/Aysnine/rc-dynamic/compare/v0.0.6...v0.0.7) (2021-02-08) 20 | 21 | 22 | ### Features 23 | 24 | * ready for hook compose config ([7a32a30](https://github.com/Aysnine/rc-dynamic/commit/7a32a30fbaddfc2d57edb9796689a65eccf4c805)) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * type for tree node meta ([cdec719](https://github.com/Aysnine/rc-dynamic/commit/cdec7196c0dbaedf7034d4d5b095fee8f6bb84f4)) 30 | 31 | ### [0.0.6](https://github.com/Aysnine/rc-dynamic/compare/v0.0.6-2...v0.0.6) (2021-02-08) 32 | 33 | ### [0.0.6-2](https://github.com/Aysnine/rc-dynamic/compare/v0.0.6-1...v0.0.6-2) (2021-02-01) 34 | 35 | ### [0.0.6-1](https://github.com/Aysnine/rc-dynamic/compare/v0.0.6-0...v0.0.6-1) (2021-02-01) 36 | 37 | ### [0.0.6-0](https://github.com/Aysnine/rc-dynamic/compare/v0.0.5...v0.0.6-0) (2021-01-31) 38 | 39 | ### [0.0.5](https://github.com/Aysnine/rc-dynamic/compare/v0.0.4...v0.0.5) (2021-01-26) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * mode ([92029c8](https://github.com/Aysnine/rc-dynamic/commit/92029c86ef1669637aef610b65c781e1d3c3e376)) 45 | 46 | ### [0.0.4](https://github.com/Aysnine/rc-dynamic/compare/v0.0.3...v0.0.4) (2021-01-21) 47 | 48 | 49 | ### Features 50 | 51 | * deep duplicate ([17a4239](https://github.com/Aysnine/rc-dynamic/commit/17a4239e977bc1110681c2112acfc1e853e36109)) 52 | * **mode:** creative and runtime ([9abfe84](https://github.com/Aysnine/rc-dynamic/commit/9abfe8403cd28e98fa23cffff953203514587630)) 53 | 54 | ### [0.0.3](https://github.com/Aysnine/rc-dynamic/compare/v0.0.3-0...v0.0.3) (2021-01-17) 55 | 56 | ### [0.0.3-0](https://github.com/Aysnine/rc-dynamic/compare/v0.0.2...v0.0.3-0) (2021-01-09) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * mock data ([a56cc7b](https://github.com/Aysnine/rc-dynamic/commit/a56cc7bcd2037cb08d41091bdecf5f7f017af307)) 62 | 63 | ### [0.0.2](https://github.com/Aysnine/rc-dynamic/compare/v0.0.1...v0.0.2) (2021-01-09) 64 | 65 | ### 0.0.1 (2021-01-09) 66 | 67 | 68 | ### Features 69 | 70 | * **components/gauge-chart:** basic ([9718452](https://github.com/Aysnine/rc-dynamic/commit/9718452137f6714df79f89f693fbf9c535770d93)) 71 | * localStorage with version ([9240ff8](https://github.com/Aysnine/rc-dynamic/commit/9240ff864fef5e92bf087af5087ebef16d9dfb00)) 72 | * **component/text:** more config ([3ccb254](https://github.com/Aysnine/rc-dynamic/commit/3ccb2541de7985492d368f9c5bbf03caaac36d90)) 73 | * add component ([75607e2](https://github.com/Aysnine/rc-dynamic/commit/75607e212bdc25ff982b2bb986cc25f80a7e76fa)) 74 | * base work layout ([14ab424](https://github.com/Aysnine/rc-dynamic/commit/14ab424e31dd566c0ca165f30f526d84df3d7df2)) 75 | * better types ([78d8085](https://github.com/Aysnine/rc-dynamic/commit/78d80856d356e895e17c5e2cf1a678e0da7352de)) 76 | * clear and reset default ([a019331](https://github.com/Aysnine/rc-dynamic/commit/a0193310a997547be08ca98f3e46b79e170c6232)) 77 | * configure ([c5641fb](https://github.com/Aysnine/rc-dynamic/commit/c5641fb6367a8da577860851d360a186602a2dd4)) 78 | * container with direction ([a758026](https://github.com/Aysnine/rc-dynamic/commit/a7580267a1f90b4a43f3c1c23acb1c10b55aefa2)) 79 | * empty placeholder ([360deed](https://github.com/Aysnine/rc-dynamic/commit/360deedcf0f107d6e63f1efddedf2204763493d8)) 80 | * extract styles ([5633576](https://github.com/Aysnine/rc-dynamic/commit/56335764fd0beaf7ed44145d1b274620021c95e3)) 81 | * line chart ([be66394](https://github.com/Aysnine/rc-dynamic/commit/be66394ebe4d0cb930f7f6216a1810ba839a85e8)) 82 | * remove ([69c595e](https://github.com/Aysnine/rc-dynamic/commit/69c595eb4d33d0e15e7b2afcf96f7fee3f8b3157)) 83 | * remove by keyboard ([ea2d162](https://github.com/Aysnine/rc-dynamic/commit/ea2d1620c707aaf8b19fb64315a43eb92685cbd0)) 84 | * simple demo ([a22acf0](https://github.com/Aysnine/rc-dynamic/commit/a22acf0f6d81963f50060207ffffaa50cee59ee7)) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * always update ([912eb0b](https://github.com/Aysnine/rc-dynamic/commit/912eb0b61ae0d9ee93f9e0a44d030ad5ec41ae0d)) 90 | * cache to localStorage logic ([1e8ac0d](https://github.com/Aysnine/rc-dynamic/commit/1e8ac0d634cfc0370e8e9819521a69dac752c0f9)) 91 | * tree node move ([e82d8d4](https://github.com/Aysnine/rc-dynamic/commit/e82d8d4b6d17215ef9c0d4f2834c7b12eea54883)) 92 | --------------------------------------------------------------------------------