1
\$\begingroup\$

I am implementing a history feature which could resemble of what an user can experience in a browser using back and forwards buttons (Chrome).

How does the following scrip workst:

  • When user visit page for first time, initial view view0 is shown.
  • When user click buttons prev/next view is switched of +1/1 and history of the view is saved.
  • When user click buttons back/forward view is shown accordingly.
  • When user click goTo (to move to a specific view), view is shown, multiple sequential click to goTo are not registered in history.

My current script works but: - I would like to know if a refactor can be applied to the logic, specially function back. - This solution use two arrays, one for back and forward history, I would like to know if you could point me out also a completely different approach if better suitable for this task.

Notes: I cannot use History Web APIs.

(function(window) {
  let historyBack = [];
  let historyForward = [];
  let activeView;

  let btnPrev;
  let btnNext;
  let btnBack;
  let btnForward;
  let btnGoTo;
  let btnLogs;

  let result;
  let views = ['view0', 'view1', 'view2', 'view3', 'view4'];


  let logs = () => {
    console.log('historyBack', historyBack, 'historyForward', historyForward, 'activeView', activeView);
  };

  let render = () => {
    result.innerHTML = activeView;

  };

  let isViewValid = (id) => {
    return views.indexOf(id) > -1 ? true : false;
  };

  let isViewAlreadyInHistory = (id) => {
    let hasHistory = historyBack.length > 0 ? true : false;
    let isViewSameAsLastView = historyBack[historyBack.length - 1] === id;
    if (hasHistory && isViewSameAsLastView) {
      return true;
    }
    return false;
  };

  let addViewToHistoryBack = (id) => {
    historyBack.push(id);
  };

  let setActiveView = (id) => {
    activeView = id;
  };

  let goTo = (id) => {
    let isValid = isViewValid(id);
    if (isValid) {
      let canSaveHistory = !isViewAlreadyInHistory(id);
      if (canSaveHistory) {
        addViewToHistoryBack(id);
      }
      setActiveView(id);
      render();
    }

  };

  let getNextView = () => {
    let activeViewIndex = views.indexOf(activeView);
    activeViewIndex++;
    return views[activeViewIndex];
  };

  let getPrevView = () => {
    let activeViewIndex = views.indexOf(activeView);
    activeViewIndex--;
    return views[activeViewIndex];
  };

  let next = () => {
    resetHistoryForward();
    let nextView = getNextView();
    goTo(nextView);
  };

  let prev = () => {
    resetHistoryForward();
    let prevView = getPrevView();
    goTo(prevView);
  };


  let back = () => {
    if (historyBack.length === 1) {
      // do not remove default view
      return;
    }

    let activeView = historyBack.pop();
    let backView = historyBack.pop();
    if (backView) {
      goTo(backView);
      historyForward.push(activeView);
    }
  };

  let forward = () => {
    let forwardView = historyForward.pop();
    if (forwardView) {
      goTo(forwardView);
    }
  };

  let resetHistoryForward = () => {
    historyForward.length = 0;
  };

  let start = () => {
    result = document.getElementById('result');
    btnPrev = document.getElementById('btnPrev');
    btnNext = document.getElementById('btnNext');
    btnBack = document.getElementById('btnBack');
    btnForward = document.getElementById('btnForward');
    btnGoTo = document.getElementById('btnGoTo');
    btnLogs = document.getElementById('btnLogs');

    btnPrev.addEventListener('click', event => {
      prev();
    });

    btnNext.addEventListener('click', event => {
      next();
    });

    btnBack.addEventListener('click', event => {
      back();
    });

    btnForward.addEventListener('click', event => {
      forward();
    });

    btnGoTo.addEventListener('click', event => {
      goTo('view3');
    });


    btnLogs.addEventListener('click', event => {
      logs();
    });
  };


  let setDefault = () => {
    goTo('view0');
    render();
  };

  document.addEventListener('DOMContentLoaded', () => {
    start();
    setDefault();

  });
})(window);
#result {
  font-size: 100px;
}
<div id="result"></div>
<button id="btnPrev">Prev</button>
<button id="btnNext">Next</button>
<br>
<button id="btnBack">Back</button>
<button id="btnForward">Forward</button>
<br>
<button id="btnGoTo">GoTo 3</button>
<br>
<button id="btnLogs">logs</button>

