Manejo de entrada de datos en Ionic: Uso de formularios

Manejo de entrada de datos en Ionic: Uso de formularios

Objetivos de la lección

Durante el siguiente módulo nos enfocaremos en el trabajo de control de estados mediante React. Para ello vamos a capturar datos mediante campos de entrada, y con ellos vamos a controlar dichos estados.

¿Cómo validar datos con Ionic?

Hasta este punto podemos crear nuevos cursos agregando un título y una fecha de inicio. Este se puede ver dentro del componente AgregarCurso.

<IonGrid>
    <IonRow>
        <IonCol>
            <IonItem>
                <IonLabel position="floating">Titulo del curso</IonLabel>
                <IonInput type="text" />
            </IonItem>
        </IonCol>
    </IonRow>
    <IonRow>
        <IonCol>
            <IonItem>
                <IonLabel position="floating">Fecha</IonLabel>
                <IonDatetime displayFormat="MM DD YY" />
            </IonItem>
        </IonCol>
    </IonRow>
    <IonRow>
        <IonCol>
            <IonButton fill="clear" onClick={props.onClickCancelar}>
                Cancelar
            </IonButton>
        </IonCol>
        <IonCol>
            <IonButton color="primary">Guardar</IonButton>
        </IonCol>
    </IonRow>
</IonGrid>

Sin embargo si deseamos cerciorarnos que los valores enviados desde el formularios tengan un valor asignado, requerimos realizar una validación de dichos valores.

Importamos los hooks useRef y useState del paquete React, y agregamos las referencias a los campos así como un estado error tipo string para el control de errores.

const [error, setError] = useState<string>("");

const tituloRef = useRef<HTMLIonInputElement>(null);
const fechaRef = useRef<HTMLIonDatetimeElement>(null);

Adicionalmente tenemos que agregar los controles ref a cada campo.

<IonGrid>
    <IonRow>
        <IonCol>
            <IonItem>
                <IonLabel position="floating">Titulo del curso</IonLabel>
                <IonInput type="text" ref={tituloCapturado} />
            </IonItem>
        </IonCol>
    </IonRow>
    <IonRow>
        <IonCol>
            <IonItem>
                <IonLabel position="floating">Fecha</IonLabel>
                <IonDatetime displayFormat="MM DD YY" ref={fechaCapturada} />
            </IonItem>
        </IonCol>
    </IonRow>
    <IonRow>
        <IonCol>
            <IonButton fill="clear" onClick={props.onClickCancelar}>
                Cancelar
            </IonButton>
        </IonCol>
        <IonCol>
            <IonButton color="primary">Guardar</IonButton>
        </IonCol>
    </IonRow>
</IonGrid>

Una ves completado lo anterior, requerimos también agregan una función que guarde controle el proceso de la validación.

const onClickGuardar = () => {
    const tituloCapturado = tituloRef.current!.value;
    const fechaCapturada = fechaRef.current!.value;
    if (
        !tituloCapturado ||
        !fechaCapturada ||
        tituloCapturado.toString().trim().length == 0 ||
        fechaCapturada.trim().length == 0
    ) {
        setError("Ingresa un título y una fecha valida");
        return;
    }
    setError("");
};

De esta forma cuando alguno de los dos campos sea invalido, el control de estado error mostrará un mensaje.

Agregamos este mensaje justo debajo de los campos de entrada.

{
    error && (
        <IonRow class="ion-text-center">
            <IonCol>
                <IonText color="danger">
                    <p>{error}</p>
                </IonText>
            </IonCol>
        </IonRow>
    );
}

Finalmente para disparar el evento enlazamos la acción al botón Guardar.

<IonButton color="primary" onClick={onClickGuardar}>
    Guardar
</IonButton>

¿Cómo enviar datos al modal en Ionic?

Creamos un contenedor para el context en React src/data/cursos-context.ts., un contenedor nos permite definir estados en un nivel superior de la jerarquía de los componentes.

import React from "react";

export interface Objetivo {
    id: string;
    text: string;
}

export interface Curso {
    id: string;
    titulo: string;
    fecha: Date;
    objetivos: Objetivo[];
}

export interface Context {
    cursos: Curso[];
    agregarCurso: (titulo: string, fecha: Date) => void;
    agregarObjetivo: () => void;
    borrarObjetivo: () => void;
    actualizarObjetivo: () => void;
}

const CursosContext = React.createContext<Context>({
    cursos: [],
    agregarCurso: () => {},
    agregarObjetivo: () => {},
    borrarObjetivo: () => {},
    actualizarObjetivo: () => {},
});

export default CursosContext;

Nuestro contenedor consta de…

  • Las interfaces Objetivo y Curso que forman parte de la estructura de datos de la interface Context.
  • La interface Context tiene ademas un arreglo llamado cursos que almacena objetos tipo Curso.
  • Los métodos para el control de los estados.

Ahora para implementar el Context que acabamos de crear requerimos de un ContentProvider este se va a encargar de controlar los estados.

import React, { useState } from "react";
import CursosContext, { Curso } from "./cursos-context";

