Envío de formularios sin cabeza con la API REST de WordPress | Programar Plus

Si está creando un sitio de WordPress, necesita una buena razón para no elegir un complemento de formulario de WordPress. Son convenientes y ofrecen muchas personalizaciones que requerirían mucho esfuerzo para construir desde cero. Representan el HTML, validan los datos, almacenan los envíos y brindan integración con servicios de terceros.

Pero supongamos que planeamos usar WordPress como un CMS sin cabeza. En este caso, interactuaremos principalmente con la API REST (o GraphQL). La parte frontal se convierte en nuestra responsabilidad por completo, y ya no podemos confiar en los complementos de formulario para hacer el trabajo pesado en esa área. Ahora estamos en el asiento del conductor cuando se trata de la parte delantera.

Los formularios eran un problema resuelto, pero ahora tenemos que decidir qué hacer con ellos. Tenemos un par de opciones:

  • ¿Usamos nuestra propia API personalizada si tenemos tal cosa? Si no, y no queremos crear uno, podemos optar por un servicio. Hay muchos buenos proveedores de formularios estáticos, y aparecen nuevos constantemente.
  • ¿Podemos seguir usando el complemento de WordPress que ya usamos y aprovechar su validación, almacenamiento e integración?

El complemento de formulario gratuito más popular, Contact Form 7, tiene un punto final de API REST de envío, al igual que el conocido complemento de pago, Gravity Forms, entre otros.

Desde un punto de vista técnico, no existe una diferencia real entre enviar los datos del formulario a un punto final provisto por un servicio o un complemento de WordPress. Entonces, tenemos que decidir en base a diferentes criterios. El precio es obvio; después de eso está la disponibilidad de la instalación de WordPress y su API REST. Enviar a un punto final presupone que siempre está disponible públicamente. Eso ya está claro cuando se trata de servicios porque pagamos para que estén disponibles. Algunas configuraciones pueden limitar el acceso de WordPress solo a los procesos de edición y creación. Otra cosa a considerar es dónde desea almacenar los datos, particularmente de una manera que cumpla con las regulaciones de GPDR.

Cuando se trata de funciones más allá del envío, los complementos de formulario de WordPress son difíciles de igualar. Tienen su ecosistema, complementos capaces de generar informes, PDF, integración fácilmente disponible con boletines y servicios de pago. Pocos servicios ofrecen tanto en un solo paquete.

Incluso si usamos WordPress de la manera “tradicional” con el front-end basado en un tema de WordPress, usar la API REST de un complemento de formulario podría tener sentido en muchos casos. Por ejemplo, si estamos desarrollando un tema utilizando un marco CSS que prioriza la utilidad, diseñar el formulario renderizado con marcado fijo estructurado con una convención de clase similar a BEM deja un sabor amargo en la boca de cualquier desarrollador.

El propósito de este artículo es presentar los dos puntos finales de envío de complementos de formulario de WordPress y mostrar una forma de recrear los comportamientos típicos relacionados con formularios a los que nos acostumbramos a salir de la caja. Al enviar un formulario, en general, tenemos que lidiar con dos problemas principales. Uno es el envío de los datos en sí, y el otro es proporcionar comentarios significativos al usuario.

Entonces, comencemos allí.

los puntos finales

El envío de datos es la parte más sencilla. Ambos extremos esperan una POST solicitud, y la parte dinámica de la URL es la ID del formulario.

Contact Form 7 REST API está disponible inmediatamente cuando se activa el complemento, y se ve así:

https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback

Si estamos trabajando con Gravity Forms, el punto final toma esta forma:

https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions

La API REST de Gravity Forms está deshabilitada de forma predeterminada. Para habilitarlo, debemos ir a la configuración del complemento, luego a la página de la API REST y marcar la opción “Habilitar el acceso a la API”. No es necesario crear una clave API, ya que el punto final de envío del formulario no lo requiere.

El cuerpo de la solicitud

Nuestro formulario de ejemplo tiene cinco campos con las siguientes reglas:

  • un campo de texto requerido
  • un campo de correo electrónico requerido
  • un campo de fecha obligatorio que acepta fechas anteriores al 4 de octubre de 1957
  • un área de texto opcional
  • una casilla de verificación obligatoria

Para las solicitudes del formulario de contacto 7 body claves, tenemos que definirlas con la sintaxis form-tags:

{
  "somebodys-name": "Marian Kenney",
  "any-email": "[email protected]",
  "before-space-age": "1922-03-11",
  "optional-message": "",
  "fake-terms": "1"
}

Gravity Forms espera las claves en un formato diferente. Tenemos que usar un ID de campo incremental generado automáticamente con el input_ prefijo. El ID es visible cuando está editando el campo.

{
  "input_1": "Marian Kenney",
  "input_2": "[email protected]",
  "input_3": "1922-03-11",
  "input_4": "",
  "input_5_1": "1"
}

Envío de los datos

Podemos ahorrarnos mucho trabajo si usamos las teclas esperadas para las entradas. name atributos De lo contrario, tenemos que asignar los nombres de entrada a las claves.

