# Formulario

# Componentes con v-model

Página oficial v-model (opens new window)

Los eventos también se pueden usar para crear entradas personalizadas que funcionen con v-model

<input v-model="buscarTexto" />

es lo mismo que:

<input
  :value="buscarTexto"
  @input="buscarTexto = $event.target.value"
/>

Cuando se usa en un componente, v-model en su lugar hace esto:

<InputPersonalizado
  :modelValue="buscarTexto"
  @update:modelValue="newValue => buscarTexto = newValue"
/>

Sin embargo, para que esto realmente funcione, el <input> interior del componente debe:

  • Vincular el atributo value a la propiedad modelValue
  • input emite un evento update:modelValue con el nuevo valor
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>
<script setup lang="ts">
const props = defineProps<{
    modelValue: any;
  }>(),

const emit = defineEmits<{
    // nombre del evento, Payload dato
    (event: 'update:modelValue', value: any): void;
}>()
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

Archivo App.vue

<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const message = ref('')

</script>

<template>
  <CustomInput v-model="message" /> {{ message }}
</template>

# BaseInput.vue

<script setup lang="ts">

defineProps({
  label: {
    type: String,
    default: "",
  },
  modelValue: {
    type: [String, Number],
    default: "",
  },
});
</script>

<template>
  <div class="container">
    <div class="mb-2">
      <label class="label">{{ label }}</label>
      <input
        type="text"
        class="input input-bordered input-accent w-full max-w-md"
        :placeholder="label"
        :value="modelValue"
        @input="$emit('update:modelValue', ($event.target as any).value)"
      />
    </div>
  </div>
</template>

# BaseInputDate.vue

<script setup lang="ts">

defineProps({
  label: {
    type: String,
    default: "",
  },
  modelValue: {
    type: [Date, String, Number],
    default: "",
  },
});
</script>

<template>
  <div class="container">
    <div class="mb-3">
      <label class="form-label">{{ label }}</label>
      <input
        type="date"
        class="form-control"
        :placeholder="label"
        :value="modelValue"
        @input="$emit('update:modelValue', ($event.target as any).value)"
      />
    </div>
  </div>
</template>

# BaseTextArea.vue

<script setup lang="ts">

defineProps({
  label: {
    type: String,
    default: "",
  },
  modelValue: {
    type: [Date, String, Number],
    default: "",
  },
});
</script>

<template>
  <div class="container">
    <div class="mb-3">
      <label class="form-label">{{ label }}</label>
      <input
        type="date"
        class="form-control"
        :placeholder="label"
        :value="modelValue"
        @input="$emit('update:modelValue', ($event.target as any).value)"
      />
    </div>
  </div>
</template>

# BaseSelec.vue

<script setup lang="ts">
defineProps({
  opciones: {
    type: Array,
    required: true,
  },
  label: {
    type: String,
    default: "",
  },
  modelValue: {
    type: [String, Number],
    default: "",
  },
});
</script>

<template>
  <div class="container">
    <div class="mb-3">
      <label class="form-label">{{ label }}</label>
      <select
        class="form-select"
        :value="modelValue"
        v-bind="{
          ...$attrs,
          onChange: ($event) => {
            $emit('update:modelValue',($event.target as any).value)
          },
        }"
      >
        <option
          v-for="opcion in opciones"
          :value="opcion"
          :key="opcion"
          :selected="opcion === modelValue"
        >
          {{ opcion }}
        </option>
      </select>
    </div>
  </div>
</template>

# BaseCheckBox.vue

<script setup lang="ts">

defineProps({
    label: {
      type: String,
      default: ''
    },
    modelValue: {
      type: Boolean,
      default: false
    }
  })
</script>

<template>
  <div class="container formulario">
    <div class="mb-3">
      <div class="form-check">
        <input
          class="form-check-input"
          type="checkbox"
          :checked="modelValue"
          @change="$emit('update:modelValue', ($event.target as any).checked)"
        />
        <label 
          class="form-check-label" for="flexCheckDefault"
          v-if="label"
          >
          {{ label }}
        </label>
      </div>
    </div>
  </div>
</template>

# BaseRadioButton.vue

<script setup lang="ts">

defineProps({
  label: {
    type: String,
    default: "",
  },
  modelValue: {
    type: [String, Number],
    default: "",
  },
  value: {
    type: [String, Number],
    required: true,
  },
});
</script>

