📜 ⬆️ ⬇️

Recursive nesting with $ .Deferred

image
Greetings Habr, I recently had a chance to write a polling service. In the admin of this service was a form with questions and notes embedded in them. And when I saved the question, I had to save all the nesting opened for editing, in which jQuery $ .Deferred helped me a lot, and I want to tell you about this in this article.

Suppose we have such a structure of questions and notes to them, as indicated in the screenshot to the right, we will analyze it. I am not a designer, stylized as I could, purely for this article, so excuse me.

Let's go in order.
First I will explain what the conditions were.
')
There are questions, there may be notes inside. When you click edit or save, the layout of the question / notes is returned from the server and replaced in the template. The task is not to lose changes to the notes while saving the question, if both the question and the attached note were edited at the same time.

There are several options for solving this problem, for sure you could send everything at once to the server and then disassemble it, but this would require a change in structure.
I liked the option of deferred saving the parent question, if there are some child elements to save. This behavior may be necessary in a variety of situations, and even in my short practice it has been required several times already.

For impatient at the very end of the article there are links to a demo.

The first question is no nesting.



Layout looks like this
<ul class="questions"> <li> <div class="question" id="id1"> <span class="text"> ,  </span> <span class="edit">edit</span> </div> </li> </ul> 


When you click “edit”, we send the question id to the server and get the layout in “edit mode”, replace it and it will look like this:

Layout in edit mode:
 <ul class="questions"> <li> <div class="question" id="id1"> <input type="text" name="title" value=" ,  "> <span class="save">save</span> </div> </li> </ul> 


Change the text, press “save” - pass the id of the question, the modified text and get the layout in “normal mode” (on the last but one screenshot).

In its simplest form, the logic of saving a question might look like this:
 $('.questions').on('click', '.save', function() { var $li = $(this).closest('li'); saveData($li); }); function saveData($li) { var $item = $li.children('.question'), id = $item.id, $input = $item.children('input'); $.ajax({ type: 'POST', url: '/save', dataType: 'json', data: $input.serialize(), success: function(response) { if ( ! response.errors) { $li.replaceWith(response.data); } } }); } 


Everything is easy here, once again I want to emphasize that after saving the server returns us a layout of the entire issue.

The second question is the first level nesting.



layout:
 <ul class="questions"> <li> <div class="question" id="id2"> <span class="text">   </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id1"> <span class="text">1 </span> <span class="edit">edit</span> </div> </li> <li> <div class="note" id="id2"> <span class="text">2 </span> <span class="edit">edit</span> </div> </li> </ul> </li> </ul> 


It is already more interesting here, because when you save / edit a question, the layout of the question is replaced, which means that all enclosures are replaced, i.e. notes. And what if during the save the question also edited the notes?
That's what i mean

If the user does not click “save” on the note, and immediately clicks on the “save” question - the edits in the note are not saved, the layout will simply be replaced with the one that comes back from the server. It turns out when saving a question, we need to see if there are no notes open for editing, and if there is, first save them and then save the question. Tobish we need to track the moment when all the attached notes are saved, it is in this place that $ .Deferred will help us.

At first I will sign how it works in theory, following the example shown in the picture above, then we will go through the code.
When you click the “save” question - in the $ .ajax method beforeSend we see if there are no nesting open for editing, if we have - we interrupt the saving of the question, create the $ .Deferred object and subscribe to finish saving all the nestings, after the completion we start saving the question again.

