Gutenberg + Frontity integration

Description

While working on fronity.org, we have tried to find the best way to integrate gutenberg blocks with frontity.

Possible solution

import { Node, Processor } from "@frontity/html2react/types";
import { connect } from "frontity";
import { Connect } from "frontity/types";
import React, { useState } from "react";

import FrontityOrg from "../../types";

const Switch: React.FC<Connect<
  FrontityOrg,
  {
    html: string;
  }
>> = connect(({ libraries, html }) => {
  const Html2React = libraries.html2react.Component;

  const [state, setState] = useState(true);

  // `match()`
  //
  // 1. Match a part of html tree
  // 2. Remove that part of the HTML from the `html` parameter
  // 3. Then, it will create a "slot" in the "html" that you can "fill"
  // using the returned Component (param 1.) You can think of a "slot" like a placeholder of somthing similar to `children` in react or slots in Vue: https://vuejs.org/v2/guide/components-slots.html
  //
  // Returns 2 params:
  //  1. a React component
  //  2. The HTML under the node that was matched
  const [SwitchButton, buttonHtml] = match("div.switchButton", html);
  const [Content, contentHtml] = match("div.content", html);

  return (
    // 2. We need to extend Html2React to be able to handle the "slots"
    <Html2React html={html}>
      {/* We can pass several children and let Html2React figure out 
          where to put them in the HTML, according to the slots */}
      <SwitchButton onClick={() => setState((state) => !state)}>
        <Html2React html={buttonHtml} />
      </SwitchButton>

      <Content>
        {state ? "on" : "off"}
        <Html2React html={contentHtml} />
      </Content>
    </Html2React>
  );
});

export const switchElement: Processor<
  React.HTMLProps<HTMLElement>,
  FrontityOrg
> = {
  name: "switch",
  test: ({ node }) =>
    node.type === "element" &&
    node.props?.className?.split(" ").includes("switch"),
  processor: ({ node, html }) => {
    if (node.type !== "element") return node;

    const element: Node<React.HTMLProps<HTMLElement> & { html: string }> = {
      type: "element",
      component: Switch,
      props: {
        // `html` should be provided as an extra parameter in the
        //  callback from the processor. Would have to modify html2React for this.
        html,
      },
    };

    return element;
  },
};

The code from the video example:

import { connect } from "frontity";
import { Connect } from "frontity/types";
import React, { useState } from "react";

import FrontityOrg from "../../types";

interface Props {
  rendered: string;
}

const Flow: React.FC<Connect<FrontityOrg, Props>> = ({
  libraries,
  rendered,
}) => {
  const Html2React = libraries.html2react.Component;

  const [activeTab, setActiveTab] = useState(0);

  // Match a part of html tree
  // Return a part of the
  const CreateButton = match("div.createButton", rendered);
  const ConnectButton = match("div.connectButton", rendered);

  const Content = ({ children }) => <section>{children}</section>;

  return (
    <Html2React html={rendered}>
      <CreateButton onClick={() => setActiveTab(0)} />
      <ConnectButton onClick={() => setActiveTab(1)} />
      <StyleButton onClick={() => setActiveTab(2)} />
      <DeployButton onClick={() => setActiveTab(3)} />

      <Content>
        <h4> Some title </h4>
        <Html2React html={} />
      </Content>
    </Html2React>
  );
};

export default connect(Flow);

For context, we were discussing this in GitHub: https://github.com/frontity/frontity.org/pull/67

And this is what we are trying to solve. We have this layout:

<div class="switch">
  <div class="switch-button"></div>
  <div class="switch-value"></div>
</div>

And when the user clicks the "switch-button" div, the "switch-value" div must appear and disappear.

Thanks for opening this topic Michal, I think this is a better place :slight_smile:

This was my first proposal:

  • If the switch state needs to be internal to React, you need to share it from a parent. That can be solved with React context.

    • switch:
      • Create visible and setVisible with React.useState.
      • Create context, add those to the context and use the <Provider>.
    • switch-button:
      • Get setVisible from the context using useContext.
    • switch-value:
      • Get visible from the context.
  • If the switch state needs to be exposed in the state manager then there’s no need for a context, just connect both components to the state manager, create an action for the switch and read the state from the “switch-value”.

    • switch: nothing.
    • switch-button: use actions.theme.toggleSwitch().
    • switch-value: read from state.theme.isSwitchOn.

And this was @David’s proposal:


I would propose to do that kind of stuff in the processor, maybe adding the match function you thought about to the nodes of the html tree.

Something like:

