0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-01 02:41:39 -05:00

Cleaned up react component rendering

no issue

- switched from needing to extend from `ReactComponent` to using a `{{react-render}}` modifier
  - modifiers are modern idiomatic Ember for handing "did-insert" hooks and associated lifecycle
- moved code from `<ReactMobiledocEditor>` into `<KoenigReactEditor>`
  - no need for the extra layering of components and need to remember two places to modify when adding passthrough args/props
This commit is contained in:
Kevin Ansfield 2022-07-28 15:52:50 +01:00
parent bd194f4ccc
commit bbf7e8ed7a
5 changed files with 110 additions and 148 deletions

View file

@ -0,0 +1 @@
<div {{react-render this.ReactComponent}}></div>

View file

@ -1,17 +1,89 @@
import ReactComponent from './react-component';
import ReactMobiledocEditor from './react-mobiledoc-editor';
import {action} from '@ember/object';
import Component from '@glimmer/component';
import React, {Suspense} from 'react';
export default class KoenigReactEditor extends ReactComponent {
@action
renderComponent(element) {
this.reactRender(
element,
<ReactMobiledocEditor
mobiledoc={this.args.mobiledoc}
didCreateEditor={this.args.didCreateEditor}
onChange={this.args.onChange}
/>
);
class ErrorHandler extends React.Component {
state = {
hasError: false
};
static getDerivedStateFromError() {
return {hasError: true};
}
render() {
if (this.state.hasError) {
return (
<p>Loading has failed. Try refreshing the browser!</p>
);
}
return this.props.children;
}
}
const fetchKoenig = function () {
let status = 'pending';
let response;
const fetchPackage = async () => {
if (window.koenigEditor) {
return window.koenigEditor.default;
}
// the removal of `https://` and it's manual addition to the import template string is
// required to work around ember-auto-import complaining about an unknown dynamic import
// during the build step
const GhostAdmin = window.Ember.Namespace.NAMESPACES.find(ns => ns.name === 'ghost-admin');
const url = GhostAdmin.__container__.lookup('service:config').get('editor.url').replace('https://', '');
await import(`https://${url}`);
return window.koenigEditor.default;
};
const suspender = fetchPackage().then(
(res) => {
status = 'success';
response = res;
},
(err) => {
status = 'error';
response = err;
}
);
const read = () => {
switch (status) {
case 'pending':
throw suspender;
case 'error':
throw response;
default:
return response;
}
};
return {read};
};
const editorResource = fetchKoenig();
const Koenig = (props) => {
const _Koenig = editorResource.read();
return <_Koenig {...props} />;
};
export default class KoenigReactEditor extends Component {
ReactComponent = () => {
return (
<ErrorHandler>
<Suspense fallback={<p>Loading editor...</p>}>
<Koenig
mobiledoc={this.args.mobiledoc}
didCreateEditor={this.args.didCreateEditor}
onChange={this.args.onChange}
/>
</Suspense>
</ErrorHandler>
);
};
}

View file

@ -1,46 +0,0 @@
/* global ReactDOM */
import Component from '@glimmer/component';
import {action} from '@ember/object';
export default class ReactComponent extends Component {
@action
renderComponent() {
// eslint-disable-next-line
console.error('Components extending ReactComponent must implement a `renderComponent()` action that calls `this.reactRender()');
}
/**
* Renders a react component as the current ember element
* @param {React.Component} reactComponent. e.g., <HelloWorld />
*/
reactRender(element, reactComponent) {
if (element !== this.elem) {
this.unmountReactElement();
}
this.elem = element;
this.root = ReactDOM.createRoot(this.elem);
this.root.render(reactComponent);
}
/**
* Removes a mounted React component from the DOM and
* cleans up its event handlers and state.
*/
unmountReactElement() {
if (!this.root) {
return;
}
this.root.unmount();
}
/**
* Cleans up the rendered react component as the ember
* component gets destroyed
*/
willDestroy() {
super.willDestroy();
this.unmountReactElement();
}
}

View file

@ -1,88 +0,0 @@
import React, {Suspense} from 'react';
class ErrorHandler extends React.Component {
state = {
hasError: false
};
static getDerivedStateFromError() {
return {hasError: true};
}
render() {
if (this.state.hasError) {
return (
<p>Loading has failed. Try refreshing the browser!</p>
);
}
return this.props.children;
}
}
const fetchKoenig = function () {
let status = 'pending';
let response;
const fetchPackage = async () => {
if (window.koenigEditor) {
return window.koenigEditor.default;
}
// the removal of `https://` and it's manual addition to the import template string is
// required to work around ember-auto-import complaining about an unknown dynamic import
// during the build step
const GhostAdmin = window.Ember.Namespace.NAMESPACES.find(ns => ns.name === 'ghost-admin');
const url = GhostAdmin.__container__.lookup('service:config').get('editor.url').replace('https://', '');
await import(`https://${url}`);
return window.koenigEditor.default;
};
const suspender = fetchPackage().then(
(res) => {
status = 'success';
response = res;
},
(err) => {
status = 'error';
response = err;
}
);
const read = () => {
switch (status) {
case 'pending':
throw suspender;
case 'error':
throw response;
default:
return response;
}
};
return {read};
};
const editorResource = fetchKoenig();
const Koenig = (props) => {
const KoenigEditor = editorResource.read();
return <KoenigEditor {...props} />;
};
const ReactMobiledocEditor = (props) => {
return (
<ErrorHandler>
<Suspense fallback={<p>Loading editor...</p>}>
<Koenig
mobiledoc={props.mobiledoc}
didCreateEditor={props.didCreateEditor}
onChange={props.onChange}
/>
</Suspense>
</ErrorHandler>
);
};
export default ReactMobiledocEditor;

View file

@ -0,0 +1,23 @@
/* global ReactDOM */
import Modifier from 'ember-modifier';
import {createElement} from 'react';
export default class ReactRenderModifier extends Modifier {
didInstall() {
const [reactComponent] = this.args.positional;
const props = this.args.named;
if (!this.root) {
this.root = ReactDOM.createRoot(this.element);
}
this.root.render(createElement(reactComponent, {...props}));
}
willDestroy() {
if (!this.root) {
return;
}
this.root.unmount();
}
}