Ajax: async or not async? callbacks, promesses

Ah ok merci :grinning:
Je vais essayer cela mercredi.

Doonc. En fait le problème ne vient pas de là mais ça me met sur la bonne piste, effectivement j’ai un souci dans mon code. Quand je fais un console.log() au cœur de la fonction ajax j’ai en fait les bonnes données, mais c’est après que ça se gâte.

Notamment j’avais mal compris cette fonction
https://datatables.net/reference/api/row().child()

Mais maintenant je suis en train de voir mes soucis ajax.

En relisant les échanges, je suis d’accord avec deneb.

Si ça marche quand tu mets en synchrone et pas en asynchrone c’est qu’il y a quelque chose que tu utilises quand tu mets à jour ton tableau qui a changé quand tu as fait le second clic, très probablement quelque chose que tu utilises dans « success ».

Est-ce que tu as bien vérifié toutes les variables qui existent dans success ? notament « row » et « tr », si ces variables correspondent à ton deuxième click ça provoquerait l’erreur que tu décrit.

1 « J'aime »

J’ai progressé mais je suis bloqué. Voici mes codes, simplifiés.

Pour préciser, ce n’est pas la première fois que j’utilise ajax, en revanche c’est la première fois que je cherche à récupérer une variable venant d’ajax pour l’afficher plus tard. D’ailleurs il y a peu ou pas d’exemples sur ce domaine dans stackoverflow sauf sur les promesses en javascript récent et zappant le côté ajax.

Tout d’abord la partie en R. Je présente cette partie pour dire que je ne peux pas mettre de js au dessus. (En réalité si, mais je voudrais éviter d’en mettre partout).

