periqles

Painless forms for GraphQL.

Periqles is a React component library for Relay and Apollo that makes it easy to collect user input.

Periqles abstracts away the dirty work of form creation — with override switches built in for the design-conscious developer — so you can be free to focus on business logic. Given the name of a GraphQL mutation, periqles introspects the project's schema and intuits a form to match it. No need to waste time debugging React state management or fussing with flexbox — just drop in a tag and go on with your life.

“Having knowledge but lacking the power to express it clearly is no better than never having any ideas at all.”
-- Pericles

v2.0.0

Getting Started

To add a <PeriqlesForm /> to your Apollo or Relay client, follow these steps.

Prerequisites

  • React (v. 16.8.0 and up)
    npm install react
    

Installation

  1. Install periqles from the terminal.
    npm install periqles
    
  2. Import PeriqlesForm into your frontend.
    // MyReactComponent.jsx
    import PeriqlesForm from 'periqles';
    

Server

Periqles relies on introspection queries to intuit the optimal form UI from your project's GraphQL schema. These queries will hit your server in the form of POST requests to /graphql. To use periqles, you must expose your schema at that /graphql endpoint.

In our demo, we use the client-agnostic express-graphql package to spin up a server in Node for our GraphQL API. See the documentation here and our code here. Apollo projects may use the Apollo Server without problems.

//server.js

const express = require('express');
const {graphqlHTTP}  = require('express-graphql');
const app = express();
const {schema} = require('./data/schema/index.js');

app.post(
  '/graphql',
  graphqlHTTP({
    schema: schema,
    pretty: true,   // pretty-print JSON responses
  }),
);

If you are not using the /graphql endpoint to serve your API, options include configuring your server to redirect requests to /graphql to the correct endpoint or using a build tool like Webpack to proxy requests to /graphql to the correct address.

Schema

Currently, the introspection query used by periqles expects to find named input types on the schema. I.e., if you tell a <PeriqlesForm /> to generate a UI for your AddUser mutation, it will query your schema for a type called AddUserInput. Then it will render an input element for each input field listed on the AddUserInput type.

This means that periqles can successfully introspect this mutation:

#schema.graphql

type Mutation {
  addUser(input: AddUserInput!): AddUserPayload
}

# The mutation input is named and defined separately from the mutation.
input AddUserInput {
  username: String!
  password: String!
  email: String!
  gender: GenderEnum!
  pizzaTopping: PizzaToppingEnum!
  age: Int!
}

... but trying to introspect this mutation will cause your GraphQL server to throw back a 400 Bad Request error:

#schema.graphql

# The mutation input is not named and is defined in-line.
type Mutation {
  addUser(input: {
    username: String!
    password: String!
    email: String!
    gender: GenderEnum!
    pizzaTopping: PizzaToppingEnum!
    age: Int!
  }!): AddUserPayload
}

This is a high-priority area of improvement for us. We welcome PRs and other contributions.


Usage

<PeriqlesForm /> takes a number of props, including optional props to override its default logic for more fine-grained control over the apperance and composition of the form, the data sent to the API on submit, and state-management behavior.

PeriqlesForm Props

These are the props available to all clients. See below for more usage information specific to your client.

  • mutationName: string (required) — The name of a mutation as it appears on your GraphQL schema, e.g. 'AddUser' or 'AddUserMutation'.

    • If this is the only prop provided, periqles will render a form with default HTML intuited based on the name and scalar data type of each input field. E.g., an input field of type 'String' will result in <input type="text">. If the name of the input field appears in periqles' dictionary of common input fields, it will render a more specifically appropriate element. For example, a string-type field with the name 'password' will result in <input type="password">, and a field named 'mobile' will result in <input type="tel">.
  • specifications: object (optional) — If you wish to control the HTML or state management of a particular field, provide the instructions here. Periqles will fall back to its default logic for any fields or details not specified here.

    • header: string (optional) — If you wish to put a header on your form, e.g. "Sign up!", pass it here.
    • fields: object (optional) — Each key on fields should correspond to the name of an input field exactly as it appears on the schema. (E.g., based on the schema example above, 'pizzaTopping' is a valid key to use.) You can override defaults for as many or as few fields as you wish.
      • element: string (optional) — The HTML element you wish to use for this field, e.g. 'textarea', 'radio', 'datetime', etc.
      • label: string or element (optional) — The text, HTML, or JSX you wish to appear as a label for this field.
      • options: array (optional) — Whether or not this field is listed as an enumerated type on the schema, you may constrain valid user input on the frontend by using 'select' or 'radio' for the element field and providing a list of options here.
        • option: object (optional) — Specifies an option for this dropdown or group of radio buttons.
          • label: string or element (required) — The label you wish to appear for this option.
          • value: string or number (required) — The value to be submitted to the API.
      • render: function(params: {formState, setFormState, handleChange}) (optional) — If you wish to completely circumvent periqles' logic for rendering input fields, you may provide your own functional component here. The component you specify will completely replace the field <PeriqlesForm /> would have otherwise rendered. Parameters:
        • formState: object (optional) — The name and current value of each input field as key-value pairs.
        • setFormState: function(newFormState) (optional) — A React setState hook. Overwrites the entirety of formState with whatever is passed in.
        • handleChange: function(Event) (optional) — Destructures the input field's name and value off event.target to pass them as arguments to setFormState.
      • src: string (optional) — When element is 'img', the URL to use for the src attribute.
      • min: number (optional) — When element is 'range,' the number to use for the min attribute.
      • max: number (optional) — When element is 'range,' the number to use for the max attribute.
  • args: object (optional) — If there are any variables that you want to submit as input for the mutation but don't want to render as elements on the form, pass them here as key-value pairs. Example use cases include client-side authentication information or the clientMutationId in Relay. E.g.: const args = {userId: '001', clientMutationId: ${mutationId++}}. Fields listed here will be excluded from the rendered form but included on the mutation when the form is submitted.

  • callbacks: object (optional) — Developer-defined functions to be invoked when the form is submitted.

    • onSuccess: function(response) (optional) — Invoked if the mutation is successfully submitted to the API. In our demo (Relay, Apollo), we use onSuccess to trigger a very simple re-fetch and re-render of a component which displays <PeriqlesForm />'s output.
    • onFailure: function(error) (optional) — Invoked if the mutation fails to fire or the API sends back an error message. Use this to display meaningful error messages to the user.

