4 Luty 2023 - przez: Karolina Kozubik

Niestandardowy input w Vue okiem początkującego

Jestem początkującą programistką, interesuję się głównie backendem. Mówi się, że najlepszym sposobem nauki jest wykonanie projektu. Tak więc od miesiąca w wolnym czasie rozwijam aplikację webową, prosty szyfrator. Jego zadaniem jest przetworzenie przekazanego mu tekstu i zwrócenie zaszyfrowanej wiadomości. Używa prostych szyfrów: Cezara, zamiany… Mam w planach dodać jeszcze kilka. Jednak w międzyczasie naszła mnie ochota na wykonanie frontu do tego projektu. Wybrałam Vue, przeszłam kilka poradników, i strona startowa zaczęła wyglądać całkiem znośnie:

Encryptor - strona główna

Udało mi się wszystko ładnie połączyć, strona prawidłowo komunikuje z API, wszystko działa jak powinno. Ale… Brakuje dość ważnej rzeczy: danych na temat szyfru.

O co chodzi?

Jak działają używane przeze mnie szyfry?

Szyfr Cezara jest dość popularny. Polega na utworzeniu nowego alfabetu poprzez przesunięcie ciągu znków o kilka pozycji i zamianie każdej litery w szyfrowanej wiadomości na odpowiadającą jej nową literę.

Przykładowo, tak wygląda klucz do szyfrowania utworzony po przesunięciu o trzy pozycje:

Szyfr Cezara

W tym przypadku zaszyfrowana wiadomość “Oh My Dev” to: Le Jv Abs

Zamiana to kolejny zaimplementowany przeze mnie szyfr. Jest nieco mniej znany, ale jego działanie jest równie proste. Wymaga klucza, który jest zestawem par niepowtarzających się liter, np. ga-de-ry-po-lu-ki. Szyfrowanie polega na zamianie każdej litery w wiadomości na literę, z którą występuje w parze. Jeśli litera nie znajduje się w kluczu, jest pozostawiana bez zmian.

Zaszyfrowana w ten sposób nazwa “Oh My Dev” (klucz: ga-de-ry-po-lu-ki) wygląda tak: Ph Mr Edv

W czym tu problem?

Moja strona radzi sobie z przesłaniem wiadomości, a nawet rodzajem szyfru do użycia. Te dane nie wystarczą jednak do przeprowadzenia operacji. Szyfr Cezara wymaga liczby oznaczającej przesunięcie względem alfabetu. Zamiana nie zadziała bez klucza w postaci par liter. Tak więc front mojej aplikacji musi dostarczyć sposobu na przekazanie tych informacji. W tym momencie napotkałam problem: każdy z szyfrów wymaga innego zestawu parametrów, a korzystają one z różnego rodzaju danych. Tak więc strona powinna generować różne pola w zależności od rodzaju wybranego szyfru.

Zabrałam się więc do roboty

W pędzie kodowania zaczęłam wymyślać i od razu wprowadzać w życie różne pomysły, po chwili je zmieniając i odrzucając coraz to dziwniejsze rozwiązania. Zmęczona nieustającym niepowodzeniem odłożyłam sprawę na później i wyłączyłam komputer. Po kilku godzinach zajmowania się innymi sprawami usiadłam z powrotem do problemu. Tym razem przed kartką i z długopisem w ręce, nawet nie włączając komputera. Po rozpisaniu wszystkiego przed sobą rozwiązanie przyszło od razu: potrzebuję komponentu zawierającego input, ale zdolnego do określania jego typu. Zapewne każdy bardziej doświadczony programista wpadłby na coś takiego bez zastanowienia :D

W każdym razie następnego dnia usiadłam do kodu z nowymi siłami i zaczęłam rozwiązywać mój problem.

Moje rozwiązanie

Używam Vue 3 z Composition API, TypeScript

Po pierwsze: generowanie pola input

Dzięki przekazaniu danych z komponentu nadrzędnego można w prosty sposób określić parametry elementu input:

<!-- CustomInput.vue -->
<script setup lang="ts">
import { capitalize, computed } from "vue";

const props = defineProps<{
  inputName: string;
  type: string;
}>();

const label = computed(() => capitalize(props.inputName) + ":");
</script>

<template>
  <label :for="`${inputName}-id`"></label
  ><input :type="type" :id="`${inputName}-id`" :name="inputName" />
</template>

Po drugie: przekazywanie danych z inputu

Vue oferuje dyrektytwę v-model, która odpowiada za wykonanie ciężkiej pracy związanej z połączeniem wartości jakiejś zmiennej z zawartością pola input. Działanie v-model bardzo dobrze opisuje dokumentacja Vue w tych miejscach:

https://vuejs.org/guide/essentials/forms.html

https://vuejs.org/guide/components/events.html#usage-with-v-model

Kierując się wskazówkami z dokumentacji dopisałam kilka kolejnych linijek:

<!-- CustomInput.vue -->
<script setup lang="ts">
import { capitalize, computed } from "vue";

const props = defineProps<{
  inputName: string;
  type: string;
  modelValue: any;
}>();

const emit = defineEmits(["update:modelValue"]);

const label = computed(() => capitalize(props.inputName) + ":");
</script>

