Permalink

Update the browser URL after filtering
Name Rating Category Price
Belly BBQ Tex Mex $$
Thirsty Shakes ★★ Healthy $
Hungry Box ★★★ Sushi $$
Smokestack House ★★★★ Ramen $
Red Kitchen ★★★★ Japanese $
YQA Gastropub ★★★★ African $
Belly Deli ★★★★★ Caribbean $$$
Red Coffee ★★ Ethiopean $$
Salty Kitchen ★★★★★ Caribbean $$
Belly Grill ★★★★ Vietnamese $$
Big Subs ★★★ German $$$
1480 Shakes ★★★★★ Greek $
Belly BBQ ★★ Bakery $
Thirsty Dragon ★★★★★ Japanese $$
Hungry Cafe ★★★★★ Bakery $$
Fast Box ★★★ Healthy $$
Sugar Bakery German $$
QD Brasserie ★★★ European $$
EID Pub ★★★ Senegalese $$$
Big Burger ★★★ Brazilian $$
Green Eats ★★ Thai $
Blue Plate Box ★★★ German $$
Hungry Steakhouse ★★★★★ Brazilian $$
Sugar Steakhouse European $$
Fat Dragon ★★★ Brazilian $
Golden Steakhouse African $
47 Coffee Korean $$$
KQ Dragon ★★★★ Brazilian $$
Green Burger ★★★★ Mexican $
Spice Grill & Tap ★★ Sushi $$$
978 Subs ★★★★★ Ramen $$
Sugar Box ★★ Japanese $$$
Fast Cafe ★★★ Healthy $
Blue Plate Bar & Grill ★★★★★ Ethiopean $$$
Orange King Bakery $$
Blue Plate Grill Senegalese $$$
Golden Diner ★★★★ Chinese $
Spice BBQ ★★ Juice & Smoothies $$$
Fast Creamery ★★★★ Argentinian $$
Big Curry ★★★ Vietnamese $$$
56 Coffee ★★★★ Vegetarian $
93 Juice Bar ★★★★ Vegan $
Fast Coffee ★★★★★ Asian $$
Red Eatery ★★★★★ Japanese $$
Fast Creamery ★★★ Healthy $$$
582 Bakery ★★ Healthy $$
Sugar Eatery ★★★★ French $$
7276 Juice Bar ★★★★ African $
Big Creamery ★★★ Mexican $
391 Brasserie ★★ Bakery $$
Belly Subs ★★★ Italian $$$
NZ Gastropub Mexican $$$
Orange Eatery ★★★★★ Ice Cream $
Hungry Eats ★★★★★ Healthy $$$
Red Gastropub Italian $
Blue Bakery ★★★★★ Sandwiches $$$
Orange Shakes Juice & Smoothies $$$
Big King ★★★★★ Burgers $
Green Deli ★★★★ Vegan $
Fat Sushi ★★ Pizza $$$
Big Box ★★ American (New) $$
Fast BBQ Greek $
Blue King ★★ Argentinian $$$
937 Sushi Thai $
Fast Kitchen Japanese $
Smokestack Burger ★★★★ Ice Cream $$$
GX Pizza ★★ Argentinian $
Blue Plate Pizza ★★★★★ Vietnamese $
32 House ★★★★★ Vegetarian $$$
Hungry Pub ★★★ Asian $
Fat Burger ★★ Ethiopean $$$
RKX Subs Burgers $$
Blue Kitchen ★★★★★ Japanese $
Sugar Box ★★★★★ Ice Cream $
Salty King ★★★★ Bakery $$
Green Shakes ★★ Thai $$$
Blue Plate Steakhouse ★★★ Greek $$$
Silver Sushi Italian $$$
Sweet Bar & Grill ★★★ Bar $$$
Spice Diner ★★★★★ Ethiopean $$$
Thirsty Juice Bar ★★★★★ Thai $
Orange BBQ ★★ French $$
Golden Coffee ★★★★ Desserts $$
14 Cafe ★★★ Vegan $$$
Hungry Coffee Mexican $$$
DAJ Bakery ★★ Senegalese $$
TY Diner ★★★★★ Ethiopean $$$
Fast House ★★★★ Korean $
Golden Steakhouse ★★★★ Thai $$$
Golden Bar & Grill ★★★★ Chinese $
ZWZ King ★★ Indian $$
Fat Shakes Italian $$
Spice Kitchen ★★ Japanese $$$
Hungry King ★★★★★ Senegalese $
OR Curry ★★★★ Mexican $
Orange Grill ★★★ Greek $$
Orange BBQ Greek $
Orange Bakery ★★★★★ Juice & Smoothies $
Salty Bakery ★★★ Brazilian $
Green Bakery ★★ Mexican $$$

