Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nokogiri equivalent of jQuery closest() method for finding first matching ancestor in tree

jQuery has a lovely if somewhat misnamed method called closest() that walks up the DOM tree looking for a matching element. For example, if I've got this HTML:

<table src="foo">
  <tr>
    <td>Yay</td>
  </tr>
</table>

Assuming element is set to <td>, then I can figure the value of src like this:

element.closest('table')['src']

And that will cleanly return "undefined" if either of the table element or its src attribute are missing.

Having gotten used to this in Javascriptland, I'd love to find something equivalent for Nokogiri in Rubyland, but the closest I've been able to come up with is this distinctly inelegant hack using ancestors():

ancestors = element.ancestors('table')
src = ancestors.any? ? first['src'] : nil

The ternary is needed because first returns nil if called on an empty array. Better ideas?

like image 273
lambshaanxy Avatar asked Nov 03 '11 23:11

lambshaanxy


People also ask

Which jQuery function can be used to get first ancestor of the selected element?

jQuery closest() Method The closest() method returns the first ancestor of the selected element. An ancestor is a parent, grandparent, great-grandparent, and so on.

What is jQuery closest?

The closest() is an inbuilt method in jQuery that returns the first ancestor of the selected element in the DOM tree. This method traverse upwards from the current element in the search of first ancestor of the element.

What is the opposite of closest jQuery?

So the opposite of closest(), searching down, would be find().

How do you use the nearest method in Javascript?

The closest() method searches up the DOM tree for elements which matches a specified CSS selector. The closest() method starts at the element itself, then the anchestors (parent, grandparent, ...) until a match is found. The closest() method returns null() if no match is found.


3 Answers

You can call first on an empty array, the problem is that it will return nil and you can't say nil['src'] without getting sad. You could do this:

src = (element.ancestors('table').first || { })['src']

And if you're in Rails, you could use try thusly:

src = element.ancestors('table').first.try(:fetch, 'src')

If you're doing this sort of thing a lot then hide the ugliness in a method:

def closest_attr_from(e, selector, attr)
  a = e.closest(selector)
  a ? a[attr] : nil
end

and then

src = closest_attr_from(element, 'table', 'src')

You could also patch it right into Nokogiri::XML::Node (but I wouldn't recommend it):

class Nokogiri::XML::Node
  def closest(selector)
    ancestors(selector).first
  end
  def closest_attr(selector, attr)
    a = closest(selector)
    a ? a[attr] : nil
  end
end
like image 198
mu is too short Avatar answered Oct 20 '22 03:10

mu is too short


You can also do this with xpath:

element.xpath('./ancestor::table[1]')
like image 6
pguardiario Avatar answered Oct 20 '22 03:10

pguardiario


You want the src attribute of the closest table ancestor, if it exists? Instead of getting an element that might exist via XPath and then maybe getting the attribute via Ruby, ask for the attribute directly in XPath:

./ancestor::table[1]/@src

You'll get either the attribute or nil:

irb(main):001:0> require 'nokogiri'
#=> true

irb(main):002:0> xml = '<r><a/><table src="foo"><tr><td /></tr></table></r>'
#=> "<r><a/><table src=\"foo\"><tr><td /></tr></table></r>"

irb(main):003:0> doc = Nokogiri.XML(xml)
#=> #<Nokogiri::XML::Document:0x195f66c name="document" children=…

irb(main):004:0> doc.at('td').at_xpath( './ancestor::table[1]/@src' )
#=> #<Nokogiri::XML::Attr:0x195f1bc name="src" value="foo">

irb(main):005:0> doc.at('a').at_xpath( './ancestor::table[1]/@src' )
#=> nil
like image 3
Phrogz Avatar answered Oct 20 '22 04:10

Phrogz