Track Filters & Pagination in the URL
Query Parameters
Declare query params on a controller (or route) so changes update the URL — and the URL re-hydrates state on reload or share.
What you'll learn
- Declare queryParams on a controller/route
- Bind to `@tracked` properties
- Use refreshModel to re-run model() on change
Query parameters keep UI state — filters, sort order, pagination — synced with the URL. Reload the page and you are back where you left off. Share the URL and the recipient sees the same view.
Declare on the Controller
// app/controllers/posts.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class PostsController extends Controller {
queryParams = ['page', 'q', 'sort'];
@tracked page = 1;
@tracked q = '';
@tracked sort = 'newest';
} The queryParams array lists which properties sync to the URL. Defaults set above (page = 1) are not put in the URL — only deviations are serialized.
Use Them in the Template
{{! app/templates/posts.hbs }}
<input
value={{this.q}}
{{on "input" (fn (mut this.q) value="target.value")}}
placeholder="Search"
/>
<select {{on "change" (fn (mut this.sort) value="target.value")}}>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
</select> Typing in the input updates this.q, which updates the URL.
Refresh the Model on Change
// app/routes/posts.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class PostsRoute extends Route {
@service store;
queryParams = {
q: { refreshModel: true },
sort: { refreshModel: true },
page: { refreshModel: true },
};
async model(params) {
return this.store.query('post', params);
}
} refreshModel: true re-runs model() whenever that query param changes — perfect for server-side search and pagination.
replace Option
queryParams = {
q: { replace: true },
}; replace: true uses history.replaceState instead of pushState, so each keystroke does not add a history entry.