Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ active_link_to 'Users', '/users', active: :inclusive
```

## Active Options
Here's a list of available options that can be used as the `:active` value
Here's a list of available options that can be used as the `:active` value (and `:exclude` value)

```
* Boolean -> true | false
Expand Down Expand Up @@ -103,6 +103,34 @@ If we need to set link to be active based on `params`, we can do that as well:
active_link_to 'Admin users', users_path(role_eq: 'admin'), active: { role_eq: 'admin' }
```

## Excluding Routes

Sometimes you want a link to be active for a broad set of URLs but exclude specific ones.
The `:exclude` option accepts the same formats as `:active`. If the exclude condition
matches, the link will not be active even if the active condition matches.

```ruby
# Active for the 'users' controller, but NOT for the 'index' action
active_link_to 'Users', users_path, active: [['users'], []], exclude: [[], ['index']]

# Active for all paths under /users, but not /users/admin
active_link_to 'Users', users_path, active: :inclusive, exclude: /\/users\/admin/

# Active for everything except the dashboard controller
active_link_to 'Other', other_path, active: [[], []], exclude: [['dashboard'], []]
```

This is useful for navigation where one link should be active for most pages, but a different
link should take over for specific pages:

```ruby
# "People" link is active only on the people controller
active_link_to 'People', people_path, active: [['people'], []]

# "Dashboard" link is active everywhere except the people controller
active_link_to 'Dashboard', dashboard_path, active: [[], []], exclude: [['people'], []]
```

## More Options
You can specify active and inactive css classes for links:

Expand Down Expand Up @@ -143,13 +171,20 @@ You may directly use methods that `active_link_to` relies on.
```ruby
is_active_link?(users_path, :inclusive)
# => true

# With exclude parameter
is_active_link?(users_path, :inclusive, /\/admin/)
# => true (unless current path matches /\/admin/)
```

`active_link_to_class` will return the css class:

```
```ruby
active_link_to_class(users_path, active: :inclusive)
# => 'active'

active_link_to_class(users_path, active: :inclusive, exclude: [[], ['index']])
# => '' (if current action is 'index')
```

### Copyright
Expand Down
83 changes: 49 additions & 34 deletions lib/active_link_to/active_link_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module ActiveLinkTo

# Wrapper around link_to. Accepts following params:
# :active => Boolean | Symbol | Regex | Controller/Action Pair
# :exclude => Boolean | Symbol | Regex | Controller/Action Pair
# :class_active => String
# :class_inactive => String
# :active_disable => Boolean
Expand All @@ -19,7 +20,7 @@ def active_link_to(*args, &block)
active_options = { }
link_options = { }
html_options.each do |k, v|
if [:active, :class_active, :class_inactive, :active_disable, :wrap_tag, :wrap_class].member?(k)
if [:active, :exclude, :class_active, :class_inactive, :active_disable, :wrap_tag, :wrap_class].member?(k)
active_options[k] = v
else
link_options[k] = v
Expand All @@ -40,9 +41,9 @@ def active_link_to(*args, &block)
end

link_options[:class] = css_class if css_class.present?
link_options['aria-current'] = 'page' if is_active_link?(url, active_options[:active])
link_options['aria-current'] = 'page' if is_active_link?(url, active_options[:active], active_options[:exclude])

