jlrdw's avatar
Level 75

Jquery to Fetch JS post

This jquery works:

    $(function () {
        $("#postjq").click(function (event)
        {
            event.preventDefault();
            var $post = {};
            $post.petid = $('#petid').val();
            $post.species = $('#species').val();
            $post.ocheck = ($("#ocheck").prop("checked") == true ? '1' : '0');
            $post._token = document.getElementsByName("_token")[0].value
            $post._method = document.getElementsByName("_method")[0].value
            alert($post.species);
            $.ajax({
                url: '<?= DIR . "pet/petupdate" ?>',
                type: 'PUT',
                data: $post,
                cache: false,
                success: function (data) {
                    var message = "data updated";
                    alertWithoutNotice(message);
                    $('#msg').append('<div>' + data.success + '</div>');

                },
                error: function (data, ajaxOptions, thrownError) {
                    var status = data.status;
                    if (data.status === 422) {
                        $.each(data.responseJSON.errors, function (key, value) {
                            $('#msg').append('<div>' + value + '</div>');
                        });
                    }

                    if (status === 403 || status === 500) {
                        $('#msg').text("Not Auth");
                    }
                }
            });
        });


        function alertWithoutNotice(message) {
            setTimeout(function () {
                alert(message);
            }, 1000);
        }

    });

However my attempt at fetch only gives undefined:

    function postFetch() {
        fetch('http://localhost/laravel70/pet/petupdate', {
            method: 'PUT',
            headers: {
            },
            body: JSON.stringify({
                petid: document.getElementById('petid').value,
                species: document.getElementById('species').value,
                _token: document.getElementsByName("_token")[0].value,
                _method: document.getElementsByName("_method")[0].value,
            })
        })
                .then(function (data) {
                    console.log('Request success: ', data);
                    alert(data.success);
                })
                .catch(function (error) {
                    console.log('Request failure: ', error);
                });
    }

The network tab is showing the data:

{"_method":"PUT","_token":"4imgVtUixwfsnX3NfSX7gGWnf2fYsO8uah83E1s6","petid":"73","species":"dog"}

I'd like to eventually convert the jquery to fetch but also have the success div and any errors display in a div as well, it was pretty straight forward in jquery.

Any idea why I get undefined?

Also in jquery I post / put an array, I have not seen any examples of posting an array in fetch js.

0 likes
6 replies
jlrdw's avatar
Level 75

I found another good example at https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

Now I have:

    function postFetch() {
        const data = {petid: document.getElementById('petid').value,
            species: document.getElementById('species').value,
            _token: document.getElementsByName("_token")[0].value,
            _method: document.getElementsByName("_method")[0].value};

        fetch('http://localhost/laravel70/pet/petupdate', {
            method: 'PUT', // or 'PUT'
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
        })
                .then(response => response.json())
                .then(data => {
                    alert(data.success);
                    document.getElementById('msg').innerHTML = data.success;
                    console.log('Success:', data);

                })
                .catch((error) => {

                    console.error('Error:', error);
                });
    }

This works. Now I need to convert this part:

                .catch((error) => {
                    console.error('Error:', error);
                });

To this:

                error: function (data, ajaxOptions, thrownError) {
                    var status = data.status;
                    if (data.status === 422) {
                        $.each(data.responseJSON.errors, function (key, value) {
                            $('#msg').append('<div>' + value + '</div>');
                        });
                    }

                    if (status === 403 || status === 500) {
                        $('#msg').text("Not Auth");
                    }
                }

Not the jquery, but the handling of errors, validation and Auth I know I will need to use:

document.getElementById('msg').innerHTML = what goes here;

EDIT

For success this works:

                .then(response => response.json())
                .then(data => {
                    var div = document.getElementById('msg');
                    for (var key in data) {
                        div.innerHTML += data[key];
                    }
              })
            

But I have not figured out how to display the errors (like validation) in the div, I tried:

                .catch((error) => {
                    var div2 = document.getElementById('msg');
                    for (var k in error) {
                        div2.innerHTML += error[k];
                    }
               });

@tray2 or someone, any idea?

jlrdw's avatar
Level 75

I discovered another problem, I even added a Listener

document.getElementById("postjq").addEventListener("click", function (event) {
        event.preventDefault()
    });

If I use jquery, leave the species blank I get this

message	"The given data was invalid."
errors	Object { species: […] }
species	[ "The species field is required." ]
0	"The species field is required.

