Skip to content

Instantly share code, notes, and snippets.

@kinduff
Created June 19, 2025 04:35
Show Gist options
  • Save kinduff/078391fdbbd53bbb168cb14084e9bd64 to your computer and use it in GitHub Desktop.
Save kinduff/078391fdbbd53bbb168cb14084e9bd64 to your computer and use it in GitHub Desktop.

NavMatcher

A simple Ruby class for matching Rails controller/action patterns against current routes. Perfect for adding active states to navigation elements.

Installation

Drop nav_matcher.rb into your Rails app (e.g., lib/nav_matcher.rb or app/models/nav_matcher.rb).

DSL Syntax

NavMatcher uses a simple pattern-based DSL:

Basic Patterns

# Single controller#action
'users#index'           # Matches users controller, index action

# Multiple actions for same controller  
'users#index,show,edit' # Matches users controller with index, show, OR edit actions

Mixed Controller Patterns

# Different controllers and actions
'users#index,posts#show'           # Matches users#index OR posts#show
'users#index,show,posts#create'    # Matches users#index, users#show, OR posts#create

Namespaced Controllers

'api/users#show'         # Matches api/users controller, show action
'admin/posts#index,edit' # Matches admin/posts controller with index OR edit actions

Usage

Basic Usage

# Check if current route matches pattern
NavMatcher.active?('users#index', 'users', 'index')
# => true

NavMatcher.active?('users#index,show', 'users', 'edit') 
# => false

Rails Integration

As a Helper Method

# app/helpers/application_helper.rb
class ApplicationHelper
  def active_nav?(pattern)
    NavMatcher.active?(pattern, params[:controller], params[:action])
  end
  
  def nav_class(pattern, active_class: 'active', inactive_class: '')
    active_nav?(pattern) ? active_class : inactive_class
  end
end

In Views

<!-- Simple active class -->
<%= link_to "Users", users_path, class: nav_class('users#index') %>

<!-- Multiple actions -->
<%= link_to "User Management", users_path, 
    class: nav_class('users#index,show,edit') %>

<!-- Mixed controllers -->
<%= link_to "Content", "#", 
    class: nav_class('users#index,posts#show,create') %>

<!-- Conditional rendering -->
<% if active_nav?('admin/users#index,show') %>
  <div class="admin-notice">You're managing users</div>
<% end %>

Pattern Examples

Pattern Matches Description
users#index users#index Single route
users#index,show users#index, users#show Multiple actions, same controller
users#index,posts#show users#index, posts#show Different controllers
users#index,show,posts#create,edit users#index, users#show, posts#create, posts#edit Mixed pattern
api/users#show api/users#show Namespaced controller
admin/users#index,show,api/posts#create admin/users#index, admin/users#show, api/posts#create Mixed with namespaces
class NavMatcher
def self.active?(pattern, controller, action)
new(pattern, controller, action).active?
end
def initialize(pattern, controller, action)
@pattern = pattern
@controller = controller
@action = action
end
def active?
return false if @pattern.blank?
parsed_patterns = parse_pattern
parsed_patterns.any? do |expected_controller, expected_action|
@controller == expected_controller && @action == expected_action
end
end
private
def parse_pattern
results = []
current_controller = nil
@pattern.split(',').each do |part|
part = part.strip
if part.include?('#')
controller, action = part.split('#', 2)
current_controller = controller
results << [controller, action]
elsif current_controller
results << [current_controller, part]
end
end
results
end
end
test_cases = [
# [pattern, controller, action, expected_result, description]
['users#index', 'users', 'index', true, 'Simple controller#action'],
['users#index', 'users', 'show', false, 'Simple controller#action - no match'],
# Multiple actions for same controller
['users#index,show', 'users', 'index', true, 'Multiple actions - first action'],
['users#index,show', 'users', 'show', true, 'Multiple actions - second action'],
['users#index,show', 'users', 'edit', false, 'Multiple actions - no match'],
# Mixed controllers and actions - this is the key test case
['users#index,show,posts#create', 'users', 'index', true, 'Mixed pattern - users#index'],
['users#index,show,posts#create', 'users', 'show', true, 'Mixed pattern - users#show'],
['users#index,show,posts#create', 'posts', 'create', true, 'Mixed pattern - posts#create'],
['users#index,show,posts#create', 'posts', 'show', false, 'Mixed pattern - posts#show (no match)'],
['users#index,show,posts#create', 'users', 'edit', false, 'Mixed pattern - users#edit (no match)'],
# More complex mixed patterns
['users#index,show,edit,posts#create,destroy', 'users', 'edit', true, 'Complex - users#edit'],
['users#index,show,edit,posts#create,destroy', 'posts', 'destroy', true, 'Complex - posts#destroy'],
['users#index,show,edit,posts#create,destroy', 'comments', 'show', false, 'Complex - no match'],
# Namespaced controllers
['api/users#show', 'api/users', 'show', true, 'Namespaced controller'],
['api/users#show', 'users', 'show', false, 'Namespaced vs non-namespaced'],
# Edge cases
['', 'users', 'index', false, 'Empty pattern'],
[nil, 'users', 'index', false, 'Nil pattern']
]
puts "Running NavMatcher tests..."
puts "=" * 50
passed = 0
test_cases.each_with_index do |(pattern, controller, action, expected, description), i|
result = NavMatcher.active?(pattern, controller, action)
status = result == expected ? "PASS" : "FAIL"
if result == expected
passed += 1
else
puts "#{status}: Test #{i+1} - #{description}"
puts " Pattern: '#{pattern}', Route: #{controller}##{action}"
puts " Expected: #{expected}, Got: #{result}"
puts
end
end
puts "#{passed}/#{test_cases.length} tests passed"
puts
puts "Example usage:"
puts "NavMatcher.active?('users#index,show,posts#create', 'users', 'show')"
puts "=> #{NavMatcher.active?('users#index,show,posts#create', 'users', 'show')}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment