Last week I solved a recurring problem in my job as a software developer: Combining different parts of a user interface which all need to change the URL parameters to work properly.

The Problem

Take for a example a dataset presented using a paginated table. The table can be ordered using any of the displayed columns and the dataset can be filtered using a form and some links.

The resulting URL could consist of the following parameters:

  • pagination: a Hash containing page, the current page being displayed and per_page, the number of items on display
  • order: attribute to order the data by
  • direction: direction to order the data in
  • filter: hash containing different filter options

Changing the current page, adjusting the filter or ordering should not change the remaining URL parameters - you don’t want your users to always start on page one just because they changed the ordering in the table.

A Common Solution

In Ruby on Rails one can merge parts of the URL parameters using a Hash and passing the resulting Hash to any path helper, e.g.

root_path({ pagination: { page: 1 } }.merge({ order: "attribute" }))

will generate something like this

/dashboard?pagination[page]=1&order=attribute

Now this code is tedious to write and ugly to read. As you add more and more options to the URL this code gets more and more out of control.

Enter UrlPlumber

Instead of merging Hashs you specify the attribute you want to change using a keypath and a value. The other URL parameters will remain unchanged.

The above example could be rewritten as follows:

plumber = ::UrlPlumber::Plumber.new(params)
root_path(plumber.plumb("order", "attribute"))

Moving UrlPlumber into a simple helper function allows us to further simplify the code:

root_path(plumb("order", "attribute"))

This also works with arbitrary nesting; some examples:

plumb("table.pagination.page", 2) # => changes table[pagination][page] param to 2
plumb("pagination") # => removes pagination from the URL

UrlPlumber is designed for this common scenario. It works just as you might expect, and is completly tested. Integration in existing Ruby on Rails projects should be painless and simple.

Give it a try if you stumble across this problem one day :)


comments powered by Disqus