0.1.3 • Published 5 years ago

@xornot/react-indirect v0.1.3

Weekly downloads
30
License
ISC
Repository
github
Last release
5 years ago

@xornot/react-indirect

Utility library for creating indirect parent/child relationships between React components, similar to portals but with more control.

Getting Started

Define a key Symbol which uniquely identifies your parent and child combination. This will generally be kept private and not shared outside of your component library.

const $MyIndirectKey = Symbol();

Create decorated parent and child component classes.

@indirectParent($MyIndirectKey)
class Parent extends React.Component<{ name?: string | symbol }> {
  public render() {
    return <div className="a-parent">{this.props.children}</div>;
  }
}

@indirectChild($MyIndirectKey)
class Child extends React.Component<{ parentName?: string | symbol }> {
  public render() {
    return <div className="a-child">{this.props.children}</div>;
  }
}

Now let's say you render something like this:

ReactDOM.render(
  <React.Fragment>
    <Parent />
    <div className="this-will-be-empty">
      <Child>Hello, World!</Child>
    </div>
    <Child>Hello, Susan!</Child>
  </React.Fragment>,
  document.getElementById("root")
);

Your DOM will look as follows:

<div class="a-parent">
  <div class="a-child">Hello, World!</div>
  <div class="a-child">Hello, Susan!</div>
</div>
<div class="this-will-be-empty"></div>

Parent Uniqueness

Your @indirectParent component must have a unique name per usage (The default name is an internal Symbol, so you can still only have one "un-named" instance of your parent component).

If you do render two of your parent component with non-unique names, then a warning will be displayed and the second one will not receive any indirect children.

Here's how you would render two of your indirect parents:

ReactDOM.render(
  <React.Fragment>
    <Parent name="A">
      <div>Parent A</div>
    </Parent>
    <Parent name="B">
      <div>Parent B</div>
    </Parent>
    <Child parentName="A">I will be a child of parent "A"</Child>
    <Child parentName="B">I will be a child of parent "B"</Child>
  </React.Fragment>,
  document.getElementById("root")
);

And here's how your DOM will look:

<div class="a-parent">
  <div>Parent A</div>
  <div class="a-child">I will be a child of parent "A"</div>
</div>
<div class="a-parent">
  <div>Parent B</div>
  <div class="a-child">I will be a child of parent "B"</div>
</div>

Name uniqueness does not apply across @indirectParent components with different key symbols. For instance, if you have another indirect parent as follows:

const $MyOtherIndirectKey = Symbol();

@indirectParent($MyOtherIndirectKey)
class OtherParent extends React.Component {
  ...
}

Then the following would be correct:

<Parent name="A" />
<OtherParent name="A" />

Any <Child name="A" /> component would only render to the <Parent name="A" /> component, which has the same key symbol.

Direct and Indirect Children

An indirect parent can have both direct and indirect children. By default, the direct children are rendered first, and indirect children are last. If you want to handle them differently, you can use the getIndirectChildren utility to separate them. The following example only renders its direct children if it doesn't have any indirect children.

@indirectParent($ThisOrThat)
class This extends React.Component {
  public render() {
    const [directChildren, indirectChildren] = getIndirectChildren(this.props.children);

    return React.Children.Count(indirectChildren) > 0 ? indirectChildren : directChildren;
  }
}

@indirectChild($ThisOrThat)
class That extends React.Component {
  public render() {
    return this.props.children;
  }
}

Given the above, if you rendered the following:

<This><span>This</span></This>

Then the <This> component is transparent and simply renders its children:

<span>This</span>

But if there is a <That> component also:

<This><span>This</span></This>
<That><span>That</span></That>

Then <This> will render only <That> children.

<span>That</span>

Alternative Name Properties

By default @indirectParent components take their name from a name property, and @indirectChild components take their parent name from a parentName property. But you can override this if you need to.

@indirectParent($Key, "id")
class IdParent extends React.Component<{ id?: string }> {
  ...
}

@indirectChild($Key, "parentId")
class IdChild extends React.Component<{ parentId?: string }> {
  ...
}

Now each <IdParent> element must be unique based on its id property instead of its name property, and <IdChild> elements will render to the parent with an id property matching the child parentId value.

Children as Data

Unlike standard portals, your indirect parent component gets an opportunity to intercept and consume indirect children in any way it sees fit. This means you don't have to render them as DOM elements. You could create a child component which actually just provides data to the parent.

@indirectChild($Key)
class Datum extends React.Component<{ value: string }> {
  public render() {
    // If you did render me, I would be a no-op.
    return null;
  }
}

@indirectParent($Key)
class DataCsv extends React.Component {
  public render() {
    const [,data] = getIndirectChildren(this.props.children);
    const csv = (data as Datum[])
      .map((datum) => datum.props.value)
      .join(", ");
    
    return <span>{csv}</span>
  }
}

Now if you rendered the following:

<DataCsv />
<Datum value="foo" />
<Datum value="bar" />

Then you would see this in the DOM:

<span>foo, bar</span>

Without Decorators

If you don't want to use the decorator syntax or you want to decorate a function component, you can call indirectParent and indirectChild as regular functions. They will receive your component as a parameter and return a decorated component.

const ParentFromClass = indirectParent($Key, class extends React.Component {
  public render() {
    ...
  }
});

const ParentFromFunction = indirectParent($Key, () => {
  ...
});

const ChildFromClass = indirectChild($Key, class extends React.Component {
  public render() {
    ...
  }
});

const ChildFromFunction = indirectChild($Key, () => {
  ...
});