export const switchElement: Processor = {
  name: "switch",
  test: ({ node }) =>
    node.type === "element" &&
    node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    // `node.match` would return the elements that match the query
    const switchButtonNodes = node.match("div.switchButton");
    const switchContentNodes = node.match("div.content");

    // Replace `div` by our components in that nodes
    const switchButtons = switchButtonNodes.map(node => ({
      ...node,
      component: SwitchButton
    }));
    const switchContent = switchContentNodes.map(node => ({
      ...node,
      component: SwitchContent
    }));

    return {
      component: Switch,
      children: [
        // Add the nodes to `children` so they will be rendered by Html2React
        ...switchButtons,
        ...switchContent
      ]
    };
  }
};

This way you won’t need to nest Html2React components :+1:

First, I believe that what @mmczaplinski wants to do can be done without adding new APIs:

  1. Using the state manager.
  2. Using React contexts.

I admit that it would be nice to be able to move from HTML to JSX and I agree that if we find a nice way to do so we can implement it.

But remember that one of our design principles is that Frontity should have as less concepts to learn as possible so we have to be really careful when introducing new APIs.

How to solve this problem today

Using the state manager

This is the code of my first proposal, using the state manager.

As you can see, you don’t even need to create any React component, because all the nodes of Html2React will end up being React components, so you can just pass an onClick prop and it should work:

const switchButton = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-button"),
  processor: ({ node, actions }) => {
    node.props.onClick = actions.theme.toggleSwitch;
  },
};
const switchValue = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-value"),
  processor: ({ node, state }) => (state.theme.isSwitchOn ? node : null),
};

This is super simple, but the problem with this approach is that this will only work for one block. If you have more than one, all of them will switch on and off at the same time.

To solve that, we could use a count. It is the same technique we use to count the number of paragraphs and insert ads and it works great, although I admit that for this case it’s not very elegant.

const switchCount = 1;

const switchButton = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-button"),
  processor: ({ node, actions }) => {
    node.props.onClick = () => actions.theme.toggleSwitch(switchCount);
  },
};
const switchValue = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-value"),
  processor: ({ node, state }) =>
    state.theme.isSwitchOn[switchCount++] ? node : null,
};

Using React context

Multiple blocks work perfectly with React contexts though. This is the code I was proposing:

const SwitchContext = React.createContext();

const Switch = ({ tag, children, ...props }) => {
  const [isSwitchOn, setSwitch] = React.useState(false);
  return (
    <SwitchContext.Provider value={{ isSwitchOn, setSwitch }}>
      <tag {...props}>{children}</tag>
    </SwitchContext.Provider>
  );
};
const SwitchButton = ({ tag, children, ...props }) => {
  const { isSwitchOn, setSwitch } = React.useContext(SwitchContext);
  return (
    <tag {...props} onClick={() => setSwitch(!isSwitchOn)}>
      {children}
    </tag>
  );
};
const SwitchValue = ({ tag, children, ...props }) => {
  const { isSwitchOn } = React.useContext(SwitchContext);
  return isSwitchOn ? <tag {...props}>{children}</tag> : null;
};

const switch = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    node.tag = node.component;
    node.component = Switch;
  },
};
const switchButton = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-button"),
  processor: ({ node, actions }) => {
    node.tag = node.component;
    node.component = SwitchButton;
  },
};
const switchValue = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-value"),
  processor: ({ node, state }) => {
    node.tag = node.component;
    node.component = SwitchValue;
  },
};

Enhancing the processors API

Let’s talk now about the ways we can enhance the processors API without introducing new concepts that need to be learned.

A CSS selector

I agree with you that a CSS selector API could be introduced. After all, that is something that everybody knows. Actually, I proposed it a while ago in this topic: Better class name tests in processors. I even found a library to do it.

test: ({ node }) => node.is(".switch");
test: ({ node }) => node.is(".switch-button");
test: ({ node }) => node.is(".switch-value");

Finding nodes inside nodes

I also agree that introducing a way to find node inside nodes would be nice. Again, I already proposed it a while ago on the same topic: Better class name tests in processors, but with a twist: the API is the processors API. So there’s nothing new to learn.

This would be the way to find the subcomponents of switch:

const switchElement = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    node.process({
      test: ({ node }) =>
        node.props?.className?.split(" ").includes("switch-button"),
      processor: ({ node }) => {
        node.component = SwitchButton;
      },
    });
    node.process({
      test: ({ node }) =>
        node.props?.className?.split(" ").includes("switch-value"),
      processor: ({ node }) => {
        node.component = SwitchValue;
      },
    });
    node.component = Switch;
  },
};

If we apply both APIs, it would be:

const switchElement = {
  test: ({ node }) => node.is("switch"),
  processor: ({ node }) => {
    node.process({
      test: ({ node }) => node.is("switch-button"),
      processor: ({ node }) => {
        node.component = SwitchButton;
      },
    });
    node.process({
      test: ({ node }) => node.is("switch-value"),
      processor: ({ node }) => {
        node.component = SwitchValue;
      },
    });
    node.component = Switch;
  },
};

