Similar Posts
DOM Tool: transforma HTML en código DOM
A la hora de generar nuevo código HTML mediante Javascript, nos enfrentamos a la decisión de utilizar innerHTML o DOM. El problema de usar DOM es que nos puede llevar mucho tiempo generar el código Javascript necesario para crear el HTML necesario.
Todo este proceso se puede hacer de forma mucho más sencilla mediante DOM Tool, una aplicación que nos devolverá el código Javascript necesario para pasar un HTML que introduciremos mediante un formulario.
Ya no hay excusas para no usar DOM.
DOM Tool
VÃa / dzone
Manipulate Colors in All Imaginable Combinations with xColor
The xcolor plugin is an easy-to-use jQuery extension to manipulate colors in all imaginable combinations. This plugin implements an extensiv color parser and a featureful set of color-manipulation methods. There is also an animate() extension to smooth CSS colors.Another useful method isReadable() completes the whole, by allowing you to check if a …
Laboratorio: reordenar lista mediante mootools
Estoy empezando a curiosear Mootools, y aunque me está gustando bastante, si que echo en falta una documentación más detallada, no solo explicar como va el API, ya que a veces son necesarios ejemplos más básicos para no tener que estar buscando en el API.
En este caso voy a reorganizar mediante drag&drop una lista de elementos, la cual puede tener subelementos también, un tÃpico ejemplo de categorÃas y subcategorÃas. Para ello me crearé una lista cuyos elementos tendrán el estilo categoria y cuyos subelementos tendrán el estilo subcategorias, necesario para diferenciar estilos y comportamiento.
<ul id="lista_categorias">
<li class="categoria">Elemento 1</li>
<li class="categoria">Elemento 2
<ul>
<li class="subcategoria">Elemento 2.1</li>
<li class="subcategoria">Elemento 2.2</li>
<li class="subcategoria">Elemento 2.3</li>
<li class="subcategoria">Elemento 2.4</li>
<li class="subcategoria">Elemento 2.5</li>
<li class="subcategoria">Elemento 2.6</li>
</ul>
</li>
<li class="categoria">Elemento 3</li>
<li class="categoria">Elemento 4
<ul>
<li class="subcategoria">Elemento 4.1</li>
<li class="subcategoria">Elemento 4.2</li>
<li class="subcategoria">Elemento 4.3</li>
<li class="subcategoria">Elemento 4.4</li>
</ul>
</li>
<li class="categoria">Elemento 5</li>
<li class="categoria">Elemento 6</li>
<li class="categoria">Elemento 7</li>
<li class="categoria">Elemento 8</li>
<li class="categoria">Elemento 9</li>
</ul>
Para ser lo menos intrusivo que se pueda, anadiré al evento load del objeto window la carga de las funciones necesarias para ello, siempre que exista el objeto lista_categorias (el primer ul de todos).
window.addEventListener('load', function() {
_lista();
}
, false);
function _lista() {
if ($('lista_categorias')) {
...
}
Habrá tres operaciones distintas: crear efecto drag, añadir evento drop a las categorÃas y añadir evento drop a las subcategorÃas. Digo crear efecto drag porque realmente lo que se hace es que cuando se ejecuta el evento mousedown, se crea una copia del elemento, y es este el que se mueve por la pantalla, guardando una referencia al elemento original. Hay distinción entre el evento drop de las categorÃas y las subcategorÃas porque si se trata de una categorÃa se añade sin más, y si es una subcategorÃa se añade en la categorÃa. A parte, si se intenta mover una categorÃa que contiene subcategorÃas, solo se añadirán las subcategorÃas, no la categorÃa.
// Drag & Drop de listado
function _lista() {
if ($('lista_categorias')) {
// Drag de cada categoria/subcategoria
// Para cada elemento li añado el evento mousedown, para que cuando se ejecute se cree una copia de ese elemento, se le de estilo semi transparente y se permita hacer drag
$$('#lista_categorias li').each(function(elemento){
elemento.addEvent('mousedown', function(e) {
e = new Event(e).stop();
window.item = elemento;
window.categoria_clone = this.clone()
.setStyles(this.getCoordinates())
.setStyles({'opacity': 0.7, 'position': 'absolute'})
.addEvent('drop', function() {console.log('DROP')})
.addEvent('emptydrop', function() {
this.remove();
}).inject(document.body);
categoria_clone.makeDraggable({
droppables: $$('#lista_categorias li')
}).start(e);
});
});
// Drop en las categorias
// Para cada categoria, le añadimos el evento drop, que permite recibir elementos "drag". Si el elemento que se recibe tiene subcategorias, entonces se cogen estas y se añaden al elemento ul que contiene, si no lo contiene, lo creamos. Si no contiene subcategorÃas, se añade sin más.
$$('#lista_categorias li.categoria').each(function(drop){
drop.addEvents({
'drop': function() {
window.categoria_clone.remove();
var ul = window.item.getElement('ul');
if (!drop.getElement('ul')) {
(new Element('ul')).inject(drop);
}
dropul = drop.getElement('ul');
if (ul) {
ul.getElements('li').each(function(li) {
li.inject(dropul);
});
ul.remove();
} else {
window.item.inject(dropul);
}
drop.setStyle('background', '#FFFFFF');
//dropFx.start('7389AE').chain(dropFx.start.pass('ffffff', dropFx));
},
'over': function() {
drop.setStyle('background', '#DDDDFF');
//dropFx.start('98B5C1');
},
'leave': function() {
drop.setStyle('background', '#FFFFFF');
// window.categoria_clone.remove();
//dropFx.start('ffffff');
}
})
});
// Si cae el drop en una subcategoria lo inserto en el padre
$$('#lista_categorias li.subcategoria').each(function(drop){
drop.addEvents({
'drop': function() {
window.categoria_clone.remove();
var ul = window.item.getElement('ul');
dropul = drop.parentNode;
if (ul) {
ul.getElements('li').each(function(li) {
li.inject(dropul);
});
ul.remove();
} else {
window.item.inject(dropul);
}
}
})
});
}
}
Misma altura en bloques mediante jQuery
Buen truco para darle la misma altura a diferentes bloques cuando mediante CSS no es fácil de hacer.
function equalHeight(group) {
tallest = 0;
group.each(function() {
thisHeight = $(this).height();
if(thisHeight > tallest) {
tallest = thisHeight;
}
});
group.height(tallest);
}
Equal Height Columns with jQuery
Vía / CSS Globe
Tutorial básico de HTML storage
Una de las características más interesantes de HTML5 es el browser storage, el cual nos permite almacenar datos en el navegador del cliente.
A parte de su uso básico:
localStorage.setItem('name', 'arman');
var value = localStorage.getItem('name');
localStorage.removeItem('name');
Me gustaría destacar dos puntos importantes: detectar si el navegador lo soporta y añadir eventos:
var webStorageSupported = ('localStorage' in window) && window['localStorage'] !== null;
if (window.addEventListener) {
window.addEventListener("storage", handleStorageChange, false);
} else {
window.attachEvent("onstorage", handleStorageChange);
}
function handleStorageChange(event) {
alert("Algo esta cambiando en el almacenamiento");
}
El resto del tutorial explica todo paso a paso
Hapi.js + Vue.js llamadas autenticadas al servidor con JWT (login)
Hoy toca realizar conexiones con el servidor usando llamadas autenticadas. Para eso usaremos JWT (JSON Web Token), que viene a ser el envío de un token en cada llamada que necesita autenticación. El token está formado por tres cadenas codificadas en base64 y separadas por un punto (header.payload.signature). En el payload se envía toda la info que queramos, pero sin pasarse porque el token no debería ser muy largo y sobre todo sin enviar información sensible, porque el token no está encriptado, es por ello que la comunicación navegador/servidor debe ser mediante HTTPS. Si quieres una explicación más detallada, aquí te lo explican mejor.
En el payload vamos a guardar varios claims, entre ellos el username y un uuid que usaremos para validar que el usuario es correcto. JWT asegura que el token es válido, pero no viene mal añadir cierta seguridad. Cuando el usuario se loguea un nuevo uuid es generado, por lo que se comprobará si coinciden para el username enviado.
Ya tenemos la explicación teórica, ahora vamos con la parte de programación, primero la parte servidor y luego el frontend.
Lo primero que tenemos que hacer es añadir un nuevo plugin a hapi.js que se encargue de la autenticación, registrando un nuevo strategy a server.auth que use JWT. El plugin hapi-auth-jwt2 se encargará de toda la autenticación JWT y añadiremos una capa extra de validación comprobando que el username y el uuid coinciden.
/**
* Auth controller
*
* It uses JWT
*/
const jwt2 = require( 'hapi-auth-jwt2' );
const User = require( '../models/users' );
const validate = async function( decoded ) {
const user = await User.findByUUID( decoded.id );
return { isValid: !! user && user.username === decoded.username };
};
exports.plugin = {
name: 'auth',
register: async function( server, options ) {
await server.register( jwt2 );
server.auth.strategy( 'jwt', 'jwt',
{
key: options.jwt.secret,
validate: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // pick a strong algorithm
}
);
server.auth.default( 'jwt' );
},
};
En el modelo user añadiremos 2 nuevos métodos estáticos, que permitirán encontrar usuarios por username/email y por uuid:
userSchema.static( 'findByUserOrEmail', async function( username, email ) {
const result = await new Promise( ( resolve, reject ) => {
this.model( 'User' )
.findOne( {
$or: [
{ username: username },
{ email: email },
],
} )
.exec( ( error, data ) => {
if ( error ) {
reject( error );
}
resolve( data );
} );
} );
return result;
} );
userSchema.static( 'findByUUID', async function( uuid ) {
const result = await new Promise( ( resolve, reject ) => {
this.model( 'User' )
.findOne( {
uuid,
} )
.exec( ( error, data ) => {
if ( error ) {
reject( error );
}
resolve( data );
} );
} );
return result;
} );
En el manifest de Glue hay que añadir la configuración para CORS, ya que el token viajará usando la cabecera Authorization:
const manifest = {
server: {
port: Config.get( '/server/port' ),
routes: {
cors: {
origin: [ '*' ],
credentials: true,
exposedHeaders: [ 'Authorization' ],
},
},
},
};
Y en la configuración general del servidor hay que añadir una cadena que se usará para encriptar el signature del token JWT.
const config = {
auth: {
jwt: {
secret: process.env.JWT_SECRET,
},
},
};
Y por último añadimos una nueva ruta para autenticar (login) el usuario, comprobamos que el usuario y la contraseña coinciden, y si es así, creamos un uuid que guardamos en la bd y generamos el JWT y lo enviamos en la cabecera Authorization:
/**
* Authenticates an user
*/
server.route( {
method: 'POST',
path: '/user/auth',
options: {
tags: [ 'api', 'user', 'auth' ],
description: 'Server authenticate user',
notes: 'Server authenticate user',
auth: false,
validate: {
payload: {
username: Joi.string().alphanum().min( 3 ).max( 20 ).required(),
password: Joi.string().min( 8 ).required(),
},
},
},
/**
* Route handler
*
* @param {object} request
* @param {object} h Hapi object
* @returns {object}
*/
handler: async( request, h ) => { // eslint-disable-line
try {
const user = await User.findByUserOrEmail( request.payload.username, request.payload.email );
if ( ! user ) {
return Boom.badData( 'User or password incorrect' );
}
const isValidPassword = await bcrypt.compare( request.payload.password, user.password );
if ( ! isValidPassword ) {
return Boom.badData( 'User or password incorrect' );
}
const claims = {
id: uuid(),
exp: new Date().getTime() + ( 180 * 24 * 60 * 60 * 1000 ), // 3 months
username: user.username,
};
user.uuid = claims.id;
user.save();
const token = JWT.sign( claims, Config.get( '/auth' ).jwt.secret ); // synchronous
return h.response( {
response: true,
message: 'Check Auth Header for your Token',
} ).header( 'Authorization', token );
} catch ( error ) {
return Boom.badImplementation( 'Error', { error } );
}
},
} );
Vale, ya tenemos la parte del servidor, ahora la parte del frontend. Primero es añadir las rutas para /login, /account y /logout. He añadido un meta que indica si la ruta tiene que ser obligatoriamente autenticada, obligatoriamente no autenticada o como sea. Para ello, para cada ruta comprobará si el JWT token está almacenado o no y según el meta redirigirá a home o continuará:
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/js/components/Home';
import Login from '@/js/components/Login';
import Account from '@/js/components/Account';
import Logout from '@/js/components/Logout';
import Storage from '@/js/utils/storage';
Vue.use( Router );
const router = new Router( {
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
auth: false,
},
},
{
path: '/account',
name: 'Account',
component: Account,
meta: {
auth: true,
},
},
{
path: '/logout',
name: 'Logout',
component: Logout,
meta: {
auth: true,
},
},
],
} );
router.beforeEach( ( to, from, next ) => {
if ( !! to.meta ) {
const storage = new Storage();
const jwtToken = storage.getJWTToken();
// Can't be logged
if ( to.meta.auth === false ) {
if ( jwtToken ) {
next( '/' );
}
// Must be logged
} else if ( to.meta.auth === true ) {
if ( ! jwtToken ) {
next( '/' );
}
}
next();
} else {
next();
}
} );
export default router;
Para gestionar el almacenamiento en el navegador en vez de cookies vamos a usar sessionStorage y localStorage para guardar el token JWT. Como el formulario de login permite recordar la sesión, vamos a usar ambos storages. Si no se recuerda usaremos sessionStorage, que se borrará cuando se cierra el navegador, en caso contrario usaremos localStorage.
import Config from '@/js/config';
/**
* API methods for sesion/local storage.
* Depending on `this.session` it saves only on `sessionStorage` or also in `localStorage`
*
* @since v0.8.0
*/
class storage {
/**
* Constructor
*
* @param {boolean} session If stored only in session
*/
constructor( session = false ) {
this.session = session;
}
/**
* It saves the token in the session (and local storage is this.session === false),
*
* @param {strig} token JWT token
*/
setJWTToken( token ) {
sessionStorage.setItem( Config.jwt.storageKey, token );
if ( ! this.session ) {
localStorage.setItem( Config.jwt.storageKey, token );
}
}
/**
* It gets a value from session storage or in local if session = false
*
* @returns {string}
*/
getJWTToken() {
const sessionValue = sessionStorage.getItem( Config.jwt.storageKey );
if ( sessionValue ) {
return sessionValue;
}
if ( ! this.session ) {
const storedValue = localStorage.getItem( Config.jwt.storageKey );
return storedValue;
}
return null;
}
/**
* Removes JWT token from session and local storage
*/
removeJWTToken() {
sessionStorage.removeItem( Config.jwt.storageKey );
localStorage.removeItem( Config.jwt.storageKey );
}
}
export default storage;
También hemos creado una librería para tratar las llamadas a la API. Hay dos métodos, uno para autenticar el usuario (login) que mirará la cabecera Authorization, y otro método que obtiene los datos del usuario actual realizando una llamada autenticada enviando el JWT token en la cabecera Authorization:
import Config from '@/js/config';
import Storage from '@/js/utils/storage';
/**
* API backend methods
*
* @since 0.8.0
*/
class apiFetch {
/**
* Authenticate an user, if ok, JWT token is sent by the server in Authorization header
*
* @param {string} username User name
* @param {string} password Password
*
* @returns {Promise}
*/
auth( username, password ) {
return fetch( Config.api.user.auth, {
method: 'POST',
body: JSON.stringify(
{ username, password }
),
mode: 'cors',
} ).then( response => {
const auth = response.headers.get( 'Authorization' );
if ( auth ) {
return {
response: true,
token: auth,
};
}
return {
response: false,
message: 'Username or password not valid',
};
} );
}
getUser( username ) {
return fetch( `${ Config.api.user.get }${ username }`, {
method: 'GET',
headers: {
Authorization: new Storage().getJWTToken(),
},
} ).then( response => response.json() );
}
}
export default apiFetch;
Ahora solo faltan los controladores para login, account y logout. Login realizar la llamada al servidor y si se obtiene el JWT se guarda:
<template>
<section class="section">
<div class="container">
<h1 class="title">
Login
</h1>
<div
v-if="error"
class="columns is-centered has-margin-bottom-2"
>
<b-notification
class="column is-7-tablet is-6-desktop is-5-widescreen"
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
size="is-small "
>
{{ error }}
</b-notification>
</div>
</div>
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen has-background-light login-form">
<b-field label="Username">
<b-input
v-model="username"
value=""
maxlength="30"
icon="account-circle-outline"
/>
</b-field>
<b-field label="Password">
<b-input
v-model="password"
value=""
type="password"
icon="lock-outline"
/>
</b-field>
<div class="field">
<b-checkbox v-model="remember">
Remember me
</b-checkbox>
</div>
<div class="has-text-right">
<b-button
type="is-primary"
@click="submit"
>
Log in
</b-button>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import ApiFectch from '@/js/utils/api';
import Storage from '@/js/utils/storage';
export default {
name: 'Login',
// Form data and error messages if login fails
data() {
return {
username: '',
password: '',
remember: false,
error: '',
};
},
methods: {
// It logs in, using the backend API for authenticate the user data.
// If user logs in, it saves the JWT token in the browser. If not, shows error message.
submit: function() {
const api = new ApiFectch();
api.auth( this.username, this.password )
.then( response => {
const storage = new Storage( ! this.remember );
if ( !! response.response && !! response.token ) {
storage.setJWTToken( response.token );
this.error = false;
// `go` refreshes the page, so user data is updated
this.$router.go( '/' );
} else {
storage.removeJWTToken();
this.error = response.message;
}
} );
},
},
};
</script>
<style lang="scss" scoped>
.login-form {
border-radius: 4px;
}
</style>
Logout borra los datos JWT del navegador y redirige a home:
<template>
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
{{ message }}
</h1>
<h2 class="subtitle">
Miss you 💛
</h2>
</div>
</div>
</section>
</template>
<script>
import User from '@/js/utils/user';
export default {
name: 'Logout',
// Dummy data
data() {
return {
message: 'Bye',
};
},
// After being created it logs out and go to home
created() {
new User().logout();
// `go` instead of `push` because refreshing the page updates the user data
// Maybe using vuex is a better way to do it, or not...
this.$router.go( '/' );
},
};
</script>
Y Account recupera los datos del usuario una vez que ha creado el controlador:
<template>
<div id="account">
<section class="hero is-primary is-bold">
<div class="hero-body">
<div class="has-text-centered">
<h1 class="title">
{{ message }}
</h1>
<h2 class="subtitle">
Your data
</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="tile is-ancestor">
<div class="tile is-parent">
<p class="tile is-child notification">
Some content
</p>
</div>
<div class="tile is-8 is-parent">
<div class="tile is-child notification is-info">
<ul id="data">
<li
v-for="( value, key ) in user"
:key="key"
>
{{ key }} : {{ value }}
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
// Dummy component
import ApiFectch from '@/js/utils/api';
import User from '@/js/utils/user';
export default {
name: 'Account',
data() {
return {
message: 'Account',
user: {},
};
},
created() {
const user = new User().getCurrentUser();
new ApiFectch().getUser( user.username )
.then( response => this.user = response );
},
};
</script>
Recuerda que tienes todo el código aquí
