0

Currently I'm trying to make repeating Ajax-Calls dynamic so my code gets better manageable. While doing so I encountered that I sometimes need dynamic data-attributes and -values. There is always just one data-value that changes, the other parameters stay the same. Doing so I could easily chain promises.

So here is an example of what I use as template for those Ajax-Calls:

var prepareAjax = {
    iterateValues: [1230,1280,4000,9000],
    ajaxOptions: [{
        timeout: 10000,
        type: "GET",
        data: `{
            param1: item,
            param2: obj.sessionId,
            param3: 1
        }`,     
        url: 'https://someurl.tld/target'
    }],
    sessionId: '<somestring>'
};

After this object I'm calling a function that should extract the ajaxOptions from the object like so:

function fetchChain(obj)=>{

    var ajaxPromises    = [], tempObj;

    obj.iterateValues.map((item, index)=> {

        tempObj         = obj.ajaxOptions[0];
        tempObj.data    = eval('('+tempObj.data+')');

        ajaxPromises.push(new Promise(
            (resolve, reject)=>{

                namespace.fetchData(tempObj);

            }
        );
    }
}

What I'm doing here is creating a promise and Ajax-Call for every ìterateValue. Then I'm using eval (yes, evil) to resolve the variables of the current context (fetchChain) and feed it to fetchData. The functions are executed withhin a namespace, so I'm calling them with namespace.fetchChain(prepareAjax) in example.

The problem

This example only works for one iteration since eval also seems to change obj permanently, even if I only eval/modify the tempObj, but obviously I want to reuse the template on every iteration. The only value that needs to be resolved is item, the parameters stay the same.

I also know new Function() is a better alternative to eval but I couldn't get it to work neither. What is more weird for me is that the function worked previously when eval'ing the data-attributes directly inside the Ajax-Call without using a preparation function like for fetchChain(). I'm stuck at this point, even after reading through several Answers on SO.

For completeness, here is the fetchData function:

function fetchData(obj)=>{

    // single ajax-calls should use a delay of 0
    obj.timeout = ((obj.timeout) ? obj.timeout : 10000),
    obj.retries = ((obj.retries) ? obj.retries : 5),
    obj.delay   = ((obj.delay) ? obj.delay : 1000),
    obj.type    = ((obj.type) ? obj.type : "GET"),
    obj.cnt     = ((obj.cnt) ? obj.cnt++ : 0);

    var sumDelay = obj.delay*(obj.cnt+1);

    setTimeout(()=>{
        return new Promise((resolve, reject)=>{

            return $.ajax(obj)
            .done((response)=>{

                return resolve(response);

            }).fail((error)=>{
                if(obj.cnt++ >= obj.retries){   
                    return resolve('Error');
                }
                fun.fetchData(obj);
            }).always((xd)=>{
            })
        })
    }, sumDelay)
}

A Solution

A solution I'm thinking of would be to prepare the object before feeding it to fetchChain(). Or to be more clear: in the context where prepareAjax gets created. Obviously I would prefer to directly handle this process inside fetchChain().

The Error

When executing fetchChain like described above I'm getting the Error Unexpected Identifier on second iteration inside of the eval. ([object Object]) When debugging one could see the obj also changed its value for data.

Pandora
  • 361
  • 3
  • 14
  • This all looks very convoluted. Use `$.extend()` or native `Object.assign()` to merge objects. Your string data concept makes no sens at all to me. Also don't understand why `ajaxOptions` is array. Third issue is `$.ajax` itself returns a promise so wrapping in `new Promise` is an antl-pattern – charlietfl Dec 04 '18 at 12:46
  • Have a look at my answer, I already changed it to be used without eval, which negates the need to use template-strings and makes it more performant. Depending the extra promises I was able to remove all of them, but now I'm still having problems to delay the ajax-calls. When I wrap the ajax inside timeout, the promise doesn't gets passed through. A solution would be to add a separate promise, so not sure why you complain about it, since other SO-answers suggest what I did here. A timeout inside `.done()` doesn't help when chaining those calls. – Pandora Dec 05 '18 at 01:51

2 Answers2

1

Why would not you do something like to get that obj dynamically?

e.g.:

const iterateValues = [1230,1280,4000,9000];
const getAjaxOpts = value => {
  return {
    ajaxOptions: [{
      timeout: 10000,
      type: "GET",
      data: /* apply data here from value */    
      url: 'https://someurl.tld/target'
    }],
  sessionId: '<somestring>'
  };
};

and do iterations like:

const mapped = iterateValues.map(x => getAjaxOpts(x));
// do your promise things afterwards
Nika
  • 1,864
  • 3
  • 23
  • 44
  • The amount and names of data properties can change, else this would be easier to achieve. But granted your approach could be used before calling `fetchChain()`. – Pandora Dec 04 '18 at 00:25
  • What do you mean by "amount and names of data properties can change"? Can you please give us more details on that? – Nika Dec 04 '18 at 00:27
  • Let's say I've a data-object `{p1: sessionid, p2: obj.id[index]}` and another data-object `{p1:'abc',p2:${value}, p3: obj.id[index]}`. The amount of iterations depends on `iterateValues`. When I prepare those objects outside of `fetchData` I have to wrap them in backticks, else they would get resolved. Then you think "let's separate the one changing parameter" but `fetchData` cannot know (w/o a second parameter) where to insert this changing value while iterating. Is this better understandable? Else I'm giving a longer example if needed. – Pandora Dec 04 '18 at 00:40
  • Hm, another idea would be to leave the one parameter that needs to be changed on iterations blank and replace it with the current map item. This way I wouldn't need to use eval. Maybe u meant this? – Pandora Dec 04 '18 at 00:45
  • I would suggest to use whole object that you want to pass. By guessing "either this changes or that" is a bad practice IMO. I would pass whole object instead by myself. Because what you're trying to do, instead of being simple, it becomes more complex and often will lead confusions afterwards. EDIT: not to mention the 'eval'. You should ALWAYS avoid it at any cost (because there always are options at 99% of cases). – Nika Dec 04 '18 at 10:58
  • You're right, but my use-case is special in a way where I can tell that most, if not all ajax requests share the same structure. I rewrote the function I posted as answer to be used without eval, I just had to add 2 more maps/loops to prepare the objects and I'm matching the property-names of `iterateValues` with propery-names inside `data`. IMO using a config like this is self explaining and the underlying functions doesn't need to be touched anymore. Most importantly I can unify repetitive error-handling now. – Pandora Dec 04 '18 at 11:11
  • Not totally understood what you're trying to achieve by that, but I would really avoid writing something like that though. I don't see any problems by transforming those variables into objects you want there, e.g. `iterateValues.map(x => ({ appid: x, session_id: '', ...etc });` and pass it to `getAjaxOpts`. EDIT: by noting that you want 'more manageable code' -- what you're trying to do is not manageable. What if properties change? What if you want to add more properties? What if you want some certain prop only for 1 item and not for the rest? It will get difficult time by time. – Nika Dec 04 '18 at 11:20
  • I updated my answer to reflect the changes I made by your suggestions, maybe it is getting more clear now. The `iterateValues[0][propertyname[0]]` must match `data[propertyname]` and this way I can replace multiple parameters if needed on every iteration and I always use corresponding arrays anyway. Thanks for your help btw, much appreciated! :) – Pandora Dec 04 '18 at 11:38
  • Well, you know your thing more ;d thanks and have a good day! – Nika Dec 04 '18 at 11:58
0

While trying to get rid of eval() I encountered the same problems as with eval → The original object always got changed too and this way all dynamic parameters were the same by the end of execution.

Turns out I had a fundamental misunderstanding on how '='-Operator works on objects. The objects doesn't get cloned and instead referenced. Since there are already better explained answers depending cloning objects in js, I'm just linking to one answer here.

So what actually happens when I use eval on the tempObj is that it not only turns the template-string (data-attribute) of tempObj into an object, but also the template-string of obj it references to.

An easy approach to fix this problem, which seems to be commonly used:

var A = JSON.parse(JSON.stringify(obj));

Others, that doesn't work for me, due to how they work:

var A = Object.create(obj);
var A = Object.assign({},obj);

There are even more and better solutions, but just have a look at the link above.

Since I was asking for a solution utilizing eval, I'm giving an example which actually works and even supports multiple dynamic parameters and parameter-values.

function chainAjax(){
    var ajaxPromises = [], tempObj,
        iterateProps = Object.getOwnPropertyNames(obj.iterateValues[0])

    obj.iterateValues[0][iterateProps].map((item, index)=> {

        tempObj         = JSON.parse(JSON.stringify(obj.ajaxOptions[0]));
        tempObj.data    = eval('('+tempObj.data+')');

        ajaxPromises.push(new Promise(
            (resolve, reject)=>{

                fetchData(tempObj).then(...)

            }
        ))
    })
    return Promise.all(ajaxPromises);
}

As template I would use something like this:

var prepareAjax = {
    iterateValues: [{
        appid: [1230,1280,4000,9000]
    }],
    ajaxOptions: [{
        timeout: 10000,
        type: "GET",
        data: `{
            appid: obj.iterateValues[0].appid[index],
            sessionid: '<somestring>',
            wizard_ajax: 1
        }`,     
        url: 'https://someurl.tld/target'
    }]
}

Last but not least an example on how to do this w/o eval:

function fetchChain(obj)=>{

    var ajaxPromises = tempArray = [], tempObject,
        iterateProps = Object.getOwnPropertyNames(obj.iterateValues[0]);

    // Prepare Data-Objects and resolve dynamic vars
    obj.iterateValues[0][iterateProps[0]].map((item, index)=> {
        tempObject = JSON.parse(JSON.stringify(obj.ajaxOptions[0])); // clone trick

        iterateProps.map(keyname => 
            tempObject.data[keyname] = obj.iterateValues[0][keyname][index]
        )
        tempArray.push(tempObject);
    });

    tempArray.map((item, index)=> {;
        ajaxPromises.push(
            fun.fetchData(item).then((response)=>{
                return response;
            }); 
        )
    })

    return Promise.all(ajaxPromises);
}

And a slightly different template:

var prepareAjax = {
    iterateValues: [{ // matching property-names
        appid: [1230,1280,4000,9000]//═══════╗
    }],                             //       ║
    ajaxOptions: [{                 //       ║
        data: {                     //       ║
            appid: '',//<════════════════════╝
            sessionid: '<somestring>',
            wizard_ajax: 1
        },      
        url: 'https://somedomain.tld/target'
    }]
}
Pandora
  • 361
  • 3
  • 14