Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form handling #24

Open
ElvisCaballero opened this issue Apr 18, 2023 · 4 comments
Open

Form handling #24

ElvisCaballero opened this issue Apr 18, 2023 · 4 comments

Comments

@ElvisCaballero
Copy link

ElvisCaballero commented Apr 18, 2023

Hello, thanks for this awsome adapeter!

I have been using this plugin to create full webpage and have simple contact form that needs to submit data to WordPress and return errors if any.

On Frontend I did create Form helper and on WordPress I did create Rest route. My issue was, that this plugin doesn't document how to use Inertia.js form with Wordpress and I did need a quite a lot of research and debugging to get this working.
Now I have solution to handle errors in Inertia.js as it's required in their docs.

So my question - is possible to integrate or maybe document form use case in plugin?

I was not sure if I should include code snippets, but here you go.

Wordpress code for handling rest route

/**
 * Handle form
 *
 * @param \WP_REST_Request $request The request.
 */
public function handle_contact_form( \WP_REST_Request $request ) {
	$data = $request->get_params();

	// Validate the data.
	$rules = [
		'first_name' => [ 'required' ],
		'email'      => [ 'required', 'email' ],
	];

	$v = new \Valitron\Validator( $data );
	$v->mapFieldsRules( $rules );

	// If validation fails, store errors in session and redirect back to the previous page.
	if ( ! $v->validate() ) {
		$_SESSION['errors'] = $v->errors();

		wp_redirect( wp_get_referer() );
		exit;
	}

	// Code to save data in WP...

	// Redirect back.
	wp_redirect( wp_get_referer() );
	exit;
}

Code for WP session

if ( ! session_id() ) {
	session_start();
}

Frontend code for Svelte

<script> 
  import { useForm, page } from '@inertiajs/svelte';

  const form = useForm({
    first_name: '',
    email: '',
  });

  function handleSubmit() {
    $form.clearErrors();

    // Rest endpoint in WP
    const url = $page.props.ajax_url + '/contacts';

    $form.post(url, {
      preserveScroll: true,
      onSuccess: () => {
        $form.reset();
      },
    });
  }
<script>

