logoCoderhouse.png
By Nicolas Alliaume • febrero 10, 2016

Desarrolla tu propia Trivia Web | Tutorial de JS y jQuery

Hola! Soy Nico Alliaume, profesor de Coderhouse en Montevideo, y en este tutorial les contaré paso a paso de cómo podemos crear nuestra propia trivia web! Me enfocaré principalmente en la parte de Javascript y el uso de jQuery.

El juego

El juego consiste en una serie de preguntas con cuatro opciones, de las cuales sólo una es válida. El jugador tendrá un tiempo límite para responder cada una. Si el tiempo se acaba, el juego termina.

De las cuatro respuestas posibles, sólo una será correcta. Si el jugador acierta, sumará 10 puntos.

Al contestar todas las preguntas o quedarse sin tiempo, se muestra en pantalla los puntos obtenidos.

Suena fácil, ¿no?

El HTML

El HTML de nuestro trivia se ve bastante sencillo.

<!DOCTYPE html>
<html>
<head>
<title>Quizzer</title>
<meta charset="utf-8">
<link href="quizzer.css" type="text/css" rel="stylesheet">
<script src="jquery-2.2.0.min.js"></script>
<script src="underscore-1.8.3.min.js"></script>
<script src="quizzer.js"></script>
</head>
<body>
<header>
<div class="title">
<h1>Quizzer</h1>
<h2>¿Sabes tanto como creías?</h2>
</div>
<div class="countdown"><span class="time_left">12s</span></div>
<div class="points"><span class="points">0 points</span></div>
</header>
<div class="start">
<div class="card">
<p>¿Listo para comenzar?</p>
<button class="start">Comenzar</button>
</div>
</div>
<div class="play">
<div class="questions"></div>
</div>
<div class="finish card">
<p class="times_up">¡Se acabó el tiempo!</p>
<p>Has obtenido</p>
<p class="final_points">8 puntos</p>
<div class="play_again">
<button>Volver a jugar</button>
</div>
</div>
</body>
</html>

 

No se asustes si mirás este HTML con un navegador ahora mismo ¡es horrible! pero es porque aún no hemos hecho la magia del CSS.

Le voy a dar imaginariamente 10 puntos extra al que se dijo ¿dónde están las preguntas? ¡Ya llegaremos a eso! Las preguntas las incluiremos más adelante desde Javascript, por ahora seguime con el CSS.

El CSS

He aquí el CSS del trivia.

body {
background-color: #7CBB7A;
margin: 0;
font-family: 'Open sans', sans-serif;
}

* {
box-sizing: border-box;
}

button {
border: none;
border-radius: 6px;
padding: 10px 20px;
font-size: 20px;
color: #fff;
background-color: #ccc;
}

header .title {
background-color: #767D76;
color: #fff;
text-align: center;
padding: 15px;
}

header h1, header h2 {
margin: 0;
}

header div.countdown {
float: right;
}

header div.points {
float: left;
-webkit-transition: color 0.5s;
transition: color 0.5s;
}

header div.points.animate.right {
color: #767D76;
}

header div.points.animate.wrong {
color: #bf0000;
}

header div.countdown, header div.points {
font-weight: bold;
margin: 10px;
color: #fff;
padding: 0px;
font-size: 26px;
}

.card {
background-color: red;
width: 500px;
background-color: #fff;
margin: 100px auto 30px auto;
border-radius: 6px;
text-align: center;
padding: 20px;
}

div.start .card {
display: block;
}

div.start .card p, .question.card span.question {
font-size: 25px;
}

.card.question {
display: none;
}

.card.question.active {
display: block;
}

.card.question .options {
padding: 0;
list-style: none;
margin-top: 35px;
}

.card.question .options > li {
margin: 10px 80px;
}

.card.question .options > li label {
background-color: #7ED2CF;
display: block;
padding: 10px;
border-radius: 20px;
}

.card.question .options > li label:hover {
background-color: #8EE8E5;
}

.card.question .options > li input:checked + label {
background-color: #4D8E8C !important;
color: #fff;
}

.card.question .options > li input {
opacity: 0;
display: none;
}

.finish {
display: none;
}

.card.finish p {
font-size: 22px;
}

.card.finish .final_points {
font-size: 40px;
font-weight: bolder;
}

.play_again,
button.start {
width: 200px;
margin: 0 auto 20px auto;
color: #fff;
}

 

Bueno ¡de CSS sí hay bastante código! Pero verás que es sencillo.

Me detendré solamente en uno de los selectores (soy fanático de los selectores ¿viste mi post anterior? Podés verlo acá):

.card.question .options > li input:checked + label

Antes de explicar qué es este selector, una apreciación: como comenté al comienzo, el juego muestra preguntas con cuatro opciones de respuesta, de las cuales sólo una es correcta.

