Skip to content

Commit

Permalink
Fix JSON editor when it is "controlled"
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga committed Sep 3, 2020
1 parent 6d152dd commit bba9d40
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@

export * from './json_editor';

export { OnJsonEditorUpdateHandler } from './use_json';
export { OnJsonEditorUpdateHandler, JsonEditorState } from './use_json';
134 changes: 60 additions & 74 deletions src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,93 +21,79 @@ import React, { useCallback, useMemo } from 'react';
import { EuiFormRow, EuiCodeEditor } from '@elastic/eui';
import { debounce } from 'lodash';

import { isJSON } from '../../../static/validators/string';
import { useJson, OnJsonEditorUpdateHandler } from './use_json';

interface Props {
onUpdate: OnJsonEditorUpdateHandler;
interface Props<T extends object = { [key: string]: any }> {
onUpdate: OnJsonEditorUpdateHandler<T>;
label?: string;
helpText?: React.ReactNode;
value?: string;
defaultValue?: { [key: string]: any };
defaultValue?: T;
euiCodeEditorProps?: { [key: string]: any };
error?: string | null;
}

export const JsonEditor = React.memo(
({
label,
helpText,
function JsonEditorComp<T extends object = { [key: string]: any }>({
label,
helpText,
onUpdate,
value,
defaultValue,
euiCodeEditorProps,
error: propsError,
}: Props<T>) {
const { content, setContent, error: internalError, isControlled } = useJson<T>({
defaultValue,
onUpdate,
value,
defaultValue,
euiCodeEditorProps,
error: propsError,
}: Props) => {
const isControlled = value !== undefined;
});

const { content, setContent, error: internalError } = useJson({
defaultValue,
onUpdate,
isControlled,
});
const debouncedSetContent = useMemo(() => {
return debounce(setContent, 300);
}, [setContent]);

const debouncedSetContent = useMemo(() => {
return debounce(setContent, 300);
}, [setContent]);
// We let the consumer control the validation and the error message.
const error = isControlled ? propsError : internalError;

// We let the consumer control the validation and the error message.
const error = isControlled ? propsError : internalError;
const onEuiCodeEditorChange = useCallback(
(updated: string) => {
if (isControlled) {
setContent(updated);
} else {
debouncedSetContent(updated);
}
},
[isControlled, setContent, debouncedSetContent]
);

const onEuiCodeEditorChange = useCallback(
(updated: string) => {
if (isControlled) {
onUpdate({
data: {
raw: updated,
format() {
return JSON.parse(updated);
},
},
validate() {
return isJSON(updated);
},
isValid: undefined,
});
} else {
debouncedSetContent(updated);
}
},
[isControlled, onUpdate, debouncedSetContent]
);
return (
<EuiFormRow
label={label}
helpText={helpText}
isInvalid={typeof error === 'string'}
error={error}
fullWidth
>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="500px"
setOptions={{
showLineNumbers: false,
tabSize: 2,
}}
editorProps={{
$blockScrolling: Infinity,
}}
showGutter={false}
minLines={6}
value={isControlled ? value : content}
onChange={onEuiCodeEditorChange}
{...euiCodeEditorProps}
/>
</EuiFormRow>
);
}

return (
<EuiFormRow
label={label}
helpText={helpText}
isInvalid={typeof error === 'string'}
error={error}
fullWidth
>
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="500px"
setOptions={{
showLineNumbers: false,
tabSize: 2,
}}
editorProps={{
$blockScrolling: Infinity,
}}
showGutter={false}
minLines={6}
value={isControlled ? value : content}
onChange={onEuiCodeEditorChange}
{...euiCodeEditorProps}
/>
</EuiFormRow>
);
}
);
export const JsonEditor = React.memo(JsonEditorComp) as typeof JsonEditorComp;
24 changes: 16 additions & 8 deletions src/plugins/es_ui_shared/public/components/json_editor/use_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ import { i18n } from '@kbn/i18n';

import { isJSON } from '../../../static/validators/string';

export type OnJsonEditorUpdateHandler<T = { [key: string]: any }> = (arg: {
export interface JsonEditorState<T = { [key: string]: any }> {
data: {
raw: string;
format(): T;
};
validate(): boolean;
isValid: boolean | undefined;
}) => void;
isValid: boolean;
}

export type OnJsonEditorUpdateHandler<T = { [key: string]: any }> = (
arg: JsonEditorState<T>
) => void;

interface Parameters<T extends object> {
onUpdate: OnJsonEditorUpdateHandler<T>;
defaultValue?: T;
isControlled?: boolean;
value?: string;
}

const stringifyJson = (json: { [key: string]: any }) =>
Expand All @@ -43,10 +47,13 @@ const stringifyJson = (json: { [key: string]: any }) =>
export const useJson = <T extends object = { [key: string]: any }>({
defaultValue = {} as T,
onUpdate,
isControlled = false,
value,
}: Parameters<T>) => {
const isControlled = value !== undefined;
const isMounted = useRef(false);
const [content, setContent] = useState<string>(stringifyJson(defaultValue));
const [content, setContent] = useState<string>(
isControlled ? value! : stringifyJson(defaultValue)
);
const [error, setError] = useState<string | null>(null);

const validate = useCallback(() => {
Expand Down Expand Up @@ -75,7 +82,7 @@ export const useJson = <T extends object = { [key: string]: any }>({
return;
}

const isValid = isControlled ? undefined : validate();
const isValid = validate();

onUpdate({
data: {
Expand All @@ -85,7 +92,7 @@ export const useJson = <T extends object = { [key: string]: any }>({
validate,
isValid,
});
}, [onUpdate, content, isControlled, formatContent, validate]);
}, [onUpdate, content, formatContent, validate]);

useEffect(() => {
isMounted.current = true;
Expand All @@ -99,5 +106,6 @@ export const useJson = <T extends object = { [key: string]: any }>({
content,
setContent,
error,
isControlled,
};
};
2 changes: 1 addition & 1 deletion src/plugins/es_ui_shared/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import * as Monaco from './monaco';
import * as ace from './ace';
import * as GlobalFlyout from './global_flyout';

export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor';
export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor';

export { SectionLoading } from './components/section_loading';

Expand Down

0 comments on commit bba9d40

Please sign in to comment.