npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@amazeelabs/react-framework-bridge

v2.3.20

Published

Bridge code to implement framework independent react components.

Downloads

280

Readme

⚠️ The package is deprecated ⚠️

We do not upgrade its dependencies anymore. See Renovate config.

React Framework Bridge

This module provides helper functions that allow to separate React component libraries by providing exchangeable builders for components that are controlled by the framework.

The Problem

When building a React component library to be consumed by a framework like Gatsby or Next.js, some fundamental components are bound to the framework. The most prominent candidate is the simple Link component that should be used instead of a simple a tag to enable fast navigation features. If a component then imports Link from gatsby, it is tightly bound to the framework from then on. This means it can't be used in another context, and even showcasing it in Storybook causes problems, because Gatsby internals have to be mocked.

The Solution

This package aims to provide a pattern and set of helper functions to inject these dependencies along with the data structures that require them.

Consider this example:

import React from 'react';
import { Link } from 'gatsby';

type TeaserProps = React.PropsWithChildren<{
  title: string;
  description: string;
  url: string;
}>;

export const Teaser = (props) => (
  <div className="teaser">
    <h2>{props.title}</h2>
    <div dangerouslySetInnerHTML={{ __html: props.description }} />
    <Link to={props.url} className="teaser__link">
      Learn more ...
    </Link>
  </div>
);

The component can only be used within a Gatsby project and links within the rich text part won't even benefit from it! Apart from being solved in a very questionable way using dangerouslySetInnerHtml.

We can use the types defined in this package to build our component in a less dependent way:

import React from 'react';
import { Link, Html } from '@amazeelabs/react-framework-bridge';

type TeaserProps = React.PropsWithChildren<{
  title: string;
  Description: Html;
  Link: Link;
}>;

export const Teaser = (props) => (
  <div className="teaser">
    <h2>{props.title}</h2>
    <div>
      <Description />
    </div>
    <Link className="teaser__link">Learn more ...</Link>
  </div>
);

Now we can use the builder functions provided to showcase the component in a Story:

import React from 'react';
import { Meta } from '@storybook/react';
import { Teaser as TeaserComponent } from '../teaser';
import {
  buildHtml,
  buildLink,
} from '@amazeelabs/react-framework-bridge/storybook';

export default {
  title: 'Components/Molecules/Teaser',
  component: TeaserComponent,
} as Meta;

export const Teaser = () => (
  <TeaserComponent
    title={'This is the title'}
    Description={buildHtml(
      '<p>This is a text with a <a href="https://www.amazeelabs.com">Link</a>.<p>',
    )}
    Link={buildLink({ href: '/about-us' })}
  />
);

When actually using the component in Gatsby, we simply use the helpers tailored to this framework:

import React from 'react';
import { Teaser } from 'my-ui-library';
import {
  buildHtml,
  buildLink,
} from '@amazeelabs/react-framework-bridge/gatsby';

export const query = graphql`...`;

const Homepage = (data) => (
  <div>
    <h1>Latest news</h1>
    {data.teasers.map((teaser) => (
      <Teaser
        title={teaser.title}
        Description={buildHtml(teaser.description)}
        Link={buildLink({ href: teaser.url })}
      />
    ))}
  </div>
);
export default Homepage;

Storybook and Gatsby are cleanly separated while Typescript still makes sure that everything fits together!

Supported Frameworks

Currently Gatsby and Storybook are supported.

Supported components

Link

The buildLink for Gatsby and Storybook functions accept a set of properties that is equivalent to the allowed attributes of a standard Anchor element, except the CSS-class attribute. Instead, it is possible to add className and activeClassName properties to the resulting component to control the visual appearance within the component library.

In Storybook, the activeClassName will be applied if the href attribute contains active. In Gatsby it will use the built-in active-link functionality.

const Link = buildLink({ href: '/active' });

//...

<Link className={'text-blue'} activeClassName={'text-red'}>
  I'm red!
</Link>;

The Link also exposes a navigate method that will cause the application to navigate to its target.

const Link = buildLink({ href: '/active' });

//...

<Button onClick={() => Link.navigate()}>To infinity and beyond!</Button>;

Both rendering Link and using navigate allow adding or override query parameters and fragments from the user interface code.

