Todo app Django + Reactjs Parte 2 (Front-End).

Guía Básica para crear una todo app con django y reactjs.

Nota: En el siguiente post, se asumirá que tiene el ambiente virtual de python listo, que ya se tiene experiencia previa en django(básica como mínimo) y ya tenga iniciado un paquete de nodejs en el mismo directorio.

Front-End.

Antes de iniciar con el frontend, hay que configurar la aplicación. Si no has preparado el backend y no has instalado las dependencias del frontend, te recomiendo que leas Todo app Django + Reactjs Parte 1 (Back-End).

Primero crearemos el archivo .babelrc, en la raíz de nuestro proyecto, dicho archivo contendrá lo siguiente:

{
"presets": ["es2015", "stage-0", "react"]
}

Los siguientes presets, sirven para transformar mi fuente de javascript ES6 y JSX a javascript ES5, que es soportado por todos los navegadores web. Ahora, vamos a crear el archivo webpack.config.js en la raíz del proyecto, el cual va a definir la configuración que va a usar webpack, para procesar todos mis archivos JSX y generar un único modulo de javascript ES5. Dicho archivo de configuración va a contener lo siguiente:

var path = require("path");
var webpack = require('webpack');
var BundleTracker = require('webpack-bundle-tracker');
module.exports = {
context: __dirname,
entry: [
'webpack-dev-server/client?http://localhost:3001',
'webpack/hot/only-dev-server',
'./client/index'
],
output: {
path: path.resolve('./ptodo/core/static/js'),
filename: "[name]-[hash].js",
publicPath: 'http://localhost:3001/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new BundleTracker({filename: './webpack-stats.json'}),
],
module: {
loaders: [
{
test: /\.(js|jsx)?$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
],
},
resolve: {
extensions: ['.js', '.jsx']
},
}

En esta configuración le estoy diciendo a babel, que me convierta solo los archivos que posean la extensión *.js,*.jsx y que no me incluya los archivos que se encuentran en el directorio node_modules, también le estoy diciendo que me genere el archivo de stat llamado webpack-stats.json y que me minifique el código de javascript ES5 y las demás opciones son para indicarle que usare un servidor en nodejs + express, el cual se va a encargar de compilar el fuente de React y javascript ES6, cada vez que se realice un cambio a uno de los módulos.

Ahora crearemos el modulo llamado server.js que es el servidor encargado de transformar de React y ES6 a javascript ES5.

var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
inline: true,
historyApiFallback: true
}).listen(3001, '0.0.0.0', function (err, result) {
if (err) {
console.log(err)
}
console.log('Listening at 0.0.0.0:3001')
});

Ahora podemos crear el directorio, donde se va a guardar los módulos y/o componentes de React, se va a llamar client y lo vamos a crear en la raíz del proyecto ptodo, en dicho directorio recién creado, vamos a crear los siguientes 5 modulos: App.js, Task.js, TaskInput.js, endpoints.js, index.js.

El árbol de directorios, va a quedar algo así:

ptodo/
|
| - ptodo/
| |
| | - core/
| | |
| | | - static/
| | | |
| | | | - js/
| | |
| | | - templates/
| | | |
| | | | - home.html
| | |
| | | - __init__.py
| | | - views.py
| |
| | - __init__.py
| | - settings.py
| | - urls.py
| | - wsgi.py
|
| - tasks/
| |
| | - __init__.py
| | - admin.py
| | - apps.py
| | - models.py
| | - tests.py
| | - views.py
| | - migrations/
| | |
| | | - __init__.py
|
| - client/
| |
| | - App.js
| | - Task.js
| | - TaskInput.js
| | - endpoints.js
| | - index.js
|
| - package.json
| - server.js
| - webpack.config.js
| - manage.py
| - .babelrc

Antes de empezar a escribir componentes de Reactjs, tenemos que agregar tags de la aplicación webpack_loader al template home.html, dicho template se ubica en /ptodo/core/templates/home.html, el template va a quedar algo así:

