Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions example/.dumi/global.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as G2 from '@antv/g2';
import React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createRoot } from 'react-dom/client';

/**
* 增加自己的全局变量,用于 DEMO 中的依赖,以 G2 为例
*/
if (typeof window !== 'undefined' && window) {
(window as any).g2 = extendG2(G2);
(window as any)['@antv/g2'] = G2;
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
(window as any).globalAdd = (x, y) => x + y;
(window as any).globalCard = globalCard;
(window as any).d3Regression = require('d3-regression');
Expand Down
87 changes: 81 additions & 6 deletions example/docs/manual/codeblock.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ title: Codeblock
span.textContent = 1;
span.style.fontSize = '30px';

const timer = setInterval(
() => (span.textContent = +span.textContent + 1),
1000,
);
const timer = setInterval(() => (span.textContent = +span.textContent + 1), 1000);

// 清空监听器
span.clear = () => {
Expand Down Expand Up @@ -176,7 +173,7 @@ globalCard('world');

## G2 inject

```js | ob { inject: true }
```js | ob { inject: true }
import { Chart } from '@antv/g2';

const chart = new Chart({
Expand All @@ -201,7 +198,7 @@ chart.render();

## G2 inject & unpin

```js | ob { pin: false, inject: true }
```js | ob { pin: false, inject: true }
import { Chart } from '@antv/g2';

const chart = new Chart({
Expand All @@ -223,3 +220,81 @@ chart

chart.render();
```

## React Component

```js | ob
import React from 'react';

export default () => {
return (
<div
style={{
padding: '20px',
backgroundColor: '#f0f8ff',
border: '2px solid #4169e1',
borderRadius: '8px',
textAlign: 'center',
}}
>
<h2 style={{ color: '#4169e1', margin: '0 0 10px 0' }}>🎉 React Component Works!</h2>
<p style={{ margin: 0, fontSize: '16px' }}>This is a simple React component rendered successfully.</p>
</div>
);
};
```

## G2 React

```js | ob
import React, { useState, useEffect, useRef } from 'react';
import { Chart } from '@antv/g2';

// 渲染条形图
function renderBarChart(container) {
const chart = new Chart({
container,
});

// 准备数据
const data = [
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
];

// 声明可视化
chart
.interval() // 创建一个 Interval 标记
.data(data) // 绑定数据
.encode('x', 'genre') // 编码 x 通道
.encode('y', 'sold') // 编码 y 通道
.encode('key', 'genre') // 指定 key
.animate('update', { duration: 300 }); // 指定更新动画的时间

// 渲染可视化
chart.render();

return chart;
}

export default () => {
const container = useRef(null);
const chart = useRef(null);

useEffect(() => {
if (!chart.current) {
chart.current = renderBarChart(container.current);
}

return () => {
chart.current.destroy();
chart.current = null;
};
}, []);

return <div ref={container}></div>;
};
```
16 changes: 16 additions & 0 deletions src/slots/LiveExample/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import SourceCodeEditor from 'dumi/theme-default/slots/SourceCodeEditor';
import React, { FC } from 'react';

interface CodeEditorProps {
codeRef: React.RefObject<HTMLDivElement>;
value: string;
onChange: (v: string) => void;
lang?: string;
isVisible: boolean;
}

export const CodeEditor: FC<CodeEditorProps> = ({ value, onChange, lang, codeRef, isVisible }) => (
<div ref={codeRef} style={{ display: isVisible ? 'block' : 'none' }}>
<SourceCodeEditor onChange={onChange} initialValue={value} lang={lang} />
</div>
);
183 changes: 183 additions & 0 deletions src/slots/LiveExample/Previews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { debounce, uniqueId, type DebouncedFunc } from 'lodash-es';
import { default as React } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { compile } from '../CodeEditor/utils';
import { safeEval } from '../ManualContent/utils';

export interface BasePreviewProps {
code: string;
refresh: number;
}

export abstract class BasePreview<P extends BasePreviewProps = BasePreviewProps, S = {}> extends React.Component<P, S> {
abstract errorMessage: string;

protected ref = React.createRef<HTMLDivElement>();

protected rootRef: Root | null = null;

private debouncedDoRender: DebouncedFunc<() => void>;

private debounceTime = 300;

constructor(props: P) {
super(props);
this.debouncedDoRender = debounce(this.doRender.bind(this), this.debounceTime);
}

public abstract doRender(): void;

public renderError(msg: string) {
if (this.ref.current) {
const message = this.errorMessage ? `${this.errorMessage}: ${msg}` : msg;
this.ref.current.innerHTML = `<div style='color:red;'>${message}</div>`;
}
}

componentDidMount() {
this.debouncedDoRender();
}

componentDidUpdate(prevProps: BasePreviewProps) {
if (prevProps.code !== this.props.code || prevProps.refresh !== this.props.refresh) {
this.debouncedDoRender();
}
}

componentWillUnmount() {
this.debouncedDoRender.cancel();
if (this.rootRef) {
this.rootRef.unmount();
this.rootRef = null;
}
if (this.ref.current) this.ref.current.innerHTML = '';
}

render() {
return <div ref={this.ref} />;
}
}

export class ReactPreview extends BasePreview<BasePreviewProps> {
errorMessage = 'React 渲染错误';

public doRender() {
if (!this.ref.current) return;
if (!this.rootRef) this.rootRef = createRoot(this.ref.current);
const prevDefine = (window as any).define;
try {
(window as any).define = undefined;
const compiled = compile(this.props.code, '', true);
const moduleExports: any = {};

const fakeRequire = (name: string) => {
if (name === 'react') return (window as any).React;
if (name === 'react-dom') return (window as any).ReactDOM;
if ((window as any)[name]) return (window as any)[name];
throw new Error(`require('${name}') 未被支持,请在 window 上挂载`);
};

const func = new Function('exports', 'module', 'require', 'React', 'ReactDOM', compiled);
func(moduleExports, { exports: moduleExports }, fakeRequire, (window as any).React, (window as any).ReactDOM);
const Component = moduleExports.default || moduleExports;
if (typeof Component === 'function' || React.isValidElement(Component)) {
this.rootRef.render(React.createElement(Component));
} else {
this.renderError('未监测到有效 React 组件,请用 <b>export default</b> 导出');
}
} catch (e) {
this.renderError(String(e));
} finally {
(window as any).define = prevDefine;
}
}
}

export class ChartPreview extends BasePreview<BasePreviewProps> {
errorMessage = '图表渲染错误';

containerId = `live-chart-${uniqueId()}`;

public doRender() {
if (!this.ref.current) return;
this.ref.current.innerHTML = `<div id="${this.containerId}"></div>`;
const prevDefine = (window as any).define;
try {
(window as any).define = undefined;
const compiled = compile(this.props.code, '', true);
const script = document.createElement('script');
script.textContent = compiled.replace(/'container'|"container"/g, `'${this.containerId}'`);
document.body.appendChild(script);
setTimeout(() => {
if (script.parentNode) script.parentNode.removeChild(script);
}, 100);
} catch (e) {
this.renderError(String(e));
} finally {
(window as any).define = prevDefine;
}
}
}

export class IIFEPreview extends BasePreview<BasePreviewProps> {
errorMessage = 'IIFE 执行错误';

private renderIIFEResult(result: any) {
if (!this.ref.current) return;
if (result instanceof HTMLElement) {
this.ref.current.appendChild(result);
} else if (typeof result === 'string') {
this.ref.current.innerHTML = result;
} else if (React.isValidElement(result)) {
if (!this.rootRef) this.rootRef = createRoot(this.ref.current);
this.rootRef.render(result);
} else if (result !== undefined && result !== null) {
this.ref.current.innerHTML = `<div>${JSON.stringify(result)}</div>`;
} else {
this.renderError('IIFE 未返回 DOM 节点');
}
}

public doRender() {
if (!this.ref.current) return;
this.ref.current.innerHTML = '';
try {
const result = safeEval(this.props.code);

if (result && typeof result.then === 'function') {
result
.then((val: any) => this.renderIIFEResult(val))
.catch((e: any) => {
this.renderError(`Promise 执行错误: ${e}`);
});
} else {
this.renderIIFEResult(result);
}
} catch (e) {
this.renderError(String(e));
}
}
}

export class PurePreview extends BasePreview<BasePreviewProps> {
errorMessage = 'JS 执行错误';

public doRender() {
if (!this.ref.current) return;
this.ref.current.innerHTML = '';
try {
const result = Function('return (function(){ with (window) { return ' + this.props.code + ' } })')()();
if (result instanceof HTMLElement) {
this.ref.current.appendChild(result);
} else if (typeof result === 'string') {
this.ref.current.innerHTML = result;
} else if (result !== undefined && result !== null) {
this.ref.current.innerHTML = `<div>${JSON.stringify(result)}</div>`;
} else {
this.renderError('未返回有效结果');
}
} catch (e) {
this.renderError(String(e));
}
}
}
20 changes: 20 additions & 0 deletions src/slots/LiveExample/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PlayCircleOutlined, PushpinOutlined } from '@ant-design/icons';
import React, { FC } from 'react';
import styles from './index.module.less';

interface ToolbarProps {
onToggleCode: () => void;
onRun: () => void;
toolbarRef: React.RefObject<HTMLUListElement>;
}

export const Toolbar: FC<ToolbarProps> = ({ onToggleCode, onRun, toolbarRef }) => (
<ul className={styles.ul} ref={toolbarRef}>
<li onClick={onToggleCode} className={styles.li} title="Toggle Code Editor">
<PushpinOutlined />
</li>
<li onClick={onRun} className={styles.li} title="Run Code">
<PlayCircleOutlined />
</li>
</ul>
);
Loading