Which is correct since validation failed (species is required).

However leaving species blank in fetch js I get

Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException: The PUT method is not supported for this route. Supported methods: GET, HEAD. in file C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\AbstractRouteCollection.php on line 117

#0 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\AbstractRouteCollection.php(103): Illuminate\Routing\AbstractRouteCollection->methodNotAllowed()
#1 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\AbstractRouteCollection.php(40): Illuminate\Routing\AbstractRouteCollection->getRouteForMethods()
#2 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\RouteCollection.php(162): Illuminate\Routing\AbstractRouteCollection->handleMatchedRoute()
#3 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\Router.php(635): Illuminate\Routing\RouteCollection->match()
#4 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\Router.php(624): Illuminate\Routing\Router->findRoute()
#5 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Routing\Router.php(613): Illuminate\Routing\Router->dispatchToRoute()
#6 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php(165): Illuminate\Routing\Router->dispatch()
#7 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(128): Illuminate\Foundation\Http\Kernel->Illuminate\Foundation\Http\{closure}()
#8 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\TransformsRequest.php(21): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#9 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(167): Illuminate\Foundation\Http\Middleware\TransformsRequest->handle()
#10 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\TransformsRequest.php(21): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#11 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(167): Illuminate\Foundation\Http\Middleware\TransformsRequest->handle()
#12 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\ValidatePostSize.php(27): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#13 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(167): Illuminate\Foundation\Http\Middleware\ValidatePostSize->handle()
#14 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode.php(63): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#15 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(167): Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode->handle()
#16 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\fruitcake\laravel-cors\src\HandleCors.php(36): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#17 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(167): Fruitcake\Cors\HandleCors->handle()
#18 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\fideloper\proxy\src\TrustProxies.php(57): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#19 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(167): Fideloper\Proxy\TrustProxies->handle()
#20 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#21 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php(140): Illuminate\Pipeline\Pipeline->then()
#22 C:\BitNami\wampstack-7.4.7-0\apache2\laravel70up\vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php(109): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter()
#23 C:\BitNami\wampstack-7.4.7-0\apache2\htdocs\laravel70\index.php(64): Illuminate\Foundation\Http\Kernel->handle()
#24 {main}

So I also need help, I guess properly setting event.preventDefault(). I am only guessing that may be causing a problem. Any help properly posting (put) is appreciated.

jlrdw's avatar
Level 75

I got the event.preventDefault() figured out, that wasn't the problem. And it does post or put fine.

However the problems:

  • Still I get a 405 instead of a 422 for validation errors in fetch js.

  • In a regular XMLHttpRequest post or put request I get 200 even if a validation error.

Should I stay with jquery or try axios?

Bottom line I can now post or put, just can't get or deal with the other errors.

Is there some trick to fetch js to show validation errors I'm missing.

artcore's avatar
artcore
Best Answer
Level 5

I found it's generally best to let the browser figure out the content type and use formData() in stead of stringifying an data object

Here are my working parts.

class HttpProvider
{
  constructor(method = 'GET')
  {
    this.method = method;
    this.headers = {};
    this.credentials = null;
    this.body = null;
  }

  request(url)
  {
    return new Promise((resolve, reject) =>
    {
      fetch(url, this.options())
        .then(response =>
        {
          if (response.ok || response.status === 422)
            response.json()
                    .then(data => resolve(data))
                    .catch(error => console.error("Fetch Load Error - Invalid JSON returned", error));
          else
            console.error("Fetch Load Error - Connection Error: " + response.status, response.statusText);
        })
        .catch(error => console.error("Fetch Load Error - Connection Error: ", error));
    });
  }

  options()
  {
    const csrf        = document.querySelector('meta[name=csrf-token]'),
          contentType = this.body ? "multipart-form-data" : "application/json",//application/x-www-form-urlencoded
          headers     =
            {
              // "Content-Type":     contentType,
              // Accept:             'application/json',
              "X-Requested-With": 'XMLHttpRequest',
              'X-CSRF-TOKEN':     csrf ? csrf.getAttribute('content') : null
            };

    let config = {
      method:      this.method,
      credentials: this.credentials ? this.credentials : 'include',
      headers:     this.headers ? Object.assign(headers,this.headers) : headers

      //mode: "cors", // no-cors, cors, *same-origin
      //cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
      //redirect: "follow", // manual, *follow, error
      //referrer: "no-referrer", // no-referrer, *client
    };

    if (this.method !== 'GET')
      config.body = this.body;//JSON.stringify

    return config;
  }
}

