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 authored and adamkudrna committed Jul 31, 2024
1 parent 2ce015e commit 7f33c70
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 4 deletions.
50 changes: 50 additions & 0 deletions src/docs/customize/global-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,53 @@ React.createElement(() => {
);
});
```

## Nesting

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`.

```docoff-react-preview
React.createElement(() => {
const [variant, setVariant] = React.useState('filled');
return (
<RUIProvider globalProps={{
Grid: {
columns: {
xs: '1fr',
md: '1fr 1fr',
},
justifyItems: 'center',
rows: {
xs: '50px',
md: '100px',
},
},
}}>
<RUIProvider globalProps={{
Grid: {
columns: {
sm: '1fr 1fr 1fr',
},
justifyItems: 'undefined',
rows: undefined,
},
}}>
<Grid>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
<docoff-placeholder bordered>Grid item</docoff-placeholder>
</Grid>
</RUIProvider>
</RUIProvider>
);
});
```
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: 3,
},
};
const expectedObj = {
props: {
children: null,
className: 'class1 class2',
parent: 'parent',
style: {
backgroundColor: 'black',
color: 'white',
},
},
state: {
items: [3, 4, 5],
itemsSize: 3,
},
};

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

/**
* 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 previousVal = prev[key];
const currentVal = obj[key];

if (isObject(previousVal) && isObject(currentVal)) {
newObject[key] = mergeDeep(previousVal, currentVal);
} else {
newObject[key] = currentVal;
}
});

return newObject;
}, {});

0 comments on commit 7f33c70

Please sign in to comment.