ReactからAceEditorを使ってリッチなエディタを表示する
これはなに
リッチなエディタとしてajaxorg/aceというものがあり、Reactから使おうとするとsecuringsincity/react-aceが便利。
react-aceが提供するコンポーネントをラップするような独自のコンポーネントを実装する。
具体的には、以下の三点。
- ファイルタイプに応じて動的にrequireする
- React hooksを使う
- readonlyモードを用意する
実装
大した量でもないので実装を貼り付ける
import * as React from 'react'; import AceEditor from 'react-ace'; // AceEditor周りで使うデータ export type EditorProps = { fileType: string; contents: string; readOnly: boolean; onChange?: (content: string) => void; theme?: string; }; const MIN_LINE = 10; const MAX_LINE = 50; const DEFAULT_THEME = 'monokai'; export const EditorComponent = (props: EditorProps) => { const theme = props.theme || DEFAULT_THEME; // load theme React.useMemo(() => { try { require(`ace-builds/src-noconflict/theme-${theme}`); } catch (e) { console.log(`error new theme(${theme}): ${e}`); } }, [props.theme]); // load mode React.useMemo(() => { if (props.fileType == null) { return; } try { require(`ace-builds/src-noconflict/mode-${props.fileType}`); } catch (e) { console.log(`error new mode(${props.fileType}): ${e}`); } }, [props.fileType]); const onChange: (string) => void = props.onChange ? props.onChange : () => {}; if (props.readOnly) { return ( <AceEditor mode={props.fileType} theme={theme} value={props.contents} width={null} // nullにしておくとwidthを外から変えられる minLines={MIN_LINE} maxLines={MAX_LINE} readOnly={true} focus={false} highlightActiveLine={false} enableBasicAutocompletion={false} onLoad={editor => { // readOnlyでも表示されるカーソルを消す editor.renderer.$cursorLayer.element.style.opacity = 0; }} /> ); } else { return ( <AceEditor mode={props.fileType} theme={theme} value={props.contents} width={null} minLines={MIN_LINE} maxLines={MAX_LINE} readOnly={props.readOnly} enableBasicAutocompletion={true} onChange={onChange} /> ); } };
動的なtheme, modeの読み込み
aceで用意されているthemeやmodeはここから見ることが出来る。 https://github.com/ajaxorg/ace-builds/tree/master/src-min-noconflict
本当にこれでいいのかはよくわからんが、ace-builds/src-noconflict/
配下をrequireすることで動的に読み込み出来ている様子。
そしてtheme, modeの読み込みにはReact.useMemo
を使用しているが、初期化が遅いとかではなく副作用を起こすようなものなのだからReact.useEffect
の方が適切では?と思うかもしれないが、実行タイミングが異なり、useMemo
は同期的だがuseEffect
はコンポーネントの初期化が終わったあとに実行される。
ドキュメントにもあるように従来のcomponentDidMount
と同じに思えばよい。
そのためAceEditor
コンポーネントをレンダリングする前にthemeやmodeは読み込み終わっていてほしいため、useEffect
だとタイミングが遅すぎるため、useMemo
(あるいはuseContext
)の方が適切となる。
readonlyモード
readOnly
にtrue
を与えればreadonlyにはなるが、それは単に更新は出来ないだけでカーソルはフォーカス出来てしまう。
そこでonLoad
でstyleを弄って透明にしてしまうことで見た目上はフォーカスされていないように見せかけることで対応。
readOnly={true} focus={false} highlightActiveLine={false} onLoad={editor => { // readOnlyでも表示されるカーソルを消す editor.renderer.$cursorLayer.element.style.opacity = 0; }}
随分古いissueのコメントを参考にした。 Option to make editor disabled · Issue #266 · ajaxorg/ace