\$\endgroup\$
  • \$\begingroup\$ Is this desired behavior? Clicking on "Next" -> "Back" -> "GoTo 3" -> "Forward" leads me to "view1" because the forward history isn't pruned when a navigation event occurs. \$\endgroup\$ – le_m Apr 28 '17 at 14:15
  • \$\begingroup\$ @le_m thanks for commenting, yes it is not the desired behavior. \$\endgroup\$ – GibboK Apr 28 '17 at 18:13
2
\$\begingroup\$

You currently don't prune the forward history when clicking on 'goto'. I assume this is a flaw.

Instead of splitting the history into a backward and forward array, I propose using a single array and keeping track of the currently active index. This allows you to simplify your logic, i.e. you can replace historyForward.pop() and historyBackward.push() with a simple index--.

I also suggest separating and encapsulating the logic and datastructures needed to keep track of and browsing through a history. A dedicated history module or class is simpler to understand, easier to test and can be reused.

The same can be applied to the view navigation logic.

Depending on your view/controller logic, you might want to apply the observer pattern to your navigation module or class and listen to navigation events which would then lead to a rendering of the new view.

Here is an implementation of the history and navigation as separate classes (without the observer pattern):

class History {
  constructor() {
    this.states = [];
    this.index = -1;
  }
  get current() {
    return this.states[this.index];
  }
  forward() {
    if (this.index < this.states.length - 1) this.index++;
  }
  back() {
    if (this.index > 0) this.index--;
  }
  push(state) {
    this.index++;
    this.states.splice(this.index, this.states.length - 1, state);
  }
}

class Navigation {
  constructor(...views) {
    this.history = new History();
    this.history.push(0);
    this.views = views;
  }
  get current() {
    return this.views[this.history.current];
  }
  next() {
    let index = this.history.current + 1;
    if (index < this.views.length) {
      this.history.push(index);
    }
  }
  prev() {
    let index = this.history.current - 1;
    if (index >= 0) {
      this.history.push(index);
    }
  }
  forward() {
    this.history.forward();
  }
  back() {
    this.history.back();
  }
  goto(index) {
    if (index >= 0 && index < this.views.length && index != this.history.current) {
      this.history.push(index);
    }
  }
}

let nav = new Navigation('view0', 'view1', 'view2', 'view3', 'view4');

let result = document.getElementById('result');
let btnPrev = document.getElementById('btnPrev');
let btnNext = document.getElementById('btnNext');
let btnBack = document.getElementById('btnBack');
let btnForward = document.getElementById('btnForward');
let btnGoTo = document.getElementById('btnGoTo');

btnPrev.addEventListener('click', event => {
  nav.prev();
  render();
});

btnNext.addEventListener('click', event => {
  nav.next();
  render();
});

btnBack.addEventListener('click', event => {
  nav.back();
  render();
});

btnForward.addEventListener('click', event => {
  nav.forward();
  render();
});

btnGoTo.addEventListener('click', event => {
  nav.goto(3);
  render();
});

function render() {
  result.innerHTML = nav.current;
}

render();
#result {
  font-size: 100px;
}
<div id="result"></div>
<button id="btnPrev">Prev</button>
<button id="btnNext">Next</button>
<br>
<button id="btnBack">Back</button>
<button id="btnForward">Forward</button>
<br>
<button id="btnGoTo">GoTo 3</button>

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ In my real app unfortunately I have to stick with double array. Nevertheless I accept this answer as I found it informative and of interest. Thanks! \$\endgroup\$ – GibboK May 2 '17 at 6:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.