{% load render_bundle from webpack_loader %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Home - todoapp</title>
<base data-token="{{ csrf_token }}"/>
{% render_bundle 'main' 'css' %}
</head>
<body>
<div id="root"></div>
{% render_bundle 'main' %}
</body>
</html>

dichos tags son utilizados, agregar los módulos ya compilados de javascript y css al template home.html.

Ahora si, vamos a iniciar con el modulo endpoints.js y después vamos con los componentes de reactjs.

endpoints.js:

Va a guardar los endpoints del backend.

export const addTask        = '/task/add/'        ;
export const listTasks = '/task/list/' ;
export const deleteTask = '/task/delete/' ;
export const deleteAllTasks = '/task/delete/all/' ;
export const updateTaskName = '/task/update/' ;

Task.js:

Este modulo va a renderizar una tarea, dicha tarea va a tener la habilidad de poder actualizarse y borrarse. Para poder llamar a los endpoints del backend, primero tenemos que obtenerlos del modulo endpoints.js. Este componente va a recibir tres props, onError(función), onUpdateTasks(función) y task(objeto de javascript).

El método onError, va a llamarse, cuando ocurra un error en las llamadas al endpoint.

El método onUpdateTasks, va a llamarse, cuando se necesite actualizar el estado de la aplicación(agregar, borrar tareas).

El objeto Task, es la tarea, que se necesita renderizar en el componente.

Para poder llamar el endpoint /task/update/, tenemos que enviar como cabecera http, el token csrf, que se encuentra en el tag <base/> del documento html.

Para poder eliminar una tarea del sistema, se tiene que llamar al endpoint /task/delete/ y se le tiene que enviar como parámetro el id de la tarea a eliminar.

import React from 'react'
import axios from 'axios'
import {
deleteTask,
updateTaskName
} from './endpoints'
export default class Task extends React.Component {
   constructor(props) {
super(props);
       this.onError  = this.props.onError || function(nothing){};
this.onUpdateTasks = this.props.onUpdateTasks ||
function(nothing){};
this.state = {updateTaskName: false, task_name:
this.props.task.task_name, old_task_name:
this.props.task.task_name};
}
    onDeleteTask(ev) {
ev.preventDefault();
const task = this.props.task;
axios.get(deleteTask, {
params: {
task_id: task.task_id,
}
}).then((res) => {
let code = res.data.code;
if(code === 200) {
let task = res.data.task;
this.onUpdateTasks(task);
} else {
this.onError(res.data.message);
}
})
}
    onUpdateTaskName(task_id, task_end_time, new_task_name) {
axios({
method: 'put',
url: updateTaskName,
data: {
task_id: task_id,
task_name: new_task_name,
},
headers: {
'X-CSRFToken': document.getElementsByTagName('base')[0]
.getAttribute('data-token')
}
}).then((res) => {
let code = res.data.code;
if(code === 202) {
this.setState({
updateTaskName: false,
task_name: new_task_name,
old_task_name: new_task_name
});
} else {
this.setState({
updateTaskName: false,
task_name: this.state.old_task_name,
old_task_name: this.state.old_task_name });
}
}).catch(err => {
this.setState({
updateTaskName: false,
task_name: this.state.old_task_name,
old_task_name: this.state.old_task_name });
})
}
    render() {
const task = this.props.task;
return (
<div>
{
!this.state.updateTaskName?
<div>
<p onClick={(ev) => {
ev.preventDefault();
this.setState({updateTaskName: true})
}}>{this.state.task_name ||
task.task_name}</p>
<p>Created: {task.task_created_time}</p>
</div>:
<div>
<input type="text"
value={this.state.task_name}
onChange={e =>
this.setState({
task_name: e.target.value})
}/>
                            <button onClick={ev => {
ev.preventDefault();
this.onUpdateTaskName(
task.task_id,
task.task_end_time,
this.state.task_name
);
}}>save</button>
                            <button onClick={ev => {
ev.preventDefault();
this.setState({
updateTaskName: false,
task_name: this.state.old_task_name,
old_task_name:
this.state.old_task_name
});
}}>no save</button>
</div>
}
{
!this.state.updateTaskName?
<button onClick={
this.onDeleteTask.bind(this)
}>delete</button>
: null
}
</div>
)
}
}

TaskInput.js:

El componente actual, se va a encargar de renderizar los campos de entrada y del boton para eliminar todar las tareas del sistema.

Este componente va a recibir solo dos props, onUpdateTasks y onError, tienen la misma funcionalidad ya descrita mas arriba. Para poder agregar una tarea al sistema, hay que obtener el csrf token de el tag <base />, que se encuentra en la cabecera del documento html.

import React from 'react'
import axios from 'axios'
import {
addTask,
deleteAllTasks
} from './endpoints'
export default class TaskInput extends React.Component {
    constructor(props) {
super(props);
        this.onError  = this.props.onError || function(nothing){};
this.onUpdateTasks = this.props.onUpdateTasks ||
function(nothing){};
}
    onAddTask(ev) {
ev.preventDefault();
let datetime = new Date();
let task = {
task_name: this.taskInput.value,
};
        axios({
method: 'post',
url: addTask,
data: task,
headers: {'X-CSRFToken': document
.getElementsByTagName('base')[0]
.getAttribute('data-token')}
}).then((res) => {
let code = res.data.code;
if (code === 201) {
let tmpTask = res.data.task;
let tasks = [{
task_name: tmpTask.task_name,
task_id: tmpTask.task_id,
task_created_time: new Date(
tmpTask.task_created_time).toLocaleString(),
}];
            this.onUpdateTasks(tasks);
this.taskInput.value = ""
} else {
this.onError(res.data.message);
}
})
}
    onDeleteAllTasks(ev) {
ev.preventDefault();
axios.get(deleteAllTasks)
.then(res => {
let code = res.data.code;
if(code === 200) {
this.onUpdateTasks([]);
} else {
this.onError(res.data.message);
}
})
}
    render() {
return (
<div>
<input type="text" ref={n => this.taskInput = n}/>
<button onClick={this.onAddTask.bind(this)}>Add</button>
<button onClick={
this.onDeleteAllTasks.bind(this)
}>
Delete all</button>
</div>
)
}
}

App.js:

Este modulo, se encarga de montar los componentes Task y TaskInput . Cuando el componente App va a ser montado o renderizado, se llama el método, ComponentWillMount, donde se va a llamar al endpoint /task/list/, para mostrar las tareas que se encuentra en el sistema. Aparte, se definen dos métodos: 1) onUpdateTasks 2) onError, estos serán pasados como atributos a los componentes ya antes mencionados y así ellos puedan actualizar el estado de la aplicación o mostrar algún error.

