Add a class to an element with Nokogiri
Asked Answered
V

3

14

Apparently Nokogiri's add_class method only works on NodeLists, making this code invalid:

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  anchor.add_class("whatever") # WHOOPS!
end

What can I do to make this code work? I figured it'd be something like

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  Nokogiri::XML::NodeSet.new(anchor).add_class("whatever")
end

but this doesn't work either. Please tell me I don't have to implement my own add_class for single nodes!

Vasques answered 30/1, 2011 at 4:55 Comment(0)
E
17

A CSS class is just another attribute on an element:

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  anchor['class']="whatever"
end

Since CSS classes are space-delimited in the attribute, if you're not sure if one or more classes might already exist you'll need something like

anchor['class'] ||= ""
anchor['class'] = anchor['class'] << " whatever"

You need to explicitly set the attribute using = instead of just mutating the string returned for the attribute. This, for example, will not change the DOM:

anchor['class'] ||= ""
anchor['class'] << " whatever"

Even though it results in more work being done, I'd probably do this like so:

class Nokogiri::XML::Node
  def add_css_class( *classes )
    existing = (self['class'] || "").split(/\s+/)
    self['class'] = existing.concat(classes).uniq.join(" ")
  end
end

If you don't want to monkey-patch the class, you could alternatively:

module ClassMutator
  def add_css_class( *classes )
    existing = (self['class'] || "").split(/\s+/)
    self['class'] = existing.concat(classes).uniq.join(" ")
  end
end

anchor.extend ClassMutator
anchor.add_css_class "whatever"

Edit: You can see that this is basically what Nokogiri does internally for the add_class method you found by clicking on the class to view the source:

# File lib/nokogiri/xml/node_set.rb, line 136
def add_class name
  each do |el|
    next unless el.respond_to? :get_attribute
    classes = el.get_attribute('class').to_s.split(" ")
    el.set_attribute('class', classes.push(name).uniq.join(" "))
  end
  self
end
Estaminet answered 30/1, 2011 at 5:2 Comment(0)
M
3

Nokogiri's add_class, works on a NodeSet, like you found. Trying to add the class inside the each block wouldn't work though, because at that point you are working on an individual node.

Instead:

require 'nokogiri'

html = '<p>one</p><p>two</p>'
doc = Nokogiri::HTML(html)

doc.search('p').tap{ |ns| ns.add_class('boo') }.each do |n|
  puts n.text
end
puts doc.to_html

Which outputs:

# >> one
# >> two
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html><body>
# >> <p class="boo">one</p>
# >> <p class="boo">two</p>
# >> </body></html>

The tap method, implemented in Ruby 1.9+, gives access to the nodelist itself, allowing the add_class method to add the "boo" class to the <p> tags.

Merlinmerlina answered 30/1, 2011 at 6:13 Comment(2)
Why can't you just do doc.search('p').add_class('boo').each do ...Electrodialysis
You can use doc = Nokogiri::HTML.fragment(html) if you don't want Nokogiri to add the doctype and other html and body tagsAjay
I
2

Old thread, but it's the top Google hit. You can now do this with the append_class method without having to mess with space-delimiters:

doc.search('a').each do |anchor|
  anchor.inner_text = "hello!"
  anchor.append_class('whatever')
end
Intermolecular answered 11/2, 2021 at 22:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.