link = if active_options[:active_disable] === true && is_active_link?(url, active_options[:active])
link = if active_options[:active_disable] === true && is_active_link?(url, active_options[:active], active_options[:exclude])
content_tag(:span, name, link_options)
else
link_to(name, url, link_options)
Expand All @@ -56,7 +57,7 @@ def active_link_to(*args, &block)
# active_link_to_class('/root', class_active: 'on', class_inactive: 'off')
#
def active_link_to_class(url, options = {})
if is_active_link?(url, options[:active])
if is_active_link?(url, options[:active], options[:exclude])
options[:class_active] || 'active'
else
options[:class_inactive] || ''
Expand All @@ -70,44 +71,58 @@ def active_link_to_class(url, options = {})
# Regex -> /regex/
# Controller/Action Pair -> [[:controller], [:action_a, :action_b]]
#
# The exclude parameter uses the same format as condition. If the exclude
# condition matches, the link will not be active even if condition matches.
#
# Example usage:
#
# is_active_link?('/root', true)
# is_active_link?('/root', :exclusive)
# is_active_link?('/root', /^\/root/)
# is_active_link?('/root', ['users', ['show', 'edit']])
# is_active_link?('/root', [['users'], []], [[], ['dashboard']])
#
def is_active_link?(url, condition = nil)
def is_active_link?(url, condition = nil, exclude = nil)
@is_active_link ||= {}
@is_active_link[[url, condition]] ||= begin
original_url = url
url = Addressable::URI::parse(url).path
path = request.original_fullpath
case condition
when :inclusive, nil
!path.match(/^#{Regexp.escape(url).chomp('/')}(\/.*|\?.*)?$/).blank?
when :exclusive
!path.match(/^#{Regexp.escape(url)}\/?(\?.*)?$/).blank?
when :exact
path == original_url
when Regexp
!path.match(condition).blank?
when Array
controllers = [*condition[0]]
actions = [*condition[1]]
(controllers.blank? || controllers.member?(params[:controller])) &&
(actions.blank? || actions.member?(params[:action])) ||
controllers.any? do |controller, action|
params[:controller] == controller.to_s && params[:action] == action.to_s
end
when TrueClass
true
when FalseClass
false
when Hash
condition.all? do |key, value|
params[key].to_s == value.to_s
end
@is_active_link[[url, condition, exclude]] ||= begin
active = check_active_condition(url, condition)
if active && exclude
active = !check_active_condition(url, exclude)
end
active
end
end

private

def check_active_condition(url, condition)
original_url = url
url = Addressable::URI::parse(url).path
path = request.original_fullpath
case condition
when :inclusive, nil
!path.match(/^#{Regexp.escape(url).chomp('/')}(\/.*|\?.*)?$/).blank?
when :exclusive
!path.match(/^#{Regexp.escape(url)}\/?(\?.*)?$/).blank?
when :exact
path == original_url
when Regexp
!path.match(condition).blank?
when Array
controllers = [*condition[0]]
actions = [*condition[1]]
(controllers.blank? || controllers.member?(params[:controller])) &&
(actions.blank? || actions.member?(params[:action])) ||
controllers.any? do |controller, action|
params[:controller] == controller.to_s && params[:action] == action.to_s
end
when TrueClass
true
when FalseClass
false
when Hash
condition.all? do |key, value|
params[key].to_s == value.to_s
end
end
end
Expand Down
109 changes: 109 additions & 0 deletions test/active_link_to_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,72 @@ def test_is_active_link_hash
assert is_active_link?('/', {b: 2})
end

def test_is_active_link_with_exclude_array
params[:controller], params[:action] = 'people', 'index'

# Active for people controller, no exclusions
assert is_active_link?('/', [['people'], []])

# Active for people controller, but exclude index action
refute is_active_link?('/', [['people'], []], [[], ['index']])

# Active for people controller, exclude show action (not current)
assert is_active_link?('/', [['people'], []], [[], ['show']])

# Active for any controller, exclude dashboard action
assert is_active_link?('/', [[], []], [[], ['dashboard']])

params[:controller], params[:action] = 'dashboard', 'index'

# Active for any controller, exclude dashboard controller
refute is_active_link?('/', [[], []], [['dashboard'], []])

# Active for people controller (not matching), so exclude doesn't matter
refute is_active_link?('/', [['people'], []], [['dashboard'], []])
end

def test_is_active_link_with_exclude_path
set_path('/root')

# Active inclusively, no exclusions
assert is_active_link?('/root', :inclusive)

# Active inclusively, but exclude via regex
refute is_active_link?('/root', :inclusive, /^\/root$/)

# Active inclusively, exclude non-matching regex
assert is_active_link?('/root', :inclusive, /^\/other/)

set_path('/root/child')

# Active inclusively for /root, exclude /root/child via regex
refute is_active_link?('/root', :inclusive, /^\/root\/child$/)

# Active inclusively for /root, exclude doesn't match /root/child
assert is_active_link?('/root', :inclusive, /^\/root\/other/)
end

def test_is_active_link_with_exclude_boolean
set_path('/root')

# Active, but exclude is true (always excluded)
refute is_active_link?('/root', :inclusive, true)

# Active, exclude is false (never excluded)
assert is_active_link?('/root', :inclusive, false)
end

def test_is_active_link_with_exclude_hash
params[:a] = 1
params[:b] = 2

# Active based on param a, exclude based on param b
refute is_active_link?('/', {a: 1}, {b: 2})

# Active based on param a, exclude based on param c (not present)
assert is_active_link?('/', {a: 1}, {c: 3})
end

def test_is_active_link_with_anchor
set_path('/foo')
assert is_active_link?('/foo#anchor', :exclusive)
Expand All @@ -157,6 +223,13 @@ def test_active_link_to_class
assert_equal 'off', active_link_to_class('/other', class_inactive: 'off')
end

def test_active_link_to_class_with_exclude
set_path('/root')
assert_equal 'active', active_link_to_class('/root', active: :inclusive)
assert_equal '', active_link_to_class('/root', active: :inclusive, exclude: /^\/root$/)
assert_equal 'off', active_link_to_class('/root', active: :inclusive, exclude: /^\/root$/, class_inactive: 'off')
end

def test_active_link_to
set_path('/root')
link = active_link_to('label', '/root')
Expand Down Expand Up @@ -230,4 +303,40 @@ def test_active_link_to_with_utf8
link = active_link_to('label', '/äöü')
assert_html link, 'a.active[href="/äöü"]', 'label'
end

def test_active_link_to_with_exclude
set_path('/root')

# Without exclude, link is active
link = active_link_to('label', '/root')
assert_html link, 'a.active[href="/root"]', 'label'

# With matching exclude, link is not active
link = active_link_to('label', '/root', exclude: /^\/root$/)
assert_html link, 'a[href="/root"]', 'label'
refute link.include?('active')
refute link.include?('aria-current')

# With non-matching exclude, link is still active
link = active_link_to('label', '/root', exclude: /^\/other/)
assert_html link, 'a.active[href="/root"]', 'label'
end

def test_active_link_to_with_exclude_controller_action
params[:controller], params[:action] = 'users', 'index'
set_path('/users')

# Active for users controller
link = active_link_to('label', '/users', active: [['users'], []])
assert_html link, 'a.active[href="/users"]', 'label'

# Active for users controller, but exclude index action
link = active_link_to('label', '/users', active: [['users'], []], exclude: [[], ['index']])
assert_html link, 'a[href="/users"]', 'label'
refute link.include?('class="active"')

# Active for users controller, exclude show action (not matching)
link = active_link_to('label', '/users', active: [['users'], []], exclude: [[], ['show']])
assert_html link, 'a.active[href="/users"]', 'label'
end
end