Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(withWebComponent): Bind unknown events & cleanup #182

Merged
merged 1 commit into from
Oct 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 49 additions & 67 deletions packages/main/src/internal/withWebComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { Event } from '@ui5/webcomponents-react-base/lib/Event';
import React, {
Children,
cloneElement,
CSSProperties,
MutableRefObject,
ReactElement,
Ref,
RefForwardingComponent,
Expand All @@ -20,19 +18,22 @@ function capitalizeFirstLetter(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}

function convertEventListenerPropToEventKey(s: string) {
const eventName = s.replace('on', '');
return eventName.charAt(0).toLowerCase() + eventName.slice(1);
}

function toKebabCase(s: string) {
return s.replace(/([A-Z])/g, (a, b) => `-${b.toLowerCase()}`);
}

const propBlacklist = {
className: true,
innerStyles: true
className: true
};

export interface WithWebComponentPropTypes extends CommonProps {
ref?: Ref<any>;
children?: any | void;
innerStyles?: CSSProperties;
}

export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5DomRef, T & WithWebComponentPropTypes> {
Expand All @@ -54,13 +55,17 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
};
};

const getMetadataBooleans = () => {
const getBooleanPropsFromMetadata = () => {
return Object.entries(getWebComponentMetadata().getProperties())
.filter(([key, value]) => value.type === Boolean)
.map(([key]) => key);
};

const getMetadataEvents = () => {
const getSlotsFromMetadata = () => {
return Object.keys(getWebComponentMetadata().getSlots());
};

const getEventsFromMetadata = () => {
return Object.keys(getWebComponentMetadata().metadata.events || {});
};

Expand All @@ -73,7 +78,10 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
return acc;
}, {});

