No pretendo implementar todas las funcionalidades con AngularJS que tiene Google Keep, sólo alguna de ellas. Al final de la entrada hay enlace al código en GitHub y vídeo. Se podrán crear nuevas tareas, eliminarlas, editar su título y su contenido, cambiar los colores de fondo y filtrarlas. Una tarea estará compuesta por:
- Título
- Descripción
- Color de fondo
- Fecha de creación
- Fecha de última actualización
La API la he construido con Django Rest Framework. Al final de la entrada colocaré el código necesario. El cliente está construido con AngularJS, con el generador yeoman.io
Cliente AngularJS - Servicios y Controladores
Las peticiones GET, POST, PUT y DELETE se hacen a Django Rest Framework, cuya definición está en el archivo service.js
. No hay mucho que explicar, sólo comentar que se usa $resource
, dada la sencillez de la aplicación.
'use strict';
angular.module('gkeepApp')
.factory('TaskServices', ['$resource',
function ($resource) {
var url_one = 'http://127.0.0.1:8000/tasks/:id';
var url_all = 'http://127.0.0.1:8000/tasks';
return $resource(
url_one, {}, {
get: {method: 'GET', cache: false, isArray: false},
save: {method: 'POST', cache: false, isArray: false},
upctime: {method: 'PUT', cache: false, isArray: false},
delete: {method: 'DELETE', cache: false, isArray: false},
get_all: {method: 'GET', url: url_all, cache: false, isArray: true},
}
);
}]);
En el controlador me detendré un poco más. El primer método, getAll()
, obtiene todos los registros.
$scope.getAll = function() {
TaskServices.get_all({},
function success(response) {
$scope.tasks = response;
},
function error(errorResponse){
console.log("Error: "+ JSON.stringify(errorResponse));
}
)
}
El método save(data)
será para crear un nuevo registro. Si algún campo falta (Título o Descripción -el resto de campos son autorellenados-) saldrá un alert. Si todo fue bien, dentro de la función success
, se actualizan los cambios llamando a getAll()
y reseteando el contenido de las etiquetas y vaciando el modelo.
$scope.save = function(data) {
TaskServices.save(data,
function success(response) {
document.getElementById('newtask').style.display = "none";
$scope.newtask='';
$scope.getAll();
},
function error(errorResponse) {
alert("Empty fields");
}
)
};
El método delete(id)
toma la id de la tarea que se va a eliminar.
$scope.delete = function(id) {
TaskServices.delete({id: id},
function success(response) {
$scope.getAll();
},
function error(errorResponse) {
console.log("Error:" + JSON.stringify(errorResponse));
}
)
};
El método saveColor(id, data, color)
toma tres parámetros con los que se construirá un nuevo modelo, que será usado para actualizar la tarea seleccionada.
$scope.saveColor = function(id, data, color) {
var task = {
'id': id,
'title': data.title,
'description': data.description,
'color': 'rgba('+color+',0.90);'
};
TaskServices.update({id: id}, task,
function success(response) {
$scope.getAll();
},
function error(errorResponse) {
console.log("Error:" + JSON.stringify(errorResponse));
}
)
};
El método
$scope.update = function(id, data) {
if (data.description !== '' && data.title !== ''){
TaskServices.update({id: id}, data,
function success(response) {
$scope.getAll();
},
function error(errorResponse) {
console.log("Error:" + JSON.stringify(errorResponse));
}
)
} else {
$scope.getAll();
alert("Empty fields");
}
};
El método cancel()
será llamado cuando no se quiera crear una una nueva tarea.
$scope.cancel = function() {
document.getElementById('newtask').style.display = "none";
$scope.newtask='';
};
Cliente AngularJS - Vistas
En index.html
se añade dentro de la navegación el input para buscar y el vínculo para hacer visible el formulario para crear nueva tarea. Para activar el filtro de búsqueda se llama a la directiva ng-model="search"
y en la directiva ng-repeat
se aplica el filtro ng-repeat="task in tasks | filter:search"
. Para abrir el formulario de creación de nueva tarea se usa el evento onclick
de javascript que cambia el display del contenedor del formulario.
En main.html
se pintan todas las tareas. Para ello, iniciamos todas las tareas con ng-init="getAll()"
. Cada tarea estará dentro de una etiqueta <article>
. Cada <article>
se divide en tres zonas: <header>
, <div>
y <footer>
.
- El
<header>
contiene el título de la tarea dentro de un<input>
al que se resetean los estilos y se añade la directivang-change
, que en caso de cambiar su contenido cuando el<input>
pierda el focong-model-options
se llama al métodoupdate(task.id, task)
. Otro elemento que contiene el<header>
es un submenú para el cambio de colores y la eliminación de la tarea. Con Bootstrap se habilita el menú desplegable. Cada uno de los elementos<li>
contiene un color diferente que en caso de llamarse el evento clickng-click="saveColor(task.id, task, '132,198,100');"
se guardará en color que se hubiera seleccionado. También, aunque no necesario, cuando se activa el evento se guarda en una variableselected_color
el color seleccionado, que puede ser aplicado mediante la directivang-style
al<article>
- La etiqueta
<div>
con clasecontent
contiene un<textarea>
que al igual que el input para el título ha sido reseteado, y usa la directivang-change
que llama al métodoupdate(task.id, task)
cuando el<textarea>
pierde el foco y el contenido a cambiado, todo ello gracias ang-model-options="{updateOn: 'blur'}"
- En
<footer>
se pintan las fechas de creación{{task.created | date:"dd/MM/yyyy"}}
y la última edición de la tarea{{task.updated | date:"dd/MM/yyyy h:mma"}}
. Para la creación de una nueva tarea se dispone de un formulario con<input>
(para el título) y<textarea>
(para la descripción). Por defecto el formulario no está visible. No hay mucho que explicar, hay botón para guardar, que en caso de que estén los datos rellenos se guardan, se resetean y se oculta el formulario, y si se cancela se resetea el formulario y se cierra
Código Rest con Django Rest Framework:
##############
## settings.py
##############
INSTALLED_APPS = (
## ...
'rest_framework',
'app',
'corsheaders',
)
MIDDLEWARE_CLASSES = (
## ...
'django.middleware.csrf.CsrfViewMiddleware',
'corsheaders.middleware.CorsMiddleware',
)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.permissions.AllowAny',
)
}
APPEND_SLASH = False
CORS_ORIGIN_ALLOW_ALL = True
############
## models.py
############
from django.db import models
class Task(models.Model):
title = models.TextField(max_length=100)
description = models.TextField(max_length=1000)
color = models.TextField(max_length=30, blank=True, default='rgba(62,75,78,0.90);')
created = models.DateTimeField(auto_now_add=True, auto_now=False)
updated = models.DateTimeField(auto_now_add=False, auto_now=True)
#################
## serializers.py
#################
from rest_framework.serializers import ModelSerializer
from .models import Task
class TaskSerializer(ModelSerializer):
class Meta:
model = Task
fields = ('id', 'title', 'description', 'color', 'updated', 'created')
##############
## viewsets.py
##############
from .models import Task
from .serializers import TaskSerializer
from rest_framework import viewsets
class TaskViewSet(viewsets.ModelViewSet):
serializer_class = TaskSerializer
queryset = Task.objects.all()
##########
## urls.py
##########
from django.conf.urls import patterns, include, url
from django.contrib import admin
admin.autodiscover()
from app.viewsets import TaskViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter(trailing_slash=False)
router.register(r'tasks', TaskViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^admin/', include(admin.site.urls)),
]
El código completo está publicado en mi GitHub.
Y aquí un vídeo en el que explico todo lo anterior.