<form on:submit|preventDefault={handleSubmit}>
  <div>
    <label for="cf-fist-name">Fist name</label>
    <input id="cf-fist-name" type="text" bind:value={$form.first_name} />
    {#if $form.errors.first_name}
      <span class="error">{$form.errors.first_name}</span>
    {/if}
  </div>

  <div>
    <label for="cf-email">Email</label>
    <input id="cf-email" type="email" bind:value={$form.email} />
    {#if $form.errors.email}
      <span class="error">{$form.errors.email}</span>
    {/if}
  </div>

  <button type="submit">Submit</button>
</form>
@boxybird
Copy link
Owner

boxybird commented Apr 18, 2023

Clever approach! I've never used the WP REST API with Inertia. The beauty of Inertia is doesn't require the need to create any API endpoints at all. Matter of fact, Inertia doesn't play nice with the traditional AJAX/REST API patterns.

Many features, including the "Form helper", expect that each round-trip to the server will always return an Inertia response. I.e returning Inertia::render(...). Your API endpoint (handle_contact_form) does not return a valid Inertia response, nor is a page visit.

Sadly, WP is not Laravel, and creating "routes" is a bit more crude. WP was not built for elegant front-end CRUD.

Here's a silly little proof-of-concept using a single text input that requires the user to enter a name...

Imagine a WP page with this URI: https://example.com/form-helper.

The page template lives in the theme here: wp-content/themes/your-theme/page-form-helper.php and looks like this:

<?php

// The initial page load will be a GET request, so this code will not run...
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // If the request is a POST (Meaning "form.post('/form-helper')" was called), 
    // then we can attempt to get the JSON POST data
    $data = json_decode(file_get_contents('php://input') ?: '[]', true);

    // Check if the name field is empty. If it is, add it to the error bag.
    if (empty($data['name'])) {
        // Pass the errors to Inertia.
        Inertia::share([
            'errors' => [
                'name' => 'Name is required',
            ],
        ]);
    }
}

return Inertia::render('PageFormHelper');

I use Vue, but the idea is the same. The important bit is the form.post('/form-helper') piece. It points to the same URI the form was initially rendered.

<script setup>
import { useForm } from '@inertiajs/inertia-vue3'

const form = useForm({
  name: '',
})
</script>

<template>
  <section>
    <form @submit.prevent="form.post('/form-helper')">
      <input type="text" v-model="form.name" />
      <div v-if="form.errors.name">{{ form.errors.name }}</div>
      <button type="submit" :disabled="form.processing">Login</button>
    </form>
  </section>
</template>

I'll admit procedural handling of a POST request is kinda gross, but hopefully makes it a bit easier to understand the round-trip.

@ElvisCaballero
Copy link
Author

It does not require any rest routes. Initially I did solution with admin-ajax, but in rest we get native WP $request->get_params(); function which is a lot nicer then file_get_contents('php://input').

expect that each round-trip to the server will always return an Inertia response

This is not true, as Inertia docs state we redirect user back with errors in SESSION.

Instead, you redirect (server-side) the user back to the form page they were previously on, flashing the validation errors in the session. Some frameworks, such as Laravel, do this automatically.

Just to clarify, my solution is working, but the question remains, can you include any guidance to others about form handling in WP with Inertia?

Forgot to add that I already share any errors from session to Inertia

$errors = $_SESSION['errors'] ?? null;
unset( $_SESSION['errors'] );
if ( $errors ) {
Inertia::share( 'errors', fn () => $errors );
}

@boxybird
Copy link
Owner

boxybird commented Apr 19, 2023

Thanks for the additional code snippet! I can now see how you got this working, very cool.

This is not true, as Inertia docs state we redirect user back with errors in SESSION.

You're probably right in regards to the official Laravel adapter. It has far more features and niceties built in. Plus, it sits on top of Laravel, which is also powerful. Especially when it comes to SESSION handling.

But now that I understand your solution better, you are successfully calling Inertia::render() on each trip.

When you call wp_redirect( wp_get_referer() ); in your REST endpoint, you're redirecting back to the PHP template containing the Inertia::render(). Which returns the needed valid Inertia response.


Just to clarify, my solution is working, but the question remains, can you include any guidance to others about form handling in WP with Inertia?

I'd love to include guidance to others. I'm just not sure of the best implementation here.

Your solution is great but requires SESSIONS. Not the end of the world, but it would require the user to handroll a solution like yours or we'd have to create some helper method like Ineria::back()->with([...]). Which would handle the SESSION and share data.

However, the part I don't love is the need to create a REST endpoint to handle the request. After all, the headline on the Inertia website is "Build single-page apps, without building an API.". I'm trying my best to stay in that lane.

My contrived example using procedural code and the nasty file_get_contents('php://input') also doesn't feel like a win.

I've always thought of the WP template hierarchy as a "router". And now that we render all templates in JS and not in PHP, I wonder if we could treat files like page.php and single.php more like controllers? I'm not sure what the looks like quite yet, but could be fun.

That said, I know WP is not Laravel. And if the REST idea you use ends up being the best solution, I'd be open to changing my "no REST endpoint" position.

@ElvisCaballero
Copy link
Author

ElvisCaballero commented Apr 20, 2023

You're probably right in regards to the official Laravel adapter. It has far more features and niceties built in. Plus, it sits on top of Laravel, which is also powerful. Especially when it comes to SESSION handling.

I see your point, but I don't think Inertia docs refer to Laravel for this section:

First, you submit your form using Inertia. If there are server-side validation errors, you don't return those errors as a 422 JSON response. Instead, you redirect (server-side) the user back to the form page they were previously on, flashing the validation errors in the session.

Anyway, I think including session management in plugin would be awesome! And to use Inertia::back would be very intuitive.

However, the part I don't love is the need to create a REST endpoint to handle the request.

For form handling, maybe you could create action user can hook into?
For example do_action('boxybird_inertia_form' function($request){}); and $request would be data from form?

I've always thought of the WP template hierarchy as a "router". And now that we render all templates in JS and not in PHP, I wonder if we could treat files like page.php and single.php more like controllers? I'm not sure what the looks like quite yet, but could be fun.

Well, there's solution with controllers using your plugin. Maybe thats what you are looking for?

I have different approach, I have one entry for whole page - index.php.
It contains loader, loader gets current page template and gets data for template from controller. Here's sample github repo I created - it's very basic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants