refactor(nucleus): use portals when mounting entry components

This commit is contained in:
Miralem Drek
2019-05-08 17:23:31 +02:00
parent 1836dfbcc2
commit 335e800c07
10 changed files with 251 additions and 174 deletions

View File

@@ -0,0 +1,75 @@
import React, {
useMemo,
} from 'react';
import ReactDOM from 'react-dom';
import {
createTheme,
ThemeProvider,
StylesProvider,
createGenerateClassName,
} from '@nebula.js/ui/theme';
const THEME_PREFIX = (process.env.NEBULA_VERSION || '').replace(/[.-]/g, '_');
let counter = 0;
function App({
children,
}) {
const { theme, generator } = useMemo(() => ({
theme: createTheme(),
generator: createGenerateClassName({
productionPrefix: `${THEME_PREFIX}-`,
disableGlobal: true,
seed: `nebulajs-${counter++}`,
}),
}), []);
return (
<StylesProvider generateClassName={generator}>
<ThemeProvider theme={theme}>
<>
{children}
</>
</ThemeProvider>
</StylesProvider>
);
}
export default function boot({
app,
}) {
const element = document.createElement('div');
element.style.display = 'none';
element.setAttribute('data-nebulajs-version', process.env.NEBULA_VERSION || '');
element.setAttribute('data-app-id', app.id);
document.body.appendChild(element);
const components = [];
const update = () => {
ReactDOM.render(
<App app={app}>{components}</App>,
element,
);
};
// const unmount = () => {
// ReactDOM.unmountComponentAtNode(element);
// };
update();
return {
add(component) {
components.push(component);
update();
},
remove(component) {
const idx = components.indexOf(component);
if (idx !== -1) {
components.splice(idx, 1);
}
},
};
}

View File