Introducing helpers

We could provide helpers for common cases, like:

  • Adding CSS to a node:
const blue = css`
  color: blue;
`;

cssProcessor(".switch-button", blue);
  • Replacing node with React component:
replaceProcessor(".switch-button", SwitchButton);

Underneath, they are just normal processors:

const cssProcessor = (selector, css) => ({
  test: ({ node }) => node.is(selector),
  processor: ({ node }) => {
    node.props.css = [node.props.css, css];
  }
};
const replaceProcessor = (selector, component) => ({
  test: ({ node }) => node.is(selector),
  processor: ({ node }) => {
    node.component = component;
  }
};

I think this could simplify a little bit Michal’s concern about the need to create 4 separate processors for 4 separete nodes.

Although I am a bit against providing abstractions at this moment because people need to learn what’s going on underneath first.


All the previous things were a summary of things that can be done right now and new APIs that can be introduced without introducing new concepts, but neither of those is solving what Michal said in his last message: Create a React component where the HTML nodes become React components and you can arrange them using JSX.

So let’s keep working on that. I like Michal’s suggestion of using Slot and Fill, maybe we can find a way to make this work with that concept.

EDIT: I’m sorry for being such a taliban about not introducing new concepts to learn, but I strongly believe that it is really important to do our best to keep the Frontity’s APIs as small as possible.

I’ve been thinking a little bit at this yesterday and I want to do an update on my thoughts.

From HTML to JSX

As Michal suggested, we can use the slot and fill pattern to move from HTML to JSX.

I don’t think the slot and fill utils we are going to build for the themes apply here, because those are linked using strings and for this we need instances.

So I think we would need to introduce a slot and fill creator, that returns two linked instances.

let SwitchButton = null;
let SwitchValue = null;

const Switch = ({ SwitchButton, SwitchValue }) => {
  const [visible, setVisible] = React.useState(false);
  return (
    <>
      <SwitchButton onClick={() => setVisible(!visible)} />
      {visible && <SwitchValue />}
    </>
  );
};

const switch = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    node.component = Switch;
    // Create instances of slot-and-fill's.
    SwitchButton = SlotAndFill();
    SwitchValue = SlotAndFill();
    // Pass the slots to Switch.
    node.props.SwitchButton = SwitchButton.Slot;
    node.props.SwitchValue = SwitchValue.Slot;
  },
};
const switchButton = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-button"),
  processor: ({ node, actions }) => {
    // Turn the button in the fill.
    node.component = SwitchButton.Fill;
  },
};
const switchValue = {
  test: ({ node }) =>
    node.props?.className?.split(" ").includes("switch-value"),
  processor: ({ node, state }) => {
    // Turn the value in the fill.
    node.component = SwitchValue.Fill;
  },
};

This, of course, works because we are traversing the tree from top to bottom, but it doesn’t feel solid to me. It would be better to find the children nodes inside the parent node.

const Switch = ({ SwitchButton, SwitchValue }) => {
  const [visible, setVisible] = React.useState(false);
  return (
    <>
      <SwitchButton onClick={() => setVisible(!visible)} />
      {visible && <SwitchValue />}
    </>
  );
};

const switch = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    node.component = Switch;
    // Create instances of slot-and-fill's.
    SwitchButton = SlotAndFill();
    SwitchValue = SlotAndFill();
    // Pass the slots to Switch.
    node.props.SwitchButton = SwitchButton.Slot;
    node.props.SwitchValue = SwitchValue.Slot;
    // Find the children and turn them into fills
    const button = node.find(".switch-button");
    button.component = SwitchButton.Fill;
    const value = node.find(".switch-value");
    value.component = SwitchValue.Fill;
  },
};

Finding nodes

Right now we use a test function to find nodes:

const processor = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
};

If we want to be able to find nodes inside nodes easily, we need to find a way to do so. Michal and David (and even @orballo a while ago in Better class name tests in processors) proposed to do so with a selector:

const processor = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    const switchButton = node.find(".switch-button");
    switchButton.component = SwitchButton;
  },
};

I proposed to use a processor:

const processor = {
  test: ({ node }) => node.props?.className?.split(" ").includes("switch"),
  processor: ({ node }) => {
    node.process({
      test: ({ node }) =>
        node.props?.className?.split(" ").includes("switch-button"),
      processor: ({ node }) => {
        node.component = SwitchButton;
      },
    });
  },
};

but introduce the selector API in this system:

const processor = {
  test: ({ node }) => node.is(".switch"),
  processor: ({ node }) => {
    node.process({
      test: ({ node }) => node.is(".switch-button"),
      processor: ({ node }) => {
        node.component = SwitchButton;
      },
    });
  },
};

I’ve been thinking about the node.find API that:

  • Receives either a test function or a selector string.
  • Returns an array of nodes.

