Skip to content

Commit

Permalink
feat: add useComponentPropOverrides hook and withComponentPropOverrid…
Browse files Browse the repository at this point in the history
…es HOC for custom props
  • Loading branch information
adamstankiewicz committed Aug 19, 2024
1 parent ba3ff7e commit 5b53e4c
Show file tree
Hide file tree
Showing 12 changed files with 768 additions and 227 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { getBaseConfig } = require('@openedx/frontend-build');

const config = getBaseConfig('eslint');
Expand Down
2 changes: 1 addition & 1 deletion docs/template/edx/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ exports.publish = (memberData, opts, tutorials) => {
generateSourceFiles(sourceFiles, opts.encoding);
}

// if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }
if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }

// index page displays information from package.json and lists files
files = find({kind: 'file'});
Expand Down
37 changes: 35 additions & 2 deletions env.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
// NOTE: This file is used by the example app. frontend-build expects the file
/* eslint-disable no-console */

// NOTE: This file is used by the example app. frontend-build expects the file
// to be in the root of the repository. This is not used by the actual frontend-platform library.
// Also note that in an actual application this file would be added to .gitignore.
// Also note that in an actual application, this file would be added to .gitignore.
const config = {
JS_FILE_VAR: 'JS_FILE_VAR_VALUE_FOR_EXAMPLE_APP',
componentPropOverrides: {
targets: {
example: {
'data-dd-privacy': 'mask', // Custom `data-*` attribute (e.g., Datadog)
'data-hj-suppress': '', // Custom `data-*` attribute (e.g., Hotjar)
className: 'fs-mask', // Custom `className` attribute (e.g., Fullstory)
onClick: (e) => { // Custom `onClick` attribute
console.log('[env.config] onClick event for example', e);
},
style: { // Custom `style` attribute
background: 'blue',
color: 'white',
},
},
example2: {
'data-dd-privacy': 'mask', // Custom `data-*` attribute (e.g., Datadog)
'data-hj-suppress': '', // Custom `data-*` attribute (e.g., Hotjar)
className: 'fs-mask', // Custom `className` attribute (e.g., Fullstory)
onClick: (e) => { // Custom `onClick` attribute
console.log('[env.config] onClick event for example2', e);
},
style: { // Custom `style` attribute
background: 'blue',
color: 'white',
},
},
example3: {
'data-dd-action-name': 'example name', // Custom `data-*` attribute (e.g., Datadog)
},
},
},
};

export default config;
152 changes: 152 additions & 0 deletions example/ComponentPropOverridesPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
forwardRef, useContext, useEffect, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';

import { AppContext, useComponentPropOverrides, withComponentPropOverrides } from '@edx/frontend-platform/react';

// Example via `useComponentPropOverrides` (hook)
const ExampleComponentWithDefaultPropOverrides = forwardRef(({ children, ...rest }, ref) => {
const propOverrides = useComponentPropOverrides('example', rest);
return <span ref={ref} {...propOverrides}>{children}</span>;
});
ExampleComponentWithDefaultPropOverrides.displayName = 'ExampleComponentWithDefaultPropOverrides';
ExampleComponentWithDefaultPropOverrides.propTypes = {
children: PropTypes.node.isRequired,
};

const ExampleComponentWithAllowedPropOverrides = forwardRef(({ children, ...rest }, ref) => {
const propOverrides = useComponentPropOverrides('example2', rest, {
allowedPropNames: ['className', 'style', 'onClick'],
});
return <span ref={ref} {...propOverrides}>{children}</span>;
});
ExampleComponentWithAllowedPropOverrides.displayName = 'ExampleComponentWithAllowedPropOverrides';
ExampleComponentWithAllowedPropOverrides.propTypes = {
children: PropTypes.node.isRequired,
};

// Example via `withComponentPropOverrides` (HOC)
const ExampleComponent = forwardRef(({ children, ...rest }, ref) => (
<span ref={ref} {...rest}>{children}</span>
));
ExampleComponent.displayName = 'ExampleComponent';
ExampleComponent.propTypes = {
children: PropTypes.node.isRequired,
};
const ExampleComponentWithPropOverrides3 = withComponentPropOverrides('example3')(ExampleComponent);

function jsonStringify(obj) {
const replacer = (key, value) => {
if (typeof value === 'function') {
return '[Function]';
}
return value;
};
return JSON.stringify(obj, replacer, 2);
}

function useExample() {
const ref = useRef(null);
const [node, setNode] = useState(null);

useEffect(() => {
if (ref.current) {
setNode(ref.current.outerHTML);
}
}, []);

return {
ref,
node,
};
}

