Skip to content

[SPIKE] Extension via Portals #1894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed

Conversation

sirugh
Copy link
Contributor

@sirugh sirugh commented Oct 15, 2019

Closes #1892

  • Extensions live in client code (ie. venia-concept)
  • Extensions should be added within providers (how do we handle form context providers?)
  • Extension base component could be HOC but implemented as wrapper here in venia-ui.

Still much to do, but this is a starting point.

@PWAStudioBot
Copy link
Contributor

PWAStudioBot commented Oct 15, 2019

Fails
🚫 Missing "Description" section. Please add it back, with detail.
🚫 Missing "Verification Steps" section. Please add it back, with detail.
🚫 A version label is required. A maintainer must add one.
Messages
📖

Access a deployed version of this PR here. Make sure to wait for the "pwa-pull-request-deploy" job to complete.

📖 DangerCI Failures related to missing labels/description/linked issues/etc will persist until the next push or next nightly build run (assuming they are fixed).

If your PR is missing information, check against the original template here. At a minimum you must have the section headers from the template and provide some information in each section.

Generated by 🚫 dangerJS against 70fe646

@sirugh sirugh changed the title Initial commit with some examples of extensions [SPIKE] Extension via Portals Oct 15, 2019
@sirugh sirugh force-pushed the rugh/extension-portal-spike branch from 0f8f4f8 to 67e0876 Compare October 15, 2019 18:49
}, [addToast, signedIn, firstname, lastname]);

// Gotta return something or React complains.
return <></>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get around having to return something? This has to be either a fragment or null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no element to return, return null.

return isMounted && !!extensionPoint
? createPortal(<Fragment>{children}</Fragment>, extensionPoint)
: null;
};
Copy link
Contributor Author

@sirugh sirugh Oct 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading up on Portals it seems there is an alternative method to mounting which doesn't require the target to have been mounted. The following does still wait to append/remove the extension DOM until mount but it immediately creates the DOM node on which to stick the children. I think we should stick with what we have as it's probably a better experience for extension developers who may want to access state/size/etc but I just wanted to point out an alternative render approach.

export const Extension = props => {
    const { children, targetId = 'root' } = props;
    const extensionRoot = useMemo(() => document.createElement('div'), []);

    useEffect(() => {
        const extensionPoint = document.getElementById(targetId);
        extensionPoint.appendChild(extensionRoot);
        return () => {
            extensionPoint.removeChild(extensionRoot);
        }
    }, [extensionRoot, targetId]);

    return createPortal(children, extensionRoot);
};

Someone discussed the pros/cons. It really comes down to the expectation of how extensions should behave/interact with each other and venia.

