Providing this was necessary during some painful email developments because:
- some styles only work inline
- some styles only work inline on immediate element
This function is to be used as such:
// inside a component, grabbing `children` from the props:
<div>
{applyRecursiveProps(children, { style: {color: 'blue'} })}
</div>
applyRecursiveProps.js
The applyRecursiveProps
service is as follows:
import React from 'react'
/**
* Assign properties to react `children`, and all their recursive `children`.
*/
export default function applyRecursiveProps(children, newProps = {}) {
if (typeof children === 'undefined') return // has no children elements
return React.Children.map(children, (child) => {
if (!child.props) return child // is text node, without a tag
const style = { ...child.props.style, ...newProps.style } // to avoid all the style objects merging together into one
const recursiveGrandchildren = applyRecursiveProps(child.props.children, newProps)
const childNewProps = { children: recursiveGrandchildren, ...newProps, style }
return React.cloneElement(child, childNewProps)
})
}
applyRecursiveProps.test.js
And for bonus, here are the tests for it:
import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render } from '@testing-library/react'
import applyRecursiveProps from '../applyRecursiveProps'
const TestElement = ({ children, ...props }) => (
<div {...props}>{applyRecursiveProps(children, { testprop: 'test value' })}</div>
)
test('with one level of elements, new props are applied', () => {
const { getByTestId } = render(
<TestElement>
<p data-testid="p1">1</p>
<p data-testid="p2">2</p>
</TestElement>,
)
expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
})
test('with two level of elements, new props are applied', () => {
const { getByTestId } = render(
<TestElement>
<p data-testid="p1">
<span data-testid="span">span1</span>
</p>
<p data-testid="p2">2</p>
</TestElement>,
)
expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
expect(getByTestId('span')).toHaveAttribute('testprop', 'test value')
})
test('with three level of elements, new props are applied', () => {
const { getByTestId } = render(
<TestElement>
<p data-testid="p1">
<span data-testid="span1">
<span data-testid="span2">span2</span>
</span>
</p>
<p data-testid="p2">2</p>
</TestElement>,
)
expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
expect(getByTestId('p1')).toHaveAttribute('testprop', 'test value')
expect(getByTestId('span1')).toHaveAttribute('testprop', 'test value')
expect(getByTestId('span2')).toHaveAttribute('testprop', 'test value')
})
test('styles object is not mixed when passed onto the next level', () => {
const { getByTestId } = render(
<TestElement>
<p data-testid="p1" style={{ display: 'none' }}>
<span data-testid="span1">
<span data-testid="span2">span2</span>
</span>
</p>
<p data-testid="p2">2</p>
</TestElement>,
)
expect(getByTestId('p1')).toHaveStyle('display: none')
expect(getByTestId('p2')).not.toHaveStyle('display: none')
expect(getByTestId('span1')).not.toHaveStyle('display: none')
expect(getByTestId('span2')).not.toHaveStyle('display: none')
})