Beee
Beee

Reputation: 150

How do I limit an ajax call to the scope with the provided parameter?

I am trying to 'fill' a select in a form, based on a stored (array) value: city_selector_vars. This stored value shows the correct values (see code below).

I loop through this array and try to get the the cities (get_states) for the defined stateCode. I pass stateCode as a value into the function.

If I log state_data.country_code before $.post I get the expected return. If I log state_data.country_code inside $.post it's not always the same as the passed country_code. I think it has something to do with scope but my knowledge is not good enough to figure it out.

/**
 * city_selector_vars
 * [
 *      'countryCode' => 'NL',
 *      'stateCode' => 'NL-FL',
 *      'cityName' => 'A name'
 * ]
 * [
 *      'countryCode' => 'BE',
 *      'stateCode' => 'BE-BR',
 *      'cityName' => 'Another name'
 * ]
 */
if ( true === Array.isArray(city_selector_vars) ) {
    for (i = 0; i < city_selector_vars.length; i++ ) {
        get_states(city_selector_vars[i].countryCode, (response)=> {
            // console.log(response);
        });
    }
}

function get_states(countryCode, callback) {
    const state_data = {
        action: 'get_states_call',
        country_code: countryCode
    };
    // console.log(state_data.country_code) shows correct country code
    $.post(ajaxurl, state_data, (response)=> {
        // here the wrong response first shows
        // state_data.country_code is not always the same as state_data.country_code
        callback(response);
    });
}
function get_states_call( $country_code = false ) {
    // $country_code = always false, even though it is passed
    if ( false == $country_code ) {
        if ( isset( $_POST[ 'country_code' ] ) ) {
            // here it sometimes picks up the incorrect value
            $country_code = $_POST[ 'country_code' ];
        }
    }
    // code removed, since depends on code above
    $items = [];
    echo json_encode( $items );
    die();
}

Because it only sometimes picks up the wrong value, I think it has to do with scope, but I don't know enough to pinpoint it.

Upvotes: 0

Views: 322

Answers (2)

mateleco
mateleco

Reputation: 589

As to the admin_post_edit_load_states function, I think Promise.all is the correct approach:

function admin_post_edit_load_states() {
    if (true === Array.isArray(city_selector_vars)) {
        // preparing the response array
        const response_states = []
        for (i = 0; i < city_selector_vars.length; i++) {
            // try - catch to handle errors
            try {
                // await the response
                const d = get_states(city_selector_vars[i].countryCode);
                // add response to the response array
                response_states.push(d)
            } catch (err) {
                // handle error
                console.log(err)
            }
        }
        console.log(response_states);

        Promise.all(response_states).then(function(jsonResults) {
            var instance_count = 0;
            for (i = 0; i < jsonResults.length; i++) {
                var obj = JSON.parse(jsonResults[i]);
                var len = obj.length;
                var $stateValues = '';
                var select_state = $('select[name*="row-' + instance_count + '"][name*="stateCode"]');
                var stored_state = city_selector_vars[instance_count].stateCode;
                select_state.fadeIn();
                for (j = 0; j < len; j++) {
                    $selected = '';
                    var state = obj[j];
                    var current_state = state.country_code + '-' + state.state_code;
                    if (current_state === stored_state) {
                        $selected = ' selected="selected"';
                    }
                    var selected = $selected;
                    $stateValues += '<option value="' + state.country_code + '-' + state.state_code + '"' + selected + '>' + state.state_name + '</option>';
                }
                select_state.append($stateValues);
                instance_count++;
            }
        });
    }
}

This should keep the original order while being asynchronous. Notice what we are doing here:

First we fill the array response_states with promises (each call to get_states returns a promise). Then, Promise.all(response_states) creates a new promise that will be resolved when all the promises in the response_states array are resolved. Finally, the parameter jsonResults is an array containing the corresponding value for each resolved promise in the array response_states.

Let's see if it works.

Upvotes: 0

muka.gergely
muka.gergely

Reputation: 8329

From your code it seems that you are implementing a function to query Wordpress data.

I don't think your problem is one of variable scope - it's rather a problem of async functions. You send out one AJAX request per country - every request travels to the server "individually", get processed by the server, and the response comes back. Nothing ensures that the response arrives in the same order as the requests were sent out.

Consider this:

const arr = []

function getSingleItem(id) {
  fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
    .then(response => response.json())
    .then(json => {
      console.log(json.id)
    })
}

function getAllItems() {
  for (let i = 1; i < 10; i++) {
    getSingleItem(i)
  }
}

getAllItems()

You can see in the console, that the responses do not arrive in the order the requests were sent (json.id is actually a running number from 1 to i - they should be in order).

You have to take care of ordering yourself:

const arr = []

function getSingleItem(id) {
  return fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
    .then(response => response.json())
    .then(json => {
      return json.id
    })
}

async function getAllItems() {
  for (let i = 1; i < 10; i++) {
    const r = await getSingleItem(i)
    arr.push(r)
  }
  console.log(arr)
}

getAllItems()

By setting up an async await in the functions I can be sure, that the responses show up in the order the requests were sent out. (await does not "let" the for() loop to go further until the awaited response does not arrive - kinda' like synchronous code.)

In this case I use that the fetch() function returns a Promise object - an object that can be awaited in my other function.

YOUR CODE

Your Javascript code can be modified:

/**
 * city_selector_vars
 * [
 *      'countryCode' => 'NL',
 *      'stateCode' => 'NL-FL',
 *      'cityName' => 'A name'
 * ]
 * [
 *      'countryCode' => 'BE',
 *      'stateCode' => 'BE-BR',
 *      'cityName' => 'Another name'
 * ]
 */
async function get_all_state(city_selector_vars) {
  if (true === Array.isArray(city_selector_vars)) {
    // preparing the response array
    const response_states = []
    for (i = 0; i < city_selector_vars.length; i++) {
      // try - catch to handle errors
      try {
        // await the response
        const d = await get_states(city_selector_vars[i].countryCode);
        // add response to the response array
        response_states.push(d)
      } catch (err) {
        // handle error
        console.log(err)
      }
    }
    // return the array - in order!
    return response_states
  }
}

function get_states(countryCode, callback) {
  const state_data = {
    action: 'get_states_call',
    country_code: countryCode
  };
  // console.log(state_data.country_code) shows correct country code

  // returning a Promise object, so await works in the other function
  return new Promise((resolve, reject) => {
    $.post(ajaxurl, state_data, (response) => {
      // here the wrong response first shows
      // state_data.country_code is not always the same as state_data.country_code
      // resolving the Promise when the response arrives
      resolve(response)
    });
  })


}

In your modified code a Promise had to be explicitly created (remember - fetch() returns a Promise?), so it can be handled by async-await.

I think the snippet should work as it is, but if not, then only a small debugging is needed :)

WORDPRESS NOTE

If you send JSON with Wordpress, it's advised to use the wp_send_json() function.

More: https://developer.wordpress.org/reference/functions/wp_send_json/

Upvotes: 1

Related Questions