Practice makes purrfect

Created on April 6, 2020.

When i want to practice componentization, i usually look for a candidate in a library like Bootstrap. The nice thing about these exercises is that if you create the same component more than once, you can gradually see the progress you’ve made. This time we’ll practice on the Alert component.

Basic

So according to the documentation an alert, in it’s most basic form, looks like this:

<div class="alert alert-primary" role="alert">
  A simple primary alert—check it out!
</div>
  • a div with alert role
  • root class: alert
  • contextual classes: primary, secondary, success, danger, warning, info, light and dark (alert-{contextual} class)

Result

import React, { ReactNode } from 'react';

export type AlertProps = {
  children: ReactNode;
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark';
};

const BS_ROOT = 'alert';

function Alert({ children, variant }: AlertProps): JSX.Element {
  return (
    <div role="alert" className={`${BS_ROOT} ${BS_ROOT}-${variant}`}>
      {children}
    </div>
  );
}

export default Alert;

Usage

import React from 'react';
import Alert from './components/Alerts/Alert';

function App(): JSX.Element {
  return (
    <Alert variant="primary">
      A simple primary alert—check it out!
    </Alert>
  );
}

Heading

<div class="alert alert-success" role="alert">
  <h4 class="alert-heading">Well done!</h4>
  <p>You made it.</p>
</div>

As there is no extra behavior tied to the heading, a string property looks like a perfect match otherwise an utility component <AlertHeader /> would have been in order.

Result

import React, { ReactNode } from 'react';

export type AlertProps = {
  children: ReactNode;
  heading?: string;
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark';
};

const BS_ROOT = 'alert';

function Alert({
  children,
  variant,
  heading,
}: AlertProps): JSX.Element {
  return (
    <div role="alert" className={`${BS_ROOT} ${BS_ROOT}-${variant}`}>
      {heading && <h4 className={`${BS_ROOT}-heading`}>{heading}</h4>}
      {children}
    </div>
  );
}

export default Alert;

And if you do need to have more fine-grained control of the html of the heading, you could transform the heading property into a slot, by changing the type from string to ReactNode.

Usage

import React from 'react';
import Alert from './components/Alerts/Alert';

function App(): JSX.Element {
  return (
    <Alert heading="Well done!" variant="success">
      <p>You made it!</p>
    </Alert>
  );
}

Dismissing

<div
  class="alert alert-warning alert-dismissible fade show"
  role="alert"
>
  <strong>Holy guacamole!</strong> You should check in on some of those
  fields below.
  <button
    type="button"
    class="close"
    data-dismiss="alert"
    aria-label="Close"
  >
    <span aria-hidden="true">&times;</span>
  </button>
</div>
  • a button with the class close
  • the alert needs to be enriched with the alert-dismissible class
  • when the user clicks on the dismiss button the alert should no longer be visible

I choose not to use the Bootstrap JS Library and let the alert itself control the dismissed state, if you ever need to do something more than just hiding the alert, you can always add an optional onAfterDismiss function property.

Result

import React, { ReactNode, useState } from 'react';
import classNames from 'classnames';

export type AlertProps = {
  children: ReactNode;
  dismissible?: boolean;
  heading?: string;
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark';
};

const BS_ROOT = 'alert';

function Alert({
  children,
  dismissible,
  heading,
  variant,
}: AlertProps): JSX.Element {
  const [dismissed, setDismissed] = useState(false);

  return (
    <div
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`, {
        [`${BS_ROOT}-dismissible`]: dismissable,
      })}
      style={{ display: dismissed ? 'none' : undefined }}
      role="alert"
    >
      {heading && <h4 className={`${BS_ROOT}-heading`}>{heading}</h4>}
      {children}
      {dismissible && (
        <button
          type="button"
          className="close"
          aria-label="Close"
          onClick={() => setDismissed(true)}
        >
          <span aria-hidden="true">&times;</span>
        </button>
      )}
    </div>
  );
}

export default Alert;

Usage

import React from 'react';
import Alert from './components/Alerts/Alert';

function App(): JSX.Element {
  return (
    <Alert variant="warning" dismissible>
      <strong>Holy guacamole!</strong> You should check in on some of
      those fields below.
    </Alert>
  );
}

If you are interested in my setup or in the tests using React Testing Library you can take a look at my React Playground Repository.

Next

Push me

Previous

From undefined to zero