PagerJS how to accomplish "two-way binding" with URL params?

knockout.js, pagerjs

Solution

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

- Extend an existing pager.js binding

- Create subscriptions to all observables that need to be updated in the URL

- Automatically update the hash when values change; use a `rateLimit` extension to prevent an overload of updates

Doing 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 }
    }
};

Problem

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.

Original source