Skip to content

Instantly share code, notes, and snippets.

@dmiller9911
Last active August 2, 2018 14:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmiller9911/96867c4881e8a238f1d667059056d046 to your computer and use it in GitHub Desktop.
Save dmiller9911/96867c4881e8a238f1d667059056d046 to your computer and use it in GitHub Desktop.
Patternfly 4 React RFC
  • Start Date: 05-22-2018

Summary

Provide an approach that allows patternfly-react to consume patternfly-next. This approach will cover the following items:

  1. Keep patternfly-react in sync with patternfly-next core
  2. Make it easy for consumers to include patternfly-react in their projects
    • Enable code-splitting without workarounds
    • No need for extra Webpack loaders
    • No CSS to import
  3. Support patternfly-react providing its own styles outside of core.
  4. Allow contributors to maintain the current version.

Motivation

The current version of patternfly-react is not easy to consume and code split. The current implementation relies on importing CSS or SASS, does not provide a way to import components directly, and at times does not match core's components.

As a consumer, I want a way to just import components without needing to worry about importing CSS or how to code split/tree shake the components. I also do not want to worry about applying classes. All styling should be encapsulated within the React Components provided from patternfly-react and driven by props.

Basic example

patternfly-react could take a two-tiered approach to providing styles alongside the JS output at build time.

  1. A babel transform plugin is provided to either inline CSS imports into the requiring js file or output to another js file.
  2. A styles package would be provided to enable injecting the styles into the DOM at render.

This approach is very similar to how css-loader/style-loader work in webpack. We are just doing this at build time so that consumers no longer need to do this.

babelrc

{
  "plugins": [
    ["@patternfly/react-styles/babel", {
      "outDir": "./dist/styles"
    }]
  ]
}

In

src/components/Button.overrides.css
.bg {
  background-color: #000;
}
.unused-class {}
src/components/Button.js

This reflects how contributors will author components.

import React from 'react';
import { css } from '@patternfly/react-styles'
import styles from '@patternfly/patternfly-next/components/Button/index.css';
import overrides from './Button.overrides.css'

const Button = ({ children, variant }) => (
  <button className={css(styles.button, overrides.bg)}>
    {children}
  </button>
);

Out

dist/styles/components/Button.overrides.js
import { StyleSheet } from '@patternfly/react-styles'
export default StyleSheet.parse('.bg{background-color:#000}');
dist/styles/node/@patternfly/patternfly-next/components/Button/index.js
import { StyleSheet } from '@patternfly/react-styles'
const styles = StyleSheet.parse('.button{color: black;}');
dist/components/Button.js
import React from 'react';
import { css } from '@patternfly/react-styles'
import overrides from '../../styles/components/Button/Button.overrides.js';
import styles from '../../styles/node/@patternfly/patternfly-next/components/Button/index.js';

const Button = ({ children }) => (
  <button className={css(styles.button)}>
    {children}
  </button>
);

Consumption

import { Button } from '@patternfly/react-core';

const btn = <Button>Hello World</Button>;
/*
Renders to:

<button class="pf-c-button">Hello World</button>
*/

Detailed design

This would approach would create a new package @patternfly/react-styles. This package would include the following APIs

Babel plugin @patternfly/react-styles/babel

The babel plugin will take care of transforming and .css imports to javascript files that use the StyleSheet API listed below.

Example

.babelrc

{
  "plugins": [
    ["@patternfly/react-styles/babel", {
      // dir to output compiled style files
      // This might be able to be removed and we can output along side
      "outDir": "./dist/styles"
    }]
  ]
}

StyleSheet.parse(cssString): { [key: string]: PFStyleObject }

Parses a string of CSS and extracts classes out so that they can be referenced from an object instead of as a string value. CSS is injected through the css utility. The keys provided are a mapping to a camel-cased version of the className with pf-(l|c|p)- removed.

pf-c-button --> button
pf-is-primary --> isPrimary
pf-l-grid --> grid
pf-is-active --> isActive

Example

import { StyleSheet, css } from '@patternfly/react-styles';

const styles = StyleSheet.parse(`
  .pf-c-button { background: green }
  .pf-is-active { background: red }
`);

const btn = document.createElement('button');
btn.classList.add(css(styles.button, styles.isActive));
// <button class="pf-c-button pf-is-active" />

css(...styles: Array<PFStyleObject | string | void>): string

Joins classes together into a single space-delimited string. If a PFStyleObject is passed it will inject the CSS related to that object. This is similar to the ClassNames package.

Example

import { css } from '@patternfly/react-styles'
import styles from './Button.css';

