
Vamos a programar una herramienta para definirnos un DAFO online con React
y con paneles al estilo Trello
Prólogo
Por DAFO se entiende el típico gráfico de Debilidades, Amenazas, Fortalezas y Oportunidades, al que también se le llama FODA, o en inglés SWOT
Se trata de un simple esquema a rellenar que nos ayuda a estructurar nuestras ideas y a saber dónde estamos, bien sea nosotros como personas, o nuestro proyecto, o nuestra empresa
Lo mejor del DAFO es que es un ejercicio gratuito y que nos da un punto de partida para situarnos y empezar (o no empezar) con cierto criterio
Lo peor son sus riesgos que se resumen en lo subjetivo del asunto, y es que al final podemos tirar de sesgo de confirmación y ganar una falsa seguridad que nos deje peor de lo que estábamos (tienes una lista de sesgos aquí)
En este curso vamos a construir una herramienta que nos va a permitir
- Rellenar este diagrama en una página web
- Tener movilidad al estilo Trello
- Guardar esos datos en el storage del navegador para implementar un backend persistente dentro del navegador del usuario
Cómo queda al final? Tienes la herramienta en dafo
Vamos allá
Recordando react-beautiful-dnd
Tienes el curso de la librería aquí, necesario para explorar cómo funciona esa librería
Aquí la estructura de datos que voy a utilizar aquí tendrá esta forma
const data = [
{
columnId: 'd',
text: 'Debilidades',
ref: 0,
items: [
{
id: 'elemento-1',
idNum: 1,
text: 'Debilidad 1',
},
],
},
{
columnId: 'a',
text: 'Amenazas',
ref: 1,
items: [
{
id: 'elemento-2',
idNum: 2,
text: 'Amenaza 1',
},
],
},
{
columnId: 'f',
text: 'Fortalezas',
ref: 2,
items: [
{
id: 'elemento-3',
idNum: 3,
text: 'Fortaleza 1',
},
],
},
{
columnId: 'o',
text: 'Oportunidades',
ref: 3,
items: [
{
id: 'elemento-4',
idNum: 4,
text: 'Oportunidad 1',
},
],
},
]
Esto es, un array con 4 columnas fijas definidas por un objeto, y cada objeto cuenta con columnId
, text
, ref
y items
columnId
es un identificador y tiene que ser únicotext
es el texto que nos saldrá en cada columna (si queremos)ref
es el lugar donde almacenaremos las referencias de cada columna (conuseRef
), pero que aquí sólo almacenan un índiceitems
es un array con cada ítem que tendremos en cada columna, al que definiremos conid
,idNum
ytext
Te podrías preguntar porque en la variable ref
no asignamos ya un useRef
en lugar de un número
El motivo es que esta estructura la almacenaremos en localStorage
, y allí no podemos almacenar funciones (que es lo que es un useRef
)
La solución? Almacenar el índice que nos servirá de baliza para localizar la referencia real
Otra solución sería crear otra estructura para almacenar las referencias, o simplemente borrar esa propiedad antes de almacenarlo en localStorage
(no necesitamos almacenarla en absoluto)
Luego necesitamos volcar esta estructura, y lo hacemos con algo tipo esto
<DragDropContext onDragEnd={onDragEnd}>
<div>
{data.map((el, i) => (
<div key={`colum${i}`}>
<div>{el.text}</div>
<Droppable droppableId={el.columnId} key={el.columnId}>
{(provided, snapshot) => (
<div ref={provided.innerRef}>
{el.items.map((it, i) => (
<Draggable draggableId={it.id} index={i} key={it.id}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
{it.text}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
))}
</div>
</DragDropContext>
Si te fijas aquí no aparece por ningún lado idNum
Para qué me sirve? Es un análogo a id
, pero mientras id
es un texto con un número, idNum
almacena sólo el número
Lo que yo querría es sólo el número
Por qué? Porque cada vez que se añada algún elemento en esta estructura tendré que asignarle un nuevo identificador, y para asegurarme que no repito buscaré el índice más alto y le sumaré 1
Si no tengo un número y tengo un texto tendré que procesar el texto para extraer un número, por lo que si ya tengo un número -> perfecto
Pero y para qué necesito un string
? Porque nos lo piden en la documentación de la librería documentación
Como antes, podría fabricar ese string sobre la marcha, pero si ya lo tengo en una variable todo eso que me ahorro (y me sirve de recordatorio de este requerimiento)
Luego necesitamos establecer las funciones que reordenan los ítems
const reorder = (list, startColumn, endColumn, startIndex, endIndex) => {
const newList = Array.from(list)
const startColumnIndex = newList.findIndex(item => item.columnId === startColumn)
const endColumnIndex = newList.findIndex(item => item.columnId === endColumn)
const [removed] = newList[startColumnIndex].items.splice(startIndex, 1)
newList[endColumnIndex].items.splice(endIndex, 0, removed)
return newList
}
Esto ya lo vimos en el curso que enlazo arriba, básicamente localizamos el panel (el de origen y el de llegada) y le quitamos / añadimos el ítem al panel correspondiente
Luego necesitamos definir la función que llama a la anterior en cuanto soltamos el ítem
const onDragEnd = result => {
const { destination, source, draggableId } = result
if (!destination) return
if (destination.droppableId === source.droppableId && destination.index === source.index) return
const new_elements = reorder(dafo, source.droppableId, destination.droppableId, source.index, destination.index)
setDafo(new_elements)
}
Y ya lo tenemos
useLocalStorage
Este custom hook nos permitirá guardar el estado, algo que normalmente haríamos con useState
, pero que ahora lo haremos en el storage del navegador por lo que a cada nueva sesión seremos capaces de recuperar lo que teníamos en la última sesión
Por qué crear un custom hook?
Porque así podré utilizarlo del mismo modo que utilizo useState
, fantástico
El código se basa ampliamente en los varios usuarios que han hecho implementaciones muy similares entre ellas, por ejemplo:
Éste es el código
import { useState } from 'react'
// definimos la función para recibir una key y un initialValue
export const useLocalStorage = (key, initialValue) => {
// generamos un estado donde el valor inicial es una función
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.log(error)
return initialValue
}
})
// definimos una función para asignar valor
const setValue = value => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.log(error)
}
}
// devolvemos el valor y la función para asignar valor, lo mismo que nos devuelve useState()
return [storedValue, setValue]
}
El valor inicial que le damos al useState
se basa en window.localStorage
(documentación oficial)
Y en la función para asignar un nuevo valor setValue
, también tenemos la opción de pasarle una función en lugar de un valor, y si es el caso ejecutamos la función con el valor almacenado (algo que aquí no utilizaremos, pero da más versatilidad al hook)
const valueToStore = value instanceof Function ? value(storedValue) : value
Para almacenar una estructura de datos utilizamos JSON.stringify
, que nos convierte los objetos en strings
, y luego JSON.parse
que nos revierte la conversión
El quid de todo esto es que cambiamos en la misma operación el estado useState
, que nos repinta la aplicación, con el valor de localStorage
, que no nos repinta nada
Y para utilizar el hook, pues como con el useState
const [dafo, setDafo] = useLocalStorage('kwdafo', data))
Es decir, le mandamos una clave de nombre kwdafo
y el valor con el que queremos inicializar ese estado, que será con la variable data
El código
Ya podemos pasar al código (con cada sección descrita en los comentarios)
import React, { useRef, useState } from 'react'
import styled from 'styled-components'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
/**
* Custom hook para sustituir el useState para utilizar localStorage
*/
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.log(error)
return initialValue
}
})
const setValue = value => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.log(error)
}
}
return [storedValue, setValue]
}
/**
* La estructura de datos de los paneles
* Esta estructura la modificamos y luego la transformamos a HTML
*/
const data = [
{
columnId: 'd',
text: 'Debilidades',
ref: 0,
items: [
{
id: 'elemento-1',
idNum: 1,
text: 'Debilidad 1',
},
],
},
{
columnId: 'a',
text: 'Amenazas',
ref: 1,
items: [
{
id: 'elemento-2',
idNum: 2,
text: 'Amenaza 1',
},
],
},
{
columnId: 'f',
text: 'Fortalezas',
ref: 2,
items: [
{
id: 'elemento-3',
idNum: 3,
text: 'Fortaleza 1',
},
],
},
{
columnId: 'o',
text: 'Oportunidades',
ref: 3,
items: [
{
id: 'elemento-4',
idNum: 4,
text: 'Oportunidad 1',
},
],
},
]
/**
* Función para reordenar la estructura de datos
*/
const reorder = (list, startColumn, endColumn, startIndex, endIndex) => {
const newList = Array.from(list)
const startColumnIndex = newList.findIndex(item => item.columnId === startColumn)
const endColumnIndex = newList.findIndex(item => item.columnId === endColumn)
const [removed] = newList[startColumnIndex].items.splice(startIndex, 1)
newList[endColumnIndex].items.splice(endIndex, 0, removed)
return newList
}
/**
* Elemento principal
*/
export const Tool = () => {
// 4 referencias para los 4 paneles del DAFO - los 4 inputs
const refs = [useRef(), useRef(), useRef(), useRef()]
// Para guardar la estructura de datos
const [dafo, setDafo] = useLocalStorage('kwdafo', JSON.parse(JSON.stringify(data)))
// Función para resetear la estructura de datos
const resetLocalStorage = () => setDafo(JSON.parse(JSON.stringify()))
// Función que se ejecuta al dejar los elementos y que llama la función anterior de reorder
const onDragEnd = result => {
const { destination, source, draggableId } = result
if (!destination) return
if (destination.droppableId === source.droppableId && destination.index === source.index) return
const new_elements = reorder(dafo, source.droppableId, destination.droppableId, source.index, destination.index)
setDafo(new_elements)
}
// función para añadir un elemento
const addElement = panel => {
// generamos el que será el índice para el nuevo elemento
const highestIndex = Math.max.apply(null, dafo.map(el => el.items.map(it => it.idNum)).flat()) + 1
// encontramos la columna
const index = dafo.findIndex(el => el.columnId === panel.columnId)
// copiamos la estructura anterior para no cambiarla
const newDafo = [...dafo]
// añadimos a la nueva estructura el nuevo elemento
newDafo[index].items = [
{ id: 'elemento-' + highestIndex, idNum: highestIndex, text: refs[panel.ref].current.value },
...dafo[index].items,
]
// guardamos la nueva estructura
setDafo(newDafo)
// Borramos el valor del input
refs[panel.ref].current.value = ''
}
// función para borrar un elemento
const removeElement = el => {
// encontramos el índice del elemento a quitar
const index = dafo.findIndex(elem => elem.items.findIndex(it => it.id === el.id) !== -1)
// copiamos la estructura
const newDafo = [...dafo]
// volvemos a copiar los elementos filtrando el que queremos remover (una estrategia para quitar el elemento)
newDafo[index].items = dafo[index].items.filter(it => it.id !== el.id)
// volvemos a asignar la estructura al estado
setDafo(newDafo)
}
// volcamos el contenido a html siguiendo las instrucciones de la librería para el drag-n-drop
return (
<>
<Reset onClick={resetLocalStorage}>Resetear</Reset>
<DragDropContext onDragEnd={onDragEnd}>
<Grid>
{dafo.map((el, i) => (
<div key={`colum${i}`}>
<div>{el.text}</div>
<Droppable droppableId={el.columnId} key={el.columnId}>
{(provided, snapshot) => (
<div ref={provided.innerRef}>
{el.items.map((el, i) => (
<Draggable draggableId={el.id} index={i} key={el.id}>
{(provided, snapshot) => (
<Item ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
{el.text}
<span onClick={() => removeElement(el)}>❌</span>
</Item>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
<div>{/*expand*/}</div>
<div>
<TextArea ref={refs[el.ref]} /> <Button onClick={() => addElement(el)}>Añadir</Button>
</div>
</div>
))}
</Grid>
</DragDropContext>
</>
)
}
// necesitamos exportar un default para que nos funcione el codesandbox
export default Tool
/**
* Y aquí ya vienen todos los estilos con styled-components
*/
const Title = styled.div`
font-weight: 700;
text-transform: uppercase;
font-size: 1.5em;
`
const Item = styled.div`
text-transform: uppercase;
background: #eecaca;
border-radius: 8px;
padding: 10px;
margin: 10px 0px;
`
const TextArea = styled.textarea`
background: #f1efef;
border: unset;
border-radius: 8px;
width: -webkit-fill-available;
margin: 10px 0px;
resize: none;
height: 38px;
`
const Button = styled.button`
border-radius: 8px;
height: 40px;
font-weight: 700;
text-transform: uppercase;
background: #ff0000b0;
mix-blend-mode: luminosity;
color: #fff;
cursor: pointer;
border: unset;
transition: 0.2s ease;
&:hover {
background: #000;
}
`
const Grid = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 10px;
`
const Reset = styled.div`
cursor: pointer;
border-radius: 8px;
height: 100%;
font-weight: 700;
text-transform: uppercase;
background: #ff0000b0;
mix-blend-mode: luminosity;
color: #fff;
transition: 0.2s ease;
width: fit-content;
padding: 10px;
margin-bottom: 20px;
&:hover {
background: #000;
}
`
🙋♂️