Skip to content

Commit

Permalink
Support for custom components (web components) (#320)
Browse files Browse the repository at this point in the history
* 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
nkylstad authored Jul 8, 2022
1 parent 8738fb3 commit fff8602
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
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 src/altinn-app-frontend/src/components/custom/CustomWebComponent.tsx
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;
7 changes: 7 additions & 0 deletions src/altinn-app-frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { IGrid } from 'src/features/form/layout';
import { createContext } from 'react';
import { LikertComponent } from 'src/components/base/LikertComponent';
import { PrintButtonComponent } from './base/PrintButtonComponent';
import CustomComponent from './custom/CustomWebComponent';

export interface IComponent {
name: string;
Expand Down Expand Up @@ -55,6 +56,7 @@ export enum ComponentTypes {
NavigationBar,
Likert,
Panel,
Custom,
PrintButton,
}

Expand Down Expand Up @@ -195,6 +197,11 @@ export const advancedComponents: IComponent[] = [
readOnly: false,
},
},
{
name: 'Custom',
Tag: CustomComponent,
Type: ComponentTypes.Custom,
},
];

export interface IComponentProps extends IGenericComponentProps {
Expand Down

0 comments on commit fff8602

Please sign in to comment.