Skip to content

Commit

Permalink
Automatically wrap non-layout components in box containers (#804)
Browse files Browse the repository at this point in the history
* Automatically wrap non-layout components in box containers

* Fix controls spacing

* Better names

* Start with prop default value if available

* Fix default values with bindings

* Fix layout box bindings

* Better layout prop names

* Fix type errors

* Improve layout prop interface and layout alignment prop names

* Prevent binding collisions and simplify layour prop names

* Fix binding id
  • Loading branch information
apedroferreira authored Aug 22, 2022
1 parent 7ea4eaa commit fffed02
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 221 deletions.
3 changes: 3 additions & 0 deletions packages/toolpad-app/src/appDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SecretAttrValue,
} from '@mui/toolpad-core';
import invariant from 'invariant';
import { BoxProps } from '@mui/material';
import { ConnectionStatus, AppTheme } from './types';
import { omit, update, updateOrCreate } from './utils/immutability';
import { camelCase, generateUniqueString, removeDiacritics } from './utils/strings';
Expand Down Expand Up @@ -91,6 +92,8 @@ export interface ElementNode<P = any> extends AppDomNodeBase {
};
readonly props?: BindableAttrValues<P>;
readonly layout?: {
readonly horizontalAlign?: ConstantAttrValue<BoxProps['justifyContent']>;
readonly verticalAlign?: ConstantAttrValue<BoxProps['alignItems']>;
readonly columnSize?: ConstantAttrValue<number>;
};
}
Expand Down
63 changes: 57 additions & 6 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Stack,
CssBaseline,
Alert,
Box,
styled,
AlertTitle,
LinearProgress,
Expand Down Expand Up @@ -42,7 +43,12 @@ import { pick } from 'lodash-es';
import * as appDom from '../appDom';
import { VersionOrPreview } from '../types';
import { createProvidedContext } from '../utils/react';
import { getElementNodeComponentId, isPageRow, PAGE_ROW_COMPONENT_ID } from '../toolpadComponents';
import {
getElementNodeComponentId,
isPageLayoutComponent,
isPageRow,
PAGE_ROW_COMPONENT_ID,
} from '../toolpadComponents';
import AppOverview from './AppOverview';
import AppThemeProvider from './AppThemeProvider';
import evalJsBindings, {
Expand All @@ -57,6 +63,7 @@ import usePageTitle from '../utils/usePageTitle';
import ComponentsContext, { useComponents, useComponent } from './ComponentsContext';
import { AppModulesProvider, useAppModules } from './AppModulesProvider';
import Pre from '../components/Pre';
import { layoutBoxArgTypes } from '../toolpadComponents/layoutBox';

const USE_DATA_QUERY_CONFIG_KEYS: readonly (keyof UseDataQueryConfig)[] = [
'enabled',
Expand Down Expand Up @@ -161,6 +168,9 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
const componentConfig = Component[TOOLPAD_COMPONENT];
const { argTypes, errorProp, loadingProp, loadingPropSource } = componentConfig;

const isLayoutNode =
appDom.isPage(node) || (appDom.isElement(node) && isPageLayoutComponent(node));

const liveBindings = useBindingsContext();
const boundProps: Record<string, any> = React.useMemo(() => {
const loadingPropSourceSet = new Set(loadingPropSource);
Expand Down Expand Up @@ -203,6 +213,24 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
return hookResult;
}, [argTypes, errorProp, liveBindings, loadingProp, loadingPropSource, nodeId]);

const boundLayoutProps: Record<string, any> = React.useMemo(() => {
const hookResult: Record<string, any> = {};

for (const [propName, argType] of isLayoutNode ? [] : Object.entries(layoutBoxArgTypes)) {
const bindingId = `${nodeId}.layout.${propName}`;
const binding = liveBindings[bindingId];
if (binding) {
hookResult[propName] = binding.value;
}

if (typeof hookResult[propName] === 'undefined' && argType) {
hookResult[propName] = argType.defaultValue;
}
}

return hookResult;
}, [isLayoutNode, liveBindings, nodeId]);

const onChangeHandlers: Record<string, (param: any) => void> = React.useMemo(
() =>
mapProperties(argTypes, ([key, argType]) => {
Expand All @@ -229,7 +257,7 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
return null;
}

const action = node.props?.[key];
const action = (node as appDom.ElementNode).props?.[key];

if (action?.type === 'navigationAction') {
const handler = () => {
Expand Down Expand Up @@ -260,7 +288,7 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
childNodes.map((child) => <RenderedNode key={child.id} nodeId={child.id} />),
);

const layoutProps = React.useMemo(() => {
const layoutElementProps = React.useMemo(() => {
if (appDom.isElement(node) && isPageRow(node)) {
return {
layoutColumnSizes: childNodeGroups.children.map((child) => child.layout?.columnSize?.value),
Expand All @@ -274,10 +302,10 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
...boundProps,
...onChangeHandlers,
...eventHandlers,
...layoutProps,
...layoutElementProps,
...reactChildren,
};
}, [boundProps, eventHandlers, layoutProps, onChangeHandlers, reactChildren]);
}, [boundProps, eventHandlers, layoutElementProps, onChangeHandlers, reactChildren]);

// Wrap with slots
for (const [propName, argType] of Object.entries(argTypes)) {
Expand All @@ -294,7 +322,19 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC

return (
<NodeRuntimeWrapper nodeId={nodeId} componentConfig={Component[TOOLPAD_COMPONENT]}>
<Component {...props} />
{isLayoutNode ? (
<Component {...props} />
) : (
<Box
sx={{
display: 'flex',
alignItems: boundLayoutProps.verticalAlign,
justifyContent: boundLayoutProps.horizontalAlign,
}}
>
<Component {...props} />
</Box>
)}
</NodeRuntimeWrapper>
);
}
Expand Down Expand Up @@ -437,6 +477,17 @@ function parseBindings(
}
}
}

if (!isPageLayoutComponent(elm)) {
for (const [propName, argType] of Object.entries(layoutBoxArgTypes)) {
const binding =
elm.layout?.[propName as keyof typeof layoutBoxArgTypes] ||
appDom.createConst(argType?.defaultValue ?? undefined);
const bindingId = `${elm.id}.layout.${propName}`;
const scopePath = `${elm.name}.@layout.${propName}`;
parsedBindingsMap.set(bindingId, parseBinding(binding, { scopePath }));
}
}
}

if (appDom.isQuery(elm)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import ErrorAlert from './ErrorAlert';
import NodeNameEditor from '../NodeNameEditor';
import { useToolpadComponent } from '../toolpadComponents';
import { getElementNodeComponentId } from '../../../toolpadComponents';
import {
layoutBoxArgTypes,
LAYOUT_DIRECTION_BOTH,
LAYOUT_DIRECTION_HORIZONTAL,
LAYOUT_DIRECTION_VERTICAL,
} from '../../../toolpadComponents/layoutBox';

const classes = {
control: 'Toolpad_Control',
Expand All @@ -35,8 +41,46 @@ interface ComponentPropsEditorProps<P> {
}

function ComponentPropsEditor<P>({ componentConfig, node }: ComponentPropsEditorProps<P>) {
const { layoutDirection } = componentConfig;

const hasLayoutHorizontalControls =
layoutDirection === LAYOUT_DIRECTION_HORIZONTAL || layoutDirection === LAYOUT_DIRECTION_BOTH;
const hasLayoutVerticalControls =
layoutDirection === LAYOUT_DIRECTION_VERTICAL || layoutDirection === LAYOUT_DIRECTION_BOTH;
const hasLayoutControls = hasLayoutHorizontalControls || hasLayoutVerticalControls;

return (
<ComponentPropsEditorRoot>
{hasLayoutControls ? (
<React.Fragment>
<Typography variant="subtitle2" sx={{ mt: 1 }}>
Layout:
</Typography>
{hasLayoutHorizontalControls ? (
<div className={classes.control}>
<NodeAttributeEditor
node={node}
namespace="layout"
name="horizontalAlign"
argType={layoutBoxArgTypes.horizontalAlign}
/>
</div>
) : null}
{hasLayoutVerticalControls ? (
<div className={classes.control}>
<NodeAttributeEditor
node={node}
namespace="layout"
name="verticalAlign"
argType={layoutBoxArgTypes.verticalAlign}
/>
</div>
) : null}
</React.Fragment>
) : null}
<Typography variant="subtitle2" sx={{ mt: hasLayoutControls ? 2 : 1 }}>
Properties:
</Typography>
{(Object.entries(componentConfig.argTypes) as ExactEntriesOf<ArgTypeDefinitions<P>>).map(
([propName, propTypeDef]) =>
propTypeDef && shouldRenderControl(propTypeDef) ? (
Expand Down Expand Up @@ -78,9 +122,6 @@ function SelectedNodeEditor({ node }: SelectedNodeEditorProps) {
{nodeError ? <ErrorAlert error={nodeError} /> : null}
{node ? (
<React.Fragment>
<Typography variant="subtitle1" sx={{ mt: 2 }}>
Properties:
</Typography>
<ComponentPropsEditor componentConfig={componentConfig} node={node} />
</React.Fragment>
) : null}
Expand Down
30 changes: 30 additions & 0 deletions packages/toolpad-app/src/toolpadComponents/layoutBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BoxProps } from '@mui/material';
import { ArgTypeDefinition } from '@mui/toolpad-core';

export const LAYOUT_DIRECTION_HORIZONTAL = 'horizontal';
export const LAYOUT_DIRECTION_VERTICAL = 'vertical';
export const LAYOUT_DIRECTION_BOTH = 'both';

export const layoutBoxArgTypes: {
horizontalAlign: ArgTypeDefinition<BoxProps['justifyContent']>;
verticalAlign: ArgTypeDefinition<BoxProps['alignItems']>;
} = {
horizontalAlign: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Horizontal alignment',
control: { type: 'HorizontalAlign' },
defaultValue: 'start',
},
verticalAlign: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Vertical alignment',
control: { type: 'VerticalAlign' },
defaultValue: 'center',
},
};
36 changes: 3 additions & 33 deletions packages/toolpad-components/src/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import * as React from 'react';
import { LoadingButton as MuiButton, LoadingButtonProps as MuiButtonProps } from '@mui/lab';
import { createComponent } from '@mui/toolpad-core';
import { Box, BoxProps } from '@mui/material';

interface ButtonProps extends Omit<MuiButtonProps, 'children'> {
content: string;
alignItems?: BoxProps['alignItems'];
justifyContent?: BoxProps['justifyContent'];
}

function Button({ content, alignItems, justifyContent, ...props }: ButtonProps) {
return (
<Box
sx={{
display: 'flex',
alignItems,
justifyContent,
}}
>
<MuiButton {...props}>{content}</MuiButton>
</Box>
);
function Button({ content, ...rest }: ButtonProps) {
return <MuiButton {...rest}>{content}</MuiButton>;
}

export default createComponent(Button, {
layoutDirection: 'both',
argTypes: {
content: {
typeDef: { type: 'string' },
Expand All @@ -39,24 +27,6 @@ export default createComponent(Button, {
disabled: {
typeDef: { type: 'boolean' },
},
alignItems: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Vertical alignment',
control: { type: 'VerticalAlign' },
defaultValue: 'center',
},
justifyContent: {
typeDef: {
type: 'string',
enum: ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'],
},
label: 'Horizontal alignment',
control: { type: 'HorizontalAlign' },
defaultValue: 'start',
},
fullWidth: {
typeDef: { type: 'boolean' },
},
Expand Down
42 changes: 20 additions & 22 deletions packages/toolpad-components/src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,28 +252,26 @@ const DataGridComponent = React.forwardRef(function DataGridComponent(
const columns: GridColumns = columnsProp || EMPTY_COLUMNS;

return (
<Box>
<div ref={ref} style={{ height: heightProp, minHeight: '100%', width: '100%' }}>
<DataGridPro
components={{ Toolbar: GridToolbar, LoadingOverlay: SkeletonLoadingOverlay }}
onColumnResize={handleResize}
onColumnOrderChange={handleColumnOrderChange}
rows={rows}
columns={columns}
key={rowIdFieldProp}
getRowId={getRowId}
onSelectionModelChange={onSelectionModelChange}
selectionModel={selectionModel}
error={errorProp}
componentsProps={{
errorOverlay: {
message: typeof errorProp === 'string' ? errorProp : errorProp?.message,
},
}}
{...props}
/>
</div>
</Box>
<div ref={ref} style={{ height: heightProp, minHeight: '100%', width: '100%' }}>
<DataGridPro
components={{ Toolbar: GridToolbar, LoadingOverlay: SkeletonLoadingOverlay }}
onColumnResize={handleResize}
onColumnOrderChange={handleColumnOrderChange}
rows={rows}
columns={columns}
key={rowIdFieldProp}
getRowId={getRowId}
onSelectionModelChange={onSelectionModelChange}
selectionModel={selectionModel}
error={errorProp}
componentsProps={{
errorOverlay: {
message: typeof errorProp === 'string' ? errorProp : errorProp?.message,
},
}}
{...props}
/>
</div>
);
});

Expand Down
Loading

0 comments on commit fffed02

Please sign in to comment.