View code:
 $('.question').on('click', '.save', function() { saveData.call(this); }); function saveData() { // this     (   ,   ) var self = this, //     / $button = $(this), $item = $button.closest('div'), $li = $item.closest('li'), //        id = $item.attr('id').replace(/[^0-9.]/g, ""), inputs = $item.find(':input'), type = $item.attr('class'); //       (  ) return $.Deferred(function() { var def = this; $.ajax({ type: 'POST', url: '/save', dataType: 'json', data: inputs.serialize() + '&id=' + id + '&type=' + type, beforeSend: function(xhr){ //      ,       .ignore // .ignore      ,        // (              //     ) var $inner_notes = $li.find('ul .save').not('.ignore'); //    .. if($inner_notes.length) { //         var deferreds = []; $inner_notes.each(function() { //    ,         //  this  .save   deferreds.push(saveData.call(this)); }); //       $.when.apply(null, deferreds).always(function() { //      -    . // self         .save   saveData.call(self); }); //    xhr.abort(); } }, success: function(response){ if ( ! response.errors) { //    ,   $li.replaceWith(response.data); } else { //    ,       $button.addClass('ignore'); } }, error: function() { //    ,       $button.addClass('ignore'); } }).complete(function() { //       -    resolve() def.resolve(); }); }); } 


Surely there are a lot of “WTF? moments, so I will sign out more what I meant.

  1. What is this inside saveData?
    Answer:
    this is always equal to the save element of the saved question / notes.
    It was just easier for me to navigate the elements.
     //   saveData: function() { var self = this, $button = $(this), $item = $button.closest('div'), $li = $item.closest('li'); } //      $('.question').on('click', '.save', function() { saveData.call(this); }); //   var $inner_notes = $li.find('ul .save').not('.ignore'); $inner_notes .each(function() { deferreds.push(saveData.call(this)); }); //   saveData: function() { var self = this; .... $.when.apply(null, deferreds).always(function() { saveData.call(self); }); } 


  2. How do we track the completion of saving all nestings?
    Answer:
    With the help of deferred.
     var deferreds = []; $children_notes.each(function() { deferreds.push(app.saveData.call(this)); }); $.when.apply(null, deferreds).always(function() { saveData.call(self); }); 

    • We create an array, we add a call to the save function (the same function), but in the context of all nested comments opened for editing.
    • $ .when (func1, func2) .done (func3)
      The syntax speaks for itself; when running func1 and func2, run func3.
      We have an array of functions, so we pass them using .apply ().
    • Here, deferred begins to perform all the functions that we have transferred to it and waits while they are executed.
      Each nested function will notify of its completion using .resolve ()
       saveData: function() { ... return $.Deferred(function() { var def = this; ... $.ajax({...}).complete(function() { def.resolve(); }); } } 

    • As soon as all of them are executed, func3 will start, tobish in our case .always (function () {saveData.call (self);)), where self indicates the .save question element.


  3. Why make return $ .Deferred (function () {}); ?
    Answer:
    The deferred works in such a way that if you simultaneously subscribe to several AJAX requests and one of them fails (returns error), the further chain of function calls to the deferred will terminate and all remaining functions / requests will not be executed. We do not have an inference for the AJAX and we cannot change its behavior, but we can wrap the AYAX request in a kind of wrapper and upon completion, despite the success of the request, we must notify us of the successful execution.
     return $.Deferred(function() { var def = this; $.ajax({...}).complete(function() { def.resolve(); }); }); 


  4. Why return exactly $ .Deferred (function () {}); ?
    Answer:
    $ .Deferred can be created in two ways ...
     //    function someFunc(){ //  var def = $.Deferred(); setTimeout(function(){ //    def.resolve(); }, 1000); //  return def.promise(); } function someFunc(){ //    return $.Deferred(function() { var def = this; setTimeout(function(){ //    def.resolve(); }, 1000); })/* .promise() */; //    ,    .promise() , deferred    . } //someFunc.done(function() {}); 

    ... but if I did according to the first method, the Ajax would have to be rendered into a separate function, but I did not want to, so this is purely a matter of aesthetics.

  5. Why a class .ignore?
    Answer:
    The short answer is: not to get into the eternal cycle.
    Extended answer: it will be easier to explain, passing line by line, how the code does it.
    Suppose we have an open question for editing and 2 internal notes, click to save the question.
    1. After clicking .save for the question, we look for .save for all nested notes. Found 2, stopped saving the issue.
       beforeSend: function(xhr){ xhr.abort(); } 

    2. Successfully saved the first note, its layout was replaced.
    3. When saving the second note, some error occurred, the layout was not replaced, the .save button remained.
    4. Since there are no more notes left in the array for execution of the notes
       $.when.apply(null, deferreds).always(function() { saveData.call(self); }); 

    5. Again, we get to beforeSend and check for the presence of not saved nestings.
       var $inner_notes = $li.find('ul .save')/*.not('.ignore')*/; //  .not()  if($inner_notes.length) {} 

    6. Since one of the notes has not been preserved - we find it, and here one of two things happens.
      • Or, for some reason, the note will be saved successfully for the second time, after which the question will be saved successfully.
      • Or failure again and we end up in an endless loop.

      Thanks to the .ignore class, we can protect ourselves from such cases.
      Failed to save note? Well, ce la vie, we need to keep the main question.




The third question is layered nesting.



Layout:
 <ul class="questions"> <li> <div class="question" id="id3"> <span class="text"> c  </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id1"> <span class="text">1 </span> <span class="edit">edit</span> </div> </li> <li> <div class="note" id="id2"> <span class="text">2 </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id4"> <span class="text">4 </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id7"> <span class="text">7 </span> <span class="edit">edit</span> </div> </li> <li> <div class="note" id="id8"> <span class="text">8 </span> <span class="edit">edit</span> </div> </li> </ul> </li> <li> <div class="note" id="id5"> <span class="text">5 </span> <span class="edit">edit</span> </div> <ul> <li> <div class="note" id="id6"> <span class="text">6 </span> <span class="edit">edit</span> </div> </li> </ul> </li> </ul> </li> <li> <div class="note" id="id3"> <span class="text">3 </span> <span class="edit">edit</span> </div> </li> </ul> </li> </ul> 


It would seem that here the code will become absolutely terrible, but in fact the previous implementation is already practically suitable for this. It is necessary to add only one function.
The fact is that..
 var $inner_notes = $li.find('ul .save').not('.ignore') 

... will collect notes at all levels of nesting, and if the parent note is saved before the child, the child will not have time to save. The problem repeats. All we have to do is make each parent note behave like a question in relation to its children.
Those. if we save a note, and in beforeSend it turns out that inside it there are not yet saved notes - we suspend the saving of the parent notes and wait until all children are executed.
With a large number of nestings, such a deep recursion is obtained.

Let's say our user is completely mad and decided to save the question when he has such a branch open for editing.

We can not immediately save the “2nd note”, because then edits of all other notes will be lost.
So we have to go from the bottom up. What do you think will be the correct sequence for storing notes so as not to miss anything? 8.6, then 4, then 2 and the question.
But we press then we save on the question, which means we need a function that will find the nearest child elements, such a .closest () method itself, but vice versa.

Implementation and application of the function
 //      beforeSend ,   - getClosestChildrens saveData: function() { .... beforeSend: function(xhr){ var $inner_notes = $li.find('ul .save').not('.ignore'), //          $children_notes = getClosestChildrens($inner_notes); if($children_notes.length) { var deferreds = []; $children_notes.each(function() { deferreds.push(app.saveData.call(this)); }); //          $.when.apply(null, deferreds).always(function() { //       //      ,   . // self        . app.saveData.call(self); }); //   / xhr.abort(); } }, .... } //      (    ). //            //        ,    . // ..   " ",     . function getClosestChildrens($inner_notes) { var children_notes = $.grep($inner_notes, function(value, key) { var is_child_of = false, $btn = $(value), $parent_li = $btn.closest('li'); $inner_notes.not($btn).each(function(key,v) { if($(this).closest($parent_li).length) { is_child_of = true; } }); return is_child_of ? false : true; }); return $(children_notes); } 


Now the execution logic looks like this:
  1. Click the “save” question
  2. In beforeSend we find all the notes, discard everything except the closest children. Remains the 2nd note. We stop saving the question, subscribe to the completion of saving the 2nd note.
  3. We start saving the 2nd note, we see that there are nesting, we find them (4, 8, 6), discard everything except the closest ones. There remain 4 and 6. We stop saving the 2nd note, we subscribe to the end of saving the 4th and 6th.
  4. Begin saving the 6th note, no attachments, save it.
  5. Begin saving the 4th note, find the nesting (8e). We stop saving the 4th note, we subscribe to the end of the save on the 8th.
  6. Begin saving the 8th note, no attachments, save it.
  7. We start saving the parent note for the 8th, i.e. 4th note. There are no editable nestings, save.
  8. We start saving the parent note for the 8th and 6th, i.e. 2nd note. There are no editable nestings, save.
  9. We start saving the question. There are no editable nestings, save.


ps1 I have in all examples considered saving since the question, but this is only because the question is at the top, the most difficult option, so to speak. Of course, if in such a situation as shown in the last example, click save on one of the notes, say 2m, all notes attached to it are also saved successfully.

ps2 In all the examples I showed the function saveData, which is called when the item is saved. You also need to add the beforeSend function to the editData function, which is called when you click on the edit element. After all, if we click to edit a question, and edited notes remain inside - they also need to be saved, but you can already see this in the demo.

Thus, it is possible to preserve the structure of any nesting without losing editable data.

For the demonstration I had to use a little php (for answers from the server), which is why I cannot demonstrate the jsFiddle demo.
I filled in a fully working example on Github , so whoever is interested can download and play around.
Also I uploaded a demo for one stray hosting, see a demo .

I added a delay of 1 second so that you can see how the layout is replaced. Each time a question / note is edited or saved, the background of the current block flashes when the layout is changed. So it is easier to understand what is happening.

That's all, I will be glad to answer any questions in the comments.

Source: https://habr.com/ru/post/204456/


All Articles