payload = Object.keys(getWebComponentMetadata().metadata.events[eventIdentifier]).reduce((acc, val) => {
const eventMeta =
(getWebComponentMetadata().metadata.events && getWebComponentMetadata().metadata.events[eventIdentifier]) || {};

payload = Object.keys(eventMeta).reduce((acc, val) => {
if (val === 'detail' && e[val]) {
return {
...acc,
Expand All @@ -87,27 +95,20 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
eventHandler(Event.of(null, e, payload));
};

const getMetadataSlots = () => {
return Object.keys(getWebComponentMetadata().getSlots());
};

const WithWebComponent = React.forwardRef((props: T & WithWebComponentPropTypes, wcRef: RefObject<Ui5DomRef>) => {
const { className = '' } = props;

const [updateAfterMount, setUpdateAfterMount] = useState(false);
const prevInnerStylesProp = useRef(null);
const shadowRootRef: MutableRefObject<HTMLElement> = useRef();
const [_, setUpdateAfterMount] = useState(false);
const eventRegistry = useRef({});
const eventRegistryWrapped = useRef({});
const localWcRef = useRef(null);

const CustomTag = WebComponent.getMetadata().getTag();
const slots = WebComponent.getMetadata().getSlots();

const getWcRef = () => wcRef || localWcRef;

const getBooleanProps = () => {
return getMetadataBooleans().reduce((acc, key) => {
return getBooleanPropsFromMetadata().reduce((acc, key) => {
if (props[key]) {
acc[toKebabCase(key)] = true;
}
Expand All @@ -116,7 +117,8 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
};

const bindEvents = () => {
getMetadataEvents().forEach((eventIdentifier) => {
const knownEvents = getEventsFromMetadata();
knownEvents.forEach((eventIdentifier) => {
const alternativeKey = 'on' + capitalizeFirstLetter(eventIdentifier);
const eventHandler = props[eventIdentifier] || props[alternativeKey];
if (typeof eventHandler === 'function' && eventRegistry.current[alternativeKey] !== eventHandler) {
Expand All @@ -130,33 +132,47 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
getWcRef().current.removeEventListener(eventIdentifier, eventRegistryWrapped.current[alternativeKey]);
}
});
};

const getShadowDomRef = () => {
if (shadowRootRef.current) {
return shadowRootRef.current;
}
return (shadowRootRef.current =
getWcRef().current && getWcRef().current.getDomRef ? getWcRef().current.getDomRef() : null);
/*
* TODO Remove this after https://github.com/SAP/ui5-webcomponents/issues/833 has been fixed.
* This is a workaround for binding unknown event attributes
*/
const unknownPassedEvents = Object.entries(props)
.filter(([prop, value]) => /^on/.test(prop) && !!value)
.map(([prop]) => prop)
.filter((prop) => !knownEvents.includes(`on${prop}`));

unknownPassedEvents.forEach((eventIdentifier) => {
const eventHandler = props[eventIdentifier];
const eventKey = convertEventListenerPropToEventKey(eventIdentifier);
if (typeof eventHandler === 'function' && eventRegistry.current[eventIdentifier] !== eventHandler) {
if (eventRegistry.current[eventIdentifier]) {
getWcRef().current.removeEventListener(eventKey, eventRegistryWrapped.current[eventIdentifier]);
}
eventRegistryWrapped.current[eventIdentifier] = createEventWrapperFor(eventKey, eventHandler);
getWcRef().current.addEventListener(eventKey, eventRegistryWrapped.current[eventIdentifier]);
eventRegistry.current[eventIdentifier] = eventHandler;
} else if (eventRegistry.current[eventIdentifier] && !eventHandler) {
getWcRef().current.removeEventListener(eventKey, eventRegistryWrapped.current[eventIdentifier]);
}
});
};

const getRegularProps = () => {
if (getWcRef().current) {
bindEvents();
}

const regularProps = {};
const slotProps = {};

Object.entries(props)
.filter(([key]) => !getMetadataBooleans().includes(key))
.filter(([key]) => !getBooleanPropsFromMetadata().includes(key))
.filter(
([key]) =>
!getMetadataEvents().some((eventKey) => `on${capitalizeFirstLetter(eventKey)}` === key || key === eventKey)
!getEventsFromMetadata().some(
(eventKey) => `on${capitalizeFirstLetter(eventKey)}` === key || key === eventKey
)
)
.filter(([key]) => !propBlacklist[key])
.forEach(([key, value]) => {
if (getMetadataSlots().includes(key)) {
if (getSlotsFromMetadata().includes(key)) {
slotProps[key] = value;
} else {
regularProps[toKebabCase(key)] = value;
Expand All @@ -166,43 +182,9 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
return { regularProps, slotProps };
};

const applyInnerStyles = () => {
const { innerStyles } = props;
const shadowRef = getShadowDomRef();
if (!shadowRef) {
return;
}
if (innerStyles) {
Object.entries(innerStyles).forEach(([key, value]) => {
shadowRef.style[key] = value;
});
}
};

const removeOldStyles = (prevStyles) => {
if (prevStyles) {
Object.keys(prevStyles).forEach((key) => {
getShadowDomRef().style[key] = '';
});
}
};

// effects
useEffect(() => {
requestAnimationFrame(() => {
removeOldStyles(prevInnerStylesProp.current);
applyInnerStyles();
});

prevInnerStylesProp.current = props.innerStyles;
}, [props.innerStyles]);

useEffect(() => {
if (getWcRef().current) {
bindEvents();
requestAnimationFrame(() => {
applyInnerStyles();
});
} else {
setUpdateAfterMount(true);
}
Expand All @@ -214,7 +196,7 @@ export function withWebComponent<T>(WebComponent): RefForwardingComponent<Ui5Dom
const { children, tooltip, ...rest } = passedProps as T & WithWebComponentPropTypes;
return (
<CustomTag {...getBooleanProps()} ref={getWcRef()} {...rest} title={tooltip} class={className}>
{Object.keys(slots).map((slot) => {
{getSlotsFromMetadata().map((slot) => {
if (actualSlotProps[slot]) {
return Children.map(actualSlotProps[slot], (item: ReactElement<any>, index) =>
cloneElement(item, {
Expand Down
21 changes: 11 additions & 10 deletions packages/main/src/webComponents/Dialog/demo.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { boolean } from '@storybook/addon-knobs';
import React from 'react';
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs';
import { Dialog } from '@ui5/webcomponents-react/lib/Dialog';
import React from 'react';

export default {
title: 'UI5 Web Components | Dialog',
Expand All @@ -11,17 +12,17 @@ export const generatedDefaultStory = () => (
<Dialog
initialFocus={''}
hideHeader={boolean('hideHeader', false)}
headerText={'generatedString'}
headerText={text('headerText', 'My Dialog Title')}
stretch={boolean('stretch', false)}
open={boolean('open', true)}
onBeforeOpen={null}
onAfterOpen={null}
onBeforeClose={null}
onAfterClose={null}
content={<div>DialogContent</div>}
header={null}
onBeforeOpen={action('onBeforeOpen')}
onAfterOpen={action('onAfterOpen')}
onBeforeClose={action('onBeforeClose')}
onAfterClose={action('onAfterClose')}
footer={<div>Footer</div>}
/>
>
<div style={{ width: '200px', height: '200px' }}>DialogContent</div>
</Dialog>
);

generatedDefaultStory.story = {
Expand Down