Skip to content

Commit

Permalink
experiment with useImperitiveHandle & forwardRef
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant committed Jun 3, 2020
1 parent 3035778 commit cf87a82
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 80 deletions.
29 changes: 22 additions & 7 deletions examples/embeddable_explorer/public/todo_embeddable_example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import { TodoInput } from '../../../examples/embeddable_examples/public/todo';
import { TodoEmbeddable, TodoInput } from '../../../examples/embeddable_examples/public/todo';
import { TodoEmbeddableFactory } from '../../../examples/embeddable_examples/public';
import { EmbeddableRenderer } from '../../../src/plugins/embeddable/public';

Expand All @@ -50,24 +50,32 @@ interface State {
input: TodoInput;
}

const initialInput: TodoInput = {
id: '1',
task: 'Take out the trash',
icon: 'broom',
title: 'Trash',
};

export class TodoEmbeddableExample extends React.Component<Props, State> {
private embeddableRef = React.createRef<TodoEmbeddable>();

constructor(props: Props) {
super(props);

this.state = {
loading: true,
input: {
id: '1',
task: 'Take out the trash',
icon: 'broom',
title: 'Trash',
},
input: initialInput,
};
}

private onUpdateEmbeddableInput = () => {
const { task, title, icon, input } = this.state;
this.setState({ input: { ...input, task: task ?? '', title, icon } });

if (this.embeddableRef.current) {
this.embeddableRef.current.updateInput({ task: task ?? '', title, icon });
}
};

public render() {
Expand Down Expand Up @@ -130,6 +138,13 @@ export class TodoEmbeddableExample extends React.Component<Props, State> {
input={this.state.input}
/>
</EuiPanel>
<EuiPanel data-test-subj="todoEmbeddableImperative" paddingSize="none" role="figure">
<EmbeddableRenderer
factory={this.props.todoEmbeddableFactory}
input={initialInput}
ref={this.embeddableRef}
/>
</EuiPanel>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
Expand Down
173 changes: 100 additions & 73 deletions src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,97 +17,124 @@
* under the License.
*/

import React, { useEffect, useState } from 'react';
import { EmbeddableInput, IEmbeddable } from './i_embeddable';
import { Subscription } from 'rxjs';
import React, { useEffect, useImperativeHandle, useState } from 'react';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { EmbeddableRoot } from './embeddable_root';
import { EmbeddableFactory } from './embeddable_factory';
import { ErrorEmbeddable } from './error_embeddable';

/**
* This type is needed for strict, one of, public api
*/
export type EmeddableRendererProps<I extends EmbeddableInput> =
| { input: I; onInputUpdated?: (newInput: I) => void; factory: EmbeddableFactory<I> }
| { input: I; onInputUpdated?: (newInput: I) => void; embeddable: IEmbeddable<I> };
export type EmeddableRendererProps<
I extends EmbeddableInput,
O extends EmbeddableOutput,
E extends IEmbeddable<I, O>
> = {
input: I;
onInputUpdated?: (newInput: I) => void;
} & ({ factory: EmbeddableFactory<I, O, E> } | { embeddable: E });

/**
* This one is for internal implementation
*/
interface InnerProps {
input: EmbeddableInput;
onInputUpdated?: (newInput: EmbeddableInput) => void;
factory?: EmbeddableFactory;
embeddable?: IEmbeddable;
interface InnerProps<
I extends EmbeddableInput,
O extends EmbeddableOutput,
E extends IEmbeddable<I, O>
> {
input: I;
onInputUpdated?: (newInput: I) => void;
factory?: EmbeddableFactory<I, O, E>;
embeddable?: E;
}

export const EmbeddableRenderer = <I extends EmbeddableInput>(
publicProps: EmeddableRendererProps<I>
) => {
const props = publicProps as InnerProps;
const [embeddable, setEmbeddable] = useState<IEmbeddable | undefined>(props.embeddable);
const [loading, setLoading] = useState<boolean>(!props.embeddable);
const [error, setError] = useState<string | undefined>();
const latestInput = React.useRef(props.input);
useEffect(() => {
latestInput.current = props.input;
}, [props.input]);
export const EmbeddableRenderer = React.forwardRef(
<
I extends EmbeddableInput = EmbeddableInput,
O extends EmbeddableOutput = EmbeddableOutput,
E extends IEmbeddable<I, O> = IEmbeddable<I, O>
>(
publicProps: EmeddableRendererProps<I, O, E>,
// TODO: ref could be ErrorEmbeddable,
// TODO: result type for E is not ideal here
ref: React.Ref<E | undefined>
) => {
const props = publicProps as InnerProps<I, O, E>;
const [embeddable, setEmbeddable] = useState<E | undefined>(props.embeddable);
const [loading, setLoading] = useState<boolean>(!props.embeddable);
const [error, setError] = useState<string | undefined>();
const latestInput = React.useRef(props.input);
useEffect(() => {
latestInput.current = props.input;
}, [props.input]);

useEffect(() => {
let canceled = false;
if (props.embeddable) {
setEmbeddable(props.embeddable);
return;
}
const embeddableRef = React.useRef<E | undefined>(embeddable);
useEffect(() => {
embeddableRef.current = embeddable;
}, [embeddable]);

// keeping track of embeddables created by this component to be able to destroy them
let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined;
if (props.factory) {
setEmbeddable(undefined);
setLoading(true);
props.factory
.create(latestInput.current)
.then((createdEmbeddable) => {
if (canceled) {
if (createdEmbeddable) {
createdEmbeddable.destroy();
}
}
createdEmbeddableRef = createdEmbeddable;
setEmbeddable(createdEmbeddable);
})
.catch((err) => {
if (canceled) return;
setError(err?.message);
})
.finally(() => {
if (canceled) return;
setLoading(false);
});
}
useImperativeHandle(ref, () => embeddableRef.current);

return () => {
canceled = true;
if (createdEmbeddableRef) {
createdEmbeddableRef.destroy();
useEffect(() => {
let canceled = false;
if (props.embeddable) {
setEmbeddable(props.embeddable);
return;
}
};
}, [props.factory, props.embeddable]);

const { onInputUpdated } = props;
useEffect(() => {
const sub = embeddable?.getInput$().subscribe((input) => {
if (onInputUpdated) {
onInputUpdated(input);
// keeping track of embeddables created by this component to be able to destroy them
let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined;
if (props.factory) {
setEmbeddable(undefined);
setLoading(true);
props.factory
.create(latestInput.current)
.then((createdEmbeddable) => {
if (canceled) {
if (createdEmbeddable) {
createdEmbeddable.destroy();
}
}
createdEmbeddableRef = createdEmbeddable;
setEmbeddable(createdEmbeddable as E); // TODO: ref could be ErrorEmbeddable
})
.catch((err) => {
if (canceled) return;
setError(err?.message);
})
.finally(() => {
if (canceled) return;
setLoading(false);
});
}
});
return () => {
if (sub) {
sub.unsubscribe();

return () => {
canceled = true;
if (createdEmbeddableRef) {
createdEmbeddableRef.destroy();
}
};
}, [props.factory, props.embeddable]);

const { onInputUpdated } = props;
useEffect(() => {
let sub: Subscription | undefined;
if (onInputUpdated) {
sub = embeddable?.getInput$().subscribe((input) => {
onInputUpdated(input);
});
}
};
}, [embeddable, onInputUpdated]);
return () => {
if (sub) {
sub.unsubscribe();
}
};
}, [embeddable, onInputUpdated]);

return (
<EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={props.input} />
);
};
return (
<EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={props.input} />
);
}
);

0 comments on commit cf87a82

Please sign in to comment.