Quick and Dirty DOM Filtering jQuery Plugin

Another tutorial, re-doing my old poor man’s list filter right, as a generalized plugin.

Code

$.fn.filteract=->
	# Return (implicitly) self for chaining.
	@each ->
		p=$ @ 
		ctrls=$ '<input class="filteract" type="text" 
			placeholder="Filter keywords"
			><span>✗</span
			><label><input type="checkbox">Inverted</label>'
		p.before(ctrls) # Insert into DOM.
		q=ctrls.first() # The input, q for "query".
		inv=ctrls.find '[type="checkbox"]' 
		.click -> q.trigger 'input' # Refresh q.
		q.on 'input keyup',(e)->
			# Esc to clear/reset inputs.
			if e.keyCode is 27
				q.val ''
				inv.prop 'checked',false

			words=q.val()
			.toLocaleLowerCase() # Why???
			.trim() # Prevent empty words.
			.split /\s+/ # Space delimited words.
			re=new RegExp (if words[0] then '(?=.*'+words.join(')(?=.*')+').+' else ''),'gim' 
			invert=inv.prop 'checked'

			p.children().each ->
				match=@.outerHTML.search(re) isnt -1
				$(@).toggle if invert then not match else match 

			p.resize()

		# Clear/reset button.
		p.on 'click','input.filteract+span',-> 
			q.val('').trigger 'input'
			inv.prop 'checked',false

Explained

  1. CoffeeScript!
  2. jQuery’s fn…
  3. >Who’s “this” (the @ in @each)? The object on which the filteract function is invoked:
  4. $('.filteractable').filteract();

    Thus iterating on the jQuery wrapped list of elements found:

    $('.filteractable').each(function(){...
  5. Inside the callback passed to “each”, “this” (the @ in “p=$ @”) would be each element in the list, of course. jQuery doesn’t wrap each DOMElement in the list, thus this annoyingly recurring $(this) idiom.
  6. Would’ve used (options)-> to allow control over features. But, other ways to configure: add classes to filteractables and check p.hasClass, or use data attributes? Maybe later.
  7. Splitting query to space delimited keywords so can ignore order. Using look-aheads, eg, “(?=.*foo)(?=.*bar).+” would match where “foo” and “bar” appear.
  8. Weirdly enough, splitting en empty string, or one with just whitespace in it, doesn’t produce an empty array. Thus testing words[0] isn’t empty.
  9. This one came out ugly!
    re=new RegExp (if words[0] then '(?=.*'+words.join(')(?=.*')+').+' else ''),'gim'
  10. “gim”: repeat search, case insensitive. Multi-line doesn’t help because matching still done line by line. Should remove newlines from haystack first… BTW, there’s a gazillion such sites, but Regex101.com is neat.
  11. Performance: this works too fast for me. But, maybe in other scenarios, like longer DOMs, could throttle or debounce filtering instead of on every key press? Also, queue instead of looping to allow reflows while filtering? Does $.delay() yield to reflow?
  12. Resize: because in my app the list is in a tabbed UI and browser doesn’t adjust container’s height.
  13. if/then/else: no xor operator. Meh.
  14. outerHTML instead of text(): exposes HTML markup, allowing me to search for words in, eg, URLs in links. NB: one is a DOM API, the other jQuery’s, and search is a JS built-in.
  15. I also wanted to comment on some common, needlessly complicated patterns
  16. Oh, and a CSS trick to overlap reset button and input:
    input.filteract+span{margin-left:-1em;margin-right:1em;}

    And pad-right the input.

Enhancements

  1. Publish this plugin? Soon.
  2. jsFiddle demo…
  3. Bookmarklet?!
  4. .litcoffee?!


Comments are closed.