DT::datatable(
  data=mesdonnees(),
  callback = JS("   // mon code JS "
)

Voici le code js d’appel en on click:

table.on('click', 'td.details-control', function () {
	// A priori td et row fonctionnent puisque je vois bien la bonne valeur dans le console.log de ajaxFn ()
	var td = $(this),
	  row = table.row(td.closest('tr'));
	if (row.child.isShown()) {
	  row.child.hide();
	  td.html('⊕');
	} else {
	  setTimeout(function() {
		//-- 1 --- chargement_ajax() est bien  appelé. J'avais appelé 2 fois la fonction child() de datatables    de suite, ce n'était pas bon mais ce n'était pas en cause car ça continuait à fonctionner...en synchrone. De toute façon j'ai corrigé mon erreur.
		row.child(chargement_ajax(row.data())).show();
		td.html('⊖');
	  }, 1);  // avec des timeout de 2000 ms (2 sec) ça fonctionne en asynchrone, mais c'est insupportable
	}
})

Et le code appelé par le code précédent


var chargement_ajax = function (d) {
	// essai de variable globale pour  result0, mais c'est naze et ça ne donne rien
	var result0='rien0';
	var result2='rien2' ;

	var timeOutId;
       
        // d[2] c'est le nb de lignes attendues pré-calculées
	if(d[2]>0) {
	
	  function ajaxFn () {
	  
	  return $.ajax(mon_url, {
		  async: true,
		  data: {MonParam: d[1]},
		  error: function (XMLHttpRequest, textStatus, errorThrown) {
			alert(errorThrown);
		  },
		}).done(function(data) {
			  if (typeof data !== 'undefined') {
				  result_ajax_json= data;
				  tableau_html=result_ajax_json.toString();

				  result0 ='<b>éléments trouvée: </b> </br>'+
				  '<div class=\"TableauPlusHtml\">'+tableau_html+'</div>';
				  clearTimeout(timeOutId);

			  } else {

				 // j'ai vu ce système sur stackoverflow, et je n'ai pas de soucis a priori. 
				 timeOutId = setTimeout(ajaxFn, 10);
			  }
			  //-- 3 --- ça fonctionne jusqu'ici je récupère les bonnes valeurs quand je regarde avec F12
			  return result0;
		});

	  }
	  //-- 2 --- Je passe bien là car ajaxFn() est bien appelé, et  je vois bien dans F12 le GET avec les bonnes valeurs,
	  var promise = ajaxFn();
	  //-- 4 ---  mais je n'arrive pas à récupérer la valeur de sortie du done()
	  // J'ai même essayé avec en mettant en variable globale result0 mais c'est mal et de toute façon ça ne marche pas
	  result2= ???? fonction de promise ?;

	} else {
	  // là ça marche bien, normal je ne suis pas passé par l'ajax 
	  return result2;
	}
}

Alors là tout de suite j’ai la flemme mais un codepen ou équivalent pourrait graaave aider à ce qu’on t’aide à debugger ton bidule.

Sinon j’essaierai de regarder demain mais là ce soir la flemme :sweat_smile:

Ah et sors les fonctions de tes fonctions.

tu ne peux pas travailler comme ça avec de l’asynchrone, il te faut utiliser soit un « callback » c’est à dire une fonction qui sera lancée dès que les données sont disponibles, soit un promise (je te le mets cette version pour la science mais peut être la version callback est plus simple).

Je te propose ça comme solution, à adapter/corriger pour tes besoin:

//CALLBACK
table.on('click', 'td.details-control', function () {

// A priori td et row fonctionnent puisque je vois bien la bonne valeur dans le console.log de ajaxFn ()
	var td = $(this),
	  row = table.row(td.closest('tr'));
	if (row.child.isShown()) {
	  row.child.hide();
	  td.html('&oplus;');
	} else {
                var callback = function(data){
                       row.child(data).show();
                       td.html('&CercleMinus;');
                }	
		chargement_ajax(row.data(),callback)
	} 
})

var chargement_ajax = function (d,callback) {
	
        // d[2] c'est le nb de lignes attendues pré-calculées
	if(d[2]>0) {
	
	   $.ajax(mon_url, {
		  async: true,
		  data: {MonParam: d[1]},
		  error: function (XMLHttpRequest, textStatus, errorThrown) {
			alert(errorThrown);
		  },
		}).done(function(data) {
			  if (typeof data !== 'undefined') {
				  result_ajax_json= data;
				  tableau_html=result_ajax_json.toString();

				  var result0 ='<b>éléments trouvée: </b> </br>'+
				  '<div class=\"TableauPlusHtml\">'+tableau_html+'</div>';
				  clearTimeout(timeOutId);
                                  callback(result0);
			  }
		});

	  }
}
1 « J'aime »

Une fois n’est pas coutume, je mets la version promise dans un autre post pour bien séparer.
J’ai essayé de commenter pour décortiquer le code mais en vrai c’est assez compliqué à appréhender au début, la version callback est largement suffisante pour toi.

//PROMISE
table.on('click', 'td.details-control', 
// on rajoute async ici, sans ça le mot clé await ne fonctionne pas, mais attention la fonction aussi devient asynchrone. Mais dans ce cas précis ça ne pose aucun problème, la fonction n'est pas utilisée ailleurs. :)
async function () {

// A priori td et row fonctionnent puisque je vois bien la bonne valeur dans le console.log de ajaxFn ()
	var td = $(this),
	  row = table.row(td.closest('tr'));
	if (row.child.isShown()) {
	  row.child.hide();
	  td.html('&oplus;');
	} else {
                try{
                    // chargement_ajax renvoit maintenant une promesse, grace au mot clé await, on sait qu'on veut attendre le résultat avant de passer à la suite. Avec du code asynchrone classique ça ne peut pas marcher mais grâce à cette façon de coder on retrouve un code quasi équivalent à du synchrone.
                     data = await chargement_ajax(row.data());
                     row.child(data).show();
                     td.html('&CercleMinus;');
                } catch(error){
                     alert(error);
                }
		
	} 
})

var chargement_ajax = function (d) {
        // On transforme l'appel ajax en promise, resolve et reject permettent d'indiquer à quels endroits la Promesse se résout en succès ou en erreur. Leur traitement est totalement géré en dehors de la promesse.
	return new Promise(function(resolve, reject){
        // d[2] c'est le nb de lignes attendues pré-calculées
	if(d[2]>0) {
	
	   $.ajax(mon_url, {
		  async: true,
		  data: {MonParam: d[1]},
		  error: function (XMLHttpRequest, textStatus, errorThrown) {
			// C'est ici qu'on a l'erreur.
                         reject(errorThrown);
		  },
		}).done(function(data) {
			  if (typeof data !== 'undefined') {
				  result_ajax_json= data;
				  tableau_html=result_ajax_json.toString();

				  var result0 ='<b>éléments trouvée: </b> </br>'+
				  '<div class=\"TableauPlusHtml\">'+tableau_html+'</div>';
				  clearTimeout(timeOutId);
                                  
                                   // C'est ici qu'on a le succès
                                   resolve(result0);
			  }
		});

	  }
          }
}

Merci :slight_smile: je teste cela demain.

Merci merci @Mistermick :partying_face: :sunglasses: Ça fonctionne très bien, même sur les réponses longues et en cliquant rapidement :slight_smile:

J’ai effectivement choisi la solution des callback plus conforme à mes besoins. Mais merci tout de même pour la solution avec des promesses.

1 « J'aime »

J’ai suivi une formation Angular sur internet, et j’avais découvert à ce moment là les promesses. (Note que je n’ai pas pratiqué sur Angular.)

Mais j’ignorais qu’en js « normal » hors framework les promesses sont utilisées.

Oui c’est assez récent, dans la vie de JS.

Et puis il y a autre chose : pour éviter les problèmes de compatibilité c’est plutôt déconseillé de retrouver ça dans les scripts javascript, en particulier les mots clé async et await qui sont encore plus récents.
Cependant quand on code avec des frameworks genre Angular, en réalité on est obligé de compiler nos sources parce que tel quel ça ne marcherait tout simplement pas sur nos navigateurs, c’est à dire changer le source Angular en javascript vanilla, html et css, au passage la moulinette va transformer tout le code javascript moderne en un équivalent plus compatible (tu peux voir comment ça marche sur https://babeljs.io/).

Autrement dit, quand on a tout l’outillage dont on a de toutes façon besoin quand on code avec un framework on peut se permettre d’utiliser les derniers gadgets javascript, mais si comme dans ton example ton code se retrouve utilisé directement il vaut mieux rester sur les classiques.

1 « J'aime »

Alors oui et non. En vrai la plupart des navigateurs qi ont 3 ou 4 ans supportent toutes les fonctions récentes du JS. Il y a une stratégie intéressante à ce sujet pour arrêter de bundler inutilement le JS en ES5 : https://philipwalton.com/articles/deploying-es2015-code-in-production-today/

J’ai beau lire 3 fois ton article @Thomasorus, je n’ai pas compris si babel (avec npm ou webpack) était nécessaire dans son explication. :slight_smile: J’ai l’impression que si. Et donc ce n’est pas pertinent pour mes 30 lignes en js.

Je m’en tiens donc à la conclusion de @Mistermick en tout cas dans ce cas précis de R Shiny en back qui nécessite pas ou peu de js en front, puisque c’est Shiny qui génére l’UI et les échanges de données avec celle-ci.

https://shiny.rstudio.com/

Édit : et un second lien :

https://mastering-shiny.org/basic-app.html

J’aurais dû être plus clair désolé : en gros l’article dit qu’il y a pas besoin de compiler ton JS si tu utilises les fonctionnalités ES2015 et que tes utilisateurs ont un navigateur de moins de 3 ans. Ils comprendront ton fichier.

Et dans le cas où tu devrais supporter de vieux navigateurs tout en utilisant des fonctionnalités ES2015 et en faisant attention aux performances, il vaut mieux faire deux bundles: un avec compilé en ES5 et un autre pas compilé.

1 « J'aime »

Merci maintenant pour ta version promesse de la réponse !

Je suis revenu à ton explication (*) pour des promesses imbriquées que je n’arrivais pas à résoudre mais pour VueJs 3 donc en ES6 a priori.

Donc je viens de résoudre un souci que je viens d’avoir, 10 mois après mon premier POC en VueJs, 5 mois après le début de mon vrai gros projet en VueJs, mais je m’aperçois qu’à part les copier-coller des appels des promesses dans des API axios directement ou via Pinia qui appelle ces API, à part ça je n’utilise quasiment pas consciemment les promesses jusqu’à aujourd’hui.

Je fais appel aux setTimeout() pour 2 raisons:

Voilà ce que je fais finalement.

Une api

  getMonApiSelect() {
    return axios
      .post(API_URL + "mon_url/", ApiFct.headers_authHeader())
      .then((response) => {
        return response.data;
      })
      .catch(function (error) {
        console.log(error);
        return Promise.reject(error);
      });
  }

L’appel mon api :

  .getMonApiSelect(monid)
  .then(
	(datas) => {
	  courant = datas.data;

	  Bla1= [
		...Bla1.map((x) => Object.assign({}, x)),
		...courant ,
	  ];

	  return Promise.resolve(
		Object.values(courant ).map((x) => x.uneautrecolonneid)
	  );
	},
	(error) => {
	  return Promise.reject(error);
	}
  )
  .then((uneautrecolonneid) => {
	Bla2.push(
	  Object.assign(
		{},
		...uneautrecolonneid
	  )
	);
  });

Et je n’utilise setTimeout() que pour une variable booléenne reactive() globale sur laquelle est branché un affichage v-if. J’avais failli ne pas utiliser le 2e .then() mais utiliser de façon erronée un setTimeout() pour attendre uneautrecolonneid.

Mais j’aurais sûrement d’autres questions sur les promesses.

(*) Par hasard juste avant la clôture automatique des 2 ans par Discourse.

1 « J'aime »

Ah bah ça fait plaisir d’aider, même après tout ce temps :slight_smile:
Si tu veux un code un peu plus moderne, tu peux convertir ça avec la formule async/await comme ci-dessous, j’ai essayé de faire un code qui ressemble au tien.

Je trouve que c’est plus lisible avec les await une fois qu’on a l’habiture, ça ressemble presque à du code synchrone. :slight_smile:

const promise1 = new Promise((resolve, reject) => {
	resolve('Success!');
});

promise1
.then((value) => {
	console.log(value);
	return Promise.resolve(value+value.substr(-1));
},(err) => {
	console.log(err);
}).then((value2) => {
	console.log(value2); 
})

-----

const promise1 = new Promise((resolve, reject) => {
  resolve('Success!');
});

const f = async function(){
	try{
		const value = await promise1
		console.log(value);
		const value2 = value+value.substr(-1);
		console.log(value2);
	} catch(err) {
		console.log(err);
	}
}

f();

2 « J'aime »

:slight_smile: .

Ok je note ça :slight_smile: .

Tiens ça me fait penser, quand on est dans le then (ou le async () => { ... Try{) on peut récupérer la valeur et l’affecter ou au DOM ou à des variables réactives (ou de portée supérieure).

Mais est-ce qu’il y a un moyen de récupérer la variable par

const f = async function(){
    let value 2
	try{
		const value = await promise1
		value2 = value+value.substr(-1);
		//"success!!"
	} 
....
    return value2;
}


let toto = f(); //"success!!"

f() renvoie une valeur et non une promesse.
À part remonter la déclaration de value2 en dehors de f() et de lire après coup value2, j’ai l’impression que c’est impossible en dehors du périmètre asynchrone. Bon après c’est pas illogique.

La seule chose permise ce sont les promesses consécutives ? D’ailleurs en async () => { ... await, comment tu écris simplement l’équivalent des 2 then() se suivant ?

La fonction f est asynchrone donc effectivement elle ne peut renvoyer qu’une promesse. Pour récupérer la valeur il faut rester dans un contexte asynchrone

const f2 = async function(){
   let toto = await f();
}

L’équivalent de deux then se suivant c’est deux await, par exemple:

   axios.post()
        .then((response) => {return anotherPromise(response)})
        .then((finalData) => finalData.data)
        .catch((err) => doSomethingWith(err))

devient

const f = async function(){
   try{
      response = await axios.post()
      finalData = await anotherPromise(response)
      return finalData.data
   } catch(err) {
      doSomethingWith(err)
   }
}

Ca c’est pour les promesses consécutives, si tu veux lancer des promesses en parallèle tu peux te tourner vers Promise.all ou Promise.race par exemple. Par exemple Promise.all prend un tableau de promesses et se résout dès que toutes les promesses du tableau sont elles mêmes résolues.
Pratique si par exemple tu as plusieurs services à lancer et que tu veux changer le DOM dès que tu as les données de tous ces services.

1 « J'aime »

Tiens. J’avais complètement oublié ça. :ok_hand:

Attention au petit cas piégeux:

Si l’une des promesses de l’itérable échoue, Promise.all échoue immédiatement et utilise la raison de l’échec (que les autres promesses aient été résolues ou non).

2 « J'aime »