const CursosContextProvider: React.FC = (props) => {
    const [cursos, setCursos] = useState<Curso[]>([]);
    const agregarCurso = (titulo: string, fecha: Date) => {
        const nuevoCurso: Curso = {
            id: Math.random().toString(),
            titulo: titulo,
            fecha: fecha,
            objetivos: [],
        };
        setCursos((estadoActual) => {
            return estadoActual.concat(nuevoCurso);
        });
    };
    const agregarObjetivo = () => {};
    const borrarObjetivo = () => {};
    const actualizarObjetivo = () => {};

    return (
        <CursosContext.Provider
            value={{
                cursos,
                agregarCurso,
                agregarObjetivo,
                borrarObjetivo,
                actualizarObjetivo,
            }}
        >
            {props.children}
        </CursosContext.Provider>
    );
};

export default CursosContextProvider;

Para ello implementamos el CursosContextProvider dentro de la parte superior de nuestra aplicación, en este caso de App.

import CursosContextProvider from "./data/CursosContextProvider";

const App: React.FC = () => (
    <IonApp>
        <IonReactRouter>
            ...
            <IonTabs>
                <IonRouterOutlet id="main">
                    <CursosContextProvider>
                        <Route path="/cursos" exact>
                            <Cursos />
                        </Route>
                        <Route path="/objetivos" exact>
                            <Objetivos />
                        </Route>
                        <Route path="/filtrar" exact>
                            <Filtrado />
                        </Route>
                        <Route path="/curso/:id">
                            <Objetivo />
                        </Route>
                        <Redirect to="/cursos" />
                    </CursosContextProvider>
                </IonRouterOutlet>
                <IonTabBar slot="bottom">
                    <IonTabButton tab="a" href="/objetivos">
                        <IonLabel>Objetivos</IonLabel>
                    </IonTabButton>
                    <IonTabButton tab="b" href="/cursos">
                        <IonLabel>Cursos</IonLabel>
                    </IonTabButton>
                </IonTabBar>
            </IonTabs>
        </IonReactRouter>
    </IonApp>
);

Actualizamos el módulo AgregarCurso para hacer uso del CursosContext.

import React, { useRef, useState } from "react";
...

const AgregarCurso: React.FC<{
  esVisible: boolean;
  onClickCancelar: () => void;
  onGuardar: (titulo: string, fecha: Date) => void;
}> = (props) => {
  const [error, setError] = useState<string>("");
  const tituloRef = useRef<HTMLIonInputElement>(null);
  const fechaRef = useRef<HTMLIonDatetimeElement>(null);

  const onClickGuardar = () => {
    setError("");
    const tituloCapturado = tituloRef.current!.value;
    const fechaCapturada = fechaRef.current!.value;
    if (
      !tituloCapturado ||
      !fechaCapturada ||
      tituloCapturado.toString().trim().length === 0 ||
      fechaCapturada.toString().trim().length === 0
    ) {
      setError("Ingresa un título y una fecha valida");
      return;
    }
    props.onGuardar(
      tituloCapturado.toString().trim(),
      new Date(fechaCapturada)
    );
  };

...

Al igual que implementamos el context dentro de Cursos.

import React, { useContext, useState } from "react";

...

import CursosContext from "../data/cursos-context";

...

const Cursos: React.FC = () => {

  const [agregarCursoOverlayVisible, setAgregarCursoOverlayVisible] = useState<boolean>(false);
  const cursosCtx = useContext(CursosContext);

  const onClickAgregarCurso = () => {
    setAgregarCursoOverlayVisible(true);
  };

  const onCancelarAgregarCurso = () => {
    setAgregarCursoOverlayVisible(false);
  };

  const onGuardarCurso = (titulo: string, fecha: Date) => {
    setAgregarCursoOverlayVisible(false);
    cursosCtx.agregarCurso(titulo, fecha);
  };

  return (
    <React.Fragment>
      <AgregarCurso
        esVisible={agregarCursoOverlayVisible}
        onClickCancelar={onCancelarAgregarCurso}
        onGuardar={onGuardarCurso}
      />
      <IonPage>
        ...
        <IonContent>
          <IonGrid>
            {cursosCtx.cursos.map((curso) => {
              return (
                <IonRow key={curso.id}>
                  <IonCol>
                    <IonCard>
                      <IonCardHeader>
                        <IonCardTitle>{curso.titulo}</IonCardTitle>
                        <IonCardSubtitle>{curso.fecha.toTimeString()}</IonCardSubtitle>
                      </IonCardHeader>
                      <IonCardContent>
                        <div className="ion-text-right">
                          <IonButton
                            fill="clear"
                            color="secondary"
                            routerLink={`/curso/${curso.id}`}
                          >
                            Ver Información
                          </IonButton>
                        </div>
                      </IonCardContent>
                    </IonCard>
                  </IonCol>
                </IonRow>
              );
            })}
          </IonGrid>
          ...
        </IonContent>
      </IonPage>
    </React.Fragment>
  );
};

export default Cursos;