La forma en la que he maquetado el HTML para las opciones de respuesta es la siguiente: un checkbox, con un label asociado, de la siguiente forma:

<input type='radio' name='question[0]' value='0' id='q0o1'>

<label for='q0o1'>Opción 1</label>

Dicho esto, volvemos al selector:

.card.question .options > li input:checked + label

Este selector selecciona los elementos de tipo label que son hermanos de elementos de tipo input que estén checkeados (marcados con el tick), que a su vez son descendientes de elementos de tipo li, que sean hijos directos de elementos de clase options, descendientes de elementos de clase card y question simultáneamente ¡Wow! Eso fue un poco mucho. Veámoslo gráficamente:

Label

 

El selector recorre todo el camino desde los elementos con clase card y question hasta llegar a la label hermana de un input en estado checked. Usaremos este selector para marcar la opción elegida por el usuario.

Ahora sí, vayamos al centro de la cuestión.

El Javascript

Desde Javascript ocurrirán muchas cosas: mantendremos el estado del juego (temporizador, puntaje, en qué pregunta estamos, etc.), crearemos las preguntas, mostraremos los distintos mensajes (comienzo, final), y controlaremos la lógica del juego (aumentar los puntos, actualizar la pantalla y más).

Veamos el código. Agregué comentarios con números que luego explicaré abajo uno a uno.

// 1
var questions = [
[
"¿Que se utiliza para estilizar un sitio web?",
"Javascript",
"CSS",
"PHP",
"AngularJS",
1
],
[
"¿Qué tipo de lenguaje es PHP?",
"Interpretado",
"Compilado",
"Los 2 anteriores",
"Ninguno de los anteriores",
0
],
[
"¿jQuery es una biblioteca para qué lenguaje?",
"Python",
"PHP",
"Java",
"Ninguno de los anteriores",
3
],
[
"¿Cómo se marca el inicio de código PHP?",
"&lt;?php",
"&lt;?",
"Los 2 anteriores",
"Ninguno de los anteriores",
2
],
[
"¿Quién diseño Javascript?",
"Mark Zuckerberg",
"Bill Gates",
"Brendan Eich",
"Rasmus Lerdorf",
2
],
];

// 2
var questionTemplate = _.template(" \
<div class='card question'><span class='question'><%= question %></span> \
<ul class='options'> \
<li> \
<input type='radio' name='question[<%= index %>]' value='0' id='q<%= index %>o1'> \
<label for='q<%= index %>o1'><%= a %></label> \
</li> \
<li> \
<input type='radio' name='question[<%= index %>]' value='1' id='q<%= index %>o2'> \
<label for='q<%= index %>o2'><%= b %></label> \
</li> \
<li> \
<input type='radio' name='question[<%= index %>]' value='2' id='q<%= index %>o3'> \
<label for='q<%= index %>o3'><%= c %></label> \
</li> \
<li> \
<input type='radio' name='question[<%= index %>]' value='3' id='q<%= index %>o4'> \
<label for='q<%= index %>o4'><%= d %></label> \
</li> \
</ul> \
</div> \
");

// 3
var points,
pointsPerQuestion,
currentQuestion,
questionTimer,
timeForQuestion = 8, // seconds
timeLeftForQuestion;
// 4
$(function() {

// 5
$('button.start').click(start);
$('.play_again button').click(restart);

// 6
function restart() {
points = 0;
pointsPerQuestion = 10;
currentQuestion = 0;
timeLeftForQuestion = timeForQuestion;
// 7
$('.finish.card').hide();
$('div.start').show();
$('.times_up').hide();

generateCards();
updateTime();
updatePoints();
}

// 8
function start() {
$('div.start').fadeOut(200, function() {
moveToNextQuestion();
});
}

// 9
function generateCards() {
$('.questions').html('');
for (var i = 0; i < questions.length; i++) {
var q = questions[i];
var html = questionTemplate({
question: q[0],
index: i,
a: q[1],
b: q[2],
c: q[3],
d: q[4]
});
$('.questions').append(html);
};

// 10
$('.question.card input').change(optionSelected);
}

// 11
function moveToNextQuestion() {
currentQuestion += 1;
if (currentQuestion > 1) {
$('.question.card:nth-child(' + (currentQuestion-1) + ')').hide();
}

// 12
showQuestionCardAtIndex(currentQuestion);
setupQuestionTimer();
}

// 13
function setupQuestionTimer() {
if (currentQuestion > 1) {
clearTimeout(questionTimer);
}
timeLeftForQuestion = timeForQuestion;

// 14
questionTimer = setTimeout(countdownTick, 1000);
}

// 15
function showQuestionCardAtIndex(index) { // staring at 1
var $card = $('.question.card:nth-child(' + index + ')').show();
}

// 16
function countdownTick() {
timeLeftForQuestion -= 1;
updateTime();
if (timeLeftForQuestion == 0) {
return finish();
}
questionTimer = setTimeout(countdownTick, 1000);
}

// 17
function updateTime() {
$('.countdown .time_left').html(timeLeftForQuestion + 's');
}

// 18
function updatePoints() {
$('.points span.points').html(points + ' puntos');
}

// 19
function optionSelected() {
var selected = parseInt(this.value);
var correct = questions[currentQuestion-1][5];

if (selected == correct) {
points += pointsPerQuestion;
updatePoints();
correctAnimation();
} else {
wrongAnimation();
}

if (currentQuestion == questions.length) {
clearTimeout(questionTimer);
return finish();
}
moveToNextQuestion();
}

// 20
function correctAnimation() {
animatePoints('right');
}

// 21
function wrongAnimation() {
animatePoints('wrong');
}

// 22
function animatePoints(cls) {
$('header .points').addClass('animate ' + cls);
setTimeout(function() {
$('header .points').removeClass('animate ' + cls);
}, 500);
}

// 23
function finish() {
if (timeLeftForQuestion == 0) {
$('.times_up').show();
}
$('p.final_points').html(points + ' puntos');
$('.question.card:visible').hide();
$('.finish.card').show();
}

// 24
restart();

});

