DEV Community

Cover image for Building a Polymorphic React FormElement Component
Serif COLAKEL
Serif COLAKEL

Posted on

Building a Polymorphic React FormElement Component

When working on a complex React application, you often encounter the need for versatile form elements. These form elements can include buttons, links, inputs, selects, and text areas, each with various styling options. Instead of creating separate components for each form element, we can build a polymorphic React FormElement component. In this article, we'll explore how to create this component, its advantages, and potential future improvements.

The FormElement Component

The FormElement component is designed to handle different types of form elements and allows you to customize their styling using the variant prop. It supports various HTML elements like buttons, links, inputs, selects, and text areas, while maintaining a consistent API.

Here's the code for the FormElement component:

import React from 'react';

type FormElementBaseProps = {
  children?: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'outline' | 'link';
};

type FormElementProps = FormElementBaseProps &
  (
    | (React.ButtonHTMLAttributes<HTMLButtonElement> & {
        as: 'button';
      })
    | (React.AnchorHTMLAttributes<HTMLAnchorElement> & {
        as: 'a';
      })
    | (React.InputHTMLAttributes<HTMLInputElement> & {
        as: 'input';
      })
    | (React.SelectHTMLAttributes<HTMLSelectElement> & {
        as: 'select';
      })
    | (React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
        as: 'textarea';
      })
  );

