Fiszki Online AngularJS (Preview)
Darmowy podgląd 15 z 50 dostępnych pytań
AngularJS - Sekcja 1: Podstawy i Architektura
1. Czym jest AngularJS i jakie problemy rozwiązuje?
Odpowiedź w 30 sekund: AngularJS to framework JavaScript stworzony przez Google do budowy dynamicznych aplikacji webowych typu SPA (Single Page Application). Rozwiązuje problemy związane z manipulacją DOM, synchronizacją danych między widokiem a modelem oraz organizacją kodu w dużych aplikacjach poprzez wprowadzenie dwukierunkowego wiązania danych, dependency injection i struktury MVC/MVVM.
Odpowiedź w 2 minuty: AngularJS (wersja 1.x) został wydany w 2010 roku jako framework do tworzenia dynamicznych aplikacji webowych. Jego głównym celem było uproszczenie procesu tworzenia interaktywnych interfejsów użytkownika poprzez rozszerzenie HTML o nowe dyrektywy i wprowadzenie automatycznej synchronizacji między modelem danych a widokiem.
Framework rozwiązuje kilka kluczowych problemów: po pierwsze, eliminuje potrzebę ręcznej manipulacji DOM dzięki mechanizmowi dwukierunkowego wiązania danych (two-way data binding). Po drugie, zapewnia strukturę organizacyjną dla kodu poprzez wzorzec MVC/MVVM, co ułatwia utrzymanie i rozwój aplikacji. Po trzecie, implementuje dependency injection, który promuje luźne powiązania między komponentami i ułatwia testowanie.
AngularJS wprowadza koncepcję rozszerzania HTML poprzez dyrektywy, co pozwala na tworzenie deklaratywnego kodu bardziej czytelnego dla programistów. Framework zarządza również routingiem w aplikacjach SPA, obsługuje walidację formularzy i zapewnia mechanizmy komunikacji z API poprzez wbudowany serwis $http.
Dzięki tym rozwiązaniom AngularJS stał się popularnym wyborem dla aplikacji biznesowych wymagających bogatych interfejsów użytkownika, choć obecnie został zastąpiony przez nowsze wersje Angular (2+).
Przykład kodu:
// Podstawowa aplikacja AngularJS
var app = angular.module('myApp', []);
// Kontroler zarządzający logiką
app.controller('UserController', function($scope) {
// Model danych automatycznie synchronizowany z widokiem
$scope.user = {
name: 'Jan Kowalski',
email: 'jan@example.com'
};
// Metoda dostępna w widoku
$scope.updateUser = function() {
console.log('Zaktualizowano użytkownika:', $scope.user);
};
});
<!-- Widok z dwukierunkowym wiązaniem danych -->
<div ng-app="myApp" ng-controller="UserController">
<input type="text" ng-model="user.name">
<input type="email" ng-model="user.email">
<button ng-click="updateUser()">Zapisz</button>
<!-- Automatyczna aktualizacja przy zmianie modelu -->
<p>Witaj, {{user.name}}!</p>
</div>
Diagram:
flowchart TD
A[Widok HTML] <-->|Two-way binding| B[Model danych]
B --> C[Kontroler]
C --> D[Serwisy]
D --> E[API/Backend]
F[Dyrektywy] --> A
G[Filtry] --> A
Materiały:
- AngularJS Developer Guide - Google
- AngularJS Introduction - W3Schools
- Understanding AngularJS - Todd Motto
2. Jakie są kluczowe różnice między AngularJS (1.x) a Angular (2+)?
Odpowiedź w 30 sekund: Główne różnice to: Angular (2+) jest całkowicie przepisany w TypeScript i używa komponentów zamiast kontrolerów, nie posiada $scope, wykorzystuje RxJS i observables zamiast promises, ma znacznie lepszą wydajność dzięki nowej strategii wykrywania zmian oraz wspiera mobile-first development. AngularJS opiera się na JavaScript, używa kontrolerów i $scope, oraz dwukierunkowego wiązania danych z digest cycle.
Odpowiedź w 2 minuty: AngularJS (1.x) i Angular (2+) to zasadniczo różne frameworki, mimo podobnej nazwy. Angular 2+ został całkowicie przepisany od podstaw i nie jest wstecznie kompatybilny z AngularJS.
Architektura: AngularJS używa kontrolerów i $scope do zarządzania stanem, podczas gdy Angular 2+ opiera się wyłącznie na komponentach. W Angular każdy komponent to klasa TypeScript z dekoratorem @Component, która łączy logikę, template i style w jedną spójną jednostkę. Eliminacja $scope sprawia, że kod jest bardziej przewidywalny i łatwiejszy do debugowania.
Język programowania: AngularJS jest napisany w JavaScript ES5, podczas gdy Angular 2+ został zbudowany w TypeScript, co zapewnia silne typowanie, lepsze wsparcie IDE, interfejsy i nowoczesne funkcje ES6+. TypeScript jest opcjonalny w teorii, ale w praktyce stanowi standard.
Wydajność: Angular 2+ wprowadza nową strategię wykrywania zmian opartą na Zone.js zamiast digest cycle z AngularJS. To radykalnie poprawia wydajność, szczególnie w dużych aplikacjach. Dodatkowo Angular wspiera Ahead-of-Time (AOT) compilation, tree-shaking i lazy loading.
Dependency Injection: Oba frameworki używają DI, ale Angular 2+ ma hierarchiczny system injectorów, który jest bardziej elastyczny i wydajny. W Angular możliwe jest tworzenie własnych injectorów na poziomie komponentów.
Asynchroniczność: AngularJS używa promises ($q), podczas gdy Angular 2+ standardowo wykorzystuje RxJS i observables, co daje większą kontrolę nad strumieniami danych i operacjami asynchronicznymi.
Przykład kodu:
// AngularJS 1.x - Kontroler z $scope
angular.module('myApp', [])
.controller('UserController', function($scope, $http) {
$scope.users = [];
$scope.loadUsers = function() {
$http.get('/api/users')
.then(function(response) {
$scope.users = response.data;
});
};
});
<!-- AngularJS 1.x - Widok -->
<div ng-controller="UserController">
<button ng-click="loadUsers()">Załaduj użytkowników</button>
<div ng-repeat="user in users">{{user.name}}</div>
</div>
// Angular 2+ - Komponent
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface User {
id: number;
name: string;
}
@Component({
selector: 'app-user-list',
template: `
<button (click)="loadUsers()">Załaduj użytkowników</button>
<div *ngFor="let user of users$ | async">{{user.name}}</div>
`
})
export class UserListComponent implements OnInit {
users$: Observable<User[]>;
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.loadUsers();
}
loadUsers(): void {
this.users$ = this.http.get<User[]>('/api/users');
}
}
Diagram:
flowchart LR
subgraph AngularJS 1.x
A1[Kontrolery] --> B1[$scope]
B1 --> C1[Widok]
D1[Serwisy] --> A1
E1[Digest Cycle] -.-> B1
end
subgraph Angular 2+
A2[Komponenty] --> B2[TypeScript Class]
B2 --> C2[Template]
D2[Serwisy] --> A2
E2[Zone.js] -.-> A2
F2[RxJS] --> D2
end
Materiały:
- Angular Official Documentation
- AngularJS vs Angular - Understanding the Differences
- Migration from AngularJS to Angular
3. Czym jest moduł w AngularJS i jak go zdefiniować?
Odpowiedź w 30 sekund: Moduł w AngularJS to kontener na różne części aplikacji (kontrolery, serwisy, dyrektywy, filtry). Służy do organizacji kodu i zarządzania zależnościami między komponentami. Definiuje się go metodą angular.module(), podając nazwę i opcjonalną tablicę zależności od innych modułów.
Odpowiedź w 2 minuty: Moduł jest fundamentalnym elementem architektury AngularJS, który grupuje powiązane komponenty aplikacji w logiczną całość. Każda aplikacja AngularJS musi mieć przynajmniej jeden moduł główny, ale większe aplikacje często dzielą funkcjonalność na wiele mniejszych modułów dla lepszej organizacji i możliwości ponownego użycia kodu.
Moduły pełnią kilka kluczowych ról: po pierwsze, definiują punkt wejścia do aplikacji poprzez dyrektywę ng-app. Po drugie, zarządzają zależnościami między różnymi częściami aplikacji - można deklarować, że jeden moduł zależy od drugiego, a AngularJS automatycznie załaduje wszystkie wymagane komponenty. Po trzecie, moduły pomagają w organizacji kodu poprzez separację odpowiedzialności - można mieć osobne moduły dla autentykacji, zarządzania użytkownikami, raportowania itp.
Istnieją dwa sposoby użycia angular.module(): z tablicą zależności (tworzy nowy moduł) lub bez niej (pobiera istniejący moduł). To częste źródło błędów dla początkujących - zapomnienie tablicy przy pierwszej deklaracji powoduje błąd "module not found".
Moduły mogą zależeć od innych modułów (włączając moduły wbudowane jak ngRoute, ngResource) oraz od zewnętrznych bibliotek. Ta modularność umożliwia tworzenie reużywalnych komponentów, które można łatwo przenosić między projektami.
Przykład kodu:
// Tworzenie głównego modułu aplikacji z zależnościami
// Tablica [] oznacza utworzenie NOWEGO modułu
var app = angular.module('myApp', ['ngRoute', 'ngResource', 'myApp.users']);
// Tworzenie modułu pomocniczego bez zależności
angular.module('myApp.users', []);
// Pobranie istniejącego modułu (brak tablicy [])
// Użyj tego do dodawania kontrolerów, serwisów itp.
angular.module('myApp.users')
.controller('UserController', function($scope) {
$scope.message = 'Moduł użytkowników';
});
// Moduł z konfiguracją
angular.module('myApp.auth', ['ngCookies'])
.config(function($httpProvider) {
// Konfiguracja interceptorów HTTP
$httpProvider.interceptors.push('authInterceptor');
})
.run(function($rootScope) {
// Kod wykonywany przy starcie aplikacji
console.log('Moduł auth zainicjalizowany');
});
// Moduł z wieloma komponentami
angular.module('myApp.products', [])
.factory('ProductService', function($http) {
return {
getAll: function() {
return $http.get('/api/products');
}
};
})
.controller('ProductListController', function($scope, ProductService) {
ProductService.getAll().then(function(response) {
$scope.products = response.data;
});
})
.directive('productCard', function() {
return {
restrict: 'E',
template: '<div class="product">{{product.name}}</div>',
scope: {
product: '='
}
};
});
<!-- Deklaracja modułu głównego w HTML -->
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script src="angular.js"></script>
<script src="angular-route.js"></script>
<script src="app.js"></script>
<script src="modules/users/users.module.js"></script>
<script src="modules/auth/auth.module.js"></script>
</head>
<body>
<div ng-view></div>
</body>
</html>
Diagram:
flowchart TD
A[myApp - Moduł główny] --> B[ngRoute]
A --> C[ngResource]
A --> D[myApp.users]
A --> E[myApp.auth]
A --> F[myApp.products]
D --> G[UserController]
D --> H[UserService]
E --> I[ngCookies]
E --> J[authInterceptor]
F --> K[ProductService]
F --> L[ProductListController]
F --> M[productCard directive]
style A fill:#4CAF50
style D fill:#2196F3
style E fill:#2196F3
style F fill:#2196F3
Materiały:
↑ Powrót na górę4. Jak działa dependency injection w AngularJS?
Odpowiedź w 30 sekund: Dependency Injection (DI) w AngularJS to wzorzec projektowy, który automatycznie dostarcza zależności do komponentów zamiast wymagać ich ręcznego tworzenia. AngularJS analizuje parametry funkcji (przez nazwy lub adnotacje $inject) i automatycznie wstrzykuje odpowiednie serwisy, co promuje luźne powiązania, ułatwia testowanie i zarządzanie zależnościami.
Odpowiedź w 2 minuty: Dependency Injection jest jednym z fundamentalnych mechanizmów AngularJS, który zarządza tworzeniem i dostarczaniem obiektów (zależności) do komponentów, które ich potrzebują. Zamiast komponenty same tworzyły swoje zależności (co prowadzi do mocnego sprzężenia), framework automatycznie je dostarcza.
AngularJS rozpoznaje zależności na trzy sposoby: implicit annotation (wnioskowanie z nazw parametrów), inline array annotation (tablica z nazwami zależności) oraz $inject property annotation (właściwość $inject). Pierwszy sposób jest najkrótszy, ale zawodzi przy minifikacji kodu, dlatego w produkcji zaleca się używanie dwóch pozostałych.
System DI w AngularJS składa się z injektora, który jest odpowiedzialny za tworzenie instancji serwisów i zarządzanie ich cyklem życia. Większość wbudowanych serwisów to singletony - tworzone są raz i współdzielone w całej aplikacji. Injector wykorzystuje provider do tworzenia instancji - każdy serwis zdefiniowany przez .service(), .factory() lub .value() ma odpowiadający mu provider.
DI w AngularJS wspiera także lokalne injektory (np. dla dyrektyw), co pozwala na tworzenie izolowanych zakresów z własnymi zależnościami. To szczególnie przydatne przy tworzeniu reużywalnych komponentów, które mogą mieć różne implementacje tych samych interfejsów w różnych kontekstach.
Główne korzyści DI to: łatwiejsze testowanie (możliwość mockowania zależności), luźniejsze powiązania między komponentami, lepsza organizacja kodu i możliwość łatwej zamiany implementacji bez zmiany kodu konsumenta.
Przykład kodu:
// 1. IMPLICIT ANNOTATION - wnioskowanie z nazw parametrów
// UWAGA: Nie działa po minifikacji!
app.controller('ImplicitController', function($scope, $http, UserService) {
// AngularJS automatycznie wstrzykuje zależności po nazwach
$scope.users = [];
});
// 2. INLINE ARRAY ANNOTATION - preferowana metoda
// Bezpieczna dla minifikacji
app.controller('InlineController', ['$scope', '$http', 'UserService',
function($scope, $http, UserService) {
// Nazwy w tablicy muszą odpowiadać kolejności parametrów
$scope.loadUsers = function() {
UserService.getAll().then(function(users) {
$scope.users = users;
});
};
}
]);
// 3. $inject PROPERTY ANNOTATION - alternatywa dla inline
var PropertyController = function($scope, $http, UserService) {
$scope.users = [];
$scope.refresh = function() {
$http.get('/api/users').then(function(response) {
$scope.users = response.data;
});
};
};
// Definicja zależności przed minifikacją
PropertyController.$inject = ['$scope', '$http', 'UserService'];
app.controller('PropertyController', PropertyController);
// Tworzenie własnego serwisu z DI
app.factory('UserService', ['$http', '$q', function($http, $q) {
return {
getAll: function() {
return $http.get('/api/users')
.then(function(response) {
return response.data;
})
.catch(function(error) {
return $q.reject(error);
});
},
getById: function(id) {
return $http.get('/api/users/' + id)
.then(function(response) {
return response.data;
});
}
};
}]);
// Serwis z zależnościami od innych serwisów
app.service('AuthService', ['$http', '$window', 'UserService',
function($http, $window, UserService) {
this.login = function(credentials) {
return $http.post('/api/login', credentials)
.then(function(response) {
// Zapis tokenu
$window.localStorage.setItem('token', response.data.token);
return UserService.getById(response.data.userId);
});
};
this.logout = function() {
$window.localStorage.removeItem('token');
};
}
]);
// Provider - najbardziej elastyczna forma DI
app.provider('ApiConfig', function() {
var baseUrl = 'http://api.example.com';
// Konfiguracja przed utworzeniem instancji
this.setBaseUrl = function(url) {
baseUrl = url;
};
// Metoda $get tworzy faktyczny serwis
this.$get = ['$http', function($http) {
return {
getBaseUrl: function() {
return baseUrl;
},
request: function(endpoint) {
return $http.get(baseUrl + endpoint);
}
};
}];
});
// Konfiguracja providera
app.config(['ApiConfigProvider', function(ApiConfigProvider) {
ApiConfigProvider.setBaseUrl('http://production-api.example.com');
}]);
Diagram:
flowchart TD
A[Injector - Główny kontener DI] --> B[Provider Registry]
B --> C[Factory: UserService]
B --> D[Service: AuthService]
B --> E[Value: API_URL]
B --> F[Constant: APP_VERSION]
G[Controller] -->|Żąda zależności| A
A -->|Dostarcza instancję| G
C -->|Zależy od| H[$http]
C -->|Zależy od| I[$q]
D -->|Zależy od| C
D -->|Zależy od| H
J[Config Phase] -->|Konfiguruje| B
K[Run Phase] -->|Inicjalizuje| A
style A fill:#4CAF50
style G fill:#2196F3
style C fill:#FF9800
style D fill:#FF9800
Materiały:
- AngularJS Dependency Injection Guide
- Understanding Dependency Injection in AngularJS
- AngularJS DI Best Practices
5. Czym jest $rootScope i czym różni się od $scope?
Odpowiedź w 30 sekund: $rootScope to główny scope aplikacji AngularJS, dostępny globalnie we wszystkich kontrolerach i komponentach, który istnieje przez cały cykl życia aplikacji. $scope to lokalny zakres tworzony dla każdego kontrolera lub dyrektywy, który dziedziczy po $rootScope (lub scope rodzica) i jest niszczony wraz z komponentem. Nadużywanie $rootScope prowadzi do problemów z utrzymaniem kodu i wycieków pamięci.
Odpowiedź w 2 minuty: $rootScope i $scope są kluczowymi koncepcjami w systemie zarządzania stanem AngularJS, ale pełnią różne role w hierarchii zakresów.
$rootScope jest singletonem - istnieje dokładnie jedna instancja na aplikację, tworzona przy jej inicjalizacji. Jest to korzeń hierarchii wszystkich scope'ów w aplikacji. Wszystkie dane i metody dodane do $rootScope są dostępne globalnie w całej aplikacji, podobnie jak zmienne globalne w JavaScript. To sprawia, że $rootScope jest przydatny do przechowywania danych współdzielonych (np. informacje o zalogowanym użytkowniku, ustawienia aplikacji) oraz do komunikacji między niepowiązanymi kontrolerami poprzez $broadcast i $emit.
$scope to lokalny zakres tworzony dla każdego kontrolera, dyrektywy lub komponentu. Każdy $scope dziedziczy prototypowo od swojego rodzica (ostatecznie od $rootScope), co tworzy hierarchiczną strukturę. Dane w $scope są izolowane i dostępne tylko w kontekście danego kontrolera oraz jego widoku. Kiedy kontroler lub dyrektywa są niszczone, ich $scope również jest czyszczony, co pomaga w zarządzaniu pamięcią.
Kluczowe różnice: $rootScope żyje przez cały czas działania aplikacji, podczas gdy $scope jest niszczony wraz z komponentem. $rootScope jest współdzielony globalnie, $scope jest lokalny. Zmiany w $rootScope mogą wpływać na całą aplikację, podczas gdy zmiany w $scope są ograniczone do lokalnego kontekstu.
Best practices: Unikaj nadmiernego używania $rootScope - lepiej użyć serwisów do współdzielenia danych. Używaj $rootScope tylko do globalnych eventów i danych rzeczywiście potrzebnych w całej aplikacji. Pamiętaj o czyszczeniu event listenerów dodanych do $rootScope, aby uniknąć wycieków pamięci.
Przykład kodu:
// Aplikacja demonstrująca różnice między $rootScope i $scope
var app = angular.module('scopeDemo', []);
// Konfiguracja globalnych danych w $rootScope
app.run(['$rootScope', function($rootScope) {
// Dane globalne dostępne w całej aplikacji
$rootScope.appName = 'Moja Aplikacja';
$rootScope.currentUser = {
name: 'Jan Kowalski',
role: 'admin'
};
// Globalna metoda
$rootScope.logout = function() {
console.log('Wylogowanie użytkownika');
$rootScope.currentUser = null;
};
}]);
// Kontroler 1 - używa lokalnego $scope
app.controller('HeaderController', ['$scope', '$rootScope',
function($scope, $rootScope) {
// Lokalna zmienna - dostępna tylko w tym kontrolerze
$scope.menuOpen = false;
$scope.toggleMenu = function() {
$scope.menuOpen = !$scope.menuOpen;
};
// Dostęp do $rootScope
$scope.getUserName = function() {
return $rootScope.currentUser ? $rootScope.currentUser.name : 'Gość';
};
// Nasłuchiwanie na globalny event
var unsubscribe = $rootScope.$on('userLoggedIn', function(event, user) {
console.log('Zalogowano użytkownika:', user.name);
$scope.menuOpen = false;
});
// WAŻNE: Czyszczenie listenera przy niszczeniu scope
$scope.$on('$destroy', function() {
unsubscribe();
});
}
]);
// Kontroler 2 - niezależny $scope
app.controller('ContentController', ['$scope', '$rootScope',
function($scope, $rootScope) {
// Lokalne dane - niedostępne w HeaderController
$scope.articles = [
{ title: 'Artykuł 1', content: 'Treść...' },
{ title: 'Artykuł 2', content: 'Treść...' }
];
// Sprawdzanie uprawnień użytkownika
$scope.canEdit = function() {
return $rootScope.currentUser &&
$rootScope.currentUser.role === 'admin';
};
// Broadcast eventu - wędruje w dół hierarchii
$scope.publishArticle = function(article) {
$rootScope.$broadcast('articlePublished', article);
};
// Emit eventu - wędruje w górę hierarchii
$scope.requestHelp = function() {
$scope.$emit('helpRequested', { controller: 'Content' });
};
}
]);
// Kontroler z zagnieżdżonymi scope'ami
app.controller('ParentController', ['$scope', function($scope) {
$scope.parentData = 'Dane rodzica';
$scope.sharedData = 'Współdzielone';
// Metoda dostępna w scope dziecka
$scope.parentMethod = function() {
console.log('Metoda z rodzica');
};
}]);
app.controller('ChildController', ['$scope', function($scope) {
// Dziedziczy po ParentController
console.log($scope.parentData); // 'Dane rodzica'
console.log($scope.sharedData); // 'Współdzielone'
// Nadpisanie zmiennej - tworzy nową właściwość w child scope
$scope.sharedData = 'Nadpisane w dziecku';
// $scope.parentData nadal 'Dane rodzica' w rodzicu
// Wywołanie metody rodzica
$scope.parentMethod(); // działa!
// Lokalna zmienna - niedostępna w rodzicu
$scope.childData = 'Dane dziecka';
}]);
// Dyrektywa z izolowanym scope
app.directive('isolatedComponent', function() {
return {
restrict: 'E',
scope: {
// Izolowany scope - NIE dziedziczy po rodzicu
title: '@', // String binding
data: '=', // Two-way binding
onAction: '&' // Method binding
},
template: '<div>{{title}}: {{data}}</div>',
link: function(scope, element, attrs) {
// Ten scope NIE ma dostępu do $rootScope.currentUser
// bez jawnego wstrzyknięcia $rootScope
}
};
});
<!-- Demonstracja hierarchii scope -->
<div ng-app="scopeDemo">
<!-- HeaderController ma własny $scope -->
<div ng-controller="HeaderController">
<h1>{{appName}}</h1> <!-- Z $rootScope -->
<p>Zalogowany: {{getUserName()}}</p>
<button ng-click="toggleMenu()">Menu</button>
<div ng-show="menuOpen">Menu content</div>
</div>
<!-- ContentController ma własny $scope -->
<div ng-controller="ContentController">
<div ng-repeat="article in articles">
<h2>{{article.title}}</h2>
<button ng-click="publishArticle(article)" ng-show="canEdit()">
Opublikuj
</button>
</div>
</div>
<!-- Zagnieżdżone scope -->
<div ng-controller="ParentController">
<p>{{parentData}}</p>
<p>{{sharedData}}</p> <!-- 'Współdzielone' -->
<div ng-controller="ChildController">
<p>{{parentData}}</p> <!-- Dziedziczone: 'Dane rodzica' -->
<p>{{sharedData}}</p> <!-- Nadpisane: 'Nadpisane w dziecku' -->
<p>{{childData}}</p> <!-- Lokalne: 'Dane dziecka' -->
</div>
<p>{{childData}}</p> <!-- undefined - nie ma dostępu do child scope -->
</div>
</div>
Diagram:
flowchart TD
A[$rootScope - Globalny] --> B[HeaderController $scope]
A --> C[ContentController $scope]
A --> D[ParentController $scope]
D --> E[ChildController $scope - dziedziczy]
A --> F[IsolatedComponent $scope - izolowany]
A -->|$broadcast| B
A -->|$broadcast| C
A -->|$broadcast| D
D -->|$broadcast| E
E -->|$emit| D
D -->|$emit| A
style A fill:#FF5722
style B fill:#4CAF50
style C fill:#4CAF50
style D fill:#4CAF50
style E fill:#2196F3
style F fill:#9C27B0
Materiały:
- AngularJS Scopes Documentation
- Understanding Scopes in AngularJS
- AngularJS $rootScope vs $scope Best Practices
6. Jak działa dwukierunkowe wiązanie danych (two-way data binding)?
Odpowiedź w 30 sekund: Two-way data binding w AngularJS automatycznie synchronizuje dane między modelem ($scope) a widokiem (DOM) w obu kierunkach. Gdy użytkownik zmienia wartość w input, model jest aktualizowany, a gdy kod zmienia model, widok jest automatycznie odświeżany. Mechanizm ten opiera się na digest cycle i $watch, które monitorują zmiany i propagują je.
Odpowiedź w 2 minuty: Dwukierunkowe wiązanie danych to kluczowa funkcjonalność AngularJS, która eliminuje potrzebę ręcznej synchronizacji między modelem danych a interfejsem użytkownika. W tradycyjnym JavaScript musielibyśmy ręcznie nasłuchiwać na zdarzenia input i aktualizować DOM, oraz odwrotnie - AngularJS robi to automatycznie.
Mechanizm działania: System opiera się na trzech głównych koncepcjach: $watch (obserwatory zmian), digest cycle (cykl sprawdzania zmian) i $apply (uruchomienie digest cycle). Gdy definiujesz wiązanie przez ng-model lub {{expression}}, AngularJS automatycznie tworzy $watch na tym wyrażeniu. Podczas digest cycle, framework przechodzi przez wszystkie $watchers i sprawdza, czy wartości się zmieniły (dirty checking). Jeśli tak, wykonuje odpowiednie callback'i i aktualizuje widok.
Digest cycle jest wywoływany automatycznie po zdarzeniach AngularJS (ng-click, $http, $timeout), ale możesz też wywołać go ręcznie przez $scope.$apply() lub $scope.$digest(). Cycle może wykonać się wielokrotnie (domyślnie max 10 razy), aby zapewnić pełną propagację wszystkich zmian - to tzw. "stabilizacja" modelu.
Wydajność: Two-way binding ma swoje koszty - przy dużej liczbie $watchers (tysiące) aplikacja może zwalniać. Dlatego Angular 2+ przeszedł na jednokierunkowy przepływ danych z opcjonalnym two-way binding. W AngularJS można optymalizować przez używanie one-time binding (::expression), ograniczanie liczby $watchers i używanie track by w ng-repeat.
Praktyczne zastosowania: Formularze są głównym przypadkiem użycia - ng-model automatycznie synchronizuje input z modelem. Walidacja, formatowanie i transformacja danych odbywają się automatycznie bez dodatkowego kodu.
Przykład kodu:
var app = angular.module('bindingDemo', []);
app.controller('FormController', ['$scope', '$timeout', function($scope, $timeout) {
// Model danych - automatycznie synchronizowany z widokiem
$scope.user = {
firstName: 'Jan',
lastName: 'Kowalski',
email: 'jan@example.com',
birthDate: new Date(1990, 0, 1),
newsletter: true,
country: 'PL'
};
$scope.countries = [
{ code: 'PL', name: 'Polska' },
{ code: 'US', name: 'USA' },
{ code: 'DE', name: 'Niemcy' }
];
// Computed property - automatycznie aktualizowane
$scope.fullName = function() {
return $scope.user.firstName + ' ' + $scope.user.lastName;
};
// $watch - monitorowanie zmian w modelu
$scope.$watch('user.email', function(newValue, oldValue) {
if (newValue !== oldValue) {
console.log('Email zmieniony z', oldValue, 'na', newValue);
$scope.emailChanged = true;
}
});
// Deep watch - monitorowanie całego obiektu
$scope.$watch('user', function(newValue, oldValue) {
if (newValue !== oldValue) {
console.log('Obiekt user został zmieniony');
$scope.lastModified = new Date();
}
}, true); // true = deep watch
// Programowa zmiana modelu - automatycznie zaktualizuje widok
$scope.resetForm = function() {
$scope.user = {
firstName: '',
lastName: '',
email: '',
birthDate: null,
newsletter: false,
country: 'PL'
};
};
// $apply - ręczne uruchomienie digest cycle
// Potrzebne dla kodu spoza AngularJS (jQuery, native API)
$scope.updateFromOutside = function() {
setTimeout(function() {
// To NIE zaktualizuje widoku bez $apply!
$scope.$apply(function() {
$scope.user.firstName = 'Zaktualizowane';
});
}, 1000);
};
// $timeout AngularJS - automatycznie wywołuje $apply
$scope.updateWithTimeout = function() {
$timeout(function() {
// To automatycznie zaktualizuje widok
$scope.user.firstName = 'Zaktualizowane przez $timeout';
}, 1000);
};
// Śledzenie wydajności - liczba $watchers
$scope.getWatchersCount = function() {
var root = angular.element(document.getElementsByTagName('body'));
var watchers = [];
var f = function(element) {
angular.forEach(['$scope', '$isolateScope'], function(scopeProperty) {
if (element.data() && element.data().hasOwnProperty(scopeProperty)) {
angular.forEach(element.data()[scopeProperty].$$watchers, function(watcher) {
watchers.push(watcher);
});
}
});
angular.forEach(element.children(), function(childElement) {
f(angular.element(childElement));
});
};
f(root);
return watchers.length;
};
}]);
// Dyrektywa demonstrująca two-way binding
app.directive('customInput', function() {
return {
restrict: 'E',
scope: {
value: '=', // Two-way binding
label: '@' // One-way binding (string)
},
template: `
<div class="form-group">
<label>{{label}}</label>
<input type="text" ng-model="value" class="form-control">
<small>Wartość: {{value}}</small>
</div>
`,
link: function(scope, element, attrs) {
// Watch na zmiany w dyrektywie
scope.$watch('value', function(newVal, oldVal) {
if (newVal !== oldVal) {
console.log('Wartość w dyrektywie zmieniona:', newVal);
}
});
}
};
});
// Filtr dla two-way binding z transformacją
app.filter('uppercase', function() {
return function(input) {
return input ? input.toUpperCase() : '';
};
});
<!DOCTYPE html>
<html ng-app="bindingDemo">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
</head>
<body ng-controller="FormController">
<h1>Two-Way Data Binding Demo</h1>
<!-- Podstawowe two-way binding z ng-model -->
<div class="form">
<div>
<label>Imię:</label>
<input type="text" ng-model="user.firstName">
<!-- Automatyczna aktualizacja przy zmianie input -->
<span>Witaj, {{user.firstName}}!</span>
</div>
<div>
<label>Nazwisko:</label>
<input type="text" ng-model="user.lastName">
</div>
<!-- Computed property - aktualizuje się automatycznie -->
<div>
<strong>Pełne imię:</strong> {{fullName()}}
</div>
<!-- Email z walidacją -->
<div>
<label>Email:</label>
<input type="email" ng-model="user.email" required>
<span ng-show="emailChanged" style="color: orange;">
(zmieniono)
</span>
</div>
<!-- Data picker -->
<div>
<label>Data urodzenia:</label>
<input type="date" ng-model="user.birthDate">
<span>{{user.birthDate | date:'dd/MM/yyyy'}}</span>
</div>
<!-- Checkbox -->
<div>
<label>
<input type="checkbox" ng-model="user.newsletter">
Zapisz się na newsletter
</label>
<span ng-show="user.newsletter">✓ Zapisany</span>
</div>
<!-- Select (dropdown) -->
<div>
<label>Kraj:</label>
<select ng-model="user.country">
<option ng-repeat="country in countries" value="{{country.code}}">
{{country.name}}
</option>
</select>
<span>Wybrany kod: {{user.country}}</span>
</div>
<!-- Radio buttons -->
<div>
<label>Płeć:</label>
<label><input type="radio" ng-model="user.gender" value="M"> Mężczyzna</label>
<label><input type="radio" ng-model="user.gender" value="K"> Kobieta</label>
<span>Wybrano: {{user.gender}}</span>
</div>
<!-- Textarea -->
<div>
<label>Komentarz:</label>
<textarea ng-model="user.comment" rows="3"></textarea>
<div>Liczba znaków: {{user.comment.length || 0}}</div>
</div>
<!-- Custom directive z two-way binding -->
<custom-input value="user.customField" label="Pole niestandardowe"></custom-input>
</div>
<!-- Wyświetlenie całego modelu -->
<div style="background: #f0f0f0; padding: 10px; margin-top: 20px;">
<h3>Model danych (JSON):</h3>
<pre>{{user | json}}</pre>
</div>
<!-- Akcje -->
<div>
<button ng-click="resetForm()">Resetuj formularz</button>
<button ng-click="updateWithTimeout()">Aktualizuj przez $timeout</button>
</div>
<!-- Informacje o wydajności -->
<div style="margin-top: 20px; color: #666;">
<small>
Ostatnia modyfikacja: {{lastModified | date:'HH:mm:ss'}}<br>
Liczba $watchers: {{getWatchersCount()}}
</small>
</div>
<!-- One-time binding dla optymalizacji (nie aktualizuje się) -->
<div>
<p>One-time binding: {{::user.firstName}} (nie zmieni się)</p>
<p>Two-way binding: {{user.firstName}} (aktualizuje się)</p>
</div>
</body>
</html>
Diagram:
flowchart LR
subgraph Widok
A[Input ng-model='user.name']
B[Span {{user.name}}]
end
subgraph Model
C[$scope.user.name]
end
subgraph AngularJS Engine
D[Digest Cycle]
E[$watch listeners]
F[Dirty Checking]
end
A -->|Zmiana przez użytkownika| D
D -->|Aktualizuje| C
C -->|Wykrycie zmiany| E
E -->|Uruchamia| F
F -->|Aktualizuje| B
G[ng-click/http/timeout] -->|Wywołuje| D
H[$scope.$apply] -->|Ręcznie wywołuje| D
style C fill:#4CAF50
style D fill:#FF5722
style A fill:#2196F3
style B fill:#2196F3
Materiały:
- AngularJS Data Binding Documentation
- Understanding Angular's $apply() and $digest()
- AngularJS Digest Cycle in Depth
AngularJS - Sekcja 2: Kontrolery i Scope
11. Czym jest controller as syntax i jakie ma zalety?
Odpowiedź w 30 sekund: Controller as syntax to alternatywna składnia w AngularJS, gdzie kontroler jest przypisywany do aliasu zamiast wstrzykiwania $scope. W widoku używamy aliasu (np. vm.property) zamiast bezpośredniego odwołania do scope. Poprawia to czytelność kodu, eliminuje problemy z dziedziczeniem scope i jest zgodna z podejściem komponentowym.
Odpowiedź w 2 minuty: Controller as syntax została wprowadzona w AngularJS 1.2 jako alternatywa dla tradycyjnego podejścia ze $scope. W tej składni kontroler staje się klasą, a jego właściwości i metody są przypisywane do this zamiast $scope. W widoku kontroler jest przypisywany do aliasu (zazwyczaj vm - view model), co czyni kod bardziej jednoznacznym i czytelnym.
Główne zalety to: lepsza czytelność kodu (jawne określenie skąd pochodzi właściwość), uniknięcie problemów z dziedziczeniem prototypowym scope (każdy kontroler ma własny kontekst), przygotowanie do migracji na Angular 2+ (gdzie nie ma $scope), łatwiejsze testowanie (kontroler jest zwykłą klasą JavaScript), oraz zgodność z paradygmatem programowania obiektowego. Controller as automatycznie rozwiązuje problem "dot rule", ponieważ wszystkie właściwości są domyślnie w obiekcie (this).
Ta składnia jest szczególnie przydatna przy zagnieżdżonych kontrolerach, gdzie w tradycyjnym podejściu mogłoby dojść do konfliktów nazw lub problemów z shadowing. W połączeniu z wzorcem MVVM (Model-View-ViewModel), controller as tworzy czystszą separację warstw. Choć $scope nadal jest dostępny (dla $watch, $on, itp.), większość logiki może być realizowana bez bezpośredniego używania $scope.
Przykład kodu:
var app = angular.module('controllerAsApp', []);
// Tradycyjne podejście ze $scope
app.controller('TraditionalController', function($scope, $http) {
$scope.title = 'Tradycyjny kontroler';
$scope.items = [];
$scope.loadItems = function() {
$http.get('/api/items').then(function(response) {
$scope.items = response.data;
});
};
});
// Controller as syntax - ZALECANE
app.controller('ModernController', function($http) {
var vm = this; // vm = ViewModel
// Właściwości przypisane do this
vm.title = 'Nowoczesny kontroler';
vm.items = [];
vm.loading = false;
// Metody publiczne
vm.loadItems = loadItems;
vm.addItem = addItem;
vm.deleteItem = deleteItem;
// Inicjalizacja
activate();
////////////////
function activate() {
// Kod inicjalizacyjny
vm.loadItems();
}
function loadItems() {
vm.loading = true;
return $http.get('/api/items')
.then(function(response) {
vm.items = response.data;
return vm.items;
})
.finally(function() {
vm.loading = false;
});
}
function addItem(item) {
vm.items.push(item);
}
function deleteItem(index) {
vm.items.splice(index, 1);
}
});
// Przykład z zagnieżdżonymi kontrolerami
app.controller('ParentCtrl', function() {
var vm = this;
vm.name = 'Kontroler nadrzędny';
vm.parentData = { value: 100 };
vm.parentMethod = function() {
console.log('Metoda rodzica');
};
});
app.controller('ChildCtrl', function() {
var vm = this;
vm.name = 'Kontroler potomny';
vm.childData = { value: 50 };
vm.childMethod = function() {
console.log('Metoda dziecka');
};
});
// Controller as z dependency injection
app.controller('UserController', UserController);
UserController.$inject = ['$http', '$log', 'userService'];
function UserController($http, $log, userService) {
var vm = this;
// Publiczne właściwości
vm.user = null;
vm.users = [];
vm.searchQuery = '';
// Publiczne metody
vm.getUser = getUser;
vm.saveUser = saveUser;
vm.searchUsers = searchUsers;
// Inicjalizacja
activate();
////////////////
function activate() {
$log.info('UserController aktywowany');
getUser(1);
}
function getUser(userId) {
return userService.getUser(userId)
.then(function(user) {
vm.user = user;
$log.info('Załadowano użytkownika:', user);
return vm.user;
})
.catch(function(error) {
$log.error('Błąd ładowania użytkownika:', error);
});
}
function saveUser() {
return userService.saveUser(vm.user)
.then(function(savedUser) {
vm.user = savedUser;
$log.info('Zapisano użytkownika');
});
}
function searchUsers() {
// Prywatna metoda pomocnicza
return filterUsers(vm.searchQuery);
}
// Metoda prywatna (nie przypisana do vm)
function filterUsers(query) {
if (!query) return vm.users;
return vm.users.filter(function(user) {
return user.name.toLowerCase().indexOf(query.toLowerCase()) > -1;
});
}
}
// Controller as z $scope (gdy potrzebny jest $watch)
app.controller('HybridController', function($scope) {
var vm = this;
vm.formData = {
email: '',
password: ''
};
vm.isValid = false;
// Nadal możemy używać $scope dla $watch
$scope.$watch(function() {
return vm.formData.email;
}, function(newValue) {
vm.isValid = validateEmail(newValue);
});
function validateEmail(email) {
var regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
});
// Controller as w dyrektywie
app.directive('userCard', function() {
return {
restrict: 'E',
templateUrl: 'user-card.html',
controller: UserCardController,
controllerAs: 'card',
bindToController: true, // Binduje scope properties do kontrolera
scope: {
user: '=',
onDelete: '&'
}
};
function UserCardController() {
var card = this;
// user jest dostępne przez bindToController
card.getFullName = function() {
return card.user.firstName + ' ' + card.user.lastName;
};
card.handleDelete = function() {
if (confirm('Czy na pewno usunąć?')) {
card.onDelete({ user: card.user });
}
};
}
});
// Przykład migracji z ES5 do ES6 class
app.controller('ES6Controller', ES6Controller);
class ES6Controller {
constructor($http, $q) {
this.$http = $http;
this.$q = $q;
// Właściwości
this.data = [];
this.loading = false;
// Auto-bind metod (jeśli potrzebne)
this.loadData = this.loadData.bind(this);
}
$onInit() {
// Lifecycle hook (Angular 1.5+)
this.loadData();
}
loadData() {
this.loading = true;
return this.$http.get('/api/data')
.then(response => {
this.data = response.data;
})
.finally(() => {
this.loading = false;
});
}
processData() {
// Metody ES6
return this.data.map(item => ({
...item,
processed: true
}));
}
}
ES6Controller.$inject = ['$http', '$q'];
<!-- Tradycyjne podejście -->
<div ng-controller="TraditionalController">
<h1>{{title}}</h1>
<button ng-click="loadItems()">Załaduj</button>
<ul>
<li ng-repeat="item in items">{{item.name}}</li>
</ul>
</div>
<!-- Controller as syntax -->
<div ng-controller="ModernController as vm">
<h1>{{vm.title}}</h1>
<button ng-click="vm.loadItems()">Załaduj</button>
<div ng-show="vm.loading">Ładowanie...</div>
<ul>
<li ng-repeat="item in vm.items">{{item.name}}</li>
</ul>
</div>
<!-- Zagnieżdżone kontrolery - brak konfliktu nazw -->
<div ng-controller="ParentCtrl as parent">
<h2>{{parent.name}}</h2>
<p>Wartość: {{parent.parentData.value}}</p>
<div ng-controller="ChildCtrl as child">
<h3>{{child.name}}</h3>
<p>Wartość: {{child.childData.value}}</p>
<!-- Jasne rozróżnienie - parent vs child -->
<button ng-click="parent.parentMethod()">Metoda rodzica</button>
<button ng-click="child.childMethod()">Metoda dziecka</button>
</div>
</div>
<!-- Controller as z formularzem -->
<div ng-controller="UserController as user">
<form name="userForm">
<input ng-model="user.formData.email" required>
<input type="password" ng-model="user.formData.password" required>
<button ng-click="user.saveUser()" ng-disabled="!userForm.$valid">
Zapisz
</button>
</form>
<div class="search">
<input ng-model="user.searchQuery" placeholder="Szukaj użytkowników">
<ul>
<li ng-repeat="u in user.searchUsers()">{{u.name}}</li>
</ul>
</div>
</div>
<!-- Dyrektywa z controller as -->
<user-card
user="currentUser"
on-delete="handleUserDelete(user)">
</user-card>
<!-- Template dyrektywy: user-card.html -->
<script type="text/ng-template" id="user-card.html">
<div class="card">
<h3>{{card.getFullName()}}</h3>
<p>{{card.user.email}}</p>
<button ng-click="card.handleDelete()">Usuń</button>
</div>
</script>
<!-- Controller as w routes (ui-router) -->
<!--
$stateProvider.state('users', {
url: '/users',
templateUrl: 'users.html',
controller: 'UserController',
controllerAs: 'vm'
});
-->
Materiały
- AngularJS API - ngController
- AngularJS Style Guide - Controller As Syntax
- Exploring the Angular 1.5 .component() method
AngularJS - Sekcja 4: Serwisy i Fabryki
21. Jaka jest różnica między service, factory a provider?
Odpowiedź w 30 sekund:
Service tworzy instancję używając new, factory zwraca obiekt z funkcji, a provider jest najbardziej elastyczny i pozwala na konfigurację przed inicjalizacją aplikacji. Wszystkie trzy są singletonami i służą do współdzielenia logiki między komponentami.
Odpowiedź w 2 minuty: Service, factory i provider to trzy sposoby definiowania usług w AngularJS, różniące się głównie sposobem tworzenia i konfigurowalnością.
Service jest konstruktorem - AngularJS wywołuje go z operatorem new. Definiujemy go jak klasę JavaScript, używając this do dodawania metod i właściwości. Jest to najprostszy sposób, gdy chcemy tworzyć obiekty w stylu obiektowym.
Factory to funkcja, która zwraca obiekt. Daje większą elastyczność niż service, ponieważ możemy zwrócić dowolny typ wartości - obiekt, funkcję, czy nawet prymityw. Factory pozwala na użycie wzorca revealing module pattern i jest częściej używany w praktyce.
Provider to najbardziej złożony, ale i najbardziej elastyczny mechanizm. Jest jedynym z trzech, który można konfigurować w fazie config aplikacji, przed jej uruchomieniem. Provider musi zawierać metodę $get, która zwraca faktyczną implementację usługi. Używamy go, gdy potrzebujemy konfiguracji przed inicjalizacją (np. ustawienie URL API).
Pod maską, wszystkie trzy są providerami - service i factory to tylko wygodniejsze API. AngularJS przekształca service i factory w providery automatycznie.
Przykład kodu:
// Service - używa konstruktora
app.service('UserService', function($http) {
this.getUser = function(id) {
return $http.get('/api/users/' + id);
};
this.currentUser = null;
});
// Factory - zwraca obiekt
app.factory('UserFactory', function($http) {
var currentUser = null;
return {
getUser: function(id) {
return $http.get('/api/users/' + id);
},
getCurrentUser: function() {
return currentUser;
},
setCurrentUser: function(user) {
currentUser = user;
}
};
});
// Provider - pozwala na konfigurację
app.provider('UserProvider', function() {
var apiUrl = '/api'; // Domyślna wartość
// Metoda konfiguracyjna (dostępna w app.config)
this.setApiUrl = function(url) {
apiUrl = url;
};
// Metoda $get zwraca faktyczną implementację
this.$get = function($http) {
return {
getUser: function(id) {
return $http.get(apiUrl + '/users/' + id);
}
};
};
});
// Konfiguracja providera przed inicjalizacją aplikacji
app.config(function(UserProviderProvider) {
UserProviderProvider.setApiUrl('https://api.example.com');
});
// Użycie w kontrolerze - wszystkie trzy wyglądają podobnie
app.controller('MyCtrl', function(UserService, UserFactory, UserProvider) {
UserService.getUser(1).then(function(response) {
console.log('Service:', response.data);
});
UserFactory.getUser(1).then(function(response) {
console.log('Factory:', response.data);
});
UserProvider.getUser(1).then(function(response) {
console.log('Provider:', response.data);
});
});
Materiały:
- AngularJS Developer Guide - Providers
- Stack Overflow - Service vs Factory vs Provider
- Todd Motto - Factory vs Service
AngularJS - Sekcja 6: Routing
31. Jak skonfigurować routing za pomocą ngRoute?
Odpowiedź w 30 sekund:
ngRoute to oficjalny moduł routingu w AngularJS, który wymaga dodania zależności ngRoute do aplikacji i skonfigurowania tras za pomocą $routeProvider w bloku konfiguracyjnym. Każda trasa definiuje wzorzec URL, szablon (template) oraz kontroler, a dyrektywa ng-view w HTML określa miejsce renderowania widoków.
Odpowiedź w 2 minuty:
Konfiguracja routingu za pomocą ngRoute rozpoczyna się od dodania modułu angular-route.js do projektu oraz zadeklarowania zależności ngRoute w głównym module aplikacji. W bloku konfiguracyjnym używamy serwisu $routeProvider do definiowania tras - każda trasa mapuje wzorzec URL na konkretny szablon i kontroler.
Podstawowa konfiguracja obejmuje metodę .when() dla definiowania tras oraz .otherwise() dla trasy domyślnej (fallback). Każda trasa może zawierać właściwości takie jak template, templateUrl, controller, controllerAs, oraz resolve dla asynchronicznego ładowania danych przed aktywacją trasy.
W szablonie HTML używamy dyrektywy <ng-view> lub <div ng-view></div>, która działa jako placeholder - to tutaj AngularJS będzie renderować widoki odpowiadające aktualnej trasie. Nawigacja między trasami odbywa się poprzez linki z atrybutem href zawierającym hash (#) oraz ścieżkę, lub programowo za pomocą serwisu $location.
ngRoute monitoruje zmiany w URL (używając hash-based routing lub HTML5 mode) i automatycznie ładuje odpowiedni szablon z kontrolerem, zapewniając synchronizację między adresem URL a stanem aplikacji.
Przykład kodu:
// Dodanie zależności ngRoute do modułu
var app = angular.module('myApp', ['ngRoute']);
// Konfiguracja tras
app.config(function($routeProvider) {
$routeProvider
.when('/home', {
templateUrl: 'views/home.html',
controller: 'HomeController',
controllerAs: 'vm'
})
.when('/users', {
templateUrl: 'views/users.html',
controller: 'UsersController'
})
.when('/users/:userId', {
templateUrl: 'views/user-detail.html',
controller: 'UserDetailController',
resolve: {
// Załadowanie danych przed aktywacją trasy
user: function($route, UserService) {
return UserService.getUser($route.current.params.userId);
}
}
})
.when('/about', {
template: '<h1>O nas</h1><p>Informacje o aplikacji</p>'
})
.otherwise({
redirectTo: '/home'
});
});
// Kontroler wykorzystujący parametry trasy
app.controller('UserDetailController', function($routeParams, user) {
var vm = this;
vm.userId = $routeParams.userId;
vm.user = user; // Dane załadowane przez resolve
});
<!-- Index.html z ng-view -->
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<script src="angular.js"></script>
<script src="angular-route.js"></script>
</head>
<body>
<nav>
<a href="#/home">Strona główna</a>
<a href="#/users">Użytkownicy</a>
<a href="#/about">O nas</a>
</nav>
<!-- Tutaj będą renderowane widoki -->
<div ng-view></div>
</body>
</html>
Materiały:
↑ Powrót na góręAngularJS - Sekcja 8: Cykl życia i Wydajność
41. Jak działa cykl $digest w AngularJS?
Odpowiedź w 30 sekund: Cykl $digest to mechanizm AngularJS, który sprawdza wszystkie obserwowane wyrażenia ($watch) w celu wykrycia zmian w modelu. Gdy dane się zmieniają, AngularJS uruchamia cykl digest, który iteracyjnie sprawdza wszystkie watchers aż do momentu, gdy nie wykryje już żadnych zmian lub osiągnie limit iteracji (domyślnie 10).
Odpowiedź w 2 minuty:
Cykl $digest jest sercem mechanizmu two-way data binding w AngularJS. Proces rozpoczyna się gdy Angular wykryje zdarzenie (kliknięcie, zmiana input, $timeout, $http itp.) i wywołuje $scope.$apply(), która z kolei wywołuje $scope.$digest().
W trakcie cyklu digest, Angular przechodzi przez wszystkie zarejestrowane watchers na danym scope i jego scope'ach potomnych. Dla każdego watchera porównuje obecną wartość wyrażenia z poprzednią wartością. Jeśli wartość się zmieniła, wykonuje odpowiednią funkcję listener. Po przejściu przez wszystkie watchers, Angular rozpoczyna kolejną iterację sprawdzając czy któryś listener nie zmienił innych wartości.
Proces ten powtarza się aż do osiągnięcia stanu stabilnego (żadne wartości się nie zmieniają) lub do przekroczenia limitu TTL (Time To Live, domyślnie 10 iteracji). Przekroczenie limitu skutkuje błędem "10 $digest() iterations reached", co sygnalizuje nieskończoną pętlę w watcherach.
Cykl $digest działa na konkretnym scope i jego dzieciach. Jeśli potrzebujemy uruchomić digest na całej aplikacji (root scope), używamy $rootScope.$apply(). Należy pamiętać, że cykl digest to kosztowna operacja, więc nadmierna liczba watcherów (powyżej 2000) może znacząco obniżyć wydajność aplikacji.
Przykład kodu:
// Podstawowy przykład działania $digest
angular.module('myApp').controller('DigestController', function($scope, $timeout) {
$scope.counter = 0;
$scope.message = 'Początkowa wartość';
// Dodanie watchera - będzie sprawdzany podczas każdego cyklu $digest
$scope.$watch('counter', function(newVal, oldVal) {
if (newVal !== oldVal) {
console.log('Counter zmienił się z ' + oldVal + ' na ' + newVal);
$scope.message = 'Counter: ' + newVal;
}
});
// Operacja poza kontekstem Angular - wymaga ręcznego $apply
setTimeout(function() {
$scope.counter = 5; // Zmiana nie zostanie wykryta
$scope.$apply(); // Ręczne uruchomienie cyklu $digest
}, 1000);
// Operacja w kontekście Angular - automatyczne wywołanie $digest
$timeout(function() {
$scope.counter = 10; // Zmiana zostanie automatycznie wykryta
}, 2000);
// Przykład problematycznego watchera prowadzącego do nieskończonej pętli
$scope.$watch('message', function(newVal) {
// UWAGA: To spowoduje błąd "10 $digest() iterations reached"
// $scope.message = newVal + Math.random();
});
});
// Przykład użycia $digest zamiast $apply (na konkretnym scope)
angular.module('myApp').directive('customDirective', function() {
return {
link: function(scope, element, attrs) {
element.on('click', function() {
scope.value = Math.random();
// $digest() sprawdza tylko ten scope i jego dzieci
scope.$digest();
// $apply() sprawdziłby cały root scope - bardziej kosztowne
});
}
};
});
// Debugowanie cyklu $digest
angular.module('myApp').run(function($rootScope) {
let digestCount = 0;
// Monitorowanie każdego cyklu digest
$rootScope.$watch(function() {
digestCount++;
console.log('Cykl $digest nr: ' + digestCount);
});
});
Diagram:
flowchart TD
A[Zdarzenie: click, HTTP, timeout] --> B[$scope.$apply wywołane]
B --> C[Rozpoczęcie cyklu $digest]
C --> D[Przejdź przez wszystkie watchers]
D --> E{Czy wartość się zmieniła?}
E -->|Tak| F[Wykonaj listener function]
E -->|Nie| G[Następny watcher]
F --> G
G --> H{Czy są jeszcze watchers?}
H -->|Tak| D
H -->|Nie| I{Czy któraś wartość się zmieniła w tej iteracji?}
I -->|Tak| J{Liczba iteracji < 10?}
I -->|Nie| K[Koniec: stan stabilny]
J -->|Tak| D
J -->|Nie| L[BŁĄD: 10 iterations reached]
style A fill:#e1f5ff
style K fill:#c8e6c9
style L fill:#ffcdd2
Materiały:
- AngularJS Developer Guide - Scopes
- Understanding the $digest Cycle - Angular Blog
- AngularJS $digest and $apply Explained
AngularJS - Sekcja 3: Dyrektywy
13. Czym są dyrektywy w AngularJS i jakie są ich typy?
Odpowiedź w 30 sekund: Dyrektywy to markery w DOM (atrybuty, elementy, klasy lub komentarze), które mówią kompilatorowi HTML AngularJS, aby dołączył określone zachowanie do elementu DOM lub przekształcił element DOM i jego potomków. AngularJS posiada cztery typy dyrektyw: element (E), atrybut (A), klasa (C) i komentarz (M).
Odpowiedź w 2 minuty: Dyrektywy są jedną z najpotężniejszych funkcji AngularJS, umożliwiających rozszerzenie HTML o nowe atrybuty i elementy. Służą do tworzenia wielokrotnie używalnych komponentów i manipulacji DOM w sposób deklaratywny. Kompilator AngularJS przechodzi przez DOM, szukając dyrektyw i wykonując związaną z nimi logikę.
Istnieją cztery typy dyrektyw określone przez właściwość restrict: 'E' (Element) - dyrektywa jest używana jako element HTML (<moja-dyrektywa></moja-dyrektywa>), 'A' (Attribute) - dyrektywa jest używana jako atrybut elementu (<div moja-dyrektywa></div>), 'C' (Class) - dyrektywa jest używana jako klasa CSS (<div class="moja-dyrektywa"></div>), oraz 'M' (Comment) - dyrektywa jest używana jako komentarz HTML (<!-- directive: moja-dyrektywa -->).
Najczęściej używane są typy 'E' i 'A'. Typ 'E' jest preferowany dla komponentów samodzielnych, podczas gdy 'A' jest używany do dekorowania istniejących elementów dodatkową funkcjonalnością. AngularJS dostarcza wiele wbudowanych dyrektyw (ng-model, ng-repeat, ng-if, ng-show), które znacznie upraszczają typowe zadania w aplikacjach.
Dyrektywy mogą również definiować własny scope (izolowany lub dziedziczony), szablon HTML, funkcje kompilacji i linkowania, oraz wymagania dotyczące innych dyrektyw. Ta elastyczność czyni je potężnym narzędziem do budowania modularnych i wielokrotnie używalnych komponentów UI.
Przykład kodu:
// Przykłady różnych typów dyrektyw
angular.module('myApp', [])
// Dyrektywa jako element (E)
.directive('mojaKarta', function() {
return {
restrict: 'E',
template: '<div class="karta"><h3>{{tytul}}</h3></div>',
scope: {
tytul: '@'
}
};
})
// Dyrektywa jako atrybut (A)
.directive('podswietl', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('mouseenter', function() {
element.css('background-color', attrs.podswietl);
});
element.on('mouseleave', function() {
element.css('background-color', '');
});
}
};
})
// Dyrektywa z wieloma typami (EA)
.directive('przycisk', function() {
return {
restrict: 'EA', // Może być używana jako element lub atrybut
template: '<button ng-transclude></button>',
transclude: true
};
});
Użycie w HTML:
<!-- Dyrektywa jako element -->
<moja-karta tytul="Witaj"></moja-karta>
<!-- Dyrektywa jako atrybut -->
<div podswietl="yellow">Najedź na mnie</div>
<!-- Dyrektywa EA jako element -->
<przycisk>Kliknij</przycisk>
<!-- Dyrektywa EA jako atrybut -->
<div przycisk>Kliknij</div>
Materiały:
- AngularJS Developer Guide - Directives
- AngularJS API Reference - directive
- Understanding Directives in AngularJS
AngularJS - Sekcja 5: Filtry
27. Czym są filtry w AngularJS i jak ich używać?
Odpowiedź w 30 sekund: Filtry w AngularJS to mechanizm formatowania i transformacji danych wyświetlanych w widokach. Używa się ich poprzez operator pipe (|) w wyrażeniach lub poprzez serwis $filter w kontrolerach. Pozwalają na formatowanie tekstu, liczb, dat i filtrowanie kolekcji bez modyfikowania oryginalnych danych.
Odpowiedź w 2 minuty: Filtry w AngularJS są funkcjami transformującymi dane przed ich wyświetleniem w widoku. Stanowią czysty sposób formatowania danych, ponieważ nie modyfikują oryginalnych wartości - tworzą jedynie sformatowaną wersję do prezentacji. AngularJS dostarcza zestaw wbudowanych filtrów do typowych operacji, ale umożliwia również tworzenie własnych.
W szablonach filtry stosuje się za pomocą operatora pipe (|), który przekazuje wartość po lewej stronie do filtra po prawej. Filtry mogą przyjmować parametry, które oddziela się dwukropkiem. Można również łączyć wiele filtrów w łańcuch, gdzie wynik jednego filtra staje się wejściem dla kolejnego. Jest to potężny mechanizm pozwalający na budowanie złożonych transformacji danych.
Filtry są szczególnie użyteczne przy formatowaniu dat do określonego formatu, konwersji tekstu na wielkie lub małe litery, formatowaniu liczb jako walut, oraz przy filtrowaniu i sortowaniu list danych. Dzięki temu logika prezentacji jest oddzielona od logiki biznesowej, co czyni kod bardziej czytelnym i łatwiejszym w utrzymaniu.
Ważną cechą filtrów jest ich czystość (pure functions) - dla tych samych danych wejściowych zawsze zwracają ten sam wynik, co pozwala AngularJS na optymalizację ich wykonywania.
Przykład kodu:
// Kontroler z danymi
app.controller('FilterController', function($scope) {
$scope.cena = 1234.56;
$scope.data = new Date();
$scope.imie = 'jan kowalski';
$scope.produkty = [
{ nazwa: 'Laptop', cena: 2500, kategoria: 'elektronika' },
{ nazwa: 'Mysz', cena: 50, kategoria: 'elektronika' },
{ nazwa: 'Książka', cena: 35, kategoria: 'literatura' }
];
});
<!-- Użycie filtrów w szablonie -->
<div ng-controller="FilterController">
<!-- Filtr currency - formatowanie waluty -->
<p>Cena: {{ cena | currency:'PLN':2 }}</p>
<!-- Filtr date - formatowanie daty -->
<p>Data: {{ data | date:'dd.MM.yyyy HH:mm' }}</p>
<!-- Filtr uppercase - konwersja na wielkie litery -->
<p>Imię: {{ imie | uppercase }}</p>
<!-- Łańcuch filtrów -->
<p>Tytuł: {{ imie | lowercase | uppercase }}</p>
<!-- Filtr filter - filtrowanie kolekcji -->
<ul>
<li ng-repeat="produkt in produkty | filter:{kategoria:'elektronika'}">
{{ produkt.nazwa }} - {{ produkt.cena | currency:'PLN' }}
</li>
</ul>
<!-- Filtr orderBy - sortowanie -->
<ul>
<li ng-repeat="produkt in produkty | orderBy:'cena'">
{{ produkt.nazwa }} - {{ produkt.cena }}
</li>
</ul>
<!-- Kombinacja filtrów -->
<ul>
<li ng-repeat="produkt in produkty | filter:{kategoria:'elektronika'} | orderBy:'-cena' | limitTo:2">
{{ produkt.nazwa }}
</li>
</ul>
</div>
Materiały:
- AngularJS Official Documentation - Filters
- AngularJS API Reference - filter component
- W3Schools AngularJS Filters Tutorial
AngularJS - Sekcja 7: Formularze i Walidacja
36. Jak działa ng-model i walidacja formularzy w AngularJS?
Odpowiedź w 30 sekund:
ng-model tworzy dwukierunkowe wiązanie danych między kontrolką formularza a modelem w scope, automatycznie synchronizując wartości. AngularJS dodaje do formularzy i kontrolek właściwości walidacyjne ($valid, $invalid, $pristine, $dirty, $touched, $untouched) oraz integruje się z HTML5 validators, umożliwiając deklaratywną walidację bez JavaScript.
Odpowiedź w 2 minuty:
Dyrektywa ng-model jest fundamentem systemu formularzy w AngularJS - tworzy instancję NgModelController, który zarządza dwukierunkowym wiązaniem danych między widokiem a modelem. Każda kontrolka z ng-model automatycznie otrzymuje zestaw właściwości kontrolujących jej stan: $pristine/$dirty (czy użytkownik zmodyfikował wartość), $touched/$untouched (czy użytkownik wszedł w interakcję z kontrolką), oraz $valid/$invalid (czy wartość przeszła wszystkie walidacje).
AngularJS automatycznie dodaje klasy CSS odpowiadające tym stanom (np. ng-valid, ng-invalid, ng-dirty), co umożliwia wizualne oznaczanie błędów. System walidacji jest zintegrowany z HTML5 validators (required, pattern, min, max) oraz oferuje dedykowane dyrektywy AngularJS (ng-required, ng-minlength, ng-maxlength, ng-pattern). Walidacja wykonuje się automatycznie przy każdej zmianie wartości.
Gdy formularz posiada atrybut name, AngularJS tworzy dla niego obiekt FormController w scope, który agreguje stany wszystkich kontrolek - dzięki temu możemy łatwo sprawdzić $valid całego formularza. NgModelController aktualizuje wartość w modelu tylko wtedy, gdy wszystkie walidatory zwrócą sukces (chyba że skonfigurujemy ng-model-options="{allowInvalid: true}"). System ten pozwala na deklaratywne definiowanie reguł walidacji bezpośrednio w HTML, co czyni kod bardziej czytelnym i łatwiejszym w utrzymaniu.
Przykład kodu:
// Kontroler z modelem użytkownika
angular.module('formApp', [])
.controller('FormController', function($scope) {
$scope.user = {
name: '',
email: '',
age: null
};
$scope.submitForm = function(isValid) {
if (isValid) {
console.log('Formularz wysłany:', $scope.user);
// Resetowanie formularza
$scope.user = {};
$scope.userForm.$setPristine();
$scope.userForm.$setUntouched();
} else {
console.log('Formularz zawiera błędy');
}
};
});
<!-- Formularz z walidacją -->
<form name="userForm" ng-controller="FormController"
ng-submit="submitForm(userForm.$valid)" novalidate>
<!-- Pole nazwa z walidacją -->
<div ng-class="{'has-error': userForm.name.$invalid && userForm.name.$dirty}">
<label>Imię:</label>
<input type="text"
name="name"
ng-model="user.name"
required
ng-minlength="3"
ng-maxlength="50">
<!-- Komunikaty błędów -->
<span ng-show="userForm.name.$error.required && userForm.name.$dirty">
Imię jest wymagane
</span>
<span ng-show="userForm.name.$error.minlength">
Imię musi mieć minimum 3 znaki
</span>
</div>
<!-- Pole email z walidacją HTML5 -->
<div ng-class="{'has-error': userForm.email.$invalid && userForm.email.$touched}">
<label>Email:</label>
<input type="email"
name="email"
ng-model="user.email"
required>
<span ng-show="userForm.email.$error.email && userForm.email.$dirty">
Nieprawidłowy format email
</span>
</div>
<!-- Pole wiek z zakresem -->
<div>
<label>Wiek:</label>
<input type="number"
name="age"
ng-model="user.age"
min="18"
max="100">
<span ng-show="userForm.age.$error.min">
Minimalny wiek to 18 lat
</span>
<span ng-show="userForm.age.$error.max">
Maksymalny wiek to 100 lat
</span>
</div>
<!-- Przycisk submit - aktywny tylko gdy formularz jest poprawny -->
<button type="submit" ng-disabled="userForm.$invalid">
Wyślij
</button>
<!-- Debug: wyświetlanie stanu formularza -->
<div class="debug-info">
<p>Formularz poprawny: {{userForm.$valid}}</p>
<p>Formularz zmieniony: {{userForm.$dirty}}</p>
<p>Użytkownik interagował: {{userForm.$touched}}</p>
</div>
</form>
/* Style dla stanów walidacji */
.has-error input {
border-color: #d9534f;
background-color: #fff0f0;
}
input.ng-valid.ng-dirty {
border-color: #5cb85c;
}
input.ng-invalid.ng-dirty {
border-color: #d9534f;
}
span.ng-show {
color: #d9534f;
font-size: 12px;
}
Materiały:
↑ Powrót na góręAngularJS - Sekcja 9: Testowanie
46. Jak testować kontrolery w AngularJS z Jasmine?
Odpowiedź w 30 sekund:
Testowanie kontrolerów w AngularJS z Jasmine wymaga użycia modułu ngMock oraz serwisu $controller do instancjonowania kontrolera w testach. Przed każdym testem ładujemy moduł aplikacji za pomocą module(), następnie wstrzykujemy zależności przez inject() i tworzymy instancję kontrolera z mockowymi zależnościami.
Odpowiedź w 2 minuty:
Testowanie kontrolerów w AngularJS opiera się na frameworku Jasmine oraz bibliotece Angular Mocks (angular-mocks.js). Proces testowania rozpoczyna się od załadowania modułu aplikacji za pomocą funkcji module() w bloku beforeEach. Następnie używamy funkcji inject() do wstrzyknięcia niezbędnych serwisów, w tym $controller, który służy do tworzenia instancji testowanego kontrolera.
Kluczowym aspektem jest przygotowanie scope'a ($rootScope.$new()) oraz mockowanie wszelkich zależności kontrolera. Dzięki temu możemy testować logikę kontrolera w izolacji od reszty aplikacji. W testach weryfikujemy początkowy stan scope'a, zachowanie po wywołaniu metod kontrolera oraz reakcje na zmiany danych.
Jasmine dostarcza funkcje describe() do grupowania testów, it() do definiowania pojedynczych przypadków testowych oraz expect() do asercji. Framework ngMock automatycznie synchronizuje cykl digestu AngularJS, co upraszcza testowanie operacji asynchronicznych. Istotne jest również mockowanie serwisów HTTP i innych zależności zewnętrznych, aby testy były deterministyczne i szybkie.
Przykład kodu:
// Definicja kontrolera
angular.module('myApp', [])
.controller('UserController', function($scope, UserService) {
$scope.users = [];
$scope.loading = false;
$scope.loadUsers = function() {
$scope.loading = true;
UserService.getUsers().then(function(data) {
$scope.users = data;
$scope.loading = false;
});
};
$scope.deleteUser = function(userId) {
$scope.users = $scope.users.filter(function(user) {
return user.id !== userId;
});
};
});
// Testy kontrolera z Jasmine
describe('UserController', function() {
var $controller, $scope, $q, UserService;
// Załaduj moduł przed każdym testem
beforeEach(module('myApp'));
// Wstrzyknij zależności
beforeEach(inject(function(_$controller_, _$rootScope_, _$q_) {
$controller = _$controller_;
$scope = _$rootScope_.$new();
$q = _$q_;
// Mockowanie serwisu UserService
UserService = {
getUsers: jasmine.createSpy('getUsers').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve([
{ id: 1, name: 'Jan Kowalski' },
{ id: 2, name: 'Anna Nowak' }
]);
return deferred.promise;
})
};
// Utworzenie instancji kontrolera
$controller('UserController', {
$scope: $scope,
UserService: UserService
});
}));
// Test początkowego stanu
it('powinien zainicjalizować pustą tablicę użytkowników', function() {
expect($scope.users).toEqual([]);
expect($scope.loading).toBe(false);
});
// Test ładowania użytkowników
it('powinien załadować użytkowników z serwisu', function() {
$scope.loadUsers();
expect($scope.loading).toBe(true);
expect(UserService.getUsers).toHaveBeenCalled();
// Wywołaj cykl digestu aby rozwiązać promise
$scope.$digest();
expect($scope.users.length).toBe(2);
expect($scope.users[0].name).toBe('Jan Kowalski');
expect($scope.loading).toBe(false);
});
// Test usuwania użytkownika
it('powinien usunąć użytkownika z listy', function() {
$scope.users = [
{ id: 1, name: 'Jan Kowalski' },
{ id: 2, name: 'Anna Nowak' }
];
$scope.deleteUser(1);
expect($scope.users.length).toBe(1);
expect($scope.users[0].id).toBe(2);
});
// Test walidacji
it('nie powinien modyfikować tablicy przy usuwaniu nieistniejącego użytkownika', function() {
$scope.users = [{ id: 1, name: 'Jan Kowalski' }];
$scope.deleteUser(999);
expect($scope.users.length).toBe(1);
});
});
Materiały:
↑ Powrót na góręSekcja 10: Migracja i Dobre Praktyki
50. Jakie są najlepsze praktyki przy pisaniu aplikacji AngularJS?
Odpowiedź w 30 sekund:
Najlepsze praktyki AngularJS obejmują stosowanie komponentowej architektury, unikanie nadmiernego użycia $scope, stosowanie wzorca "Controller As", organizację kodu według funkcji (nie typu), używanie serwisów do logiki biznesowej i komunikacji z API, oraz przygotowanie kodu do przyszłej migracji na nowsze wersje Angular. Kluczowe jest także dbanie o wydajność przez ograniczanie watchers i stosowanie one-time binding gdzie to możliwe.
Odpowiedź w 2 minuty:
Struktura i organizacja kodu: Organizuj kod według funkcji biznesowych (features), a nie według typu plików. Struktura powinna odzwierciedlać domenę aplikacji, gdzie każdy moduł zawiera wszystkie niezbędne komponenty (kontrolery, serwisy, dyrektywy, style, testy). Stosuj wzorzec modułowy - dziel aplikację na małe, reużywalne moduły z jasno określonymi zależnościami. Używaj IIFE (Immediately Invoked Function Expression) do izolacji scope i unikaj zanieczyszczania globalnej przestrzeni nazw.
Kontrolery i zarządzanie stanem:
Stosuj wzorzec "Controller As" zamiast wstrzykiwania $scope - to czyni kod bardziej czytelnym i zbliża do komponentowej architektury Angular 2+. Kontrolery powinny być lekkie, zawierać tylko logikę prezentacji i delegować logikę biznesową do serwisów. Unikaj manipulacji DOM w kontrolerach - do tego służą dyrektywy. Używaj one-time binding (::) dla danych, które nie zmieniają się po pierwszym renderowaniu, aby zredukować liczbę watchers.
Serwisy i wzorce projektowe:
Używaj serwisów (factories, services) do współdzielenia danych i logiki biznesowej między kontrolerami. Stosuj wzorzec Repository dla komunikacji z API, co ułatwia testowanie i wymianę implementacji. Preferuj factories nad services dla większej elastyczności. Unikaj bezpośredniego użycia $http w kontrolerach - opakuj wszystkie wywołania HTTP w dedykowane serwisy. Implementuj mechanizmy cache'owania dla często używanych danych.
Wydajność i bezpieczeństwo:
Minimalizuj liczbę watchers - każdy watch ma wpływ na wydajność digest cycle. Unikaj watch na dużych obiektach i używaj $watchCollection lub $watch z trzecim parametrem true tylko gdy to konieczne. Stosuj track by w ng-repeat dla lepszej wydajności. Zawsze używaj $sce i ng-bind-html dla bezpiecznego renderowania HTML. Waliduj dane po stronie klienta i serwera. Przygotuj aplikację do migracji stosując komponenty zamiast dyrektyw oraz TypeScript zamiast czystego JavaScript.
Przykład kodu:
// Dobra struktura modułu według funkcji
(function() {
'use strict';
angular
.module('app.users', [
'ui.router',
'app.core'
])
.config(configureRoutes);
configureRoutes.$inject = ['$stateProvider'];
function configureRoutes($stateProvider) {
$stateProvider
.state('users', {
url: '/users',
component: 'userList'
});
}
})();
// Komponent zamiast dyrektywy (łatwiejsza migracja)
(function() {
'use strict';
angular
.module('app.users')
.component('userList', {
templateUrl: 'app/users/user-list.html',
controller: UserListController,
bindings: {
initialFilter: '<'
}
});
UserListController.$inject = ['userService', '$log'];
function UserListController(userService, $log) {
var vm = this; // Controller As pattern
vm.$onInit = onInit;
vm.loadUsers = loadUsers;
vm.deleteUser = deleteUser;
function onInit() {
loadUsers();
}
function loadUsers() {
vm.loading = true;
userService.getAll()
.then(function(users) {
vm.users = users;
})
.catch(function(error) {
$log.error('Błąd ładowania użytkowników:', error);
})
.finally(function() {
vm.loading = false;
});
}
function deleteUser(userId) {
userService.remove(userId)
.then(function() {
loadUsers();
});
}
}
})();
// Serwis z wzorcem Repository
(function() {
'use strict';
angular
.module('app.users')
.factory('userService', userService);
userService.$inject = ['$http', '$q', '$log'];
function userService($http, $q, $log) {
var apiUrl = '/api/users';
var cache = null;
var service = {
getAll: getAll,
getById: getById,
create: create,
update: update,
remove: remove,
clearCache: clearCache
};
return service;
function getAll() {
if (cache) {
return $q.resolve(cache);
}
return $http.get(apiUrl)
.then(function(response) {
cache = response.data;
return cache;
})
.catch(function(error) {
$log.error('Błąd pobierania użytkowników:', error);
return $q.reject(error);
});
}
function getById(id) {
return $http.get(apiUrl + '/' + id)
.then(function(response) {
return response.data;
});
}
function create(user) {
return $http.post(apiUrl, user)
.then(function(response) {
clearCache();
return response.data;
});
}
function update(id, user) {
return $http.put(apiUrl + '/' + id, user)
.then(function(response) {
clearCache();
return response.data;
});
}
function remove(id) {
return $http.delete(apiUrl + '/' + id)
.then(function(response) {
clearCache();
return response.data;
});
}
function clearCache() {
cache = null;
}
}
})();
// Template z one-time binding i track by
/*
<div class="user-list">
<h2>{{ ::vm.title }}</h2>
<div ng-if="vm.loading">Ładowanie...</div>
<ul ng-if="!vm.loading">
<li ng-repeat="user in vm.users track by user.id">
{{ ::user.name }} - {{ user.email }}
<button ng-click="vm.deleteUser(user.id)">Usuń</button>
</li>
</ul>
</div>
*/
// Konfiguracja aplikacji z best practices
(function() {
'use strict';
angular
.module('app', [
// Moduły zewnętrzne
'ui.router',
'ngAnimate',
// Moduły aplikacji
'app.core',
'app.users',
'app.products'
])
.config(configureApp)
.run(runApp);
configureApp.$inject = ['$compileProvider', '$logProvider'];
function configureApp($compileProvider, $logProvider) {
// Wyłącz debug info w produkcji dla lepszej wydajności
$compileProvider.debugInfoEnabled(false);
// Wyłącz logi w produkcji
$logProvider.debugEnabled(false);
// Whitelist dla bezpiecznych URL-i
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel):/);
}
runApp.$inject = ['$rootScope', '$log'];
function runApp($rootScope, $log) {
// Globalna obsługa błędów routingu
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
$log.error('Błąd zmiany stanu:', error);
});
}
})();
// Dyrektywa z best practices
(function() {
'use strict';
angular
.module('app.core')
.directive('focusOn', focusOn);
focusOn.$inject = ['$timeout'];
function focusOn($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.focusOn, function(newValue) {
if (newValue) {
$timeout(function() {
element[0].focus();
}, 0);
}
});
}
};
}
})();
// Filter z optymalizacją
(function() {
'use strict';
angular
.module('app.core')
.filter('capitalize', capitalize);
function capitalize() {
// Stateful filter dla lepszej wydajności (cache)
var cache = {};
return function(input) {
if (!input) return '';
if (cache[input]) return cache[input];
var result = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
cache[input] = result;
return result;
};
}
})();
// Interceptor dla globalnej obsługi HTTP
(function() {
'use strict';
angular
.module('app.core')
.factory('httpInterceptor', httpInterceptor)
.config(configureInterceptor);
httpInterceptor.$inject = ['$q', '$log'];
function httpInterceptor($q, $log) {
return {
request: function(config) {
// Dodaj token do każdego żądania
var token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
responseError: function(rejection) {
// Globalna obsługa błędów HTTP
if (rejection.status === 401) {
$log.warn('Nieautoryzowany dostęp');
// Przekieruj do logowania
} else if (rejection.status === 500) {
$log.error('Błąd serwera:', rejection);
}
return $q.reject(rejection);
}
};
}
configureInterceptor.$inject = ['$httpProvider'];
function configureInterceptor($httpProvider) {
$httpProvider.interceptors.push('httpInterceptor');
}
})();
Materiały
- AngularJS Style Guide by John Papa
- AngularJS Best Practices - Official Documentation
- Todd Motto's AngularJS Style Guide