How we support an array of diverse connectors

Wang Sijie - Jul 19 '23 - - Dev Community

Introduction

Connectors play a critical role in Logto. With their help, Logto enables end-users to use passwordless registration or sign-in and the capabilities of signing in with social accounts. Logto allows users to install or build their own connectors, and there are already more than 20 official connectors. Our connectors are designed to be highly flexible, and allow users to install or even build their own. At the same time, it is easy to develop a new connector. To achieve that, we designed and implemented a solution called “Dynamic Form” that is inspired of Config Driven Development (CDD).

Background

The configuration for Logto's connector is a JSON which, though flexible, can be challenging to edit and validate. In the begining, that is not a big problem, because at that time, most JSON configs are like:

{
  "appId": "xxx",
  "appSecret": "xx"
}
Enter fullscreen mode Exit fullscreen mode

But as we support more and more providers and protocals, things began to change. Take “SAML connector” as an example, there are more than 10 keys in the JSON config, and the type of value are complex, there are strings, numbers, JSONs, select values, and multi-line string for certificate.

So we think is time to bring in a fine-designed form to replace the JSON editor. The Logto Connector is designed to work as a third-party application, so hard-coding forms isn't a feasible solution. The result is a strong need for dynamic forms that are easy to manange, and have good user experience at the same time. The form for SAML connector that we mentioned above looks like:

SAML connector

What is Config Driven Development (CDD)

CDD, or Component Driven Development, presents an alternative approach to constructing applications. In the conventional method, lead architects create designs based on business needs, the application is then developed and deployed, and any modifications are executed either through additional elements or through difficult refactoring.

In contrast, CDD focuses on building independent components right from the beginning, starting at the most fundamental level. An interface, typically JSON, is established to assemble the higher-level user interface. This method, involving the combination of reusable components and a JSON blueprint, empowers developers to conveniently build applications in a more flexible and scalable manner.

The essence of CDD lies in its utilization of modularity to craft a loosely linked set of components, which are then brought together using a standardized interface.

What is a Dynamic Form?

Embracing the Config Driven Development (CDD) approach allows us to create dynamic web forms. These are not ordinary forms with static, unchanging fields; rather, they're dynamic entities with fields that are generated based on a JSON file. The beauty of these dynamic web forms lies in their flexibility – if a change in the form fields is required, all it takes is an update to the JSON configuration. This level of adaptability makes dynamic web forms an ideal solution for Logto Connectors, which require diffrent form configurations that are unknown to Logto.

Designing Schema

We designed the schema to be an array of form items, each item may have name, label, placeholder, and the most important, type. We have 6 types of form item, and defined as an enum:

enum FormItemType {
  Text = 'Text',
  Number = 'Number',
  MultilineText = 'MultilineText',
  Switch = 'Switch',
  Select = 'Select',
  Json = 'Json',
}
Enter fullscreen mode Exit fullscreen mode

And the type definition of form item is:

type FormItem = {
  type: FormItemType;
  name: string;
  label: string;
  placeholder?: string;
  required?: string;
  description?: string;
  tooltip?: string;
};
Enter fullscreen mode Exit fullscreen mode

Then the full schema is:

type Form = FormItem[];
Enter fullscreen mode Exit fullscreen mode

But there is a small problem, the form type "select" requires a list of options, so add this field to FormItem:

type BaseFormItem = {
  name: string;
  label: string;
  placeholder?: string;
  required?: string;
  description?: string;
  tooltip?: string;
};

type FormItem =
  | ({
      type:
        | FormItemType.Text
        | FormItemType.Number
        | FormItemType.MultilineText
        | FormItemType.Switch
        | FormItemType.Json;
    } & BaseFormItem)
  | ({ type: FormItemType.Select } & BaseFormItem & {
        options: Array<{ value: string; title: string }>;
      });
Enter fullscreen mode Exit fullscreen mode

Building the form with React-Hook-Form

Now that the schema is set, we can build the form in the front end based on the schema. We'll use React Hook Form to achieve that.

React-Hook-Form is a powerful tool for building efficient, easy-to-use forms in React.

The first thing is to init a form, that's assume that the JSON config is fetched and called formItems, and the data in the form is formData:

type FormData = Record<string, unknown>;

type Props = {
  formItems: FormItem[];
  formData: FormData;
};

const Form = ({ formItems, formData }: Props) => {
  const { register, control, handleSubmit } = useForm<FormData>({ defaultValues: formData });

  const onSubmit = handleSubmit(async (data: FormData) => {
    await submitFormToServer(data);
  });

  return <form onSubmit={onSubmit}></form>;
};
Enter fullscreen mode Exit fullscreen mode

Then, implement form control element according to the type, here is a simple demonstration:

<form onSubmit={onSubmit}>
  {formItems.map((formItem) => (
    <div key={formItem.name}>
      <div className="form-label">{formItem.label}</div>
      <div className="form-input">
        {formItem.type === FormItemType.Text && (
          <input type="text" {...register(formItem.name, { required: formItem.required })} />
        )}
        {formItem.type === FormItemType.Number && (
          <input type="number" {...register(formItem.name, { required: formItem.required })} />
        )}
        {formItem.type === FormItemType.MultilineText && (
          <textarea {...register(formItem.name, { required: formItem.required })} />
        )}
        {formItem.type === FormItemType.Switch && (
          <input type="checkbox" {...register(formItem.name, { required: formItem.required })} />
        )}
        {formItem.type === FormItemType.Select && (
          <select {...register(formItem.name, { required: formItem.required })}>
            {formItem.options.map((option) => (
              <option key={option.value} value={option.value}>
                {option.title}
              </option>
            ))}
          </select>
        )}
      </div>
    </div>
  ))}
</form>
Enter fullscreen mode Exit fullscreen mode

Conclusion

The versatility of Config Driven Development (CDD) shines through when applied to dynamic form creation, especially in the case of Logto's connectors. The advantages are twofold:

  1. For developers, it simplifies the process of creating interactive and user-friendly forms. Instead of dealing with intricate coding complexities, developers only need to define a JSON file to get the better-designed, intuitive user interfaces designed by Logto team.
  2. For users, this approach greatly simplifies the process of setting up a connector. It takes the complexity out of integration, making it easier for users to integrate Logto with social sign-in features, as well as email or SMS services.

Recently, articles promoting Low Code are ubiquitous on the internet. This solution was developed from the perspective of actual user needs, and we believe it is a great representation of Low Code.

Want to give it a try? Go to Logto Cloud and pick a connector.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player