hand Inicio
hand JSBloqs
hand GutenBloqs
Wallpaper

Programar una herramienta DAFO con React

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

js
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 único
  • text es el texto que nos saldrá en cada columna (si queremos)
  • ref es el lugar donde almacenaremos las referencias de cada columna (con useRef), pero que aquí sólo almacenan un índice
  • items es un array con cada ítem que tendremos en cada columna, al que definiremos con id, idNum y text

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

js
<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

js
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

js
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

js
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)

js
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

js
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)

jsx
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;
}
`

🙋‍♂️

draw of me

Hola, tienes en mente desarrollar una web?

Si quieres, te ayudo

11ty para webs ultra-rápidaseleventy js
Gatsby para webs complejasgatsby js
WordPress para webs para el usuario finalwordpress

Escríbeme

Lista de correo: escribo algo de tanto en cuanto