const Buttton = ({ isActive, isDisabled, children }) => (
  <button
    disabled={isDisabled}
    className={css(
      styles.button,
      isActive && styles.isActive,
      isDisabled && styles.isDisabled,
    )}
  >
    {children}
  </button>
)
DOM output
<button disabled="" class="pf-c-button pf-is-disabled">
  Hello World
</button>

getModifier(styles: { [key: string]: PFStyleObject }, modifier: string, default?: string): PFStyleObject | null;

Since pf-next-core is maintaining a pattern of using pf-(is|has)-modifier for modifiers we will provide a utility for consumers to easily get the modifier given the style object and the desired modifier. A default can be provided as well if the given variant does not exist. Returns null if none are found.

Example

const styles = StyleSheet.parse(`
  .button {}
  .pf-is-secondary {}
  .pf-is-primary {}
`);

const Button = ({
  variant // primary | secondary,
  children,
}) => (
  <button
    className={css(
      styles.button,
      getModifier(styles, variant, 'primary'),
    )}
  >
    {children}
  </button>
);

Advantages

  1. No more need for CSS or sass loaders. This adds support for other bundles.
  2. Support for code splitting with styles. Only the styles needed for each component are imported.
  3. Removes the possibility of a typo in class name or referencing a class that no longer exists. The build can fail in those cases.
  4. Framework agnostic. This could work for React, web components, Vue, etc.
  5. A possibility of Dead code removal by removing classes that are unused.
  6. Class name minification for smaller bundles.
  7. Opens the door to unit testing plugins similar to jest-aphrodite-react, jest-glamor-react, or jest-styled-components
  8. Similar to using css-loader/CSS Modules. Education should be easy.

Drawbacks

  1. Reliance and need to support a babel plugin.
  2. A small possibility of larger file output. This would only happen if a consumer application also imports patternfly styles outside of patternfly-react. This would be an edge case and unlikely to happen.
  3. CSS injection Ordering for overrides could potentially be an issue but should be an edge case.

Alternatives

1. Continue to require consumers to pull un CSS or SASS reference classes.

Advantages

  1. Matches current pf-react-current's setup. This means nothing to learn or change for maintainers.
  2. No babel plugin.

Drawbacks

  1. Requires consumers to set up the loading of pf-next-core CSS and/or pf-react-next CSS.
  2. Difficult to import component CSS directly. This will be desired for proper code-splitting and places that burden on the consumer.
  3. Possible typos in class names
  4. No dead class elimination.
  5. No build failure on using classes that do not exist anymore.

2. Use a CSS-in-JS solution such as Aphrodite, Glamorous, styled-components, etc

Advantages

  1. No CSS loading required for consumers.
  2. Access to colors and other CSS variables in JS.
  3. No babel plugin.
  4. CSS is scoped to the component.
  5. Dead classes/style elimination.
  6. No possibility of typos (build will fail)
  7. Access to better unit testing tools.

Drawbacks

  1. CSS duplication from the core. We would need to write out own CSS to fit into the lib that we use. Making it hard to stay consistent with the core.
  2. Need to teach contributors the new lib.

Migration strategy

Since there are many large applications using pf-react-current we need to make the migration to pf-react-next as easy as possible for consumers. To do this we will follow and provide the following things:

  1. Components API's will match current APIs as much as possible.
  2. Codemods will be provided, where they can be, for any changes to those API's.
  3. Detailed migration docs for each component.
  4. A shim will be provided to let the user "phase" their migration. This can be CSS or Components, or both.

Example

Below is an example of how a user can migrate Button components from using pf-react-current to pf-react-next

Current

import { Button } from 'patternfly-react';

const PrimaryBlockButton = <Button bsStyle="primary" block>Primary Button</Button>;

Run current-to-next-button codemod

jscodeshift -t node_modules/@patternfly/react-codemods/transforms/current-to-next-button.js src/

Output

The following is the output that is now next compatible.

import { Button } from '@patternfly/react-core'

const PrimaryBlockButton = <Button variant="primary" block>Primary Button</Button>;

As an alternative/compliment to codemods, we can also provide a mapping for common bs props for some components, and warn on the deprecated bsProps.

bsStyle --> variant
bsSize --> size

How we teach this

Add a comprehensive ReadME within the @patternfly/react-styles package on using the babel plugin and other APIs.

Do a webcast/workshop showing how to create components using the new API's and how to upgrade from pf-react-current to pf-react-next.

Create an example application for consuming the new components.

Unresolved questions

What packages should we offer for these components? Should we have one for button, alert, core, etc or have all main components under core and secondary components in their own packages (charts, etc). Myself and some others I have talked to like the idea of packages per components, but I know others have not. This needs to be a decsion that everyone gets to provide input on, especially the consumers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment