javascriptjquerydomjquery-traversing

Find closest next element in DOM with jQuery


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

Solution

  • 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>