Daj Się Poznać #10 Walidacja formularzy, losowanie produktów


Konkurs Daj Się Poznać zbliża się ku końcowi i jest to 10 post tej serii, co stanowi minimum wymagane przez regulamin. Stworzyliśmy aplikację Mistrz Makro w AngularJS, służącą do sprawdzenia rozbieżności w liczeniu kalorii "na oko". Pozostało nam tylko dopracować detale aplikacji, takie jak walidacja formularzy, czy losowa sekwencja pytań w quizie. Zaczynajmy!

Jeśli pobrałeś już moje repozytorium z Githuba, co opisałem w poprzednim poście to możesz teraz przeskoczyć do kolejnego commita, który pokaże Ci kod z tego posta:
$ git checkout d4cf597e4ca7177a2a4b5fa6a5d230993c7be14b

# Walidacja formularzy

Jednym z detali, który został nam na koniec, jest walidacja formularzy. Jednym z założeń jest pozostawienie użytkownikowi dowolności, co do wypełnionych pól. Jednak żeby aplikacja miała sens, musimy ograniczyć tą dowolność, do przynajmniej jednego pola.

Powinniśmy upewnić się, że użytkownik poda nam wartość, która jest typu numeric. Trzeba będzie poinformować Angulara, że chodzi nam tylko o wartości całkowite.

Kolejnym ograniczeniem powinna być liczba większa niż zero, ponieważ zakładamy że każde pole będzie miało jakąś wartość.

Na sam początek, zacznijmy od określenia typu inputa na taki, który pozwoli nam wprowadzać tam tylko liczby

 
<p>Kcal:</p><input type="number" ng-model="c.macro1">

Następnie ustalmy najmniejszą wartość jaką użytkownik może wprowadzić, poprzez atrybut min:

 
<p>Kcal:</p><input type="number" ng-model="c.macro1" min="0">

Sprawdzając później w Angularze czy nasz formularz jest poprawny, otrzymamy błędny wynik, ponieważ angular zignoruje ograniczenie do liczb całkowitych. W praktyce HTML5 wyświetlił by komunikat o błędzie, jednak angular określiłby formularz jako valid. Musimy więc zastosować RegExp, który określi że w inpucie mogą pojawić się tylko liczby całkowite:

 
<p>Kcal:</p><input type="number" ng-model="c.macro1" min="0" ng-pattern="/^(0|[1-9][0-9]*)$/">

Krótko objaśniając jak działa ten RegExp - / / dwa slashe oznaczają początek i koniec wyrażenia regularnego RegExp. ^ oznacza początek stringa. W nawiasie mamy naszą wartość, czyli 0 lub (oznaczone pionową kreską |) wartość zaczynającą się od jedynki, a następnie dowolnej ilości cyfr od 0-9, co oznaczone jest gwiazdką *. Dolar $ oznacza zakończenie stringa.

Musimy pamiętać jednak o jeszcze jednej rzeczy, a mianowicie o wypełnionym przynajmniej jednym polu.

 
<p>Kcal:</p><input type="number" ng-model="c.macro1" min="0" ng-pattern="/^(0|[1-9][0-9]*)$/" ng-required='!(c.macro2 ||  c.macro3 || c.macro4)'>

Dodajemy atrybut AngularJS ng-required, który dodaje atrybut required, zależnie od spełnionego warunku. W naszym przypadku required dodaje się jeśli nie jest wypełniony żaden inny input. W poprzednich przypadkach, każdy input będzie miał te same atrybuty, poza ng-model. Jednak ng-required nadamy tylko jednemu z nich, w moim przypadku - pierwszemu.

Teraz wystarczy tylko zablokować użytkownikowi wduszenie przycisku wysyłania, jeśli formularz jest $invalid.

 
<button class="nextBtn"  ng-click="c.MacroForm.$valid ? isFlipped=!isFlipped : ''; c.MacroForm.$valid ? c.check() : ''" ng-show="c.answerMode">Submit</button>

W atrybucie ng-click mamy dwa warunki, które spełniają się wtedy, kiedy formularz spełnia wszystkie nasze założenia. Jako że nie da się inaczej ustawić dwóch zadań do wykonania dla jednego ng-click, użyłem dwóch skróconych warunków if.

