Skip to content

Commit

Permalink
Introduce usage of nested RUIProvider (#541)
Browse files Browse the repository at this point in the history
  • Loading branch information
bedrich-schindler committed May 30, 2024
1 parent 0678b11 commit 1361b70
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 4 deletions.
10 changes: 10 additions & 0 deletions src/docs/customize/global-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,13 @@ React.createElement(() => {
);
});
```

## Nested Usage

Global props can be nested. This is useful e.g. when you want to configure
props across whole application and then override some of them in a specific
part of the application.

When nested `RUIProvider` is used, the props are merged deeply together. This
means that you can extend specific object with new props or override existing
ones. If you need to remove some prop, you can set it to `undefined`.
9 changes: 6 additions & 3 deletions src/provider/RUIProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import PropTypes from 'prop-types';
import React, {
useContext,
useMemo,
} from 'react';
import defaultTranslations from '../translations/en';
import { mergeDeep } from '../utils/mergeDeep';
import RUIContext from './RUIContext';

const RUIProvider = ({
children,
globalProps,
translations,
}) => {
const context = useContext(RUIContext);
const childProps = useMemo(() => ({
globalProps,
translations,
}), [globalProps, translations]);
globalProps: mergeDeep(context?.globalProps || {}, globalProps),
translations: mergeDeep(context?.translations || {}, translations),
}), [context, globalProps, translations]);

return (
<RUIContext.Provider
Expand Down
76 changes: 75 additions & 1 deletion src/provider/__tests__/RUIProvider.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
render,
within,
} from '@testing-library/react';
import { Badge } from '../../components/Badge';
import { Alert } from '../../components/Alert';
import { Badge } from '../../components/Badge';
import { Grid } from '../../components/Grid';
import RUIProvider from '../RUIProvider';

describe('rendering', () => {
Expand Down Expand Up @@ -36,4 +37,77 @@ describe('rendering', () => {

assert(dom.container.firstChild);
});

it('renders with nested providers', () => {
const dom = render((
<RUIProvider
globalProps={{
Grid: {
alignContent: {
sm: 'column',
xs: 'row dense',
},
autoFlow: {
sm: 'column',
xs: 'row dense',
},
justifyItems: 'center',
tag: 'main',
},
}}
>
<RUIProvider
globalProps={{
Grid: {
alignContent: undefined,
autoFlow: {
lg: 'column',
sm: undefined,
xs: 'row dense',
},
justifyContent: undefined,
justifyItems: undefined,
tag: 'section',
},
}}
>
<RUIProvider
globalProps={{
Grid: {
autoFlow: {
md: 'column',
},
justifyContent: 'center',
},
}}
>
<Grid>
<div>
Content text
</div>
</Grid>
</RUIProvider>
</RUIProvider>
</RUIProvider>
));

// Assert alignContent
expect(dom.container.firstChild.style.cssText.includes('--rui-local-align-content')).toBeFalsy();

// Assert autoFlow
expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-lg: column')).toBeTruthy();
expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-md: column')).toBeTruthy();
expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-sm')).toBeFalsy();
expect(dom.container.firstChild.style.cssText.includes('--rui-local-auto-flow-xs: row dense')).toBeTruthy();

// Assert justifyContent
expect(dom.container.firstChild.style.cssText.includes('--rui-local-justify-content-xs: center;')).toBeTruthy();

// Assert justifyItems
expect(dom.container.firstChild.style.cssText.includes('--rui-local-justify-items')).toBeFalsy();

// Assert tag
expect(dom.container.firstChild.tagName).toEqual('SECTION');
});
});

80 changes: 80 additions & 0 deletions src/utils/__tests__/mergeDeep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { mergeDeep } from '../mergeDeep';

describe('mergeDeep', () => {
it('adds new attributes', () => {
const obj1 = {};
const obj2 = {
props: {
className: 'class',
style: {
color: 'white',
},
},
state: {
items: [1, 2],
itemsSize: 2,
},
};
const expectedObj = {
props: {
className: 'class',
style: {
color: 'white',
},
},
state: {
items: [1, 2],
itemsSize: 2,
},
};

expect(mergeDeep(obj1, obj2)).toEqual(expectedObj);
});

it('merges with existing attributes', () => {
const obj1 = {
props: {
children: ['child1', 'child2'],
className: 'class',
parent: 'parent',
style: {
color: 'white',
},
},
state: {
items: [1, 2],
itemsSize: 2,
},
};
const obj2 = {
props: {
children: null,
className: 'class1 class2',
style: {
backgroundColor: 'black',
},
},
state: {
items: [3, 4, 5],
itemsSize: 5,
},
};
const expectedObj = {
props: {
children: null,
className: 'class1 class2',
parent: 'parent',
style: {
backgroundColor: 'black',
color: 'white',
},
},
state: {
items: [1, 2, 3, 4, 5],
itemsSize: 5,
},
};

expect(mergeDeep(obj1, obj2)).toEqual(expectedObj);
});
});
30 changes: 30 additions & 0 deletions src/utils/mergeDeep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const isObject = (obj) => obj && typeof obj === 'object';

/**
* Performs a deep merge of objects and returns new object.
*
* @param {...object} objects
* @returns {object}
*/
export const mergeDeep = (...objects) => objects.reduce((prev, obj) => {
if (obj == null) {
return prev;
}

const newObject = { ...prev };

Object.keys(obj).forEach((key) => {
const pVal = prev[key];
const oVal = obj[key];

if (Array.isArray(pVal) && Array.isArray(oVal)) {
newObject[key] = pVal.concat(...oVal);
} else if (isObject(pVal) && isObject(oVal)) {
newObject[key] = mergeDeep(pVal, oVal);
} else {
newObject[key] = oVal;
}
});

return newObject;
}, {});

0 comments on commit 1361b70

Please sign in to comment.