PagerJS can pick up URL parameters and bind them to the model. For instance, in this example from the PagerJS website (see link), when you click on the link, it will navigate to #/user?first=Philip&last=Fry
and the data-bound sub-page will appear, displaying "Philip Fry":
<a class="btn" data-bind="page-href: {path: 'user', params: {first: 'Philip', last: 'Fry'}}">Send parameter to page</a>
<div data-bind="page: {id: 'user', params: ['first','last']}" class="well-small">
<div>
<span>First name:</span>
<span data-bind="text: first"></span>
</div>
<div>
<span>Last name:</span>
<span data-bind="text: last"></span>
</div>
</div>
This is a one-way binding: if the observable changes, because of user actions on the page, the URL will not be updated.
What's the recommended way of keeping the URL parameters in sync with the observables when using PagerJS?
I'd like to store the user's dynamically created search-criteria, produced by selecting a bunch of UI controls, in the URL parameters so he/she can share the URL with others or bookmark it, all without reloading the page, of course.
Disclaimer: I don't know anything about pager.js, but I'm hoping my general knockout experience can still be of help.
Looking at the example, the page
binding seems to create observables using initial values from the url. My first instinct would be to extend this binding and make sure a subscription to each of these values updates the URL.
Let's name this binding twoway-page
:
ko.bindingHandlers["twoway-page"] = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// Call pager.js' page binding
ko.bindingHandlers.page.init(element, valueAccessor, allBindings, viewModel, bindingContext);
// ...
}
}
And call it on the example's binding:
<div data-bind="twoway-page: {
id: 'start',
params: ['first','last']
}">
After calling page.init
, the page binding has extended the viewmodel, adding the observables defined in the params
array to the viewModel
object. This means we can subscribe to changes in these observables.
The next challenge is computing the right hash. I looked up how the page-href
binding computes its href
attribute. Turns out it uses pager.page.path()
on an object with a path
and params
property. E.g.:
var hash = pager.page.path({
path: "user",
params: {
"first": "John",
"last": "Doe"
}
});
I tried to construct a similar object in a computed observable.
// ...
var options = valueAccessor();
var pathObj = ko.computed(function() {
var result = {
path: options.id,
params: {}
};
options.params.forEach(function(param) {
result.params[param] = viewModel[param]();
});
return result;
}).extend({ rateLimit: { timeout: 200, method: "notifyWhenChangesStop" } });
I couldn't find a "clean" way to update the hash via a pager.js method, but I did notice that internally, pagerjs uses location.hash = "newhash"
to set a value (although there also seems to be a history/html5 alternative...). Anyway, we can subscribe to our observable to update the hash:
// ...
pathObj.subscribe(function(newValue) {
location.hash = pager.page.path(newValue);
});
Now, instead of the text
bindings from the example, we'll use textInput
bindings so we can update the values:
<div>
<span>First name:</span>
<input type="text" data-bind="textInput: first">
</div>
So, to wrap up: my best guess would be to
rateLimit
extension to prevent an overload of updatesDoing stuff with the location hash is a bit hard to show in a fiddle, so I've recorded a gif of my proof of concept:
The complete custom binding code is:
ko.bindingHandlers["twoway-page"] = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
ko.bindingHandlers.page.init(element, valueAccessor, allBindings, viewModel, bindingContext)
var options = valueAccessor();
var pathObj = ko.computed(function() {
var result = {
path: options.id,
params: {}
};
options.params.forEach(function(param) {
result.params[param] = viewModel[param]();
});
return result;
}).extend({ rateLimit: { timeout: 200, method: "notifyWhenChangesStop" } });
pathObj.subscribe(function(newValue) {
location.hash = pager.page.path(newValue);
})
return { controlsDescendantBindings: true }
}
};
I can't add a comment so i have to add a new answer.
Very nice answer by user3297291
, it really helped me a lot.
However, some issues arise in real world, e.g.:
deep navigation: pages, subpages, subsubpages, etc. the
options.id
is not enough, we need the complete path
page/subpage/subsubpage
(without #!/
)
when sharing some URL params between pages, anytime the param change, each page will trigger a navigation, last one will be displayed. I share same ID params between some pages (productID,etc).
For issue 1, calculate complete path:
...var options = valueAccessor();
var parent = pager.getParentPage(bindingContext);
var fullroute = parent.getFullRoute()();
if(fullroute.length == 0)
var path = fullroute.join('/')+options.id;
else
var path = fullroute.join('/')+'/'+options.id;
result
object turns into:
var result = {
path: path,
params: {}
};
For issue 2, we have to tell them that they can only trigger a navigation when the active page is their own.
Inside the subscribe method, we calculate the activePage path and compares them:
pathObj.subscribe(function(newValue) {
//calculate activePath
var activeParent = pager.activePage$().parentPage.getFullRoute()();
var activeId = pager.activePage$().getCurrentId();
if(activeParent.length == 0)
var activePath = activeParent .join('/')+activeId;
else
var activePath = activeParent .join('/')+'/'+activeId;
if( path == activePath ){
location.hash = pager.page.path(newValue);
}
})
Also, be carefull when looping throw params, it can be an array or an object if you have provided default values:
<div data-bind="page: {id: 'search', params: ['product','price']}" class="well">
needs:
options.params.forEach(function(param) {...
Vs
<div data-bind="page: {id: 'search', params: {'product': null,'price': 0}}" class="well">
needs something like:
Object.keys(options.params).forEach(function(key,index) {
result.params[key] = viewModel[key]();
}
Thanks again to user3297291 for your answer, it really made the difference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With