Skip to content

Commit

Permalink
fix(withWebComponent): Bind unknown events & cleanup (#182)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Removed `innerStyles` prop in favor of css variables and UI5's `addCustomCSS` API

Fixes #181
  • Loading branch information
MarcusNotheis authored Oct 10, 2019
1 parent 88ad0c0 commit b6e2a66
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 77 deletions.
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

0 comments on commit b6e2a66

Please sign in to comment.