Validation

Currently, periqles is able to validate input fields listed as non-null and of type string on the GraphQL schema. It will prevent the user from submitting the form if required text fields are left blank, including enumerated fields (represented by dropdowns or radio buttons) that are of type string.

This is high-priority area of improvement for us. If you have specific needs around validation, please open an issue or submit a PR.


Relay

In addition to the optional and required props listed above, <PeriqlesForm /> requires the following props when used in a Relay client:

  • environment: RelayEnvironment (required) — Your client's RelayEnvironment instance; necessary to send a mutation.
  • mutationGQL: GraphQLTaggedNode (required) — Your mutation, formatted as a tagged template literal using the graphql tag imported from react-relay. (NOT the version provided by graphql-tag.)

<PeriqlesForm /> uses the commitMutation function imported from Relay to fire off mutations when the form is submitted. If you pass an onSuccess and/or an onFailure callback on the callbacks prop, they will be invoked by commitMutation's onCompleted and onError callbacks, respectively.

CommitMutation takes additional callback parameters that are not currently included on <PeriqlesForm />'s callbacks prop, namely updater, optimisticResponse, optimisticUpdater, configs, and cacheConfigs. We plan to support these callbacks soon. If this is a high priority for your use case, please let us know by opening an issue, or submit a PR.

Here is a basic example of how to use <PeriqlesForm /> in Relay:

// MyComponent.jsx

import React, {useState} from 'react';
import {graphql} from 'react-relay';
import PeriqlesForm from 'periqles';

const ADD_USER =  graphql`
  mutation UserProfile_AddUserMutation($input: AddUserInput!) {
    addUser(input: $input) {
      username
      password
      email
      gender
      pizzaTopping
      age
    }
  }`;

const MyComponent = ({relay}) => {
  return (<div>
     <h1>Sign Up</h1>
     <PeriqlesForm
      mutationName={'AddUser'}
      mutationGQL={ADD_USER}
      environment={relay.environment}
     />
  </div>);
};

Full Code Sample


Apollo

In addition to the optional and required props listed above, <PeriqlesForm /> requires one additional prop when used in an Apollo client:

  • useMutation: function (required) — Your custom mutation hook, built using the useMutation hook imported from @apollo/client.

Here is a basic example of how to use <PeriqlesForm /> in Apollo:

// MyComponent.jsx

import React from 'react';
import { gql, useMutation } from '@apollo/client';
import PeriqlesForm from 'periqles';

const Signup = () => {
  const ADD_USER = gql`
    mutation AddUser($input: AddUserInput!){
      addUser(input: $input){
          username
          password
          email
          gender
          pizzaTopping
          age
        }
    }`;

  const [addUser, response] = useMutation(ADD_USER);

  return (<div>
     <h1>Sign Up</h1>
     <PeriqlesForm
      mutationName={'AddUser'}
      useMutation={addUser}
     />
  </div>);
};

Full Code Sample


Styles

Periqles comes with its own basic stylesheet, but it also attaches class names to each of its HTML elements that you can target in CSS for additional styling. We've tried to keep our default styles in that sweet spot between "enough to be presentable" and "adaptable to any design scheme." If you think we should provide more or less CSS, give us a shout in the issues.

Each element has two class names which follow this format:

  • "periqles-[element type]": e.g., 'periqles-textarea', 'periqles-radio-option'
  • "[field name]-[element type]": e.g., 'biography-textarea', 'gender-radio-option'

GitHub

https://github.com/oslabs-beta/periqles