Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: memoize property in BasePropertyComponent
* sub-components are not rerender everytime * refactor BasePropertyComponent to separate files
- Loading branch information
1 parent
8c690f9
commit b7ac269
Showing
3 changed files
with
297 additions
and
267 deletions.
There are no files selected for viewing
138 changes: 138 additions & 0 deletions
138
src/frontend/components/property-type/base-property-component.doc.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
Component which renders properties in all the places in the AdminBro UI. By all the | ||
places I mean: | ||
|
||
- **list**: on the List, | ||
- **edit**: on default actions where user can modify the record like: {@link EditAction}, | ||
|
||
and {@link NewAction}, | ||
|
||
- **show**: on the default {@link ShowAction} where user can see the details of a record, | ||
- **filter**: and finally on the sidebar filter, | ||
|
||
Based on the type of given property and where the property is rendered **BasePropertyComponent** | ||
picks Component to use. That is how **date** fields are rendered as **datepicker** | ||
or **boolean** values as **checkbox**'es. | ||
|
||
### Overriding default render logic | ||
|
||
By default BasePropertyComponent will render corresponding | ||
component: input for string, DatePicker for dates etc. | ||
But you can override this by passing a custom component to {@link PropertyOptions}. | ||
|
||
Take a look at the following example: | ||
|
||
```javascript | ||
const AdminBro = require('admin-bro') | ||
const ResourceModel = require('./resource-model') | ||
const AdminBroOptions = { | ||
resources: [{ | ||
resource: ResourceModel | ||
options: { | ||
properties: { | ||
name: { | ||
components: { | ||
show: AdminBro.bundle('./my-react-component'), | ||
}, | ||
}, | ||
}, | ||
}, | ||
}], | ||
} | ||
``` | ||
|
||
In the example above we are altering how **name** property will look | ||
like on the Show action. We can define **my-react-component.jsx** like this: | ||
|
||
```jsx | ||
import React from 'react' | ||
import { InputGroup, Label } from '@admin-bro/design-system' | ||
* | ||
const MyReactComponent = props => { | ||
const { record, property } = props | ||
const value = record.params[property.name] | ||
return ( | ||
<InputGroup> | ||
<Label>{property.name}</Label> | ||
{value} [meters] | ||
</InputGroup> | ||
) | ||
} | ||
``` | ||
|
||
### Live example | ||
|
||
```reactComponent | ||
const booleanProperty = { | ||
isTitle: false, | ||
name: 'awesome', | ||
isId: false, | ||
position: -1, | ||
label: 'I am awesome', | ||
type: 'boolean', | ||
} | ||
const stringProperty = { | ||
isTitle: true, | ||
name: 'name', | ||
isId: false, | ||
position: -1, | ||
label: 'Name of a user', | ||
type: 'string', | ||
} | ||
// Resource is taken from the database | ||
const resource = { | ||
id: 'User', | ||
name: 'User Model', | ||
titleProperty: 'name', | ||
resourceActions: [], | ||
listProperties: [booleanProperty, stringProperty], | ||
editProperties: [booleanProperty, stringProperty], | ||
showProperties: [booleanProperty, stringProperty], | ||
filterProperties: [booleanProperty, stringProperty], | ||
} | ||
const initialRecord = { | ||
id: '1', | ||
title: 'John', | ||
params: { | ||
name: 'John', | ||
gender: 'male', | ||
}, | ||
errors: {}, | ||
recordActions: [], | ||
} | ||
const Wrapper = () => { | ||
const { record, handleChange, submit } = useRecord(initialRecord, resource.id) | ||
const params = JSON.stringify(record.params) | ||
return ( | ||
<Box py="lg"> | ||
<BasePropertyComponent | ||
property={booleanProperty} | ||
resource={resource} | ||
onChange={handleChange} | ||
where="edit" | ||
record={record} | ||
/> | ||
<BasePropertyComponent | ||
property={stringProperty} | ||
resource={resource} | ||
onChange={handleChange} | ||
where="edit" | ||
record={record} | ||
/> | ||
<Box> | ||
<Label>Params:</Label> | ||
{params} | ||
</Box> | ||
<Box my="lg"> | ||
<Button variant="primary" onClick={submit}>Submit</Button> | ||
<Text variant="sm"> | ||
This will throw an error because there is no AdminBro instance running | ||
</Text> | ||
</Box> | ||
</Box> | ||
) | ||
} | ||
return (<Wrapper />) | ||
``` |
136 changes: 136 additions & 0 deletions
136
src/frontend/components/property-type/base-property-component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import React, { useMemo } from 'react' | ||
import { ReactComponentLike } from 'prop-types' | ||
import { Box } from '@admin-bro/design-system' | ||
|
||
import ErrorBoundary from '../app/error-boundary' | ||
|
||
import * as ArrayType from './array' | ||
import * as MixedType from './mixed' | ||
|
||
import * as defaultType from './default-type' | ||
import * as boolean from './boolean' | ||
import * as datetime from './datetime' | ||
import * as richtext from './richtext' | ||
import * as reference from './reference' | ||
import * as textarea from './textarea' | ||
import * as password from './password' | ||
import { BasePropertyComponentProps } from './base-property-props' | ||
import { PropertyType } from '../../../backend/adapters/property/base-property' | ||
import { PropertyJSON } from '../../interfaces' | ||
|
||
let globalAny: any = {} | ||
|
||
try { | ||
globalAny = window | ||
} catch (error) { | ||
if (error.message !== 'window is not defined') { | ||
throw error | ||
} | ||
} | ||
|
||
const types: Record<PropertyType, any> = { | ||
textarea, | ||
boolean, | ||
datetime, | ||
reference, | ||
password, | ||
date: datetime, | ||
richtext, | ||
string: defaultType, | ||
number: defaultType, | ||
float: defaultType, | ||
mixed: null, | ||
} | ||
|
||
/** | ||
* @load ./base-property-component.doc.md | ||
* @component | ||
* @name BasePropertyComponent | ||
* @subcategory Application | ||
* @class | ||
* @hideconstructor | ||
*/ | ||
const BasePropertyComponent: React.FC<BasePropertyComponentProps> = (props) => { | ||
const { property: baseProperty, resource, record, filter, where, onChange } = props | ||
|
||
const property: PropertyJSON = useMemo(() => ({ | ||
...baseProperty, | ||
// we fill the path if it is not there. That is why all the actual Component Renderers are | ||
// called with the path set to this root path. Next mixed and array components adds to this | ||
// path either index (for array) or subProperty name. | ||
path: (baseProperty as PropertyJSON).path || baseProperty.propertyPath, | ||
}), [baseProperty]) | ||
|
||
const testId = `property-${where}-${property.path}` | ||
|
||
let Component: ReactComponentLike = (types[property.type] && types[property.type][where]) | ||
|| defaultType[where] | ||
|
||
if (property.components && property.components[where]) { | ||
const component = property.components[where] | ||
if (!component) { | ||
throw new Error(`there is no "${property.path}.components.${where}"`) | ||
} | ||
Component = globalAny.AdminBro.UserComponents[component] | ||
return ( | ||
<ErrorBoundary> | ||
<Box data-testid={testId}> | ||
<Component | ||
property={property} | ||
resource={resource} | ||
record={record} | ||
filter={filter} | ||
onChange={onChange} | ||
where={where} | ||
/> | ||
</Box> | ||
</ErrorBoundary> | ||
) | ||
} | ||
|
||
const Array = ArrayType[where] | ||
const Mixed = MixedType[where] | ||
|
||
if (baseProperty.isArray) { | ||
if (!Array) { return (<div />) } | ||
return ( | ||
<Array | ||
{...props} | ||
property={property} | ||
ItemComponent={BasePropertyComponent} | ||
testId={testId} | ||
/> | ||
) | ||
} | ||
|
||
if (baseProperty.type === 'mixed') { | ||
if (!Mixed) { return (<div />) } | ||
return ( | ||
<Mixed | ||
{...props} | ||
property={property} | ||
ItemComponent={BasePropertyComponent} | ||
testId={testId} | ||
/> | ||
) | ||
} | ||
|
||
return ( | ||
<ErrorBoundary> | ||
<Box data-testid={testId}> | ||
<Component | ||
property={property} | ||
resource={resource} | ||
record={record} | ||
filter={filter} | ||
onChange={onChange} | ||
where={where} | ||
/> | ||
</Box> | ||
</ErrorBoundary> | ||
) | ||
} | ||
export { | ||
BasePropertyComponent as default, | ||
BasePropertyComponent, | ||
} |
Oops, something went wrong.