Reputation: 2062
I need to prevent the user from getting access to a certain page by simply typing the url
and hit enter. The user can gain access to this page by following some wizard for example. I know javaScript
can monitor click events on the browser, is it possible to monitor url
changes? In other words, is it possible to know how the user got to that page? (added the path by himself, or naturally by just following the app wizard?)
edit:
Use case: online quiz with several questions and pages and eventually a results page. I want to prevent from the user getting to the results page after answering only one question.
Upvotes: 0
Views: 201
Reputation: 12796
As your question doesn't really have a start code, it is hard to know with which framework you are currently working. Because of this, I implemented a very basic quiz (it doesn't really do the answering part, doesn't show any results, and is all in all quite basic, but I wanted to provide a somewhat complete example).
To detect the changes inside your page (the current hash path), you can subscribe on the hashchange
event of the window, eg:
window.addEventListener('hashchange', handleRoute.bind(router));
However, to make sure the user doesn't directly navigate to the specific route, you can also subscribe to the load
event.
window.addEventListener('load', handleRoute.bind(router));
To validate (in my case) if the user is allowed to be on the current route, I added a validation method, that simply checks that:
This is handled in this part:
function isPageValid(route, qry) {
switch (route.scope) {
case 'questions':
// only allow the question page when the current question is the actual one in the querystring
return (parseInt(qry.split('=')[1]) === answered.length) && answered.length <= questions.length;
// only allow the results when we have all the answers
case 'results':
return answered.length === parseInt(questions.length);
}
// everything else should be valid (eg: default page)
return true;
}
As the current route is anyhow loaded based on my router, I don't have to worry that other pages would be valid at the time, so if it isn't either questions or results, I always sent true
back :)
In case it is not there, the default page will be shown
For the rest, the fiddle is quite basic, it shows you the starting page, 2 potential questions and you cannot really select anything, but the principle of only showing the results page when the question list was completed, should be clear.
I added some comments to the code, in the hope that everything is clear :)
'use strict';
var questions = [{
title: 'Is SO a good resource',
answers: ["yes", "no", "sometimes"],
correct: 2
}, {
title: 'Do you like to use stackoverflow on a daily basis',
answers: ["yes", "no"],
correct: 0
}],
answered = [];
// bind the nextQuestion element to the nextQuestion function
document.getElementById('nextQuestion').addEventListener('click', nextQuestion);
/*
* @method nextQuestion
* here the user will click when he wants to navigate to the next question
* If all questions are completed, it will navigate to the results page
* If there are more questions, it will navigate to /q=currentQuestion+1
*/
function nextQuestion(e) {
answered.push({
value: 0
});
if (answered.length < questions.length) {
document.location.href = '#/q=' + answered.length;
} else {
document.location.href = '#/results';
}
e.preventDefault();
}
/*
* @method showQuestion
* Gets called when the question gets changed
*/
function showQuestion(route, qry) {
var currentQuestion = answered.length;
document.getElementById('currentQuestion').innerHTML = currentQuestion;
document.getElementById('question').innerHTML = questions[currentQuestion].title;
if (currentQuestion === questions.length - 1) {
document.getElementById('nextQuestion').innerHTML = 'show results';
} else {
document.getElementById('nextQuestion').innerHTML = 'next question';
}
}
/*
* @method showResults
* Dummy method, answered are should contain all current answers
* just prints a message to the console
*/
function showResults(route, qry) {
console.log('can finally see the results :)');
}
/*
* @method isPageValid
* Validates the current route & qry
* @param route the current active route
* @param qry the current querystring that was validated to get to this route
* @returns true if the page is valid, false if it is not valid
*/
function isPageValid(route, qry) {
switch (route.scope) {
case 'questions':
// only allow the question page when the current question is the actual one in the querystring
return (parseInt(qry.split('=')[1]) === answered.length) && answered.length <= questions.length;
// only allow the results when we have all the answers
case 'results':
return answered.length === parseInt(questions.length);
}
// everything else should be valid (eg: default page)
return true;
}
/*
* @method handleRoute
* All routes are part of it's context (this object)
* Loops all properties of the context and checks which route matches the current part after #
*/
function handleRoute() {
var qry = document.location.href.split('#')[1],
result, defaultRoute, prop;
// hide all .scoped-block elements
document.querySelectorAll('.scoped-block').forEach(item => {
item.classList.add('hidden');
});
console.log('current query: ' + qry);
for (prop in this) {
if (this.hasOwnProperty(prop)) {
if (!this[prop].test) {
defaultRoute = this[prop];
} else {
if (this[prop].test.test(qry)) {
result = this[prop];
console.log('matches: ' + prop);
}
}
}
}
// if we have a designated page, validate it
if (result && !result.validate(result, qry)) {
// the page is not allowed to be shown (route to the default page)
console.info('cannot navigate to ' + result.scope + ' page (yet)')
result = undefined;
}
// make sure result contains the current valid route or the defaultRoute
result = result || defaultRoute;
console.log('current scope: ' + result.scope);
// set the current scope as the visible element
document.getElementById(result.scope).classList.remove('hidden');
if (result.action) {
result.action(result, qry);
}
}
// setup the available routes + potential validations
var router = {
questionPage: {
test: /\/q=[0-9]{1,2}$/,
validate: isPageValid,
action: showQuestion,
scope: 'questions'
},
resultPage: {
test: /\/results$/,
validate: isPageValid,
action: showResults,
scope: 'results'
},
startPage: {
action: function() {
// reset the answers
answered.splice(0, answered.length);
},
scope: 'startPage'
}
};
// adds the handle route method to the onload + hashchange method
window.addEventListener('hashchange', handleRoute.bind(router));
window.addEventListener('load', handleRoute.bind(router));
.hidden { display: none; visibility: hidden; }
.scoped-block {
// dummy element
}
<div id="questions" class="hidden scoped-block">
<h1>Question <span id="currentQuestion"></span></h1>
<span id="question"><select id="optSelect"></select></span>
<div>
<a href="#">Restart quiz</a> -
<a href="#" id="nextQuestion">Next question</a>
</div>
</div>
<div id="results" class="hidden scoped-block">
<h1>results</h1>
<a href="#/">Restart quiz</a>
</div>
<div id="startPage" class="scoped-block">
<h1>Welcome to a small quiz</h1>
<p>
When you completed the quiz, you will be send to <a href="#/results">this</a> results page, which shouldn't be accessible before hand.
</p>
<p>
To start the quiz, click <a href="#/q=0">here</a>
</p>
</div>
Upvotes: 2