const Link = buildLink({ href: '/active' });

//...

<Link
  className={'text-blue'}
  activeClassName={'text-red'}
  query={{ foo: 'bar' }}
  fragment={'baz'}
>
  To infinity and beyond!
</Link>;

<Button
  onClick={() => Link.navigate({ query: { foo: 'bar' }, fragment: 'baz' })}
>
  To infinity and beyond!
</Button>;

Images

Image support is very simple. The Storybook variant expects a src and an alt text (along with any other valid image attributes), while Gatsby needs the data object provided by gatsby-plugin-image. In both cases it's possible to pass className to the resulting component to control the design.

const Image = buildImage({ src: './cat.jpg', alt: 'A cat!' });

<Image className={'border-red'} />;

Html

We also provide a dedicated component for rendering strings that contain HTML markup, typically emitted by a content management system. In this case we just pass the string containing the markup to the build function. The resulting component then allows to control the visual appearance by selecting elements and adding classes to them. The selection suppoerts the syntax documented in hast-util-select.

const Html = buildHtml(
  `<p>This is a test with a <a href="https://www.amazeelabs.com">link</a>.</p>`,
);
...

<Html
  classNames={{
    p: 'text-gray',
    'a[href*=amazee]': 'text-orage',
  }}
/>

It also allows to specify custom react components for certain HTML tags. These components will receive the elements properties and children as regular properties, as well as an additional node property that is of type Element and can be used to do additional checks.

const Html = buildHtml(
  `<p>This is a test with a <a href="https://www.amazeelabs.com">link</a>.</p>`,
);
...

<Html
  components={{
    a: ({href, node, children, ...props}) =>
      node.parent.tagName === 'div'
        ? <button data-href={href} {...props}>{children}</button>
        : <a href={href} {...props}>{children}</a>
  }}
/>

By adding custom unified plugins, the UI component has even more control over rendering of the HTML.

/**
 * Inject arrows at the end of links that are alone within a paragraph.
 */
const arrowLinks: Plugin = () => (tree) => {
  visit(
    tree,
    'element',
    modifyChildren((node) => {
      if (
        isElement(node, 'p') &&
        node.children.length === 1 &&
        isElement(node.children[0], 'a')
      ) {
        node.children[0].children.push({
          type: 'element',
          tagName: 'span',
          properties: {
            className: ['arrow'],
          },
          children: [],
        });
      }
    }),
  );
};

<Html plugins={[arrowLinks]} />;

Storybook actions integration

The buildLink and buildForm functions integrate with @storybook/addon-actions and @storybook/addon-interactions. A play function that clicks a Link or submits af Form will trigger an action that is logged. Additionally, artificial arguments called wouldNavigate and wouldSubmit are added to the story context. They can be used with jest's assertions on mock functions to test actual interactions in a play function.

It needs to be added to the projects .storybook/preview.tsx file to work. First, re-export the argTypes definition provided by this package to tell Storybook that wouldNavigate and wouldSubmit are events that need to be logged and mocked. To collect all occurences you also have to add the ActionsDecorator.

export { argTypes } from '@amazeelabs/react-framework-bridge/storybook';

import { ActionsDecorator } from '@amazeelabs/react-framework-bridge/storybook';

export const decorator = [ActionsDecorator];

Now you should be able to implement assertions in play functions like this:

export const MyStory = {
  play: async (context) => {
    const canvas = within(context.canvasElement);
    fireEvent.click(await canvas.findByRole('link', { name: 'Test' }));
    await waitFor(() =>
      expect(context.args.wouldNavigate).toHaveBeenCalledWith('/test'),
    );
  },
};

Zustand integration

Organisms can accept a Zustand store as a prop to implement dynamic behaviour with fine-grained control over re-rendering. Create a store api with the createStore function and pass it to the organisms' property. Inside the organism, make use of useStore to retrieve values from it.

type Counter = {
  count: number;
  increment: () => void;
};

const store = createStore<Counter>({
  count: 0,
  increment: (state) => () => {
    state.count++;
  },
});

function CounterDisplay({ counter }: { counter: StoreApi<Counter> }) {
  const count = useStore(counter, (state) => state.count);
  return <div>{count}</div>;
}