En este código Javascript utilizamos 2 bibliotecas: jQuery y Underscore. En este post me detendré especialmente en las funcionalidades relacionadas a jQuery.

Comencemos a ver punto por punto.

//1: Al comienzo del script, inicializamos un array de arrays con la preguntas del juego. Cada elemento de este array corresponde a una pregunta, con el siguiente formato: el primer elemento (índice 0) es el texto de la pregunta, de segundo al quinto (índices 1 al 4) son las opciones de respuesta, y el último (índice 5) es la respuesta correcta (indicada de 0 a 3).

//2: Aquí utilizamos Underscore para generar un template de pregunta. Básicamente, un template de Underscore es un string que define la estructura de una cadena que generaremos más adelante, con la ventaja de poder incluir porciones dinámicas utilizando parámetros. Un ejemplo sencillo:

var t = _.template("Mi nombre es <%= name %>").

En este caso, la variable t es un template, que recibe el parámetro name. Cuando utilizamos el template y le indicamos el valor de name (por ejemplo: {name: Nico}), obtendremos una cadena que dice Mi nombre es Nico.

Utilizamos esta funcionalidad de templates de Underscore para generar el HTML de cada pregunta. Lo veremos más adelante.

//3: Definimos las variables de estado del juego y los valores iniciales (como el tiempo de respuesta de cada pregunta).

//4: Cuando utilizamos jQuery para manipular nuestra página, debemos asegurarnos que los elementos que vamos a manipular ya se encuentran en la página. Esto es muy importante, ya que usualmente puede cargarse nuestro código JS antes que varios elementos y si intentamos manipularlos obtendremos nada más que errores.

Para evitar esto, jQuery define una forma de asegurarnos que cierto código se ejecutará una vez que los elementos del DOM estén cargados.

Cuando escribimos $(function(){...}) le estamos diciendo a jQuery que llame a la función que le pasamos por parámetro cuando el DOM se haya cargado, y podemos manipular sus elementos tranquilamente. Escribirlo de la forma $(miFuncion) también es válido. En ese caso, jQuery llamará a la función miFuncion cuando el DOM esté listo. Y también puedes ver ésto de la forma $(document).ready(function(){...});

//5: Utilizamos jQuery para escuchar el evento click del botón de Comenzar y Volver a jugar. El primero es el botón que comienza el juego. El segundo es un botón que mostramos al finalizar el juego para volver a jugar.

Cuando el jugador haga click en Comenzar, ejecutaremos la función start, y cuando haga click en Volver a jugar, llamaremos a restart.

//6: La función restart inicializa los valores de las variables de estado del juego. En //7 se oculta la pantalla de finalizar y un mensaje que dice Se acabó el tiempo, y se muestra la pantalla de comienzo. Finalmente, se generan las tarjetas con las preguntas y sus opciones, y se actualiza en pantalla el tiempo restante y el puntaje.

//8: La función start se ejecuta cuando el jugador hace click en comenzar. Quita el mensaje de comienzo y muestra la primer pregunta.

//9: Esta es una de las funciones clave del juego, encargada de generar las preguntas. Lo primero que hacemos es borrar las preguntas que puedan existir en el DOM (si estamos jugando por segunda vez), utilizando la función de jQuery .html(). Con esta función le decimos a jQuery qué queremos incluir en el DOM para determinados elementos (seleccionados por el selector). En este caso, queremos el string vacío en los elementos con clase questions. Si volvés al código HTML, verás que el div con la clase questions es un div vacío, que utilizaremos para contener a todas las preguntas.