export default function ComponentPropOverridesPage() {
const { config } = useContext(AppContext);
const firstExample = useExample();
const secondExample = useExample();
const thirdExample = useExample();

const { componentPropOverrides } = config;

return (
<div>
<h1>Example usage of <code>componentPropOverrides</code> from configuration</h1>

<h2>Current configuration</h2>
{componentPropOverrides ? (
<pre>
{jsonStringify({ componentPropOverrides })}
</pre>
) : (
<p>
No <code>componentPropOverrides</code> configuration found. Consider updating this
application&apos;s <code>env.config.js</code> to configure any custom props.
</p>
)}

<h2>Examples</h2>
<p>
The following examples below demonstrate
using <code>useComponentPropOverrides</code> and <code>withComponentPropOverrides</code> to
extend any component&apos;s base props based on the application&apos;s configuration. Inspect the DOM
elements for the rendered example components below to observe the configured attributes/values.
</p>

{/* Example 1 (useComponentPropOverrides) */}
<h3><code>useComponentPropOverrides</code> (hook)</h3>
<h4>Default support prop overrides</h4>
<p>
By default, only <code>data-*</code> attributes and <code>className</code> props are
supported; other props will be ignored. You may opt-in to non-default prop
overrides by extending the <code>allowedPropNames</code> option.
</p>
<p>
<ExampleComponentWithDefaultPropOverrides
ref={firstExample.ref}
// eslint-disable-next-line no-console
onClick={(e) => console.log('ExampleComponentWithPropOverrides clicked', e)}
style={{ borderBottom: '4px solid red' }}
className="example-class"
>
Example 1
</ExampleComponentWithDefaultPropOverrides>
</p>
<i>Result:</i>{' '}
<pre>
<code>{firstExample.node}</code>
</pre>

{/* Example 2 (useComponentPropOverrides) */}
<h4>Opt-in to specific prop overrides with <code>allowedPropNames</code></h4>
<p>
<ExampleComponentWithAllowedPropOverrides
ref={secondExample.ref}
// eslint-disable-next-line no-console
onClick={(e) => console.log('ExampleComponentWithPropOverrides clicked', e)}
style={{ borderBottom: '4px solid red' }}
className="example-class"
>
Example 2
</ExampleComponentWithAllowedPropOverrides>
</p>
<i>Result:</i>{' '}
<pre>
<code>{secondExample.node}</code>
</pre>

{/* Example 3 (withComponentPropOverrides) */}
<h3><code>withComponentPropOverrides</code> (HOC)</h3>
<p>
<ExampleComponentWithPropOverrides3 ref={thirdExample.ref}>
Example 3
</ExampleComponentWithPropOverrides3>
</p>
<i>Result:</i>{' '}
<pre>
<code>{thirdExample.node}</code>
</pre>
</div>
);
}
3 changes: 2 additions & 1 deletion example/ExamplePage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { Link } from 'react-router-dom';

import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
Expand Down Expand Up @@ -48,6 +48,7 @@ class ExamplePage extends Component {
<p>EXAMPLE_VAR env var came through: <strong>{getConfig().EXAMPLE_VAR}</strong></p>
<p>JS_FILE_VAR var came through: <strong>{getConfig().JS_FILE_VAR}</strong></p>
<p>Visit <Link to="/authenticated">authenticated page</Link>.</p>
<p>Visit <Link to="/component-prop-overrides">component prop overrides page</Link>.</p>
<p>Visit <Link to="/error_example">error page</Link>.</p>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion example/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';

import React from 'react';
import ReactDOM from 'react-dom';
import {
AppProvider,
Expand All @@ -16,6 +15,7 @@ import { Routes, Route } from 'react-router-dom';
import './index.scss';
import ExamplePage from './ExamplePage';
import AuthenticatedPage from './AuthenticatedPage';
import ComponentPropOverridesPage from './ComponentPropOverridesPage';

subscribe(APP_READY, () => {
ReactDOM.render(
Expand All @@ -27,6 +27,7 @@ subscribe(APP_READY, () => {
element={<PageWrap><ErrorPage message="Test error message" /></PageWrap>}
/>
<Route path="/authenticated" element={<AuthenticatedPageRoute><AuthenticatedPage /></AuthenticatedPageRoute>} />
<Route path="/component-prop-overrides" element={<ComponentPropOverridesPage />} />
</Routes>
</AppProvider>,
document.getElementById('root'),
Expand Down
Loading

0 comments on commit 5b53e4c

Please sign in to comment.