-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for custom components (web components) (#320)
* Add support for 3rd party web components! * update naming * some updates for custom components * remove Custom component wrapper * only stringify objects/arrays + tests * handle missing tag name
- Loading branch information
Showing
3 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
src/altinn-app-frontend/src/components/custom/CustomWebComponent.test.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,87 @@ | ||
/* eslint-disable @typescript-eslint/no-empty-function */ | ||
import * as React from 'react'; | ||
import type { ICustomComponentProps } from './CustomWebComponent'; | ||
import CustomWebComponent from './CustomWebComponent'; | ||
import { renderWithProviders } from '../../../testUtils'; | ||
import type { ITextResource } from 'altinn-shared/types'; | ||
|
||
describe('components > custom > CustomWebComponent', () => { | ||
let handleDataChange: ICustomComponentProps['handleDataChange']; | ||
let mockTextResources: ITextResource[]; | ||
|
||
beforeAll(() => { | ||
handleDataChange = (value: string) => value; | ||
mockTextResources = [ | ||
{ | ||
id: 'title', | ||
value: 'Title', | ||
}, | ||
]; | ||
}); | ||
|
||
it('should render the component with the provided tag name', async () => { | ||
const screen = render({ tagName: 'test-component' }); | ||
const element = screen.getByTestId('test-component'); | ||
expect(element).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render the component with passed props as attributes', () => { | ||
const screen = render({ tagName: 'test-component' }); | ||
const element = screen.getByTestId('test-component'); | ||
expect(element.id).toEqual('test-component'); | ||
expect(element.getAttribute('text')).toEqual( | ||
JSON.stringify({ title: 'Title' }), | ||
); | ||
}); | ||
|
||
it('should render nothing if the tag name is missing', async () => { | ||
const screen = render({ tagName: undefined }); | ||
const element = await screen.queryByTestId('test-component'); | ||
expect(element).not.toBeInTheDocument(); | ||
}); | ||
|
||
const render = (providedProps?: Partial<ICustomComponentProps>) => { | ||
const allProps: ICustomComponentProps = { | ||
id: 'test-component', | ||
tagName: '', | ||
formData: { simpleBinding: 'This is a test' }, | ||
dataModelBindings: { simpleBinding: 'model' }, | ||
text: { | ||
title: 'Title', | ||
}, | ||
handleDataChange, | ||
handleFocusUpdate: () => {}, | ||
getTextResource: (key: string) => { | ||
return key; | ||
}, | ||
getTextResourceAsString: (key: string) => { | ||
return key; | ||
}, | ||
isValid: true, | ||
language: {}, | ||
shouldFocus: false, | ||
legend: null, | ||
label: null, | ||
type: 'Custom', | ||
textResourceBindings: { | ||
title: 'title', | ||
}, | ||
}; | ||
|
||
return renderWithProviders( | ||
<CustomWebComponent | ||
{...allProps} | ||
{...providedProps} | ||
/>, | ||
{ | ||
preloadedState: { | ||
textResources: { | ||
language: 'nb', | ||
resources: mockTextResources, | ||
error: null, | ||
}, | ||
}, | ||
}, | ||
); | ||
}; | ||
}); |
106 changes: 106 additions & 0 deletions
106
src/altinn-app-frontend/src/components/custom/CustomWebComponent.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,106 @@ | ||
import { getTextResourceByKey } from 'altinn-shared/utils'; | ||
import * as React from 'react'; | ||
import { useAppSelector } from 'src/common/hooks'; | ||
import type { ITextResource, ITextResourceBindings } from 'src/types'; | ||
import type { IComponentProps } from '..'; | ||
|
||
export interface ICustomComponentProps extends IComponentProps { | ||
tagName: string; | ||
} | ||
|
||
function CustomWebComponent({ | ||
tagName, | ||
formData, | ||
componentValidations, | ||
textResourceBindings, | ||
dataModelBindings, | ||
language, | ||
handleDataChange, | ||
...passThroughProps | ||
}: ICustomComponentProps) { | ||
const Tag = tagName as any; | ||
const wcRef = React.useRef(null); | ||
const textResources = useAppSelector( | ||
(state) => state.textResources.resources, | ||
); | ||
|
||
React.useLayoutEffect(() => { | ||
const { current } = wcRef; | ||
if (current) { | ||
const handleChange = (customEvent: CustomEvent) => { | ||
const { value, field } = customEvent.detail; | ||
handleDataChange(value, field); | ||
}; | ||
|
||
current.addEventListener('dataChanged', handleChange); | ||
return () => { | ||
current.removeEventListener('dataChanged', handleChange); | ||
}; | ||
} | ||
}, [handleDataChange, wcRef]); | ||
|
||
React.useLayoutEffect(() => { | ||
const { current } = wcRef; | ||
if (current) { | ||
current.texts = getTextsForComponent( | ||
textResourceBindings, | ||
textResources, | ||
false, | ||
); | ||
current.dataModelBindings = dataModelBindings; | ||
current.language = language; | ||
} | ||
}, [wcRef, textResourceBindings, textResources, dataModelBindings, language]); | ||
|
||
React.useLayoutEffect(() => { | ||
const { current } = wcRef; | ||
if (current) { | ||
current.formData = formData; | ||
current.componentValidations = componentValidations; | ||
} | ||
}, [formData, componentValidations]); | ||
|
||
if (!Tag || !textResources) { | ||
return null; | ||
} | ||
|
||
const propsAsAttributes: any = {}; | ||
Object.keys(passThroughProps).forEach((key) => { | ||
let prop = passThroughProps[key]; | ||
if (['object', 'array'].includes(typeof prop)) { | ||
prop = JSON.stringify(passThroughProps[key]); | ||
} | ||
propsAsAttributes[key] = prop; | ||
}); | ||
|
||
return ( | ||
<div> | ||
<Tag | ||
ref={wcRef} | ||
data-testid={tagName} | ||
{...propsAsAttributes} | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
function getTextsForComponent( | ||
textResourceBindings: ITextResourceBindings, | ||
textResources: ITextResource[], | ||
stringify = true, | ||
) { | ||
const result: any = {}; | ||
Object.keys(textResourceBindings).forEach((key) => { | ||
result[key] = getTextResourceByKey( | ||
textResourceBindings[key], | ||
textResources, | ||
); | ||
}); | ||
|
||
if (stringify) { | ||
return JSON.stringify(result); | ||
} | ||
return result; | ||
} | ||
|
||
export default CustomWebComponent; |
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