Poniendo todo junto, obtenemos una estructura HTML como esta para el Formulario de contacto 7:

<form action="https://your-site.tld/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback" method="post">
  <label for="somebodys-name">Somebody's name</label>
  <input id="somebodys-name" type="text" name="somebodys-name">
  <!-- Other input elements -->
  <button type="submit">Submit</button>
</form>

En el caso de Gravity Forms, solo necesitamos cambiar el action y el name atributos:

<form action="https://your-site.tld/wp-json/gf/v2/forms/<FORM_ID>/submissions" method="post">
  <label for="input_1">Somebody's name</label>
  <input id="input_1" type="text" name="input_1">
  <!-- Other input elements -->
  <button type="submit">Submit</button>
</form>

Dado que toda la información requerida está disponible en el HTML, estamos listos para enviar la solicitud. Una forma de hacer esto es usar el FormData en combinación con el fetch:

const formSubmissionHandler = (event) => {
  event.preventDefault();

  const formElement = event.target,
    { action, method } = formElement,
    body = new FormData(formElement);

  fetch(action, {
    method,
    body
  })
    .then((response) => response.json())
    .then((response) => {
      // Determine if the submission is not valid
      if (isFormSubmissionError(response)) {
        // Handle the case when there are validation errors
      }
      // Handle the happy path
    })
    .catch((error) => {
      // Handle the case when there's a problem with the request
    });
};

const formElement = document.querySelector("form");

formElement.addEventListener("submit", formSubmissionHandler);

Podemos enviar el envío con poco esfuerzo, pero la experiencia del usuario es insatisfactoria, por decir lo menos. Debemos a los usuarios tanta orientación como sea posible para enviar el formulario con éxito. Como mínimo, eso significa que tenemos que:

  • mostrar un error global o un mensaje de éxito,
  • agregar mensajes de error de validación de campo en línea y posibles direcciones, y
  • llamar la atención sobre las partes que requieren atención con clases especiales.

Validación de campo

Además de usar la validación de formulario HTML incorporada, podemos usar JavaScript para una validación adicional del lado del cliente y/o aprovechar la validación del lado del servidor.

Cuando se trata de la validación del lado del servidor, tanto Contact Form 7 como Gravity Forms ofrecen eso de forma inmediata y devuelven los mensajes de error de validación como parte de la respuesta. Esto es conveniente ya que podemos controlar las reglas de validación desde el administrador de WordPress.

Para reglas de validación más complejas, como la validación de campos condicionales, podría tener sentido depender solo del lado del servidor porque mantener la validación de JavaScript del front-end sincronizada con la configuración de los complementos puede convertirse en un problema de mantenimiento.

Si solo vamos con la validación del lado del servidor, la tarea consiste en analizar la respuesta, extraer los datos relevantes y manipular DOM como insertar elementos y alternar nombres de clase.

Mensajes de respuesta

La respuesta cuando hay un error de validación para el formulario de contacto 7 se ve así:

{
  "into": "#",
  "status": "validation_failed",
  "message": "One or more fields have an error. Please check and try again.",
  "posted_data_hash": "",
  "invalid_fields": [
    {
      "into": "span.wpcf7-form-control-wrap.somebodys-name",
      "message": "The field is required.",
      "idref": null,
      "error_id": "-ve-somebodys-name"
    },
    {
      "into": "span.wpcf7-form-control-wrap.any-email",
      "message": "The field is required.",
      "idref": null,
      "error_id": "-ve-any-email"
    },
    {
      "into": "span.wpcf7-form-control-wrap.before-space-age",
      "message": "The field is required.",
      "idref": null,
      "error_id": "-ve-before-space-age"
    },
    {
      "into": "span.wpcf7-form-control-wrap.fake-terms",
      "message": "You must accept the terms and conditions before sending your message.",
      "idref": null,
      "error_id": "-ve-fake-terms"
    }
  ]
}

En el envío exitoso, la respuesta se ve así:

{
  "into": "#",
  "status": "mail_sent",
  "message": "Thank you for your message. It has been sent.",
  "posted_data_hash": "d52f9f9de995287195409fe6dcde0c50"
}

En comparación con esto, la respuesta de error de validación de Gravity Forms es más compacta:

{
  "is_valid": false,
  "validation_messages": {
    "1": "This field is required.",
    "2": "This field is required.",
    "3": "This field is required.",
    "5": "This field is required."
  },
  "page_number": 1,
  "source_page_number": 1
}

Pero la respuesta en una presentación exitosa es mayor:

{
  "is_valid": true,
  "page_number": 0,
  "source_page_number": 1,
  "confirmation_message": "<div id='gform_confirmation_wrapper_1' class="gform_confirmation_wrapper "><div id='gform_confirmation_message_1' class="gform_confirmation_message_1 gform_confirmation_message">Thanks for contacting us! We will get in touch with you shortly.</div></div>",
  "confirmation_type": "message"
}

