|
1 | | -import React, { HTMLAttributes } from 'react' |
2 | | -import omit from 'lodash/omit' |
| 1 | +import React, { |
| 2 | + HTMLAttributes, |
| 3 | + forwardRef, |
| 4 | + useRef, |
| 5 | + useContext, |
| 6 | + useEffect, |
| 7 | + Ref, |
| 8 | + MutableRefObject, |
| 9 | + ReactElement, |
| 10 | + RefAttributes, |
| 11 | +} from 'react' |
3 | 12 | import isEqual from 'lodash/isEqual' |
4 | 13 | import cloneDeep from 'lodash/cloneDeep' |
5 | 14 | import { |
6 | 15 | Base as BasePlot, |
7 | 16 | ViewLayer, |
8 | 17 | PlotConfig, |
9 | | - StateManager, |
| 18 | + ViewConfig, |
10 | 19 | } from '@antv/g2plot' |
11 | 20 | import { StateManagerContext } from '../state-manager' |
| 21 | +import { RecursivePartial } from '@antv/g2plot/lib/interface/types' |
12 | 22 |
|
13 | 23 | type PickedAttrs = 'className' | 'style' |
14 | 24 |
|
@@ -38,87 +48,98 @@ interface StateManagerCfg { |
38 | 48 | onStateChange?: StateChangeObj[] |
39 | 49 | } |
40 | 50 |
|
41 | | -export interface Plot<C extends PlotConfig = PlotConfig> { |
| 51 | +interface ChartConfig extends PlotConfig, ViewConfig {} |
| 52 | + |
| 53 | +export interface Plot<C extends PlotConfig> { |
42 | 54 | new (container: HTMLElement, props: C): BasePlot<C, LayerCtor<C>> |
43 | 55 | } |
44 | 56 |
|
45 | | -export interface BaseChartProps<C extends PlotConfig = PlotConfig> |
| 57 | +const syncRef = <C extends PlotConfig>( |
| 58 | + source: MutableRefObject<BasePlot<C, LayerCtor<C>> | null>, |
| 59 | + target?: Ref<BasePlot<C, LayerCtor<C>> | null> |
| 60 | +) => { |
| 61 | + /* istanbul ignore else */ |
| 62 | + if (typeof target === 'function') { |
| 63 | + target(source.current) |
| 64 | + } else if (target) { |
| 65 | + ;(target as MutableRefObject<BasePlot<C, LayerCtor<C>> | null>).current = |
| 66 | + source.current |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +export interface BaseChartProps<C extends PlotConfig> |
46 | 71 | extends Pick<HTMLAttributes<HTMLDivElement>, PickedAttrs> { |
47 | 72 | chart: Plot<C> |
48 | | - onMount?: (chart: BasePlot<C, LayerCtor<C>>) => void |
49 | 73 | stateManager?: StateManagerCfg |
50 | 74 | } |
51 | 75 |
|
52 | | -export default class BaseChart< |
53 | | - C extends PlotConfig = PlotConfig |
54 | | -> extends React.Component<BaseChartProps<C>> { |
55 | | - private el: HTMLDivElement | null = null |
56 | | - private chart?: BasePlot<C, LayerCtor<C>> | null |
57 | | - private config?: any |
58 | | - private getContainer = (el: HTMLDivElement | null) => { |
59 | | - this.el = el |
60 | | - } |
61 | | - |
62 | | - context!: StateManager |
63 | | - |
64 | | - private getConfig = (props: BaseChartProps<C>) => { |
65 | | - return omit(props, [ |
66 | | - 'style', |
67 | | - 'className', |
68 | | - 'chart', |
69 | | - 'onMount', |
70 | | - 'stateManager', |
71 | | - ]) as C |
72 | | - } |
73 | | - |
74 | | - static contextType = StateManagerContext |
75 | | - |
76 | | - componentDidMount() { |
77 | | - const { chart, onMount, stateManager } = this.props |
78 | | - const config = this.getConfig(this.props) |
79 | | - const Chart = chart |
80 | | - const { data, ...restConfig } = config as any |
81 | | - this.config = cloneDeep(restConfig) |
82 | | - if (this.el) { |
83 | | - this.chart = new Chart(this.el, config) |
84 | | - this.chart.render() |
85 | | - if (typeof onMount === 'function') { |
86 | | - onMount(this.chart) |
| 76 | +const BaseChart = <C extends PlotConfig>( |
| 77 | + props: BaseChartProps<C>, |
| 78 | + ref?: Ref<BasePlot<C, LayerCtor<C>> | null> |
| 79 | +) => { |
| 80 | + const { |
| 81 | + chart: Chart, |
| 82 | + stateManager: stateManagerCfg, |
| 83 | + style, |
| 84 | + className, |
| 85 | + ...restProps |
| 86 | + } = props |
| 87 | + const chartRef = useRef<BasePlot<C, LayerCtor<C>> | null>(null) |
| 88 | + const configRef = useRef<ChartConfig>() |
| 89 | + const containerRef = useRef<HTMLDivElement>(null) |
| 90 | + const stateManager = useContext(StateManagerContext) |
| 91 | + const isFirstRenderRef = useRef<boolean>(true) |
| 92 | + |
| 93 | + useEffect(() => { |
| 94 | + const { current: container } = containerRef |
| 95 | + /* istanbul ignore else */ |
| 96 | + if (container) { |
| 97 | + const { data, ...config } = restProps as ChartConfig |
| 98 | + configRef.current = cloneDeep(config) |
| 99 | + chartRef.current = new Chart(container, restProps as C) |
| 100 | + chartRef.current.render() |
| 101 | + /* istanbul ignore else */ |
| 102 | + if (stateManager && stateManagerCfg) { |
| 103 | + chartRef.current.bindStateManager(stateManager, stateManagerCfg) |
87 | 104 | } |
88 | | - |
89 | | - if (this.context && stateManager) { |
90 | | - this.chart.bindStateManager(this.context, stateManager) |
| 105 | + } |
| 106 | + syncRef(chartRef, ref) |
| 107 | + return () => { |
| 108 | + /* istanbul ignore else */ |
| 109 | + if (chartRef.current) { |
| 110 | + chartRef.current.destroy() |
| 111 | + chartRef.current = null |
| 112 | + syncRef(chartRef, ref) |
91 | 113 | } |
92 | 114 | } |
93 | | - } |
94 | | - |
95 | | - componentDidUpdate() { |
96 | | - const config = this.getConfig(this.props) |
97 | | - const { data, ...restConfig } = config as any |
98 | | - const isConfigChanged = !isEqual(this.config, restConfig) |
| 115 | + // eslint-disable-next-line |
| 116 | + }, []) |
99 | 117 |
|
| 118 | + useEffect(() => { |
| 119 | + const { current: chart } = chartRef |
100 | 120 | /* istanbul ignore else */ |
101 | | - if (this.chart) { |
102 | | - if (isConfigChanged) { |
103 | | - this.config = cloneDeep(restConfig) |
104 | | - this.chart.updateConfig(config as any) |
105 | | - this.chart.render() |
| 121 | + if (chart) { |
| 122 | + // avoid update in first time |
| 123 | + if (!isFirstRenderRef.current) { |
| 124 | + const { data, ...config } = restProps as ChartConfig |
| 125 | + if (!isEqual(config, configRef.current)) { |
| 126 | + chart.updateConfig(config as RecursivePartial<C>) |
| 127 | + } else { |
| 128 | + if (data) { |
| 129 | + chart.changeData(data) |
| 130 | + } else { |
| 131 | + chart.changeData([]) |
| 132 | + } |
| 133 | + } |
106 | 134 | } else { |
107 | | - this.chart.changeData(data) |
| 135 | + isFirstRenderRef.current = false |
108 | 136 | } |
109 | 137 | } |
110 | | - } |
111 | | - |
112 | | - componentWillUnmount() { |
113 | | - /* istanbul ignore else */ |
114 | | - if (this.chart) { |
115 | | - this.chart.destroy() |
116 | | - this.chart = null |
117 | | - } |
118 | | - } |
| 138 | + }, [restProps]) |
119 | 139 |
|
120 | | - render() { |
121 | | - const { style, className } = this.props |
122 | | - return <div ref={this.getContainer} className={className} style={style} /> |
123 | | - } |
| 140 | + return <div style={style} className={className} ref={containerRef} /> |
124 | 141 | } |
| 142 | + |
| 143 | +export default forwardRef(BaseChart) as <C extends PlotConfig>( |
| 144 | + p: BaseChartProps<C> & RefAttributes<BasePlot<C, LayerCtor<C>> | null> |
| 145 | +) => ReactElement |
0 commit comments