From ee1b2ae88b49dd7bdeca5b4691eeaaf32a513a8d Mon Sep 17 00:00:00 2001 From: MarcusNotheis Date: Thu, 10 Oct 2019 12:36:27 +0200 Subject: [PATCH] fix(withWebComponent): Bind unknown events & cleanup BREAKING CHANGE: Removed `innerStyles` prop in favor of css variables and UI5's `addCustomCSS` API Fixes #181 --- .../main/src/internal/withWebComponent.tsx | 116 ++++++++---------- .../src/webComponents/Dialog/demo.stories.tsx | 21 ++-- 2 files changed, 60 insertions(+), 77 deletions(-) diff --git a/packages/main/src/internal/withWebComponent.tsx b/packages/main/src/internal/withWebComponent.tsx index 2910b647fb5..57906b2875a 100644 --- a/packages/main/src/internal/withWebComponent.tsx +++ b/packages/main/src/internal/withWebComponent.tsx @@ -2,8 +2,6 @@ import { Event } from '@ui5/webcomponents-react-base/lib/Event'; import React, { Children, cloneElement, - CSSProperties, - MutableRefObject, ReactElement, Ref, RefForwardingComponent, @@ -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; children?: any | void; - innerStyles?: CSSProperties; } export function withWebComponent(WebComponent): RefForwardingComponent { @@ -54,13 +55,17 @@ export function withWebComponent(WebComponent): RefForwardingComponent { + 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 || {}); }; @@ -73,7 +78,10 @@ export function withWebComponent(WebComponent): RefForwardingComponent { + const eventMeta = + (getWebComponentMetadata().metadata.events && getWebComponentMetadata().metadata.events[eventIdentifier]) || {}; + + payload = Object.keys(eventMeta).reduce((acc, val) => { if (val === 'detail' && e[val]) { return { ...acc, @@ -87,27 +95,20 @@ export function withWebComponent(WebComponent): RefForwardingComponent { - return Object.keys(getWebComponentMetadata().getSlots()); - }; - const WithWebComponent = React.forwardRef((props: T & WithWebComponentPropTypes, wcRef: RefObject) => { const { className = '' } = props; - const [updateAfterMount, setUpdateAfterMount] = useState(false); - const prevInnerStylesProp = useRef(null); - const shadowRootRef: MutableRefObject = 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; } @@ -116,7 +117,8 @@ export function withWebComponent(WebComponent): RefForwardingComponent { - 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) { @@ -130,33 +132,47 @@ export function withWebComponent(WebComponent): RefForwardingComponent { - 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; @@ -166,43 +182,9 @@ export function withWebComponent(WebComponent): RefForwardingComponent { - 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); } @@ -214,7 +196,7 @@ export function withWebComponent(WebComponent): RefForwardingComponent - {Object.keys(slots).map((slot) => { + {getSlotsFromMetadata().map((slot) => { if (actualSlotProps[slot]) { return Children.map(actualSlotProps[slot], (item: ReactElement, index) => cloneElement(item, { diff --git a/packages/main/src/webComponents/Dialog/demo.stories.tsx b/packages/main/src/webComponents/Dialog/demo.stories.tsx index d6365951941..7bae3f402f5 100644 --- a/packages/main/src/webComponents/Dialog/demo.stories.tsx +++ b/packages/main/src/webComponents/Dialog/demo.stories.tsx @@ -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', @@ -11,17 +12,17 @@ export const generatedDefaultStory = () => ( DialogContent} - header={null} + onBeforeOpen={action('onBeforeOpen')} + onAfterOpen={action('onAfterOpen')} + onBeforeClose={action('onBeforeClose')} + onAfterClose={action('onAfterClose')} footer={
Footer
} - /> + > +
DialogContent
+
); generatedDefaultStory.story = {