DeloitteAU / react-habitat

⚛️ 🛅 A React DOM Bootstrapper designed to harmonise a hybrid 'CMS + React' application.
Other
262 stars 42 forks source link

Register children as props #48

Open timkelty opened 4 years ago

timkelty commented 4 years ago

I thought it would be neat if we were able to register children of the component root:

<div data-component="SomeReactComponent">
    <div data-prop="foo"><p>Some markup rendered from the CMS that I'd rather not pass as data</p></div>
</div>

This could also be used as progressive enhancement, or a way inject SEO data.

timkelty commented 4 years ago

FWIW, I tried to make a custom factory for doing this, but by the time the factory's inject method is called, target is already the modified data-habitat element, and thus the children have been removed.

timkelty commented 4 years ago

Here is my custom factory that does this…it is a bit hacky because it relies on the replaceDisabled option being true.

class MyCustomFactory {
  inject(module, props, target) {
    if (target) {
      const childProps = this.getChildProps(target);

      ReactDOM.render(
        React.createElement(module, Object.assign({}, childProps, props || {})),
        target,
      );

    } else {
      Logger.warn('RHW07', 'Target element is null or undefined.');
    }
  }
  dispose(target) {
    if (target) {
      ReactDOM.unmountComponentAtNode(target);
    }
  }

  getChildProps(target, remove = true) {
    const {previousElementSibling} = target;
    const children = previousElementSibling && previousElementSibling.querySelectorAll('[data-prop]') || [];

    return [...children].reduce((obj, el) => {
      if (remove) {
        el.remove();
      }

      return Object.assign(obj, {
        [el.dataset.prop]: el,
      });
    }, {});
  }
}

Example Usage…

<div data-component="MyComponent" data-props='{"foo": "bar"}' data-habitat-no-replace="true">
  <div data-prop="someContent">
    <p>Some content…</p>
  </div>
</div>
<div dangerouslySetInnerHTML={{__html: this.props.someContent.innerHTML}} />
iainsimmons commented 4 years ago

Hey @timkelty, I just found a built-in way to do this, using the proxy prop that is passed to all react-habitat components:

<div dangerouslySetInnerHTML={{__html: this.props.proxy.innerHTML}} />

That wouldn't do any of the child props stuff, but you could do that with something like htmr (which is what I am using) to convert the HTML string to React components.

Then you wouldn't need the data-prop syntax:

import React from 'react';
import convert from 'htmr';

const MyComponent = ({foo, proxy}) => (
  <div>
    <p>{foo}</p>
    {convert(proxy.innerHTML)}
  </div>
);
<div data-component="MyComponent" data-props='{"foo": "bar"}'>
  <div className="my-class">
    <p>Some content…</p>
  </div>
</div>

Renders something like this:

<div data-habitat="C1">
  <div>
    <p>bar</p>
    <div class="my-class">
      <p>Some content…</p>
    </div>
  </div>
</div>
timkelty commented 4 years ago

@iainsimmons oh nice!

Every React Habitat instance is passed in a prop named proxy, this is a reference the original dom element.

Didn't realize that!