<template>
  <div class="container formulario">
    <div class="mb-3">
      <div class="form-check">
        <input
          class="form-check-input"
          type="radio"
          :checked="modelValue === value"
          @change="$emit('update:modelValue', value)"
          v-bind="$attrs"
        />
        <label 
          class="form-check-label" 
          for="flexCheckDefault" 
          v-if="label">
          {{ label }}
        </label>
      </div>
    </div>
  </div>
</template>

# Router index.js

{
  path: '/formulario',
  name: 'formulario',
  component: () => import('../views/Formulario.vue')
},

# Vista Formulario.vue

# script

<script setup>
import Navbar from '@/components/Navbar.vue'
import BaseInput from "../components/formulario/BaseInput.vue"
import BaseInputDate from "../components/formulario/BaseInputDate.vue"
import BaseTextArea from "../components/formulario/BaseTextArea.vue"
import BaseSeleccion from "../components/formulario/BaseSeleccion.vue"
import BaseCheckBox from "../components/formulario/BaseCheckBox.vue"
import BaseRadioButton from "../components/formulario/BaseRadioButton.vue"
import { onMounted } from 'vue';
import { memoria } from '../stores/formulario.js'

const datos = memoria()

onMounted(() => {
  datos.obtenerDatos()
})
</script>

# template

<template>
  <div>
    <Navbar/>
  <div class="container my-4">
  <form>
    <BaseInput
      v-model="datos.obra.author"
      type="text"
      label="Autor"
    />
    <BaseInput
      v-model="datos.obra.title"
      type="text"
      label="Título"
    />
    <BaseInputDate
      v-model="datos.obra.date"
      type="date"
      label="Fecha"
    />
    <BaseTextArea
      v-model="datos.obra.synopsis"
      type="text"
      label="Descripción"
    />
    <BaseInput
      v-model="datos.obra.link"
      type="text"
      label="Enlace"
    />
    <BaseInput
      v-model="datos.obra.photo"
      type="text"
      label="Imagen"
    />
    <base-seleccion
    v-model="datos.obra.categoria"
    :opciones="datos.categorias"
    label="Selecciona una categoría"
  />
  <base-check-box
    v-model="datos.obra.pelicula" 
    label="Película"
    />
  <base-check-box
    v-model="datos.obra.comic" 
    label="Cómic"
    />
  <base-radio-button
  v-model="datos.obra.editorial"
  :value="0"
  label="DC Cómic"
  />
  <base-radio-button
  v-model="datos.obra.editorial"
  :value="1"
  label="Marvel"
  />
    <div class="input-group my-3">
      <input type="file" @change="datos.buscarImagen($event)">
    </div>

      <div class="mt-3">  
    <button v-show="datos.editar === true" 
      @click.prevent="datos.actualizarDato(id)" 
      class="btn btn-primary">
      Actualizar
    </button>
    <button v-show="datos.editar === false" 
      @click.prevent="datos.agregarDato()" 
      class="btn btn-primary">
      Guardar
    </button>
    <div class="mt-4">
      <img :src="datos.datoImagen">
    </div>

    </div>
  </form>
  </div>
<!-- ////////// tabla ////////// -->
  <table class="table">
    <thead>
      <tr>
        <th scope="col">id</th>
        <th scope="col">Author</th>
        <th scope="col">Fecha</th>
        <th scope="col">Editar</th>
        <th scope="col">Eliminar</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in datos.obras" :key="index">
        <th scope="row">{{index}}</th>
        <td>{{item.author}}</td>
        <td>{{item.date}}</td> 
        <td>
          <button @click.prevent="datos.obtenerDatoID( item.id );this.datos.editar = !this.datos.editar;" 
            class="btn btn-primary">Editar
          </button>
        </td>

        <td>
          <button @click.prevent="datos.eliminarDato(item.id)" 
            class="btn btn-danger">Eliminar
          </button>
      </td>
      </tr>
    </tbody>
  </table>
  </div>
</template>

# Stores Formulario.vue

import { defineStore } from "pinia";
import { collection, getDocs, addDoc, deleteDoc, doc, getDoc, updateDoc  } from 'firebase/firestore';
import { db, storage } from "../firebase";
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
import { useRoute } from 'vue-router'

const router = useRoute()

