Skip to content

Commit

Permalink
fix: memoize property in BasePropertyComponent
Browse files Browse the repository at this point in the history
* sub-components are not rerender everytime
* refactor BasePropertyComponent to separate files
  • Loading branch information
wojtek-krysiak committed Oct 31, 2020
1 parent 8c690f9 commit b7ac269
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 267 deletions.
138 changes: 138 additions & 0 deletions src/frontend/components/property-type/base-property-component.doc.md
@@ -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 src/frontend/components/property-type/base-property-component.tsx
@@ -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,
}

0 comments on commit b7ac269

Please sign in to comment.