3

In the following example. I would like to find the first .invalid-feedback and .valid-feedback for both items #main and #secondary.

Obviously I am interested in the generic case, that's the reason why I wrote a prototype extension for jQuery.

$.fn.extend({
  closestNext: function (selector) {
    let found = null    
    let search = (el, selector) => {
      if (!el.length) return
      if (el.nextAll(selector).length) {
        found = el.nextAll(selector).first()
        return
      }
      search(el.parent(), selector)
    }

    search($(this), selector)
    return found
  }
})

// Proof
$('#main').closestNext('.invalid-feedback').text('main-invalid')
$('#secondary').closestNext('.invalid-feedback').text('secondary-invalid')
$('#main').closestNext('.valid-feedback').text('any-valid')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
    <div>
        <input id="main"/>
    </div>
    <div class="dummy"></div>
    <div class="invalid-feedback"></div>
    <div>
        <input id="secondary"/>
    </div>
    <div class="invalid-feedback"></div>
</div>
<div class="valid-feedback"></div>

What I wrote seems very complicated and I am expecting this kind of DOM traversal function to be part of jQuery out of the box. Unfortunately, I did not found any related function on the manual.

Is there a simpler way to achieve the same result as what closestNext does?

EDIT

From a more algorithmic side I am looking for a tree traversing function that goes in the following order, but with a complexity better than what I achieved in my example.

.
├── A1
│   ├── B1
│   │   ├── C1
│   │   ├── C2
│   │   └── C3
│   ├── B2
│   │   ├── C4 <--- Entry point
│   │   ├── C5
│   │   └── C6
│   └── B3
│       ├── C7
│       ├── C8
│       └── C9
└── A2
    ├── B4 
    │   ├── C10
    │   └── C11
    └── B5
        ├── C12
        └── C13

From the C4 Entry point, the exploration order is:

>>> traverse(C4)
C5, C6, B3, C7, C8, C9, A2, B4, C10, C11, B5, C12, C13
nowox
  • 25,978
  • 39
  • 143
  • 293
  • Do you know the html structure before hand? – Scath Jun 12 '19 at 17:34
  • expected response ? – Naga Sai A Jun 12 '19 at 17:46
  • @NagaSaiA The expected response for the given example is in my question – nowox Jun 12 '19 at 17:47
  • 1
    Given the answers (and a comment of yours on a deleted one) you likely need to clarify that the markup depth is unknown. – Asons Jun 12 '19 at 18:35
  • @HereticMonkey Simply put, OP don't know from which _ascendent_ the _next_ is to be found. – Asons Jun 12 '19 at 19:29
  • @LGSon Sure, I got that. Just noting that `closest()` does in fact move up the tree. Maybe not the way the OP would like, but it does ascend. – Heretic Monkey Jun 12 '19 at 20:39
  • In your traversal algorithm example, why are A1, B1, C1, C2, and C3 excluded from the traversal? Honestly, your traversing a good portion of the tree there; that's likely why jQuery does not have a built in method for it. It would tend to get very slow on large trees. – Heretic Monkey Jun 12 '19 at 20:43
  • Oh, never mind about why those are excluded; "closestNext", not "closestNearby" :) – Heretic Monkey Jun 12 '19 at 20:44
  • @HereticMonkey, simply because they appear *above* the item. Imagine you work on the tree example I gave, you open it in an text editor, you place your cursor at `C4` and you *find* for `B2`, you won't be able to find it unless you check the option `when reaching end, continue from the beginning of file`. – nowox Jun 12 '19 at 20:48

4 Answers4

2

I can't see how your initial code can be simpler. After all, it needs to iterate the markup.

What I did see though, was how it could be optimized, using return in favor of let found = null and only call el.nextAll(selector) once.

I also addressed cases where the element isn't found, as if not, you end up with an exception.

Stack snippet

$.fn.extend({
  closestNext: function (selector) {
    let search = (el, selector) => {
      if (!el.length) return el
      let f = el.nextAll(selector)
      return f.length ? f.first() : search(el.parent(), selector)
    }
    return search($(this), selector)
  }
})

// Proof
$('#main').closestNext('.invalid-feedback').text('main-invalid')
$('#secondary').closestNext('.invalid-feedback').text('secondary-invalid')
$('#main').closestNext('.valid-feedback').text('any-valid')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
    <div>
        <input id="main"/>
    </div>
    <div class="dummy"></div>
    <div class="invalid-feedback"></div>
    <div>
        <input id="secondary"/>
    </div>
    <div class="invalid-feedback"></div>