import React from 'react'
import axios from 'axios'
import Task from './Task'
import TaskInput from './TaskInput'
import { listTasks } from './endpoints'
const ADD_TASK        = 1;
const DELETE_TASK = 2;
const ADD_ALL_TASKS = 3;
const DELETE_ALL_TASK = 4;
export default class App extends React.Component {
    constructor(props) {
super(props);
this.state = { tasks: [] }
}
    componentWillMount() {
axios.get(listTasks)
.then((res) => {
let code = res.data.code;
if(code === 200) {
this.onUpdateTasks(res.data.tasks, ADD_ALL_TASKS);
}
})
}
    onError(message) {
alert(message);
}
    onUpdateTasks(data, updateType = ADD_TASK) {
let tasks = [];
switch (updateType) {
case ADD_TASK:
tasks.push(...data);
tasks.push(...this.state.tasks);
break;
case DELETE_ALL_TASK:
tasks = data;
break;
case ADD_ALL_TASKS:
tasks = data.map(el => {
return {
task_name: el.task_name,
task_id: el.task_id,
task_created_time: new Date(el.task_created_time)
.toLocaleString(),
}
});
break;
            case DELETE_TASK:
tasks = this.state.tasks.filter(el => {
return el.task_id !== data.task_id;
});
                break;
}
        this.setState({tasks: tasks});
}
    render() {
return (
<div>
<div>
<TaskInput onError={this.onError}
onUpdateTasks={tasks => {
this.onUpdateTasks(
tasks,
tasks.length >= 1?
ADD_TASK: DELETE_ALL_TASK)
}} />
</div>
<div>
{
this.state.tasks.length === 0?
<div><p>There's No Tasks</p></div>:
this.state.tasks.map((el) => {
return <Task key={el.task_id} task={el}
onError={this.onError}
onUpdateTasks={task =>
{this.onUpdateTasks(
task,
DELETE_TASK
)
}} />
})
}
</div>
</div>
)
}
}

index.js:

El modulo actual, se va a encargar de renderizar el componente App, dentro del tag div con un id root ==> ‘<div id=”root”></div>

import ReactDom from 'react-dom'
import React from 'react'
import App from './App'
ReactDom.render(
<App/>,
document.getElementById('root')
);

Ya el cliente se encuentra listo, ahora, hay que correr el servidor de desarrollo hecho en express y nodejs(server.js) y el servidor de desarrollo de django.

# servidor de desarrollo de django
python manage.py runserver
# servidor de desarrollo de nodejs + express
node server.js

El frontend esta listo y funcionando con el backend. en caso de que tengan preguntas o se me he equivocado en algo, comenten =).

El fuente del proyecto lo pueden encontrar en github.

Saludos.