-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(use-component): hooks replacement for attach (#277)
* 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
Showing
6 changed files
with
201 additions
and
6 deletions.
There are no files selected for viewing
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
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
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,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 }; |
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
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,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; | ||
} |
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,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'); | ||
}); |