Skip to content

Commit

Permalink
feat(use-component): hooks replacement for attach (#277)
Browse files Browse the repository at this point in the history
* feat(use-component): hooks replacement for attach

* feat(use-component): initial props from component

* test(use-component): happy path

* test(use-component): change sequentially

* fix(use-component): tune state management

* fix(use-component): merge with existing props

* test(use-component): update component

* fix(use-component): initialize with component values

* test(use-component): change component

* test(use-component): listener removed on unmount

* fix(use-component): prevent rerendering of hasComponent

* fix(use-component): useRef to avoid dep in useEffect

* feat(demo): custom component attach
  • Loading branch information
redgeoff authored Aug 16, 2021
1 parent 7cc4be9 commit 675da97
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 6 deletions.
6 changes: 6 additions & 0 deletions src/demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
CustomComponentNoProps,
ReactCustomComponentNoProps,
} from './components/custom-component-no-props';
import {
CustomComponentAttach,
ReactCustomComponentAttach,
} from './components/custom-component-attach';

// Set the site key when using the ReCAPTCHAField
globals.set({ reCAPTCHASiteKey: '6LdIbGMUAAAAAJnipR9t-SnWzCbn0ZX2myXBIauh' });
Expand All @@ -38,6 +42,8 @@ compiler.registerComponent('CustomComponentJS', CustomComponentJS);
uiComponents.CustomComponentJS = ReactCustomComponentJS;
compiler.registerComponent('CustomComponentNoProps', CustomComponentNoProps);
uiComponents.CustomComponentNoProps = ReactCustomComponentNoProps;
compiler.registerComponent('CustomComponentAttach', CustomComponentAttach);
uiComponents.CustomComponentAttach = ReactCustomComponentAttach;

// Register all the components
for (let name in components) {
Expand Down
9 changes: 9 additions & 0 deletions src/demo/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ const app = {
component: 'CustomComponentNoProps',
},
},
{
path: '/custom-components/attach',
label: 'Custom Component Attach',
content: {
component: 'CustomComponentAttach',
name: 'my-custom-component-attach',
label: 'My Custom Component Attach',
},
},
{
path: '/custom-components/contact-no-mson',
label: 'Contact No MSON',
Expand Down
39 changes: 39 additions & 0 deletions src/demo/components/custom-component-attach.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import attach from '../../attach';
import Typography from '@material-ui/core/Typography';
import compile from 'mson/lib/compiler/compile';

const CustomComponentAttach = compile({
component: 'UIComponent',
name: 'CustomComponent',
schema: {
component: 'Form',
fields: [
{
component: 'TextField',
name: 'name',
},
{
component: 'TextField',
name: 'label',
},
],
},
});

let ReactCustomComponentAttach = (props) => {
const { name, label } = props;
return (
<div>
<Typography variant="h3">Name: {name}</Typography>
<Typography variant="h4">Label: {label}</Typography>
</div>
);
};

// Bind React props to MSON component props
ReactCustomComponentAttach = attach(['name', 'label'])(
ReactCustomComponentAttach
);

export { CustomComponentAttach, ReactCustomComponentAttach };
9 changes: 3 additions & 6 deletions src/demo/components/custom-component.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import attach from '../../attach';
import useComponent from '../../use-component';
import Typography from '@material-ui/core/Typography';
import compile from 'mson/lib/compiler/compile';

Expand All @@ -21,8 +21,8 @@ const CustomComponent = compile({
},
});

let ReactCustomComponent = (props) => {
const { name, label } = props;
const ReactCustomComponent = (props) => {
const { name, label } = useComponent(props.component, ['name', 'label']);
return (
<div>
<Typography variant="h3">Name: {name}</Typography>
Expand All @@ -31,7 +31,4 @@ let ReactCustomComponent = (props) => {
);
};

// Bind React props to MSON component props
ReactCustomComponent = attach(['name', 'label'])(ReactCustomComponent);

export { CustomComponent, ReactCustomComponent };
40 changes: 40 additions & 0 deletions src/use-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState, useEffect, useRef } from 'react';

export default function useComponent(component, watchProps) {
const [props, setProps] = useState({});

// We need useRef so that we can reference watchProps in the useEffect below
const watchedProps = useRef(watchProps).current;

useEffect(() => {
// The component can be created at any time, e.g. when the definition is set. Therefore, we need
// to handle a missing component until the component is present.
const hasComponent = () => !!component;

function handleFieldChange(name, value) {
if (watchedProps.indexOf(name) !== -1) {
setProps((prevProps) => ({ ...prevProps, [name]: value }));
}
}

function addListener() {
if (hasComponent()) {
component.on('$change', handleFieldChange);

// Initialize the props using the component's values
setProps(component.get(watchedProps));
}
}

function removeListener() {
if (hasComponent()) {
component.removeListener('$change', handleFieldChange);
}
}

addListener();
return () => removeListener();
}, [component, watchedProps]); // Only rerun if component changes

return props;
}
104 changes: 104 additions & 0 deletions src/use-component.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { render, waitFor, screen, act } from '@testing-library/react';
import compile from 'mson/lib/compiler/compile';
import useComponent from './use-component';

const Contact = compile({
component: 'UIComponent',
name: 'Contact',
schema: {
component: 'Form',
fields: [
{
component: 'TextField',
name: 'firstName',
},
{
component: 'TextField',
name: 'lastName',
},
],
},
});

const ReactContact = (props) => {
const { firstName, lastName } = useComponent(props.component, [
'firstName',
'lastName',
]);
return (
<div>
<div>First Name:{firstName}</div>
<div>Last Name:{lastName}</div>
</div>
);
};

const expectNameToEqual = (first, last) => {
const firstName = screen.getByText(/First Name/);
expect(firstName).toHaveTextContent(`First Name:${first}`);
const lastName = screen.getByText(/Last Name/);
expect(lastName).toHaveTextContent(`Last Name:${last}`);
};

const createJerry = () =>
new Contact({
firstName: 'Jerry',
lastName: 'Garcia',
});

const shouldTestHappyPath = () => {
const contact = createJerry();

render(<ReactContact component={contact} />);

expectNameToEqual('Jerry', 'Garcia');

return contact;
};

it('should useComponent for happy path', () => {
shouldTestHappyPath();
});

it('should useComponent when props change sequentially', async () => {
const contact = shouldTestHappyPath();

// Change the first name
act(() => contact.set({ firstName: 'Gerry' }));
await waitFor(() => expectNameToEqual('Gerry', 'Garcia'));

// Change the last name
act(() => contact.set({ lastName: 'Jarcia' }));
await waitFor(() => expectNameToEqual('Gerry', 'Jarcia'));
});

it('should useComponent when component changes', async () => {
// Render without a MSON component
const { rerender } = render(<ReactContact />);
expectNameToEqual('', '');

// Set component
const jerry = createJerry();
rerender(<ReactContact component={jerry} />);
expectNameToEqual('Jerry', 'Garcia');

// Change the component
const bob = new Contact({
firstName: 'Bob',
lastName: 'Weir',
});
rerender(<ReactContact component={bob} />);
expectNameToEqual('Bob', 'Weir');
});

it('useComponent should remove listener when unmounting', async () => {
const jerry = createJerry();
const on = jest.spyOn(jerry, 'on');
const removeListener = jest.spyOn(jerry, 'removeListener');
const { unmount } = render(<ReactContact component={jerry} />);
expect(on.mock.calls[0][0]).toEqual('$change');
expectNameToEqual('Jerry', 'Garcia');

unmount();
expect(removeListener.mock.calls[0][0]).toEqual('$change');
});

0 comments on commit 675da97

Please sign in to comment.