En esta función usamos el template que generamos en //2 para crear el HTML de cada una de las preguntas. Con un for recorremos el array definido en //1 con las preguntas, y para cada un de ellas llamamos al template para obtener el HTML correspondiente. Este HTML lo agregamos al DOM utilizando .append(). Ésta función agrega elementos al final del contenido ya existente para los elementos seleccionados por el selector.

//10: Le indicamos a jQuery que nos interesa el evento change de los inputs dentro de los elementos con clase question y card (cada una de las preguntas). Con esto, cuando el jugador escoja una opción de respuesta, se ejecutará la función optionSelected() que verémos más adelante.

//11: Esta función cambia el estado del juego para pasar de una pregunta a la siguiente, y actualiza la página. Utilizamos la función .hide() para ocultar un elemento del DOM (la pregunta actual). En //12 mostramos la siguiente pregunta, e iniciamos el temporizador para contestar la nueva pregunta.

//13: Esta función inicializa el temporizador para responder una pregunta. Cuando nos movemos de pregunta, anulamos el temporizador anterior y lo volvemos a iniciar desde el tiempo inicial (en este caso son 8 segundos por pregunta). Cada 1 segundo, nuestro temporizador llamará a la función countdownTick() (//14).

//15: Mostramos la tarjeta de pregunta correspondiente al índice que la función recibe por parámetro. Para eso utilizamos la función de jQuery .show().

Me detendré un momento en el selector que utiliza esta función.

$('.question.card:nth-child(' + index + ')')

El objetivo es seleccionar el div de clase question y card que corresponde a la pregunta número index. Para esto utilizamos :nth-child(X), que selecciona el elemento del DOM que está en la posición X a partir del padre. Veámoslo con un ejemplo:

<div class="dad">

<span>1</span>

<span>2</span>

<span>3</span>

</div>

Dado este HTML, con el selector:

.dad span:nth-child(1): obtenemos el span que dice 1

.dad span:nth-child(2): obtenemos el span que dice 2

.dad span:nth-child(3): obtenemos el span que dice 3

Volviendo al juego, utilizamos nth-child para obtener la tarjeta de la pregunta que queremos mostrar en pantalla.

//16: La función countdownTick() se ejecuta cada un segundo, y actualiza el tiempo restante para responder en la pantalla del jugador. Si el tiempo restante es 0, entonces el juego termina.

//17: Actualiza el tiempo restante en pantalla, utilizando la función html() que hemos visto antes.

//18: Actualiza los puntos en pantalla, similar a //17.

//19: Esta es otra de las funciones claves del juego. Se ejecuta cuando el jugador escoge una respuesta.

Lo primero que hacemos es castear el valor del input que seleccionó a valor entero utilizando parseInt(). Esto es porque el HTML guarda los valores como strings, y en nuestro array de preguntas (//1), el valor de la opción correcta es un entero.

Luego, verificamos si la opción elegida por el usuario es la correcta. De ser así, aumentamos los puntos y lo mostramos en pantalla. Además, hacemos una pequeña animación de "respuesta correcta", o de "respuesta incorrecta" si no acertó. Veremos estas animaciones en breve.

Finalmente, si no hay más preguntas, el juego termina. Si las hay, nos movemos a la siguiente.

//20 y //21: Animación de respuesta correcta e incorrecta. Simplemente llaman a animatePoints() con el parámetro adecuado. Lo vemos a continuación.

//22: Esta función anima el puntaje en pantalla. La animación está hecha con CSS utilizando transition, que simplemente crea una transición (de colores en este caso) en el texto del puntaje. Dependiendo del parámetro que recibe esta función es el color de la transición. Esto se hace utilizando dos clases: right y wrong. Podés volver sobre el CSS para ver qué tienen estas clases.

La animación hace una transición a otro color, y luego vuelve al original. Esto lo hacemos con un timer, que agrega y quita la clase "animada" medio segundo después de agregarla.

//23: Cuando el juego termina, esta función es ejecutada. Si el tiempo restante para responder preguntas es 0, quiere decir que el usuario perdió por tiempo, y mostramos un mensaje de "Se acabó el tiempo!".

Por último, ocultamos las preguntas y mostramos una tarjeta con los puntos finales y un botón para volver a jugar.

//24: Cuando la página termina de cargarse, luego de definir todas las funciones del juego, llamaremos a restart() para que el juego quede en estado de listo para jugar.

¡Wow! ¡Eso ha sido bastante por esta vez!

¿Cansado de leer? Ahora ¡A jugar!

Descarga el código completo del juego desde nuestra cuenta de GitHub aquí.

¿Te animás a agregar nuevas preguntas y a que se muestren en distinto órden cada vez que jugas?

¡Mucha suerte!