Explanation

  • Uses stimulus' Data API for managing the client side state
  • Updates the browser history using pushState in an afterReflex - so you always have a permalink in the browser bar representing the current state
  • No need to build an API layer that returns JSON.
  • The permalink is picked up when the controller is loaded in a non-stimulus-reflex request (i.e. @stimulus_reflex is false.

Code (107 LOC)

ERB (54 LOC)
app/views/permalinks/_demo.html.erb (54 LOC)
<div class="form-row mb-4" data-controller="permalink" data-permalink-category="<%= params[:category] %>" data-permalink-rating="<%= params[:rating] %>" data-permalink-price="<%= params[:price] %>">
<div class="col-3 offset-3">
<%= label_tag "rating" %>
<%= select_tag "rating",
options_for_select((1..5), params[:rating]),
include_blank: true,
class: "form-control",
data: { action: "change->permalink#filter",
reflex_root: "#search-results" } %>
</div>
<div class="col-3">
<%= label_tag "category" %>
<%= select_tag "category",
options_for_select(@categories, params[:category]),
include_blank: true,
class: "form-control",
data: { action: "change->permalink#filter",
reflex_root: "#search-results" } %>
</div>
<div class="col-3">
<%= label_tag "price", "Price", class: "d-block" %>
<div class="btn-group btn-group-toggle" id="price-button-group">
<% {'$' => 1, '$$' => 2, '$$$' => 3}.each do |key, value| %>
<%= label_tag '', class: "btn btn-outline-primary hover_white #{'active' if params[:price]&.include?(value.to_s)}" do %>
<%= check_box_tag 'price', value, params[:price]&.include?(value.to_s), id: "price-#{value}", class: "", data: { action: "click->permalink#filter",
reflex_root: "#search-results,#price-button-group"} %>
<%= key %>
<% end %>
<% end %>
</div>
</div>
</div>
<div id="search-results">
<table class="table table-striped table-bordered">
<thead class="bg-primary">
<tr>
<th style="width:25%" class="text-white">Name</th>
<th style="width:25%" class="text-white">Rating </th>
<th style="width:25%" class="text-white">Category</th>
<th style="width:25%" class="text-white">Price</th>
</tr>
</thead>
<tbody>
<% @restaurants.each do |restaurant| %>
<tr>
<td><%= restaurant.name %></td>
<td><%= "★" * restaurant.stars %></td>
<td><%= restaurant.category %></td>
<td><%= "$" * restaurant.price %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
JavaScript (42 LOC)
app/javascript/controllers/permalink_controller.js (42 LOC)
import { camelize } from 'inflected'
import ApplicationController from './application_controller'
export default class extends ApplicationController {
filter (e) {
this.stimulate('PermalinkReflex#filter', e.target)
}
beforeReflex (element) {
if (element.type === 'checkbox') {
this.data.set(
element.name,
Array.from(document.querySelectorAll(`input[name=${element.name}]`))
.filter(e => e.checked)
.map(e => e.value)
.join(',')
)
} else {
this.data.set(element.name, element.value)
}
}
afterReflex (element, reflex, error) {
if (!error) {
const camelizedIdentifier = camelize(this.identifier, false)
const params = new URLSearchParams(window.location.search.slice(1))
Object.keys(Object.assign({}, this.element.dataset))
.filter(attr => attr.startsWith(camelizedIdentifier))
.forEach(attr => {
const paramName = attr.slice(camelizedIdentifier.length).toLowerCase()
const paramValue = this.data.get(paramName)
paramValue.length
? params.set(paramName, paramValue)
: params.delete(paramName)
})
const qs = params
.toString()
.replace(/%28/g, '(')
.replace(/%29/g, ')')
.replace(/%2C/g, ',')
const query = qs.length ? '?' : ''
history.pushState({}, '', `${window.location.pathname}${query}${qs}`)
}
}
}
Ruby (11 LOC)
app/controllers/permalinks_controller.rb (6 LOC)
class PermalinksController < ApplicationController
def show
@categories = Restaurant.select(:category).distinct.order(:category).map(&:category)
@restaurants = Restaurant.category(params[:category]).stars(params[:rating]).price(params[:price])
end
end
app/reflexes/permalink_reflex.rb (5 LOC)
class PermalinkReflex < ApplicationReflex
def filter
params[element[:name].to_sym] = element.value
end
end