W tym wpisie pokażę jak zrobić prosty formularz w AngularJS, który będzie podpięty do listy z danymi. Wartości z pól tekstowych będą przepisywane automatycznie w miejsce wynikowe poprzez listę.
Weźmy szablon aplikacji z wpisu, w którym był omówiony kontroler. Będzie to nasz punkt wyjściowy do rozbudowy aplikacji.
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!DOCTYPE html> <html ng-app="myApp"> <head> <meta charset="UTF-8"> <title>Hello World AngularJS</title> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script> <script src="controller.js"></script> </head> <body ng-controller="myCtrl"> <h2>Hello World AngularJS</h2> <input type="text" ng-model="myText"> {{myResult()}} </body> </html> |
controller.js
1 2 3 4 5 6 7 8 9 |
var app = angular.module('myApp', []); app.controller('myCtrl', function($scope) { $scope.myText = "World"; $scope.myResult = function () { return "Hello " + $scope.myText + "!"; }; }); |
Szablon formularza w tabeli w HTML
Chcemy mieć gdzie wpisywać dane (tytuł, imię, nazwisko itd.). Najlepiej jak będą to pola tekstowe <input>
, tak jak w poprzednim przykładzie. Tych pól będzie kilka dla jednego rekordu (jednego wystąpienia), a wierszy będzie kilka. Są przynajmniej dwa sposoby jak moglibyśmy zorganizować te pola:
- lista nieuporządkowana
<ul>
z elementami<li>
, - tabela
<table>
z wierszami<tr>
i komórkami<td>
.
Na pierwszy rzut oka wydaje się, że powinniśmy użyć listy (biorąc pod uwagę dobrych kilka lat walki z wypieraniem tabelek ze stron). Jednak mamy tu dane tabelaryczne i dlatego spróbujemy użyć tu tabeli (dzięki temu będziemy mieli też ładne nagłówki <th>
w pierwszym wierszu <tr>
).
W pierwszym pliku dodajmy tabelę:
1 2 3 4 5 6 7 8 9 10 11 12 |
<table> <tr> <td>Title</td> <td>Name</td> <td>Surname</td> </tr> <tr ng-repeat="item in list"> <td><input type="text" ng-model="item.title"></td> <td><input type="text" ng-model="item.name"></td> <td><input type="text" ng-model="item.surname"></td> </tr> </table> |
Jak widzicie, dla pól <input>
została zastosowana dyrektywa, która binduje model (tzn. odpowiednie „zmienne”). To co zostanie wstawione do modelu, będzie wyświetlone w polach tekstowych. I to co zostanie wpisane w pola tekstowe, automatycznie będzie dostępne w modelu.
Pojawiła się tu także nowa dyrektywa ng-repeat
. Najczęściej używa się jej w ten sposób: ng-repeat="element in listaElementow"
. Działa ona w ten sposób, że powtarza element HTML, w którym jest umieszczona, tyle razy, ile jest elementów w liście (tak jakby pętla for each
znana z innych języków – przechodzi po każdym elemencie). Każdy wybrany element jest dostępny w nazwanej przez nas zmiennej item
. Przyjąłem, że lista zawierała obiekty, więc możemy odwołać się do pól obiektu. I tak, możemy z modelu wybrać np. tytuł poprzez item.title
oraz zbindować go do pola tekstowego.
Dzięki temu zostanie wyświetlonych tyle linii w tabeli (z potrzebnymi polami), ile jest elementów na liście. Ale potrzebujemy właśnie tą listę i to dostępną w $scope
.
Lista w kontrolerze w JavaScript
W kontrolerze deklarujemy listę (a właściwie tablicę):
1 |
$scope.list = []; |
Jeśli teraz odwołamy się do jakiegoś elementu lub pola, będzie miało wartość undefined
. Aby pola tekstowe domyślnie zawierały puste wartości, a nie brzydką wartość nieokreśloną, musimy dodać obiekty i zainicjalizować pola. Nowy obiekt tworzymy za pomocą nawiasów klamrowych {}
a w środku wpisujemy po przecinku nazwę pola, przecinek i wartość. Wygląda to tak: {title: "", name: "", surname: "", time: ""}
.
My chcemy od razu dodać kilka obiektów na naszą listę, aby domyślnie wyświetliło kilka wierszy do wpisywania. Użyjemy do tego metody push
na liście. Czyli dodanie obiektu do listy w $scope
powinno wyglądać tak: $scope.list.push(obiekt)
.
Możemy od razu dodać kilka obiektów w pętli:
1 2 3 |
for(var i = 0; i < 3; i++) { $scope.list.push({title: "", name: "", surname: "", time: ""}); } |
Teraz powinny wyświetlić się nam trzy wiersze z polami.
Funkcja tworząca wynikowe dane
Oczywiście chcemy jeszcze, aby wpisywane dane do pól tekstowych były przepisywane zgodnie z naszymi wymaganiami. W tym celu zmodyfikujemy funkcję myResult()
z początkowego kodu.
Chcemy otrzymać listę wyników. Dlatego zadeklarujmy tablicę var result = []
. Następnie w pętli, przechodźmy po wszystkich elementach naszej listy $scope.list
. Łączmy dane z pól i wstawiajmy jako nowy element na listę wyników result.push(wynikWiersza)
. Na koniec zwróćmy listę wyników (bo przecież to jest funkcja). Kod wygląda tak:
1 2 3 4 5 6 7 8 |
$scope.myResult = function () { var result = []; for (var i = 0; i < $scope.list.length; i++) { var concat = "„" + $scope.list[i].title + "” – " + $scope.list[i].name + " " + $scope.list[i].surname; result.push(concat); } return result; }; |
W zmiennej concat
po prostu znalazły się połączone (skonkatenowane) dane z pól (na razie godziny i czas wystąpienia zostały celowo pominięte – ich obsługa będzie w kolejnych wpisach).
Wyświetlenie listy wyników
Dodajmy po prostu element <div>
, w którym przejdziemy po wszystkich elementach z listy, którą zwraca funkcja myResult()
. Element <div> zostanie powtórzony tyle razy ile jest elementów na liście. Wyświetlmy w nim połączone dane i na koniec przejście do nowej linii.
1 |
<div ng-repeat="item in myResult() track by $index">{{item}}<br /></div> |
Dlaczego nie robić nowych linii w funkcji?
Dlaczego nie mogłem połączyć wszystkich danych w funkcji, dodać znaki nowej linii i zapisać do jednej zmiennej całego tekstu? A potem tylko go wyświetlić? (Uniknęlibyśmy pracy z dodawaniem każdego wynikowego wiersza do listy, a potem wyświetlania ich w pętli)
Tzn. kod funkcji wyglądałby tak:
1 2 3 4 5 6 7 |
$scope.myResult = function () { var result = ""; for (var i = 0; i < $scope.list.length; i++) { result += "„" + $scope.list[i].title + "” – " + $scope.list[i].name + " " + $scope.list[i].surname + "<br />"; } return result; }; |
A w HTML wyświetlilibyśmy jedynie wynik funkcji:
1 |
{{myResult()}} |
Byłaby dużo prościej, prawa?
Ale nie działa. Znaczniki <br />
nie są zamieniane na nowe linie, tylko pokazują się jako zwykłe znaki. Jest to zabezpieczenie AngularJS przed umieszczaniem kodu HTML na stronie przez użytkownika.
Jeśli naprawdę chcemy, jak można to wyłączyć? We wcześniejszych wersjach Angulara istniała dyrektywa ng-bind-html-unsafe
, która pozwalała zbindować zmienna, w której jest HTML i go wyświetlać. Wyglaładałoby to tak:
1 |
<div ng-bind-html-unsafe="myResult()"></div> |
Obecnie ta dyrektywa została usunięta. Jak sobie z tym poradzić? Trzeba zrobić dwie rzeczy:
- Zastosować do bindowania dyrektywę ng-bind-html, co wygląda tak:
1<div ng-bind-html="myResult()"></div> - Wstrzyknąć moduł
ngSanitize
. Aby to zrobić należy:- W inicjalizacji aplikacji dodać moduł:
1var app = angular.module('myApp', ['ngSanitize']); - Dodać plik angular-sanitize.min.js:
1<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-sanitize.min.js"></script>
- W inicjalizacji aplikacji dodać moduł:
Teraz działa. Jednak nie jest to bezpieczne rozwiązanie i lepiej zastosować to wyżej (z listą i dodawaniem znaku <br />
już w samym kodzie HTML, a nie poprzez funkcję).
Problem: duplikaty
To by było na tyle tego wpisu. Ale można zauważyć błąd, który pokazuje się jeśli wpiszemy takie same dane w dwa wiersze: Error: [ngRepeat:dupes]
. Zostajemy odesłani do dokumentacji: Error: ngRepeat:dupes. Duplicate Key in Repeater, w której możemy znaleźć rozwiązanie problemu. W dyrektywie ng-repeat
musimy dodać track by $index
. Co daje efekt:
1 |
<div ng-repeat="item in myResult() track by $index">{{item}}<br /></div> |
O co chodzi? Po prostu nie są dozwolone duplikaty w liście po której chcemy przechodzić (jest to związane z odświeżaniem drzewa DOM, więcej tutaj). Aby umożliwić wstawianie duplikatów, trzeba dodać wyrażenie śledzące poprzez track by wyrazenieSledzace
. Wyrażeniem śledzącym musi być jakaś unikalna wartość np. ID. Możemy skorzystać również z $index
, czyli kolejno generowane numery podczas pobierania elementów z listy (będziemy z tego korzystać jeszcze nieraz).
Kosmetyczne ukrycie
Na koniec dodajmy małą rzecz, która dużo nie robi, ale dzięki niej przyjemniej będzie się korzystało z aplikacji. Zauważyliśmy pewnie, że wszystkie wiersze są przepisywane (również te puste). Dlatego w funkcji myResult()
w kontrolerze dodajmy warunek, który będzie dodawał do listy wyników jedynie, gdy tytuł będzie uzupełniony:
1 2 3 |
if($scope.list[i].title !== ""){ result.push(concat); } |
Wynikowy kod
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<!DOCTYPE html> <html ng-app="agendaEditor"> <head> <meta charset="UTF-8"> <title>Form AgendaEditor</title> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script> <script src="controller.js"></script> </head> <body ng-controller="formCtrl"> <h2>Form AgendaEditor</h2> <table> <tr> <td>Title</td> <td>Name</td> <td>Surname</td> </tr> <tr ng-repeat="item in list"> <td><input type="text" ng-model="item.title"></td> <td><input type="text" ng-model="item.name"></td> <td><input type="text" ng-model="item.surname"></td> </tr> </table> Wynik: <div ng-repeat="item in myResult() track by $index">{{item}}<br /></div> </body> </html> |
controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var app = angular.module('agendaEditor', []); app.controller('formCtrl', function($scope) { $scope.list = []; for(var i = 0; i < 3; i++) { $scope.list.push({title: "", name: "", surname: "", time: ""}); } $scope.myResult = function () { var result = []; for (var i = 0; i < $scope.list.length; i++) { var concat = "„" + $scope.list[i].title + "” – " + $scope.list[i].name + " " + $scope.list[i].surname; if($scope.list[i].title !== ""){ result.push(concat); } } return result; }; }); |
Wynik działania
Pingback: AgendaEditor: dynamiczne dodawanie pól | Marcin Kowalczyk – Blog IT
Pingback: Podsumowanie projektu AgendaEditor 2016 – Marcin Kowalczyk – Blog IT