@@ -46,6 +50,25 @@ ReactDOM.render(
<Adapter apiBase={apiBase} apollo={{ link: apolloLink }} store={store}>
<AppContextProvider>
<App />
{/* Some extensions require a target */}
<Extension targetId="main-ep-before-children">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your opinion about moving this extension creation logic to a file of its own, maybe SampleExtension.js and just use <SampleExtension /> here?

Obviously this is just an example but I am curious to know what you think of it?

My point is, there will be less change in the venia-concept related files making things simpler to maintain custom code.

Copy link
Contributor

@revanth0212 revanth0212 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the API for creating extensions. This is a nice way to hook into the DOM without touching a lot of venia-concept code.

This definitely works and scales for DOM dependent/manipulating extensions.

@sirugh sirugh marked this pull request as ready for review November 1, 2019 17:05
@@ -19,7 +19,11 @@ const Main = props => {
return (
<main className={rootClass}>
<Header />
<div className={pageClass}>{children}</div>
<div className={pageClass}>
<div id="main-ep-before-children" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fearful these extension points will get out of control.

  1. We'll have to add them all over the place
  2. That still won't satisfy every use case our clients want
  3. Seemingly innocuous changes (I'm thinking pageClass turns from flex to grid or something) could result in completely broken extensions that our versioning strategy is never going to be able to account for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fearful these extension points will get out of control.

Me too man, me too.

We'll have to add them all over the place

I don't think so. There's whitespace but there's not that much whitespace. If you break the app down into higher level components you only have maybe 5-10 components/pages we'd have to add these to. These would all be documented clearly anyways.

That still won't satisfy every use case our clients want

Yea -- but I don't think that's really something we should solve for. We are in the business of providing a pretty flexible template/theme for use out of the box with minimal necessity for customization and I think this would be a good starting point.

Seemingly innocuous changes (I'm thinking pageClass turns from flex to grid or something) could result in completely broken extensions that our versioning strategy is never going to be able to account for.

We might need to do something like make extension nodes position: relative. I wish I had a better grasp on how we could protect these nodes from our own styling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to add them all over the place [and] that still won't satisfy every use case our clients want

Like @sirugh said, we're just exploring happy paths for adding content to a storefront. We would only put extension points in the safest places to add content.

Seemingly innocuous changes (I'm thinking pageClass turns from flex to grid or something) could result in completely broken extensions that our versioning strategy is never going to be able to account for.

This is true. In a normal app where users write global CSS targeting any classname, we'd assume that after adding content to an extension point, they'd write a CSS rule targeting its parent to ensure it was laid out correctly. This use case gets tougher with scoped CSS, though, since a user would have to take over the parent (defeating the purpose of this happy path).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fearful these extension points will get out of control.

Slight cringe while reading this and hearing Ron Howard say "They did." Magento always had bottomless XML nodes that render any child, and later added a few explicit "extension placeholder" nodes.

In full honesty, I always hated these because when extensions hook onto them it never seems to be "the right place" for that extension — just an inoffensive place to put anything. Factor in responsive design (they didn't) and you always-always had to manually inject them somewhere else.

I understand it breaks the plug-n-play goal of cloud+extensions but I would've been happier to just see extensions define an explicit hook for itself and tell you (the developer integrating it) to add that one-line-of-code where it makes sense. The "safest places to add content" just weren't useful in the real client work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand it breaks the plug-n-play goal of cloud+extensions but I would've been happier to just see extensions define an explicit hook for itself and tell you (the developer integrating it) to add that one-line-of-code where it makes sense. The "safest places to add content" just weren't useful in the real client work.

I agree, and my personal experience as an agency developer is in line with yours: for the best results, "real client work" should consist of developers manually integrating changes, regardless of whether those changes incorporate third-party code. Extensions that effect a seamless change are pretty rare in reality, even if they're targeting hard extension points.

That said, the plug & play extension story happens to be important to a lot of potential PWA Studio users. The lack of automatic extension integration seems to be holding some people back from trying out PWA Studio, even though they would likely find that manual integration leads to better experiences for shoppers and developers alike. A basic extension system would probably help us ease more people into this new ecosystem.


Zooming out, though, any approach to extensions would result in one of two outcomes:

  1. If extensions are limited to only a few explicit placeholders, they'll be too limited for most scenarios, resulting in developers having to manually integrate their customizations anyway.

  2. If extensions are not limited to a few explicit places, extensions will be able to undermine most assumptions about the page structure, resulting in defects and a general lack of predictability.

The latter is a net negative that we want to avoid. But the former should be able to provide risk-free value, even if it's only a small amount. If we can find a way to do this risk-free, we'll do it; if we can't, we won't do it.

{/* Other extensions don't need target (assumes #root at least) */}
<Extension>
<WelcomeToast />
</Extension>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super cool

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work covering the different use cases. 💯

// Only render after the initial mount, and only if the extension point has
// been found.
return isMounted && !!extensionPoint
? createPortal(<Fragment>{children}</Fragment>, extensionPoint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize createPortal was so easy - nice 💪

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cool part is that the portal should render as soon as extensionPoint exists—without us having to find some lifecycle event or a callback that fires right when extensionPoint is created.

We might have to think about how often it's calling document.getElementById() though.

Copy link
Contributor

@jimbo jimbo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good exploration of this concept. I like how much value we get for so little effort and disruption.

I'm ready to archive this. We should keep an index of these spikes in an issue.

}, [addToast, signedIn, firstname, lastname]);

// Gotta return something or React complains.
return <></>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no element to return, return null.

{/* Other extensions don't need target (assumes #root at least) */}
<Extension>
<WelcomeToast />
</Extension>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work covering the different use cases. 💯

// Only render after the initial mount, and only if the extension point has
// been found.
return isMounted && !!extensionPoint
? createPortal(<Fragment>{children}</Fragment>, extensionPoint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cool part is that the portal should render as soon as extensionPoint exists—without us having to find some lifecycle event or a callback that fires right when extensionPoint is created.

We might have to think about how often it's calling document.getElementById() though.

@@ -19,7 +19,11 @@ const Main = props => {
return (
<main className={rootClass}>
<Header />
<div className={pageClass}>{children}</div>
<div className={pageClass}>
<div id="main-ep-before-children" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to add them all over the place [and] that still won't satisfy every use case our clients want

Like @sirugh said, we're just exploring happy paths for adding content to a storefront. We would only put extension points in the safest places to add content.

Seemingly innocuous changes (I'm thinking pageClass turns from flex to grid or something) could result in completely broken extensions that our versioning strategy is never going to be able to account for.

This is true. In a normal app where users write global CSS targeting any classname, we'd assume that after adding content to an extension point, they'd write a CSS rule targeting its parent to ensure it was laid out correctly. This use case gets tougher with scoped CSS, though, since a user would have to take over the parent (defeating the purpose of this happy path).

@jimbo jimbo added the research Research spike or experimental work. label Nov 7, 2019
@cherdman
Copy link
Contributor

@revanth0212 and @zetlen , I am closing out the stale spike.

@cherdman cherdman closed this Dec 11, 2019
@sirugh sirugh deleted the rugh/extension-portal-spike branch March 4, 2020 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[SPIKE]: Extensibility via React Portals
7 participants