and I can’t find any example that is not better using it instead of the test function for everything.

Each processor function will be run only once because node.find returns an array of nodes.

const Switch = ({ SwitchButton, SwitchValue }) => {
  const [visible, setVisible] = React.useState(false);
  return (
    <>
      <SwitchButton onClick={() => setVisible(!visible)} />
      {visible && <SwitchValue />}
    </>
  );
};

const processor = ({ root }) => {
  root.find(".switch").map(switch => {
    // Create instances of slot-and-fill's.
    SwitchButton = SlotAndFill();
    SwitchValue = SlotAndFill();
    // Pass the slots to Switch.
    switch.props.SwitchButton = SwitchButton.Slot;
    switch.props.SwitchValue = SwitchValue.Slot;
    // Find the children and turn them into fills
    switch.find(".switch-button").map(switchButton => {
      switchButton.component = SwitchButton.Fill;
    });
    switch.find(".switch-value").map(switchValue => {
      switchValue.component = SwitchValue.Fill;
    });
  });
};

I think that using the Slot and Fill system solves the problem we have when the rest of the children is uncertain, because when you replace a component with a fill, that component disappears and therefore there’s no need to modify children.

const Switch = ({ SwitchButton, children }) => {
  const [visible, setVisible] = React.useState(false);
  return (
    <>
      <SwitchButton onClick={() => setVisible(!visible)} />
      {/* children doesn't have switch-button because
          we turned that into a fill. */}
      {visible && children}
    </>
  );
};

const processor = ({ root }) => {
  root.find(".switch").map(switch => {
    // Create instances of slot-and-fill's.
    SwitchButton = SlotAndFill();
    // Pass the slots to Switch.
    switch.props.SwitchButton = SwitchButton.Slot;
    // Find the children and turn them into fills
    switch.find(".switch-button").map(switchButton => {
      switchButton.component = SwitchButton.Fill;
    });
  });
};

It also simplifies some complex processors where we need to count the number of nodes, for example in the one that counts paragraphs to insert ads.

const Paragraph = ({ hasAd, children, ...props }) => (
  <>
    <p {...props}>{children}</p>
    {hasAd && <AdSlot />}
  </>
);

const processor = ({ root }) => {
  const count = 0;
  root.find("p").map((p) => {
    count += 1;
    if (count % 3 === 0) p.props.hasAd = true;
  });
};

So I got pretty excited about this node.find.

Performance

The problem is that performance-wise this node.find is a N operation (it traverses the tree N times where N is the number of processors), whereas the test function is a 1 operation (it traverses the tree 1 time, no matter the number of processors):

  • test function: 1
  • node.find: N

I guess that was the reason to decide to use the test function in the first place, although it was so long ago that I don’t remember.

In case of doing nested node.find, things get worse. Let’s say that the worse case scenario is that the tree to look for with a nested node.find is half as big as the original tree.

  • node.find => node.find : N+N/2
  • test function => node.find : 1+N/2
  • test function => test function : 1+N/2

In the case of nested finds, we don’t gain performance by using the test function.

Selector libraries

To create node.find, the best option seems to be css-select combined with css-what, although we would need to write our own adapter for himalaya and get rid of domutils with Webpack. It seems viable though.

I created a createSlotAndFill function like this:

const createSlotAndFill = () => {
  const fill = {};

  const Fill = ({ children, tag, ...props }) => {
    fill.children = children;
    fill.tag = tag;
    fill.props = props;
    return null;
  };

  const Slot = ({ children, tag, ...props }) => {
    const Tag = tag || fill.tag;
    return (
      <Tag {...fill.props} {...props}>
        {fill.children}
        {children}
      </Tag>
    );
  };

  return { Fill, Slot };
};

Codesandbox: https://codesandbox.io/s/inspiring-bose-q8skd

The thing is that it has the same problem as the Slot and Fill libraries: The Fill needs to be rendered before the Slot.

Taking into account that the only reason for the Fill existence is to populate the data the Slot will use, and we already have that data in Html2React, it doesn’t make sense to do it this way. We should instead do something like:

const processor = {
  test: ({ node }) => node.is(".switch"),
  processor: ({ node }) => {
    const button = node.find(".switch-button");
    // Pass the fill to the creator:
    const Button = createSlotAndFill(button);
    // Pass the Slot to the component which will use it.
    node.props.Button = Button.Slot;
    // Replace the node with the fill.
    button.component = Button.Fill;
  }
};

And the createSlotAndFill can be something like this:

const createSlotAndFill = fill => {
  const Fill = () => null;

  const Slot = ({ children, ...props }) => (
    <fill.component {...fill.props} {...props}>
      {fill.children}
      {children}
    </fill.component>
  );

  return { Fill, Slot };
};

@david can you please check this out and let us know if it’ll work with the current implementation of html2react?