export function FormElement({ variant, ...props }: FormElementProps) {
  switch (props.as) {
    case 'button':
      return <button {...props}>{props.children}</button>;
    case 'a':
      return <a {...props}>{props.children}</a>;
    case 'input':
      return <input {...props} />;
    case 'select':
      return <select {...props}>{props.children}</select>;
    case 'textarea':
      return <textarea {...props} />;
    default:
      return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

 Key Features

  • Polymorphism - The FormElement component is polymorphic, meaning it can render different types of form elements. This is achieved by using the as prop, which accepts a string value of the HTML element to render. For example, if you want to render a button, you would pass as="button" to the FormElement component.

  • Variant Support - The FormElement component supports different variants for each form element. This is achieved by using the variant prop, which accepts a string value of the variant to render. For example, if you want to render a primary button, you would pass variant="primary" to the FormElement component.

  • Consistent API - The FormElement component maintains a consistent API across all form elements. This is achieved by using the FormElementProps type, which is a union of all the different HTML element props. For example, if you want to render a button, you would pass as="button" and variant="primary" to the FormElement component.

 Advantages of the FormElement Component

  • Code Reusability - With the FormElement component, you can reuse the same component for various form elements. This reduces code duplication and simplifies maintenance.

  • Styling Consistency - By providing a variant prop, you can ensure that all your form elements have a consistent look and feel, maintaining a unified user interface.

  • Improved Development Speed - The FormElement component speeds up development by eliminating the need to create separate components for each form element type. It simplifies your codebase and streamlines development.

  • Enhanced Accessibility - You can add accessibility attributes and functionality consistently to all form elements using the FormElement component.

  • Expand Element Types - The FormElement component supports various HTML elements like buttons, links, inputs, selects, and text areas. You can easily add support for more element types in the future.

 Future Improvements

  • Support for More Form Elements - The FormElement component currently supports buttons, links, inputs, selects, and text areas. In the future, we could add support for checkboxes, radio buttons, and other form elements.

  • Support for More Variants - The FormElement component currently supports primary, secondary, outline, and link variants. In the future, we could add support for more variants like success, warning, and danger.

  • Support for Custom Styling - The FormElement component currently supports a limited set of styling options. In the future, we could add support for custom styling using CSS-in-JS libraries like styled-components or emotion.

  • Validation Support - The FormElement component currently doesn't support validation. In the future, we could add support for validation using libraries like Formik or React Hook Form. This could involve adding error messages, validation rules, and a valid prop to indicate the input's validity.

How to Use the FormElement Component

Using the FormElement component is straightforward. You specify the as prop to define the type of form element and use the variant prop for styling. Here's an example of how to use it:

import { FormElement } from './FormElement'; // Import your component here

export default function Usage() {
  return (
    <main>
      <FormElement as="button" variant="primary">
        Submit
      </FormElement>

      <FormElement as="a" href="https://example.com" variant="secondary">
        Example
      </FormElement>
      <FormElement as="input" type="text" variant="outline" />
      <FormElement as="select" variant="link">
        <option value="1">Option 1</option>
        <option value="2">Option 2</option>
        <option value="3">Option 3</option>
      </FormElement>
      <FormElement as="textarea" variant="primary" />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

 Writing Tests for the FormElement Component

When writing tests for the FormElement component, you should test each form element type separately. For example, if you want to test the button form element, you would write a test like this:

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormElement } from './FormElement'; // Import your component here

describe('FormElement Component', () => {
  it('renders a button with text', () => {
    render(<FormElement as="button">Click Me</FormElement>);
    const button = screen.getByRole('button', { name: 'Click Me' });
    expect(button).toBeInTheDocument();
  });

  it('renders an input with a placeholder', () => {
    render(<FormElement as="input" placeholder="Type something" />);
    const input = screen.getByPlaceholderText('Type something');
    expect(input).toBeInTheDocument();
  });

  it('handles click event for a button', () => {
    const handleClick = jest.fn();
    render(
      <FormElement as="button" onClick={handleClick}>
        Click Me
      </FormElement>
    );
    const button = screen.getByRole('button', { name: 'Click Me' });

    userEvent.click(button);

    expect(handleClick).toHaveBeenCalled();
  });

  it('renders a textarea with children', () => {
    render(<FormElement as="textarea">Your text here</FormElement>);
    const textarea = screen.getByText('Your text here');
    expect(textarea).toBeInTheDocument();
  });

  it('applies a variant class to the button', () => {
    render(
      <FormElement as="button" variant="primary">
        Primary Button
      </FormElement>
    );
    const button = screen.getByRole('button', { name: 'Primary Button' });
    expect(button).toHaveClass('primary');
  });

  it('applies a variant class to the input', () => {
    render(<FormElement as="input" variant="secondary" />);
    const input = screen.getByRole('textbox');
    expect(input).toHaveClass('secondary');
  });
});
Enter fullscreen mode Exit fullscreen mode

In this test suite, we're testing various aspects of the FormElement component, including rendering, user interaction, and styling. You should adapt these tests to match your specific component behavior and any enhancements you've made.

Remember to configure Jest and React Testing Library for your project and adjust the import path for the FormElement component accordingly. You can also include more tests to cover any additional functionality or future improvements you've implemented.

 Conclusion

The FormElement component is a versatile React component that can render different types of form elements. It supports various HTML elements like buttons, links, inputs, selects, and text areas, while maintaining a consistent API. It also supports different variants for each form element, allowing you to customize their styling. This component is a great way to simplify your codebase and speed up development.

By adopting the FormElement component and considering future enhancements, you can streamline your development process and provide a better user experience to your application's users.

 Resources

Top comments (2)

Collapse
 
techjayvee profile image
Jayvee Ramos

is this possible in react native also?

Collapse
 
serifcolakel profile image
Serif COLAKEL

Hii, thank your feedback. This elements cannot use in react native directly. But approach can be useful.

I can show like this example about it :)

import React from 'react';
import {
  TouchableOpacity,
  Text,
  TextInput,
  View,
  TextInputProps,
  TouchableOpacityProps,
  StyleSheet,
} from 'react-native';

type FormElementBaseProps = {
  children?: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'outline' | 'link';
};

type ButtonProps = TouchableOpacityProps & {
  as: 'button';
};

type AnchorProps = {
  as: 'a';
  href: string;
} & TouchableOpacityProps;

type InputProps = TextInputProps & {
  as: 'input';
};

type FormElementProps =
  | (FormElementBaseProps & ButtonProps)
  | (FormElementBaseProps & AnchorProps)
  | (FormElementBaseProps & InputProps);

export function FormElement({ variant, ...props }: FormElementProps) {
  const styles = StyleSheet.create({
    button: {
      padding: 10,
      backgroundColor: variant === 'primary' ? 'blue' : 'grey',
      borderRadius: 5,
    },
    buttonText: {
      color: 'white',
      textAlign: 'center',
    },
    link: {
      color: 'blue',
      textDecorationLine: 'underline',
    },
    input: {
      padding: 10,
      borderWidth: 1,
      borderColor: 'grey',
      borderRadius: 5,
    },
  });

  switch (props.as) {
    case 'button':
      return (
        <TouchableOpacity {...props} style={[styles.button, props.style]}>
          <Text style={styles.buttonText}>{props.children}</Text>
        </TouchableOpacity>
      );
    case 'a':
      return (
        <TouchableOpacity {...props} style={props.style} onPress={() => {}}>
          <Text style={styles.link}>{props.children}</Text>
        </TouchableOpacity>
      );
    case 'input':
      return <TextInput {...props} style={[styles.input, props.style]} />;
    default:
      return null;
  }
}
Enter fullscreen mode Exit fullscreen mode