<template>
  <label :for="`${inputName}-id`"></label
  ><input
    :type="type"
    :id="`${inputName}-id`"
    :name="inputName"
    :value="modelValue"
    @input="
      $emit('update:modelValue', ($event.target as HTMLInputElement).value)
    "
  />
</template>

Teraz całość już spokojnie działa. Komponentu można używać z różnymi rodzajami danych.

Element input posiada parametr value związany z wartością modelValue. Przy zmianie zawartości pola emitowany jest event update:modelValue. Dzięki temu możemy używać v-model z naszym komponentem:

<!-- script -->

let value = ref(0);

<!-- template -->
<CustomInput type="number" input-name="transformation" v-model="value" />

Vue umożliwia również implementację funkcjonalności v-model za pomocą computed. W moim przypadku wygląda to tak:

<!-- CustomInput.vue -->
<script setup lang="ts">
import { capitalize, computed } from "vue";

const props = defineProps<{
  inputName: string;
  type: string;
  modelValue: any;
}>();

const emit = defineEmits(["update:modelValue"]);
let label = computed(() => capitalize(props.inputName) + ":");

const value = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit("update:modelValue", value);
  },
});
</script>

<template>
  <label :for="`${inputName}-id`"></label
  ><input
    :type="type"
    :id="`${inputName}-id`"
    :name="inputName"
    v-model="value"
  />
</template>

Walidacja danych

Dobrze by było, gdyby komponent mógł sprawdzać dane wprowadzane przez użytkownika i wyświetlać pomocne komunikaty. Moje API zadziała poprawnie tylko jeśli jako parametr do szyfru Cezara zostanie podana liczba. Zaś “zamiana” oczekuje ciągu liter o parzystej długości. Dodając watcher obserwujący wartość pola input można sprawdzać poprawność otrzymanych informacji i w prosty sposób zdecydować, jaki komunikat wyświetlić. Oto przykład sprawdzający, czy pole nie jest puste:

<!-- CustomInput.vue -->
<script setup lang="ts">
import { capitalize, computed, ref, watch } from "vue";

const props = defineProps<{
  inputName: string;
  type: string;
  modelValue: any;
}>();

const emit = defineEmits(["update:modelValue"]);
const errorMsg = ref("");
const label = computed(() => capitalize(props.inputName) + ":");

const value = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit("update:modelValue", value);
  },
});

watch(value, (newValue, oldValue) => {
  if (newValue == "") {
    errorMsg.value = "cannot be empty";
  } else {
    errorMsg.value = "";
  }
});
</script>

<template>
  <label :for="`${inputName}-id`"></label
  ><input
    :type="type"
    :id="`${inputName}-id`"
    :name="inputName"
    v-model="value"
  />
  <div>
    
  </div>
</template>

Możemy pójść o krok dalej i przekazywać funkcję validate z komponentu nadrzędnego:

<!-- CustomInput.vue -->
<script setup lang="ts">
import { capitalize, computed, ref, watch } from "vue";

const props = defineProps<{
  inputName: string;
  type: string;
  modelValue: any;
  validate?: Function;
}>();

const emit = defineEmits(["update:modelValue"]);
const errorMsg = ref("");
const label = computed(() => capitalize(props.inputName) + ":");

const value = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit("update:modelValue", value);
  },
});

watch(value, (newValue) => {
  errorMsg.value = props.validate ? props.validate(newValue) : "";
});
</script>

<template>
  <label :for="`${inputName}-id`"></label
  ><input
    :type="type"
    :id="`${inputName}-id`"
    :name="inputName"
    v-model="value"
  />
  <div>
    
  </div>
</template>

<!-- Parent Component -->
<script setup lang="ts">
import { ref } from "vue";
import CustomInput from "./CustomInput.vue";

const value = ref(0);

function validateNotZero(inputValue: number) {
  return inputValue == 0 ? "cannot be 0" : "";
}
</script>

<template>
  <CustomInput
    type="number"
    input-name="transformation"
    v-model="value"
    :validate="validateNotZero"
  />
</template>

Podsumowanie

W taki sposób stworzyłam komponent, który rozwiązuje mój problem. Po dodaniu stylowania tak prezentuje się efekt końcowy:

Encryptor - Gotowy niestandardowy input

Mój komponent wykorzystam w projekcie dodając do niego walidację dla każdego typu danych, jakich będę porzebować. Mam możliwość dodania kilku pól input i wybrania odpowiedniego typu dla każdego z nich.

Jako że jestem osobą początkującą, będę ogromnie wdzięczna wszystkim, którzy poinformują mnie o błędach w moim kodzie :) Jeśli macie jakieś pomysły jak inaczej można podejść do problemu lub co można usprawnić, podzielcie się swoimi przemyślaniami!

Ostateczny snippet z kodem jest zaprezentowany powyżej, można go również znaleźć na moim Githubie:

https://github.com/karo-fox/vue-custom-input

Jeśli chcesz sprawdzić moje postępy w projekcie, kod można znaleźć tutaj:

https://github.com/karo-fox/encryptor


Powyższy post został opublikowany oryginalnie 20.03.2022 na platforme OhMyDev