@@ -1,14 +1,8 @@
import React, {
useMemo,
useEffect,
useState,
} from 'react';
import {
createTheme,
ThemeProvider,
StylesProvider,
createGenerateClassName,
} from '@nebula.js/ui/theme';
import {
Grid,
} from '@nebula.js/ui/components';
@@ -20,12 +14,6 @@ import Footer from './Footer';
import Supernova from './Supernova';
import Placeholder from './Placeholder';
const generateClassName = createGenerateClassName({
productionPrefix: 'cell-',
disableGlobal: true,
seed: 'nebula',
});
const showRequirements = (sn, layout) => {
if (!sn || !sn.generator || !sn.generator.qae || !layout || !layout.qHyperCube) {
return false;
@@ -58,40 +46,50 @@ const Content = ({ children }) => (
);
export default function Cell({
objectProps,
userProps,
themeDefinition = {},
api,
}) {
const theme = useMemo(() => createTheme(themeDefinition), [JSON.stringify(themeDefinition)]);
const SN = (showRequirements(objectProps.sn, objectProps.layout) ? Requirements : Supernova);
const Comp = !objectProps.sn ? Placeholder : SN;
const err = objectProps.error || false;
const [state, setState] = useState({
objectProps: api.objectProps(),
userProps: api.userProps(),
});
useEffect(() => {
const onChanged = () => {
setState({
objectProps: api.objectProps(),
userProps: api.userProps(),
});
};
api.on('changed', onChanged);
return () => {
api.removeListener('changed', onChanged);
};
}, [api]);
const SN = (showRequirements(state.objectProps.sn, state.objectProps.layout) ? Requirements : Supernova);
const Comp = !state.objectProps.sn ? Placeholder : SN;
const err = state.objectProps.error || false;
return (
<StylesProvider generateClassName={generateClassName}>
<ThemeProvider theme={theme}>
<Grid container direction="column" spacing={0} style={{ height: '100%', padding: '8px', boxSixing: 'borderBox' }}>
<Grid item style={{ maxWidth: '100%' }}>
<Header layout={objectProps.layout} sn={objectProps.sn}>&nbsp;</Header>
</Grid>
<Grid item xs>
<Content>
{err
? (<CError {...err} />)
: (
<Comp
key={objectProps.layout.visualization}
sn={objectProps.sn}
snContext={userProps.context}
snOptions={userProps.options}
layout={objectProps.layout}
/>
)
}
</Content>
</Grid>
<Footer layout={objectProps.layout} />
</Grid>
</ThemeProvider>
</StylesProvider>
<Grid container direction="column" spacing={0} style={{ height: '100%', padding: '8px', boxSixing: 'borderBox' }}>
<Grid item style={{ maxWidth: '100%' }}>
<Header layout={state.objectProps.layout} sn={state.objectProps.sn}>&nbsp;</Header>
</Grid>
<Grid item xs>
<Content>
{err
? (<CError {...err} />)
: (
<Comp
key={state.objectProps.layout.visualization}
sn={state.objectProps.sn}
snContext={state.userProps.context}
snOptions={state.userProps.options}
layout={state.objectProps.layout}
/>
)
}
</Content>
</Grid>
<Footer layout={state.objectProps.layout} />
</Grid>
);
}

View File

@@ -15,14 +15,14 @@ const Header = ({
const showSubtitle = layout && layout.showTitles && !!layout.subtitle;
const showInSelectionActions = sn && layout && layout.qSelectionInfo && layout.qSelectionInfo.qInSelections;
return (
<Grid container wrap="nowrap" style={{ flexGrow: 0, flexWrap: 'nowrap' }}>
<Grid item wrap="nowrap" style={{ flexGrow: 1, minWidth: 0 }}>
<Grid container direction="column" style={{ flexGrow: 1, flexWrap: 'nowrap', minWidth: 0 }}>
<Grid container wrap="nowrap" style={{ flexGrow: 0 }}>
<Grid item zeroMinWidth xs>
<Grid container wrap="nowrap" direction="column">
{showTitle && (<Typography variant="h6" noWrap>{layout.title}</Typography>)}
{showSubtitle && (<Typography variant="body2" noWrap>{layout.subtitle}</Typography>)}
</Grid>
</Grid>
<Grid item style={{ flexWrap: 'nowrap', whiteSpace: 'nowrap', minHeight: '32px' }}>
<Grid item style={{ whiteSpace: 'nowrap', minHeight: '32px' }}>
{showInSelectionActions && (<SelectionToolbar inline sn={sn} />)}
</Grid>
</Grid>

View File

@@ -5,32 +5,30 @@ import Cell from './Cell';
export default function boot({
element,
model,
}, props, config) {
ReactDOM.render(
api,
nebulaContext,
}) {
const {
root,
} = nebulaContext;
const portal = ReactDOM.createPortal(
<Cell
{...props}
api={api}
model={model}
/>,
element,
);
const unmount = () => {
ReactDOM.unmountComponentAtNode(element);
};
const update = (p) => {
ReactDOM.render(<Cell
{...p}
model={model}
/>, element);
root.remove(portal);
};
model.once('closed', unmount);
return config.env.Promise.resolve({
setProps(p) {
update(p);
},
unmount,
});
root.add(portal);
return () => {
unmount();
};
}

View File

@@ -1,6 +1,4 @@
import React, {
useMemo,
} from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import {
@@ -8,52 +6,39 @@ import {
} from '@nebula.js/ui/components';
import {
createTheme,
ThemeProvider,
StylesProvider,
createGenerateClassName,
useTheme,
} from '@nebula.js/ui/theme';
import SelectedFields from './SelectedFields';
import Nav from './Nav';
const generateClassName = createGenerateClassName({
productionPrefix: 'sel-',
disableGlobal: true,
seed: 'nebula',
});
export function AppSelections({
api,
}) {
const theme = useMemo(() => createTheme(), []);
const theme = useTheme();
return (
<StylesProvider generateClassName={generateClassName}>
<ThemeProvider theme={theme}>
<Grid
container
spacing={0}
wrap="nowrap"
style={{
backgroundColor: '#fff',
minHeight: '40px',
}}
>
<Grid
item
style={{
borderRight: `1px solid ${theme.palette.divider}`,
}}
>
<Nav api={api} />
</Grid>
<Grid item xs style={{ backgroundColor: '#E5E5E5', overflow: 'hidden' }}>
<SelectedFields api={api} />
</Grid>
</Grid>
</ThemeProvider>
</StylesProvider>
<Grid
container
spacing={0}
wrap="nowrap"
style={{
backgroundColor: '#fff',
minHeight: '40px',
}}
>
<Grid
item
style={{
borderRight: `1px solid ${theme.palette.divider}`,
}}
>
<Nav api={api} />
</Grid>
<Grid item xs style={{ backgroundColor: '#E5E5E5', overflow: 'hidden' }}>
<SelectedFields api={api} />
</Grid>
</Grid>
);
}
@@ -61,18 +46,10 @@ export default function mount({
element,
api,
}) {
ReactDOM.render(
return ReactDOM.createPortal(
<AppSelections
api={api}
/>,
element,
);
const unmount = () => {
ReactDOM.unmountComponentAtNode(element);
};
return () => {
unmount();
};
}

View File

@@ -1,5 +1,10 @@
/* eslint no-underscore-dangle:0 */
import { createAppSelectionAPI } from './selections';
import App from './components/App';
import AppSelectionsPortal from './components/selections/AppSelections';
import create from './object/create-object';
import get from './object/get-object';
import types from './sn/types';
@@ -17,14 +22,22 @@ function apiGenerator(app) {
load: () => undefined,
};
const root = App({
app,
});
const context = {
nebbie: null,
app,
config,
logger: lgr,
types: types(config),
root,
};
let selectionsApi = null;
let selectionsComponentReference = null;
const api = {
get: (getCfg, userProps) => get(getCfg, userProps, context),
create: (createCfg, userProps) => create(createCfg, userProps, context),
@@ -36,7 +49,31 @@ function apiGenerator(app) {
config.load = $;
return api;
},
selections: () => app._selections, // eslint-disable-line no-underscore-dangle
selections: () => {
if (!selectionsApi) {
selectionsApi = {
...app._selections, // eslint-disable-line no-underscore-dangle
mount(element) {
if (selectionsComponentReference) {
console.error('Already mounted');
return;
}
selectionsComponentReference = AppSelectionsPortal({
element,
api: app._selections,
});
root.add(selectionsComponentReference);
},
unmount() {
if (selectionsComponentReference) {
root.remove(selectionsComponentReference);
selectionsComponentReference = null;
}
},
};
}
return selectionsApi;
},
types: context.types,
};

View File

@@ -19,7 +19,7 @@ export default function initiate(getCfg, optional, context) {
return context.app.getObject(getCfg.id).then((model) => {
const viz = vizualizationAPI({
model,
config: context.config,
context,
});
const objectAPI = new ObjectAPI(model, context, viz);

View File

@@ -1,6 +1,6 @@
/* eslint no-underscore-dangle: 0 */
import eventmixin from './event-mixin';
import visual from '../components/selections/AppSelections';
// import visual from '../components/selections/AppSelections';
import modelCache from '../object/model-cache';
import { observe } from '../object/observer';
@@ -13,7 +13,7 @@ const create = (app) => {
let canClear = false;
let modalObject;
let mounted;
// let mounted;
let lyt;
const api = {
model: app,
@@ -75,21 +75,6 @@ const create = (app) => {
clearField(field, state = '$') {
return app.getField(field, state).then(f => f.clear());
},
mount(element) {
if (mounted) {
console.error('Already mounted');
return;
}
mounted = visual({
element,
api,
});
},
unmount() {
if (mounted) {
mounted();
}
},
};
eventmixin(api);

View File

@@ -3,17 +3,17 @@ import cell from './components/boot';
import { get } from './object/observer';
import getPatches from './utils/patcher';
const noopi = Promise.resolve({
setProps() {},
unmount() {},
});
import eventMixin from './selections/event-mixin';
const noopi = () => {};
export default function ({
model,
config,
context,
initialUserProps = {},
} = {}) {
let c = noopi;
let reference = noopi;
let elementReference = null;
let userProps = {
options: {},
@@ -29,6 +29,17 @@ export default function ({
error: null,
};
const cellApi = {
userProps() {
return userProps;
},
objectProps() {
return objectProps;
},
};
eventMixin(cellApi);
let queueShow = false;
const mount = (element) => {
@@ -36,13 +47,13 @@ export default function ({
queueShow = element;
return;
}
c = cell({
elementReference = element;
reference = cell({
element,
model,
}, {
userProps,
objectProps,
}, config);
api: cellApi,
nebulaContext: context,
});
};
const update = () => {
@@ -50,10 +61,7 @@ export default function ({
mount(queueShow);
queueShow = false;
}
c.then(x => x.setProps({
objectProps,
userProps,
}));
cellApi.emit('changed');
};
const setUserProps = (up) => {
@@ -82,8 +90,8 @@ export default function ({
close() {
// TODO - destroy session object (if created as such)
model.emit('closed');
c.then(x => x.unmount());
c = noopi;
reference();
reference = noopi;
},
setTemporaryProperties(props) {
return get(model, 'effectiveProperties').then((current) => {
@@ -107,34 +115,32 @@ export default function ({
return api;
},
takeSnapshot() {
return c.then((x) => {
if (x.reference) {
const content = x.reference.querySelector('.nebulajs-sn');
if (content) {
const rect = content.getBoundingClientRect();
if (objectProps.sn) {
const snapshot = {
...objectProps.layout,
snapshotData: {
object: {
size: {
w: rect.width,
h: rect.height,
},
if (elementReference) {
const content = elementReference.querySelector('.nebulajs-sn');
if (content) {
const rect = content.getBoundingClientRect();
if (objectProps.sn) {
const snapshot = {
...objectProps.layout,
snapshotData: {
object: {
size: {
w: rect.width,
h: rect.height,
},
},
};
},
};
if (typeof objectProps.sn.component.setSnapshotData === 'function') {
return objectProps.sn.component.setSnapshotData(snapshot);
}
return Promise.resolve(snapshot);
if (typeof objectProps.sn.component.setSnapshotData === 'function') {
return objectProps.sn.component.setSnapshotData(snapshot);
}
return Promise.reject(new Error('No content'));
return Promise.resolve(snapshot);
}
return Promise.reject(new Error('No content'));
}
return Promise.reject(new Error('Not mounted yet'));
});
}
return Promise.reject(new Error('Not mounted yet'));
},
// QVisualization API

View File

@@ -91,6 +91,7 @@ const config = (isEsm) => {
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify(isEsm ? 'development' : 'production'),
'process.env.NEBULA_VERSION': JSON.stringify(version),
}),
nodeResolve({
extensions: ['.js', '.jsx'],