Si bien ambos contienen la información que necesitamos, no siguen una convención común y ambos tienen sus peculiaridades. Por ejemplo, el mensaje de confirmación en Gravity Forms contiene HTML y las claves del mensaje de validación no tienen la input_ prefijo: el prefijo que se requiere cuando enviamos la solicitud. Por otro lado, los errores de validación en el Formulario de contacto 7 contienen información que es relevante solo para su implementación frontal. Las claves de campo no se pueden usar inmediatamente; tienen que ser extraídos.

En una situación como esta, en lugar de trabajar con la respuesta que obtenemos, es mejor idear un formato ideal deseado. Una vez que tengamos eso, podemos encontrar formas de transformar la respuesta original en lo que creamos conveniente. Si combinamos lo mejor de los dos escenarios y eliminamos las partes irrelevantes para nuestro caso de uso, terminamos con algo como esto:

{
  "isSuccess": false,
  "message": "One or more fields have an error. Please check and try again.",
  "validationError": {
    "somebodys-name": "This field is required.",
    "any-email": "This field is required.",
    "input_3": "This field is required.",
    "input_5": "This field is required."
  }
}

Y en el envío exitoso, estableceríamos isSuccess a true y devolver un objeto de error de validación vacío:

{
  "isSuccess": true,
  "message": "Thanks for contacting us! We will get in touch with you shortly.",
  "validationError": {}
}

Ahora se trata de transformar lo que tenemos en lo que necesitamos. El código para normalizar la respuesta de Contact Forms 7 es este:

const normalizeContactForm7Response = (response) => {
  // The other possible statuses are different kind of errors
  const isSuccess = response.status === 'mail_sent';
  // A message is provided for all statuses
  const message = response.message;
  const validationError = isSuccess
    ? {}
    : // We transform an array of objects into an object
    Object.fromEntries(
      response.invalid_fields.map((error) => {
        // Extracts the part after "cf7-form-control-wrap"
        const key = /cf7[-a-z]*.(.*)/.exec(error.into)[1];

        return [key, error.message];
      })
    );

  return {
    isSuccess,
    message,
    validationError,
  };
};

El código para normalizar la respuesta de Gravity Forms termina siendo este:

const normalizeGravityFormsResponse = (response) => {
  // Provided already as a boolean in the response
  const isSuccess = response.is_valid;
  const message = isSuccess
    ? // Comes wrapped in a HTML and we likely don't need that
      stripHtml(response.confirmation_message)
    : // No general error message, so we set a fallback
      'There was a problem with your submission.';
  const validationError = isSuccess
    ? {}
    : // We replace the keys with the prefixed version;
      // this way the request and response matches
      Object.fromEntries(
        Object.entries(
            response.validation_messages
        ).map(([key, value]) => [`input_${key}`, value])
      );

  return {
    isSuccess,
    message,
    validationError,
  };
};

Todavía nos falta una forma de mostrar los errores de validación, los mensajes de éxito y las clases de alternancia. Sin embargo, tenemos una forma ordenada de acceder a los datos que necesitamos y eliminamos todas las inconsistencias en las respuestas con una ligera abstracción. Cuando se ensambla, está listo para colocarse en una base de código existente, o podemos continuar construyendo sobre él.

Hay muchas maneras de abordar la parte restante. Lo que tenga sentido dependerá del proyecto. Para situaciones en las que tenemos que reaccionar principalmente a los cambios de estado, una biblioteca declarativa y reactiva puede ayudar mucho. Alpine.js se cubrió aquí en CSS-Tricks, y encaja perfectamente tanto para demostraciones como para usarlo en sitios de producción. Casi sin ninguna modificación, podemos reutilizar el código del ejemplo anterior. Solo necesitamos agregar las directivas adecuadas y en los lugares correctos.

Terminando

Igualar la experiencia de front-end que brindan los complementos de formulario de WordPress se puede hacer con relativa facilidad para formularios sencillos y sin complicaciones, y de una manera que sea reutilizable de un proyecto a otro. Incluso podemos lograrlo de una manera que nos permita cambiar el complemento sin afectar la interfaz.

Claro, se necesita tiempo y esfuerzo para crear un formulario de varias páginas, vistas previas de las imágenes cargadas u otras características avanzadas que normalmente incluiríamos directamente en un complemento, pero cuanto más únicos sean los requisitos que tenemos que cumplir, más tiene sentido usar el punto final de envío, ya que no tenemos que trabajar contra la implementación de front-end dada que intenta resolver muchos problemas, pero nunca el particular que queremos.

El uso de WordPress como un CMS sin cabeza para acceder a la API REST de un complemento de formulario para llegar a los puntos finales de los envíos seguramente se convertirá en una práctica más utilizada. Es algo que vale la pena explorar y tener en cuenta. En el futuro, no me sorprendería ver complementos de formularios de WordPress diseñados principalmente para funcionar en un contexto sin cabeza como este. Puedo imaginar un complemento en el que la representación frontal sea una característica adicional que no sea una parte integral de su núcleo. Queda por explorar qué consecuencias tendría, y si podría tener éxito comercial, pero es un espacio fascinante para ver cómo evoluciona.