export const memoria = defineStore({
  id: "principal",
  state: () => ({
    file: null,
    datoImagen: null,
    error: null,
    editar: false,
    loading: false,
    urlDescarga: '',

    categorias: [
      'Los Vengadores',
      'Los Cuatro Fantásticos',
      'Guardianes de la Galaxia',
      'Superhéroe'
      ],

    obras: [],
    obra: {
    id: '',
    title: '',
    author: '',
    date: '',
    synopsis: '',
    link: '',
    photo: '',
    editorial: 0,
    pelicula: false,
    comic: false,    
  },
  }),
  actions: {
    async obtenerDatos() {
      this.obras = [];
      const querySnapshot = await getDocs(collection(db, "obras"));
      querySnapshot.forEach((doc) => {
        let obra = doc.data();
        obra.id = doc.id;
        this.obras.push(obra);
        console.log(obra);
      });
    },
    // DELETE / ELIMINAR / BORRAR
    async eliminarDato(id) {
      await deleteDoc(doc(db, "obras", id));
      router.go("/");
    },
    // GET BY ID / OBTENER POR ID
    async obtenerDatoID(id) {
      const docRef = doc(db, "obras", id);
      const docSnap = await getDoc(docRef);
      if (docSnap.exists()) {
        this.obra = docSnap.data();
        this.obra.id = docSnap.id;
      } else {
        console.log("¡No existe el documento!");
      }
    },

    // BUSCAR IMAGEN
    buscarImagen(event) {
      const tipoArchivo = event.target.files[0].type;
      if (tipoArchivo === "image/jpeg" || tipoArchivo === "image/png") {
        this.file = event.target.files[0];
        this.error = null;
      } else {
        this.error = "Archivo no válido";
        this.file = null;
        return;
      }
      const reader = new FileReader();
      reader.readAsDataURL(this.file);
      reader.onload = (e) => {
        this.datoImagen = e.target.result;
      };
    },
    // SUBIR IMAGEN STORAGE
    async agregarDato() {
      try {
        this.loading = true;
        const storageRef = ref(storage, "imagenes/" + this.file.name);
        const uploadTask = uploadBytesResumable(storageRef, this.file);
        uploadTask.on(
          "state_changed",
          (snapshot) => {
            const progress =
              (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
            console.log("Upload is " + progress + "% done");
            switch (snapshot.state) {
              case "paused":
                console.log("Upload is paused");
                break;
              case "running":
                console.log("Upload is running");
                break;
            }
          },
          (error) => {},
          () => {
            getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
              console.log("File available at", downloadURL);
            });
          }
        );

        const urlDescarga = await getDownloadURL(storageRef);

        await addDoc(collection(db, "obras"), {
          title: this.obra.title,
          author: this.obra.author,
          date: this.obra.date,
          synopsis: this.obra.synopsis,
          link: this.obra.link,
          editorial: this.obra.editorial,
          pelicula: this.obra.pelicula,
          comic: this.obra.comic,
          photo: urlDescarga,
        });
        this.error = "Imagen subida con éxito";
        this.file = null;
      } catch (error) {
        console.log(error);
      } finally {
        const router = useRoute()
        router.push("/");
        this.loading = false;
      }
    },

    // MÉTODO actualizarDato
    async actualizarDato() {
      try {
        this.loading = true;
        const storageRef = ref(storage, "imagenes/" + this.file.name);
        const uploadTask = uploadBytesResumable(storageRef, this.file);
        uploadTask.on(
          "state_changed",
          (snapshot) => {
            const progress =
              (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
            console.log("Upload is " + progress + "% done");
            switch (snapshot.state) {
              case "paused":
                console.log("Upload is paused");
                break;
              case "running":
                console.log("Upload is running");
                break;
            }
          },
          (error) => {},
          () => {
            getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
              console.log("File available at", downloadURL);
            });
          }
        );

        const urlDescarga = await getDownloadURL(storageRef);

        const elemento = doc(db, "obras", this.obra.id);
        await updateDoc(elemento, {
          title: this.obra.title,
          author: this.obra.author,
          date: this.obra.date,
          synopsis: this.obra.synopsis,
          link: this.obra.link,
          photo: urlDescarga,
          editorial: this.obra.editorial,
          pelicula: this.obra.pelicula,
          comic: this.obra.comic,
        });
        this.error = "Imagen subida con éxito";
        this.file = null;
      } catch (error) {
        console.log(error);
      } finally {
        router.push("/");
        this.loading = false;
      }
    },
  },
});