Tak prezentuje się cały kod formularza po zmianach:

 
<form name="c.MacroForm">
  <h2></h2>
  <img ng-src="/assets/">
  <p>Kcal:</p><input type="number" ng-model="c.macro1" min="0" ng-required='!(c.macro2 ||  c.macro3 || c.macro4)' ng-pattern="/^(0|[1-9][0-9]*)$/">
  <p>Białko:</p><input type="number" ng-model="c.macro2" min="0" name="macro1" ng-pattern="/^(0|[1-9][0-9]*)$/">
  <p>Węglowodany:</p><input type="number" ng-model="c.macro3" min="0" ng-pattern="/^(0|[1-9][0-9]*)$/">
  <p>Tłuszcz:</p><input type="number" ng-model="c.macro4" min="0" ng-pattern="/^(0|[1-9][0-9]*)$/">
  
  <button class="nextBtn"  ng-click="c.MacroForm.$valid ? isFlipped=!isFlipped : ''; c.MacroForm.$valid ? c.check() : ''" ng-show="c.answerMode">Submit</button>
</form>

# Losowanie produktów

Cóż by to był za monotonny quiz, jeśli cały czas pojawiałaby się nam ta sama sekwencja produktów do określania ich kaloryczności. Musimy więc stworzyć funkcje, która poda nam tablicę z losowymi, nie powtarzającymi się wartościami id. Zdecydowałem się na takie rozwiązanie, żeby nie modyfikować tak bardzo kodu, który już napisaliśmy. Zostawiłem też furtkę do określenia ilości produktów na jedną grę.

 
shuffle(y){
  let tab = [...Array(++y).keys()].splice(1);
  let arr = [];
  const x = --tab.length;
  let z = x - 1;
  while(arr.length < x){
    let i = Math.floor((Math.random() * z--) + 0);
    arr.push(tab[i]);
    tab.splice(i, 1);
  }
  return arr;
}

Nasza funkcja nazywa się shuffle i przyjmuje jeden argument - ilość elementów w tablicy do wygenerowania.

Na początku tworzymy lokalną tablicę za pomocą let, o którym więcej można poczytać tutaj. Argumenty do tablicy dodajemy za pomocą operatora rest i funkcji keys(), która tworzy tablice z kluczami tablicy stworzonej na jej potrzeby za pomocą Array(++y). Jednak musimy rozwiązać pewien problem. keys() zwróci nam wartości od 0 do y-1, a nasze id w modelu, zaczynają się od 1. Dlatego tworzymy tablicę o jeden argument większą niż y, a na samym końcu ucinamy pierwszy argument tablicy za pomocą splice(1).

Tworzymy pusta tablicę arr, którą będziemy powoli wypełniać, a na końcu zwrócimy za pomocą return. Wartość x to ilość argumentów tablicy, która pozostanie niezmienna. Wartość z, to ilość argumentów, tylko już bez słowa kluczowego const.

Pętla dodająca kolejne argumenty będzie wykonywać się tak długo, dopóki tablica będzie krótsza od zadanej ilości argumentów w stałej x. Zmienna i przyjmuje losową liczbę od zera do zmiennej z, a dopiero potem następuje zmniejszenie tej zmiennej o jeden. Dodajemy do naszej tablicy element, wylosowany za pomocą id i zarazem kasujemy go z pierwotnej tablicy.

 
export default class ProductsList {
  constructor() {
    this.list = [];
  }

  add(id, name, kcal, protein, carb, fat, img){
    const macros = new Macros(id, name, kcal, protein, carb, fat, img);
    this.list.push(macros);
  }

  getList(id){
    return id < this.list.length ? this.list[id] : false; 
  }

  listLength(){
    return this.list.length;
  }
}

W klasie ProductsList dodajemy metodę zwracającą długość tablicy listLength().

 
start() {
  this.id = 0;
  this.quizEnd = false;
  this.inProgress = true;
  this.ProductArray = this.shuffle(this.productsList.listLength());
  this.getQuestion();
}

Wywołujemy naszą funkcję shuffle() wewnątrz funkcji start(). Jako argument przekazujemy długość tablicy, jednak możemy określić żeby quiz korzystał z mniejszej ilości produktów w jednej grze.

 
getQuestion() {
  var q = this.productsList.getList(this.ProductArray[this.id]);
  if(q) {
    this.name = q.name;
    this.kcal = q.kcal;
    this.protein = q.protein;
    this.carb = q.carb;
    this.fat = q.fat;
    this.img = q.img;
    this.answerMode = true;
  } else {
    this.quizEnd = true;
  }
}

Teraz zamieniamy wczytywanie aktualnego produktu w funkcji getQuestion(). Zamiast zwykłego id, korzystamy z id w naszej nowej tablicy z dobraną losową sekwencją produktów.

Published: May 30 2017

blog comments powered by Disqus