</div>
<div class="valid-feedback"></div>
Asons
  • 84,923
  • 12
  • 110
  • 165
-1

I havent had the opportunity to run this (EDIT: it has since been run and a correction has been made) but this will do the trick. we have 2 functions - the first functions looks for siblings of an element which match the target node we are searching for. The second function gets the ball rolling and ascends up the node tree if getSiblings does not return a match for the target element. I hope this helps.. enjoy



var getSiblings = function (elem,selector) {

    // Setup siblings array and start from the parent element of our input
    //this is under the assumption that you're only looking for an element that FOLLOWS #main/#secondary


    var sibling = elem,
    target = selector;

    // Loop through each sibling and return the target element if its found
    while (sibling) {

            if (sibling.hasClass(target) ) { //return the target element if its been located
                return sibling;
            }
        //since the element has not been located, move onto the next sibling
        if (sibling.next().length == 0) {
            return 0;
        } else {
            sibling = sibling.next();
        }

    }

    return 0; //failed to locate target
};

function firstOfClass(el,selector) {
   //our variables where startingPoint is #main/#secondary and target is what we're looking for
   var startingPoint = el,
   target = selector;
   while (startingPoint.parent().length) { 
//if it has a parent lets take a look using our getSiblings function
       var targetCheck = getSiblings(startingPoint, target);
       if ( targetCheck != 0 ) { //returns 0 if no siblings are found i.e we found a match
           return targetCheck;
       } else {
//lets keep ascending
           startingPoint = startingPoint.parent();
       }
   }
   return getSiblings(startingPoint, target);

}
//returns node if found, otherwise returns 0

firstOfClass( $('#main'),'valid-feedback' );

firstOfClass( $('#secondary'),'invalid-feedback' );

As you can see the solution does not require any intimate knowledge of your DOM but instead methodically searches for the target (unless found than it stops running) and continues to ascend until it has effectively exhausted its search.

Chris Rock
  • 133
  • 7
-1

Not sure that this entirely works, but I think so, sort of depends upon how complicated your markup is, but even then, I think it will give you the next occurrence of either the valid or invalid feedback classes relative to the element in question, since your id's are unique, and index + 1 in the matched set should be what you are looking for. Probably overkill, but it should get the next closest one in the DOM tree, not just siblings, children, etc. I put in the CSS changes to validate what it is doing.

$("#main, #secondary").on("click", function(e) {
  var invalid = $(this).add('.invalid-feedback');
  var valid = $(this).add('.valid-feedback');
  $(invalid.get(invalid.index( $(this)) +1)).css("background", "black");
  $(valid.get(valid.index( $(this)) +1)).css("background", "blue");
});
SScotti
  • 2,158
  • 4
  • 23
  • 41
-2

So, this works, but it definitely ain't clean. I have to ensure that the "previous" nodes are not included, so I filter them out...

I added a few more test cases to demonstrate that it does only act on "next" items.

I'd go with your existing solution.

$.fn.extend({
  closestNext: function(selector, originalMe) {
    const me = $(this);
    // try next first
    let nextSibling = me.next(selector);
    if (nextSibling.length) return nextSibling;
    // try descendents
    let descendents = me.find(selector).not((i, e) => (originalMe || me).prevAll().is(e));
    if (descendents.length) return descendents.first();
    // try next parent
    const parent = me.parent().not((i, e) => (originalMe || me).parent().prevAll().is(e));
    return parent.closestNext(selector, me);
  }
})

// Proof
$('#main').closestNext('.invalid-feedback').text('main-invalid')
$('#secondary').closestNext('.invalid-feedback').text('secondary-invalid')
$('#main').closestNext('.valid-feedback').text('any-valid')
.dummy {
  color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="valid-feedback dummy">should not change</div>
<div>
  <div class="invalid-feedback dummy">should not change</div>
  <div>
    <input id="main" />
  </div>
  <div class="dummy">should not change</div>
  <div class="invalid-feedback">should change</div>
  <div>
    <input id="secondary" />
  </div>
  <div class="invalid-feedback">should change</div>
</div>
<div class="valid-feedback">should change</div>
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
  • 1
    Do note, if an element, e.g. the first `.invalid-feedback`, isn't found, you get an odd result with your code. I posted a simplified version of the original, which address that. – Asons Jun 12 '19 at 21:58