Polymorphism

as prop

With the polymorphic as prop you can replace the DOM element that will be rendered. This is necessary when you need to change the element to meet a semantic need, or provide a different interaction.

NOTE: Usage of the as prop is a new pattern still being adopted in the Design System and therefore is not yet fully supported on all components. If you have an immediate need for component polymorphism please reach out to the Design System team so we can expedite the conversion process for your needs.

Using a polymorphic component

One common use case for component polymorphism is to provide a more semantically meaningful HTML element. Say for example a component that renders using a div element by default but a more semantically meaningful element(like a nav el) is preferred. This need can easily be accomplished using the following approach.

import { PolymorphicDivComponent } from 'some/psds/component/path'
import { PolymorphicButton } from 'some/other/psds/component/path'
import { Link } from 'some/routing-library'
render(
<>
{/* NOTE: The components default behavior is to render a `div` element. */}
<PolymorphicDivComponent />
{/* NOTE: Using the `as` prop, you can change the component to render using
* a different HTMLElement that is more semantically meaningful.
*/}
<PolymorphicDivComponent as="footer" />
<PolymorphicDivComponent as="header" />
<PolymorphicDivComponent as="nav" />
<PolymorphicDivComponent as="section" />
{/* NOTE: Using the as prop for changing Button components based on semantic
* need is also a common use case for polymorphism
*/}
<PolymorphicButton as="a" />
<PolymorphicButton as="button" />
{/* NOTE: The `as` prop can also be used to replace the rendering element
* with a new React Element. This can be used to achieve greater
* component composition and can help when integrating with external
* libraries.
*/}
<PolymorphicButton as={Link} />
</>
)

Usage with styled-components

The Design System polymorphic approach is designed to work with styled-components. You should be able to compose and style any Design System components that support polymorphism using the common styled-components api patterns.

Official styled-component polymorphic docs

import styled from "styled-components";
import { PolymorphicButton } from 'some/other/psds/component/path'
const StyledButton = styled(PolymorphicButton)`
color: red;
`;
render(
<>
<StyledButton />
<StyledButton as="a" />
<StyledButton as="span" />
</>
)

Usage with Emotion

The Design System polymorphic approach is also designed to work with emotion. You should be able to compose and style any Design System components that supports polymorphism using the common emotion api patterns.

Official emotion polymorphic docs

import emotion from "@emotion/styled";
import { PolymorphicButton } from 'some/other/psds/component/path'
/* NOTE: I've had some issues with the emotion typings to fully support our
* polymorphic `as` typings. Below is the approach I've found works the
* best and avoids type errors - although it's not as clean or as helpful
* with its introspections as the styled-component examples. I'm not as
* experienced with emotion as i would like to be...so if anyone reading
* this has a better approach please inform the Design System team or open
* an issue on github.
*
* - danethurber 03/25/2021
*/
const BaseStyledButton = emotion("button")`
color: hotpink;
`;
const StyledButton = React.forwardRef<HTMLButtonElement, any>((props, ref) => (
<BaseStyledButton as={PolymorphicButton} ref={ref} {...props} />
));
render(
<>
<StyledButton />
<StyledButton as="a" />
<StyledButton as="span" />
</>
)

Creating a polymorphic component

The utils package of the design system exports a genericized higher order function that you can use to replace the native React.forwardRef. Using the forwardRefWithAs utility will provide you with greater type introspection of the ref, native props/attribute typings, and will include typings for the as props.

import { forwardRefWithAs } from '@pluralsight/ps-design-system-util'
const Button = forwardRefWithAs<Props, 'button'>((props, ref) => {
const { as: Comp = 'button', ...rest } = props
return <Comp ref={ref} {...rest} />
})

If you need to use it with a render prop function it's straightforward.

import { forwardRefWithAs } from '@pluralsight/ps-design-system-util'
interface Props {
children?: React.ReactNode | (() => JSX.Element);
}
const TestInput = forwardRefWithAs<Props, "input">((props, ref) => {
const { as: Comp = "input", children, ...rest } = props;
return (
<Comp ref={ref} {...rest}> {isFunction(children) ? children() : children} </Comp>
);
});

If you need a component that also support static properties (like for instance you are using the compound components pattern) we provide a higher order function to create typesafe components with typed static properties.

import { forwardRefWithAsAndStatics } from '@pluralsight/ps-design-system-util'
const Nested: React.FC = props => <div {...props} />
interface Statics {
Nested: typeof Nested
}
const Compound = forwardRefWithAsAndStatics<unknown, 'div', Statics>(
(props, ref) => {
const { as: Comp = 'div', ...rest } = props
return <Comp ref={ref} {...rest} />
}
)
Compound.Nested = Nested

In the case you need to memoize a component, there is also a utility function that can be used to replace the functionality of the core React.memo function.

import { forwardRefWithAs, memoWithAs } from '@pluralsight/ps-design-system-util'
const ButtonImpl = forwardRefWithAs<Props, 'button'>((props, ref) => {
const { as: Comp = 'button', ...rest } = props
return <Comp ref={ref} {...rest} />
})
const Button = memoWithAs(ButtonImpl)
render(
<>
<Button />
<Button as="a" />
</>
)