window.Http = new HttpProvider();

export default window.Http;

Implementation

button.addEventListener('click', e =>
  {

    Array.from(document.querySelectorAll('.text-error')).map(error =>
    {
      error.remove();
    });

    const formData = new FormData(),
          inputs   = form.querySelectorAll('[name]');


    for (const input of inputs)
    {
      if ((input.type === 'checkbox' || input.type === 'radio') && !input.checked) continue;

      formData.append(input.name,
        input.type === 'file'
          ? input.files[0]
          : input.value);
    }

    const id  = formData.get('id'),
          url = form.getAttribute('action');


    Http.method = 'POST';
    if (id)
      formData.append('_method', 'PUT');

    Http.body = formData;


    Http.request(url).then(response =>
    {
      if (!response.hasOwnProperty('redirect'))
      {
        if (Array.isArray(response))
          messages.innerHTML = `<column class="base-50 error p-5 my-20 dismiss">${response.toString().replace(/\.,/g, '<br>')}</column>`;

        for (const input of inputs)
        {
          const dotNotation = input.name.replace(/[\[\]\[?]/g, '.')
                                .replace('..', '.');//clean up regex

          let lastDotIndex = dotNotation.lastIndexOf('.'),
              fieldLength  = dotNotation.length;

          if (fieldLength - 1 === lastDotIndex)
            fieldLength = lastDotIndex

          const index = dotNotation.substr(0, fieldLength);

          if (response.hasOwnProperty(index))
            input.insertAdjacentHTML('afterend', `<span class="text-error">${response[index]}</span>`);
        }
      }
      else
      {
        messages.innerHTML = `<column class="dismiss">${response.message}</column>`;

        if (response.hasOwnProperty('redirect') && typeof response.redirect === 'string')
          window.location.href = `${window.location.origin}/${response.redirect}`;
        else if (response.hasOwnProperty('disable') && response.disable)
          form.remove();
        else if (response.redirect)
          window.history.go(-1);
      }
    });
  });

For adding errors I'm using a custom validator that returns the input fields in dot notated key values

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Validator;

class ValidateJob
{
  
  /**
   * @var Request $request
   */
  protected $request;
  public    $connection;
  protected $messages         = [];
  protected $customAttributes = [];
  
  
  public function __construct($request, $connection = "core.database.default")
  {
    $this->request    = $request;
    $this->connection = $connection;
  }
  
  
  protected function rules()
  {
    return [];
  }
  
  
  public function handle()
  {
    /**
     * @var Validator $validator
     */
    $validator = app(\Illuminate\Contracts\Validation\Factory::class)
      ->make($this->request->input(), $this->rules(), $this->messages, $this->customAttributes);
    
    if ($validator->fails())
      throw new ValidationException($validator, new JsonResponse(
        array_combine($validator->errors()->keys(),$validator->errors()->all()), 422));
  }

That will auto append/remove the errors under the input fields.

Example of controller method

public function store(Request $request)
  {
    $this->dispatchNow(new ValidateOrderJob($request));
    $id = $this->dispatchNow(new StoreOrderJob($request->input()));
    
    return $this->dispatchNow(new RespondWithJsonOrRedirectJob($request,
      [
        'message'  => __('Successfully Stored!'),
        'id'       => $id,
        'redirect' => true,
      ]));
  }

Hope it helps put you on the right path. I know it's an opinionated implementation ;)

One fetch() quirk is that have to implement your own abort which I haven't done yet.

https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort

jlrdw's avatar
Level 75

Thanks @artcore want is really weird is a regular XMLHttpRequest actually is returning a status code 200 for failed validation.

Sounds like in fetch you have to implement your own custom validation rules in order to send a 422.

Yet jQuery has worked with laravel and sending the 422 just fine, and not having to implement anything special.

I'll go ahead and close this one out installed anp do a new post on XMLHttpRequest.

I am still trying to decide whether to go with fetch or something else. Your code has given me some things to think about.

artcore's avatar

@jlrdw That was my intention ;) I do send a 422 response from my validator but I assume Laravel does the same. In any case it's handier to have the error bags as key value so you can show the error message in the right spot based on input[name] which works a treat with my edit. I'm sure you can use Laravel's validation rules as usual, but it throws just an indexed array so all you can do is slap it in a single div, bunching all errors.

Please or to participate in this conversation.