Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Imperaitive Handle to public API #1272

Merged
merged 3 commits into from
May 24, 2023
Merged
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
43 changes: 42 additions & 1 deletion apis/nucleus/src/__tests__/viz.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('viz', () => {
let setSnPlugins;
let takeSnapshot;
let exportImage;
let getImperativeHandle;
let convertToMock;

beforeAll(() => {
Expand All @@ -29,13 +30,17 @@ describe('viz', () => {
setSnPlugins = jest.fn();
takeSnapshot = jest.fn();
exportImage = jest.fn();
getImperativeHandle = jest.fn(async () => ({
api: 'api',
}));
cellRef = {
current: {
setSnOptions,
setSnContext,
setSnPlugins,
takeSnapshot,
exportImage,
getImperativeHandle,
},
};
glue = jest.fn().mockReturnValue([unmountMock, cellRef]);
Expand Down Expand Up @@ -101,11 +106,14 @@ describe('viz', () => {
test('should throw if already mounted', async () => {
try {
mounted = api.__DO_NOT_USE__.mount('element');
/*
// This code never runs, as these tests are meant to run together, don't want to mess more with it
const { onMount } = glue.mock.lastCall[0];
onMount();
await mounted;
const result = await api.__DO_NOT_USE__.mount.bind('element2');
await result();
*/
} catch (error) {
expect(error.message).toBe('Already mounted');
}
Expand Down Expand Up @@ -150,7 +158,28 @@ describe('viz', () => {
const opts = {};
api.__DO_NOT_USE__.options(opts);
await mounted;
expect(cellRef.current.setSnOptions).toHaveBeenCalledWith(opts);
const args = cellRef.current.setSnOptions.mock.lastCall[0];
expect(args.onInitialRender).toBeInstanceOf(Function);
expect(Object.keys(args).length).toEqual(1);
});

test('should set extended sn options', async () => {
const opts = { myops: 'myopts' };
api.__DO_NOT_USE__.options(opts);
await mounted;
const args = cellRef.current.setSnOptions.mock.lastCall[0];
expect(args.onInitialRender).toBeInstanceOf(Function);
expect(args.myops).toEqual('myopts');
expect(Object.keys(args).length).toEqual(2);
});

test('should override and call onIntialRender', async () => {
const opts = { myops: 'myopts', onInitialRender: jest.fn() };
api.__DO_NOT_USE__.options(opts);
await mounted;
const args = cellRef.current.setSnOptions.mock.lastCall[0];
args.onInitialRender();
expect(opts.onInitialRender).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -199,4 +228,16 @@ describe('viz', () => {
expect(props).toBe('props');
});
});

describe('getImperativeHandle', () => {
test('should await the rendering and then call cell', async () => {
const opts = { myops: 'myopts', onInitialRender: jest.fn() };
api.__DO_NOT_USE__.options(opts);
await mounted;
const args = cellRef.current.setSnOptions.mock.lastCall[0];
args.onInitialRender();
const handle = await api.getImperativeHandle();
expect(handle.api).toEqual('api');
});
});
});
6 changes: 6 additions & 0 deletions apis/nucleus/src/components/Cell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,12 @@ const Cell = forwardRef(
},
setSnOptions,
setSnPlugins,
getImperativeHandle() {
if (state.sn?.component && typeof state.sn.component.getImperativeHandle === 'function') {
return state.sn.component.getImperativeHandle();
}
return {};
},
async takeSnapshot() {
const { width, height } = cellRect;

Expand Down
25 changes: 25 additions & 0 deletions apis/nucleus/src/viz.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,46 @@ export default function viz({ model, halo, initialError, onDestroy = async () =>
let cellRef = null;
let mountedReference = null;
let onMount = null;
let onRender = null;
const mounted = new Promise((resolve) => {
onMount = resolve;
});

const rendered = new Promise((resolve) => {
onRender = resolve;
});

const createOnInitialRender = (override) => () => {
override && override();
onRender();
};

let initialSnOptions = {};
let initialSnPlugins = [];

const emitter = new EventEmitter();

const setSnOptions = async (opts) => {
const override = opts.onInitialRender;
if (mountedReference) {
(async () => {
await mounted;
cellRef.current.setSnOptions({
...initialSnOptions,
...opts,
...{
onInitialRender: createOnInitialRender(override),
},
});
})();
} else {
// Handle setting options before mount
initialSnOptions = {
...initialSnOptions,
...opts,
...{
onInitialRender: createOnInitialRender(override),
},
};
}
};
Expand Down Expand Up @@ -130,6 +147,14 @@ export default function viz({ model, halo, initialError, onDestroy = async () =>
removeListener(eventName, listener) {
emitter.removeListener(eventName, listener);
},
/**
* Gets the specific api that a Viz exposes.
* @returns {Promise<object>} object that contains the internal Viz api.
*/
async getImperativeHandle() {
await rendered;
return cellRef.current.getImperativeHandle();
},
// ===== unexposed experimental API - use at own risk ======
__DO_NOT_USE__: {
mount(element) {
Expand Down
45 changes: 45 additions & 0 deletions apis/stardust/api-spec/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,37 @@
"// it is up to you use and implement the provided options\nimport { useOptions } from '@nebula.js/stardust';\nimport { useEffect } from '@nebula.js/stardust';\n// ...\nconst options = useOptions();\nuseEffect(() => {\n if (!options.showNavigation) {\n // hide navigation\n } else {\n // show navigation\n }\n}, [options.showNavigation]);"
]
},
"useImperativeHandle": {
"description": "This is an empty object by default, but enables you to provide a custom API of your visualization to\nmake it possible to control after it has been rendered.\n\nYou can only use this hook once, calling it more than once is considered an error.",
"templates": [
{
"name": "T"
}
],
"kind": "function",
"params": [
{
"name": "factory",
"kind": "function",
"params": [],
"returns": {
"type": "T"
}
},
{
"name": "deps",
"optional": true,
"kind": "array",
"items": {
"type": "any"
}
}
],
"examples": [
"import { useImperativeHandle } form '@nebula.js/stardust';\n// ...\nuseImperativeHandle(() => ({\n resetZoom() {\n setZoomed(false);\n }\n}));",
"// when embedding the visualization, you can get a handle to this API\n// and use it to control the visualization\nconst ctl = await embed(app).render({\n element,\n type: 'my-chart',\n});\nctl.getImperativeHandle().resetZoom();"
]
},
"onTakeSnapshot": {
"description": "Registers a callback that is called when a snapshot is taken.",
"kind": "function",
Expand Down Expand Up @@ -1281,6 +1312,20 @@
"params": []
}
]
},
"getImperativeHandle": {
"description": "Gets the specific api that a Viz exposes.",
"kind": "function",
"params": [],
"returns": {
"description": "object that contains the internal Viz api.",
"type": "Promise",
"generics": [
{
"type": "object"
}
]
}
}
},
"examples": [
Expand Down
15 changes: 15 additions & 0 deletions apis/stardust/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ export function useInteractionState(): stardust.Interactions;
*/
export function useOptions(): object;

/**
* This is an empty object by default, but enables you to provide a custom API of your visualization to
* make it possible to control after it has been rendered.
*
* You can only use this hook once, calling it more than once is considered an error.
* @param factory
* @param deps
*/
export function useImperativeHandle<T>(factory: ()=>T, deps?: any[]): void;

/**
* Registers a callback that is called when a snapshot is taken.
* @param snapshotCallback
Expand Down Expand Up @@ -405,6 +415,11 @@ declare namespace stardust {
*/
removeListener(eventName: string, listener: ()=>void): void;

/**
* Gets the specific api that a Viz exposes.
*/
getImperativeHandle(): Promise<object>;

}

interface Flags {
Expand Down
4 changes: 0 additions & 4 deletions apis/supernova/src/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -983,15 +983,11 @@ export function useOptions() {
}

/**
* TODO before making public - expose getImperativeHandle on Viz
* Exposes an API to the external environment.
*
* This is an empty object by default, but enables you to provide a custom API of your visualization to
* make it possible to control after it has been rendered.
*
* You can only use this hook once, calling it more than once is considered an error.
* @entry
* @private
* @template T
* @param {function():T} factory
Comment on lines 991 to 992
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe simplify to

Suggested change
* @template T
* @param {function():T} factory
* @param {function():object} factory

The generic type T don't give any value

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, you could have the handle function return whatever you want, shouldn't it be the generic type then? Not sure how a TS setup handles it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is the only place where the type is referenced I don't think it gives any value.
(but I also don't there is any real problem with keeping it as it is)

* @param {Array<any>=} deps
Expand Down
Loading