Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve/save state of list on browser back button, direct page links #650

Open
sheffieldnikki opened this issue Feb 16, 2019 · 9 comments

Comments

@sheffieldnikki
Copy link
Contributor

sheffieldnikki commented Feb 16, 2019

Here is some rough code I've written to preserve/save the state of a list as a URL query string. e.g. mypage.html?q=jon

It can be used to bookmark a page or share a link with specific search, sort & pagination options chosen, as well as using the browser history API so the browser back button works as you'd expect.

Feel free to knock it into better shape and submit as a pull request if you want to. Note that the "plugins" parameter support was removed from list.js in v1.5.0, which is why this is just a simple function.

Gotchas:

  • Overwrites any existing query string in your page URL
  • Doesn't save filter state
  • Doesn't save sort or search state if you call those functions directly - only the 'automagical' sort/search
  • Ignores "data-order" and "data-insensitive" attributes for sort
  • Requires browser History API ( https://caniuse.com/#feat=history ) to save state, but not to load it
function ListState(list) {
  var savestate = function() {
    // Preserve DOM state so can be restored on browser back button, etc
    // overwrites any existing URL query string
    var q = '';
    if (list.pagination !== undefined) {
      if (list.i > 1) q += "&i="+list.i;
      if (list.page != list.pageDefault) q += "&n="+list.page;
    };
    // TODO: read 'data-order'? and 'data-insensitive' attributes for sort
    for (var i = 0, els = list.utils.getByClass(list.listContainer, list.sortClass); i < els.length; i++) {
      var s = list.utils.classes(els[i]);
      if (s.has('desc')) q += "&sd="+i; // numeric sort field gives a shorter URL
        else if (s.has('asc')) q += "&sa="+i;
    };
    if (list.searched) {
      var el = list.utils.getByClass(list.listContainer, list.searchClass, true);
      if (el) q += "&q="+encodeURIComponent(el.value);
    };
    history.replaceState(null, null, window.location.href.split('?')[0] + q.replace('&','?'));
  };
    
  function getUrlParameter(name) {
    name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
    var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
    var results = regex.exec(location.search);
    return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
  };

  // Restore page/search/sort state from query string if set
  // order of operations is important: search => sort => set page
  var sa = getUrlParameter('sa'), sd = getUrlParameter('sd'), q = getUrlParameter('q');
  if (q.length) {
    var el = list.utils.getByClass(list.listContainer, list.searchClass, true);
    if (el) el.value = q;
    list.search(q);
  };
  if (sa.length || sd.length) {
    var els = list.utils.getByClass(list.listContainer, list.sortClass);
    var el = els[parseInt(sa.length ? sa : sd,10)];
    var valueName = el ? list.utils.getAttribute(el, 'data-sort') : 0;
    if (valueName) list.sort(valueName, {order:sa.length ? "asc" : "desc"});
  };
  if (list.pagination !== undefined) {
    var i = parseInt(getUrlParameter('i'),10), n = parseInt(getUrlParameter('n'),10);
    list.pageDefault = list.page;
    if (i || n) list.show(i ? i : 1, n ? n : list.page);
  };      
  if (!!(window.history && history.pushState)) list.on('updated', savestate);
};
@sheffieldnikki sheffieldnikki changed the title Plugin to preserve/save state of list for browser back button & direct page links Preserve/save state of list on browser back button, direct page links Feb 16, 2019
@Julianoe
Copy link

I'd really like to pair some sort of url query with list.js but i could not implement your solution. Could you simply expand a little on the use of this?
When should i call it?

@sheffieldnikki
Copy link
Contributor Author

sheffieldnikki commented Jun 21, 2019

ListState() should be called when the page has loaded, and after the list has been created. e.g.

var myList = new List('myElement', options, values);
ListState(myList);

@Julianoe
Copy link

Thanks. It seems to be working.
In the use case you used this you could not return to the complete list of elements by clicking the filters again, right? I have to return to the base url to have a complete list of elements i can filter again, is that it?
Or did you work out a solution for that?

@sheffieldnikki
Copy link
Contributor Author

sheffieldnikki commented Jun 24, 2019

If you don't want to use the browser back button to return to the base url you could have a button/link that resets the list to show all elements. e.g.

<button onclick="myList.search()" type="button" title="Show all elements">&#10008;</button>

@Julianoe
Copy link

Thanks! I adapted it to my project and it works.
On your implementation of this does the url changes each time you click on a filter button?

@sheffieldnikki
Copy link
Contributor Author

sheffieldnikki commented Jun 25, 2019

My code doesn't support user-supplied filter() functions, only the standard search & sort.

(but yes, for search and sort, the URL changes each time)

@Julianoe
Copy link

Julianoe commented Jun 25, 2019

Indeed i tested it out and it works, adding my search keyword to the url, updated live.
It does not update when clicking the filters so i added this little piece of code:
It can work for a list of radio buttons. I'll try to work out a more universal solution.

if (list.filtered){
 var filter = list.listContainer.querySelector('input:checked').value;
 if (filter) q += "&q="+encodeURIComponent(filter);
};

@Prkns
Copy link

Prkns commented Jun 26, 2019

Thanks for this @sheffieldnick ... pretty much exactly what I was looking for, and thanks @Julianoe as you seem to be doing what I'm looking for too and succeeding.

I'm struggling with implementing it all, however. Can you explain a little more so that I can test it on my use case and see if it's fit for my purpose?

@Julianoe
Copy link

Julianoe commented Jul 10, 2019

@Prkns where are you stuck at?

I took the code that @sheffieldnick put on the first comment of this issue. I added the part i talked about, see following code:

function ListState(list) {
  var savestate = function() {
    // Preserve DOM state so can be restored on browser back button, etc
    // overwrites any existing URL query string
    var q = '';
    if (list.pagination !== undefined) {
      if (list.i > 1) q += "&i="+list.i;
      if (list.page != list.pageDefault) q += "&n="+list.page;
    };
    // TODO: read 'data-order'? and 'data-insensitive' attributes for sort
    for (var i = 0, els = list.utils.getByClass(list.listContainer, list.sortClass); i < els.length; i++) {
      var s = list.utils.classes(els[i]);
      if (s.has('desc')) q += "&sd="+i; // numeric sort field gives a shorter URL
        else if (s.has('asc')) q += "&sa="+i;
    };
    if (list.searched) {
      var el = list.utils.getByClass(list.listContainer, list.searchClass, true);
      // adding the search in the url
      if (el) q += "&s="+encodeURIComponent(el.value);
    };
    

    // THIS THE PART I ADDED
    if (list.filtered){
      var filter = list.listContainer.querySelector('input:checked').value;
      // adding the filter in the url
      if (filter) q += "&q="+encodeURIComponent(filter);
    };

    history.replaceState(null, null, window.location.href.split('?')[0] + q.replace('&','?'));
  };

  function getUrlParameter(name) {
    name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
    var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
    var results = regex.exec(location.search);
    return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
  };

  // Restore page/search/sort state from query string if set
  // order of operations is important: search => sort => set page
  var sa = getUrlParameter('sa'), sd = getUrlParameter('sd'), q = getUrlParameter('q');
  if (q.length) {
    var el = list.utils.getByClass(list.listContainer, list.searchClass, true);
    if (el) el.value = q;
    list.search(q);
  };
  if (sa.length || sd.length) {
    var els = list.utils.getByClass(list.listContainer, list.sortClass);
    var el = els[parseInt(sa.length ? sa : sd,10)];
    var valueName = el ? list.utils.getAttribute(el, 'data-sort') : 0;
    if (valueName) list.sort(valueName, {order:sa.length ? "asc" : "desc"});
  };
  if (list.pagination !== undefined) {
    var i = parseInt(getUrlParameter('i'),10), n = parseInt(getUrlParameter('n'),10);
    list.pageDefault = list.page;
    if (i || n) list.show(i ? i : 1, n ? n : list.page);
  };
  if (!!(window.history && history.pushState)) list.on('updated', savestate);
};

I've put all this in a file named listState.js and imported in the file i use List.js

<script src="./js/list.min.js"></script>
<script src="./js/listState.js"></script>

then i have my code to initialize all this

<script type="text/javascript">
            var options = {
              valueNames: [
                { data: ['color']}
              ]
            };
            var myList = new List('items', options);
            ListState(myList);
            <?php // allows the checked status to be set accordingly to the url leading you to the page
            $page = get_query_var('q');
            if( ! empty( $page ) ) : ?>
            jQuery("input[value=<?= $page ?>]").attr('checked', true);
            <?php endif; ?>

            function resetList(){
              myList.search();
              myList.filter();
              myList.update();
              jQuery(".filter-all").prop('checked', true);
              jQuery('.filter').prop('checked', false);
              jQuery('.search').val('');
            };

            function updateList(){
              var values_color = jQuery("input[name=color]:checked").val();

              myList.filter(function (item) {
                var colorFilter = false;

                if(values_color == "all")
                {
                  colorFilter = true;
                } else {
                  colorFilter = item.values().color == values_color;

                }
                return colorFilter
              });
              myList.update();
            }

            jQuery(function(){
              jQuery("input[name=color]").change(updateList);

              myList.on('updated', function (list) {
                if (list.matchingItems.length > 0) {
                  jQuery('.no-result').hide()
                } else {
                  jQuery('.no-result').show()
                }
              });
            });
             
            // adding some sort of animation when updating see issue 366
            // https://github.com/javve/list.js/issues/366
            myList.on('updated', function (list) {
              jQuery('.list--list-item').addClass('animated fadeIn');
            });
          </script>

As you can see i'm using PHP. I simply take the value of query the user is coming from and check the corresponding radio button accordingly.
The little bit i added allows it to update the query in the url when i click on of the different "color" radio buttons that i use to filter my list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants