Pytania rekrutacyjne AngularJS
50 pytań — 20 odpowiedzi dostępnych od razu
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:
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:
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:
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:
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:
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:
Kontrolery i Scope
7. Czym jest kontroler w AngularJS i jak go zdefiniować?
Odpowiedź w 30 sekund:
Kontroler w AngularJS to funkcja JavaScript, która definiuje logikę biznesową i zarządza danymi dla konkretnego widoku. Kontrolery są tworzone za pomocą metody controller() modułu i łączą model ($scope) z widokiem (HTML), umożliwiając dwukierunkowe wiązanie danych.
Odpowiedź w 2 minuty: Kontroler w AngularJS pełni rolę warstwy pośredniej między modelem danych a widokiem aplikacji. Jest to funkcja konstruktora, która otrzymuje wstrzyknięte zależności (najczęściej $scope) i definiuje właściwości oraz metody dostępne w widoku. Kontrolery odpowiadają za inicjalizację stanu aplikacji, reagowanie na zdarzenia użytkownika oraz komunikację z serwisami.
Definicja kontrolera odbywa się poprzez wywołanie metody controller() na module aplikacji, gdzie pierwszy argument to nazwa kontrolera, a drugi to funkcja konstruktora. Wstrzykiwanie zależności w AngularJS może być realizowane na trzy sposoby: inline annotation (tablica z nazwami zależności), $inject property, lub implicit annotation (nazwy parametrów - niezalecane w produkcji ze względu na minifikację).
Kontrolery powinny być małe i skoncentrowane na konkretnym zadaniu. Nie powinny manipulować DOM (to rola dyrektyw), wykonywać formatowania danych (filtry), ani współdzielić stanu między kontrolerami (serwisy). Dobrą praktyką jest używanie jednego kontrolera na widok lub komponent i delegowanie złożonej logiki do serwisów.
Przykład kodu:
// Definicja modułu aplikacji
var app = angular.module('myApp', []);
// Podstawowa definicja kontrolera
app.controller('UserController', function($scope) {
// Inicjalizacja właściwości modelu
$scope.user = {
name: 'Jan Kowalski',
email: 'jan@example.com',
age: 28
};
// Definicja metody dostępnej w widoku
$scope.updateUser = function() {
console.log('Użytkownik zaktualizowany:', $scope.user);
};
});
// Bezpieczna definicja z explicit injection (zalecana)
app.controller('ProductController', ['$scope', '$http', function($scope, $http) {
$scope.products = [];
// Pobieranie danych z API
$scope.loadProducts = function() {
$http.get('/api/products')
.then(function(response) {
$scope.products = response.data;
})
.catch(function(error) {
console.error('Błąd podczas pobierania produktów:', error);
});
};
$scope.loadProducts();
}]);
// Alternatywna składnia z $inject
var OrderController = function($scope, orderService) {
$scope.orders = [];
$scope.getOrders = function() {
$scope.orders = orderService.getAllOrders();
};
};
OrderController.$inject = ['$scope', 'orderService'];
app.controller('OrderController', OrderController);
<!-- Użycie kontrolera w widoku -->
<div ng-controller="UserController">
<h2>Profil użytkownika</h2>
<p>Imię: <input type="text" ng-model="user.name"></p>
<p>Email: <input type="text" ng-model="user.email"></p>
<p>Wiek: <input type="number" ng-model="user.age"></p>
<button ng-click="updateUser()">Zaktualizuj</button>
</div>
Materiały
8. Jak działa dziedziczenie scope w AngularJS?
Odpowiedź w 30 sekund: Dziedziczenie scope w AngularJS działa na zasadzie prototypowego dziedziczenia JavaScript, gdzie scope potomny dziedziczy właściwości z scope rodzica. Nowy scope dziecka jest tworzony przez dyrektywy takie jak ng-controller, ng-repeat, czy ng-if, tworząc hierarchiczną strukturę scopów odpowiadającą strukturze DOM.
Odpowiedź w 2 minuty: System dziedziczenia scope w AngularJS opiera się na mechanizmie prototypowego dziedziczenia JavaScript. Każda aplikacja AngularJS posiada jeden $rootScope, który jest rodzicem wszystkich innych scopów. Gdy dyrektywa tworzy nowy scope (np. ng-controller, ng-repeat, ng-if), staje się on dzieckiem scope'a nadrzędnego i dziedziczy wszystkie jego właściwości poprzez łańcuch prototypów.
Dziedziczenie prototypowe oznacza, że scope dziecka może odczytywać właściwości scope rodzica, ale gdy przypisuje wartość do prostej właściwości (primitive), tworzy własną kopię tej właściwości, która maskuje właściwość rodzica. To zjawisko nazywane jest "shadowing" i jest częstym źródłem błędów w AngularJS. W przypadku obiektów i tablic, modyfikacja następuje na referencji, więc zmiany są widoczne w scope rodzica.
AngularJS tworzy różne typy scopów: zwykły scope potomny (ng-controller), izolowany scope (dyrektywy z scope: {}), oraz scope z własnym kontekstem (ng-repeat). Zrozumienie tych różnic jest kluczowe dla właściwego zarządzania danymi w aplikacji. Najlepszą praktyką jest używanie wzorca "dot notation" - zawsze przechowywanie danych w obiektach, co zapobiega problemom z shadowing.
Przykład kodu:
var app = angular.module('scopeApp', []);
// Kontroler nadrzędny
app.controller('ParentController', function($scope) {
// Właściwości prymitywne
$scope.parentName = 'Rodzic';
$scope.counter = 0;
// Obiekt (referencja) - lepsza praktyka
$scope.data = {
message: 'Wiadomość od rodzica',
items: ['item1', 'item2']
};
$scope.parentMethod = function() {
console.log('Metoda z scope rodzica');
};
});
// Kontroler potomny
app.controller('ChildController', function($scope) {
// Odczyt właściwości rodzica - działa
console.log($scope.parentName); // "Rodzic"
console.log($scope.data.message); // "Wiadomość od rodzica"
// PROBLEM: Przypisanie primitive tworzy własną właściwość
$scope.counter = 10; // Tworzy nową właściwość w scope dziecka
// DOBRZE: Modyfikacja obiektu rodzica
$scope.data.message = 'Zmieniona przez dziecko'; // Modyfikuje obiekt rodzica
// Dziecko może wywołać metody rodzica
$scope.parentMethod(); // Działa
// Własna metoda dziecka
$scope.childMethod = function() {
console.log('Metoda tylko w scope dziecka');
};
});
// Przykład demonstrujący problem shadowing
app.controller('DemoController', function($scope) {
$scope.value = 5;
$scope.obj = { value: 5 };
$scope.incrementPrimitive = function() {
$scope.value++; // Shadowing - tworzy nową właściwość
};
$scope.incrementObject = function() {
$scope.obj.value++; // Modyfikuje obiekt rodzica - działa poprawnie
};
});
// Przykład z ng-repeat (każda iteracja ma własny scope)
app.controller('ListController', function($scope) {
$scope.items = [
{ id: 1, name: 'Element 1', selected: false },
{ id: 2, name: 'Element 2', selected: false },
{ id: 3, name: 'Element 3', selected: false }
];
$scope.selectAll = function() {
// Działa poprawnie bo modyfikujemy obiekty
angular.forEach($scope.items, function(item) {
item.selected = true;
});
};
});
<!-- Demonstracja dziedziczenia scope -->
<div ng-controller="ParentController">
<h2>Scope Rodzica</h2>
<p>Nazwa: {{parentName}}</p>
<p>Licznik: {{counter}}</p>
<p>Wiadomość: {{data.message}}</p>
<div ng-controller="ChildController">
<h3>Scope Dziecka</h3>
<p>Nazwa (odziedziczona): {{parentName}}</p>
<p>Licznik (własny): {{counter}}</p>
<p>Wiadomość (współdzielona): {{data.message}}</p>
<input ng-model="data.message"> <!-- Działa poprawnie -->
</div>
</div>
<!-- Problem shadowing z primitives -->
<div ng-controller="DemoController">
<p>Wartość: {{value}}</p>
<div ng-if="true">
<input ng-model="value"> <!-- Problem: tworzy własną zmienną -->
<input ng-model="obj.value"> <!-- Poprawnie: modyfikuje obiekt rodzica -->
</div>
</div>
<!-- ng-repeat tworzy własny scope dla każdej iteracji -->
<div ng-controller="ListController">
<button ng-click="selectAll()">Zaznacz wszystkie</button>
<ul>
<li ng-repeat="item in items">
<input type="checkbox" ng-model="item.selected">
{{item.name}}
</li>
</ul>
</div>
Materiały
9. Czym jest $scope.$watch i kiedy go używać?
Odpowiedź w 30 sekund: $scope.$watch to mechanizm w AngularJS służący do monitorowania zmian wartości wyrażenia lub właściwości na scope. Przyjmuje wyrażenie do obserwacji oraz funkcję callback, która jest wywoływana gdy wartość się zmieni. Jest podstawą systemu dwukierunkowego wiązania danych w AngularJS.
Odpowiedź w 2 minuty: $scope.$watch jest fundamentalnym mechanizmem w AngularJS odpowiedzialnym za wykrywanie zmian w modelu danych. Metoda ta rejestruje "obserwatora" (watcher), który podczas każdego cyklu digest sprawdza czy wartość monitorowanego wyrażenia uległa zmianie. Jeśli wykryje zmianę, wywołuje funkcję listener, która może zareagować na tę zmianę.
Istnieją trzy warianty watch: zwykły $watch (porównanie przez referencję lub wartość dla primitives), $watchCollection (płytkie obserwowanie kolekcji - wykrywa dodanie/usunięcie elementów), oraz $watchGroup (obserwowanie wielu wyrażeń jednocześnie). Dodatkowo można użyć głębokiego porównania (deep watch) jako trzeciego parametru, choć jest to kosztowne wydajnościowo.
$watch powinien być używany oszczędnie, głównie gdy potrzebujemy reagować na zmiany danych w sposób nie obsługiwany przez wbudowane dyrektywy. Typowe przypadki użycia to: synchronizacja danych między komponentami, wykonywanie złożonej logiki przy zmianie wartości, integracja z bibliotekami zewnętrznymi, lub debugowanie przepływu danych. Nadmierne używanie $watch może prowadzić do problemów z wydajnością, ponieważ każdy watcher jest sprawdzany podczas każdego cyklu digest.
Przykład kodu:
var app = angular.module('watchApp', []);
app.controller('WatchController', function($scope, $timeout) {
// Podstawowe użycie $watch
$scope.username = '';
// Monitorowanie pojedynczej wartości
$scope.$watch('username', function(newValue, oldValue) {
if (newValue !== oldValue) {
console.log('Username zmieniony z', oldValue, 'na', newValue);
// Walidacja w czasie rzeczywistym
$scope.isValid = newValue.length >= 3;
}
});
// Watch na wyrażeniu funkcyjnym
$scope.user = {
firstName: 'Jan',
lastName: 'Kowalski'
};
$scope.$watch(function() {
// Wyrażenie do obserwacji
return $scope.user.firstName + ' ' + $scope.user.lastName;
}, function(newValue, oldValue) {
console.log('Pełne imię:', newValue);
$scope.fullName = newValue;
});
// Deep watch - obserwowanie zagnieżdżonych obiektów
$scope.settings = {
theme: 'light',
notifications: {
email: true,
sms: false
}
};
// Trzeci parametr true włącza głębokie porównanie
$scope.$watch('settings', function(newValue, oldValue) {
if (newValue !== oldValue) {
console.log('Ustawienia zmienione:', newValue);
// Zapisz do localStorage
localStorage.setItem('settings', JSON.stringify(newValue));
}
}, true); // Deep watch - UWAGA: kosztowne!
// $watchCollection - wydajniejsze dla kolekcji
$scope.items = ['jabłko', 'gruszka', 'śliwka'];
$scope.$watchCollection('items', function(newCollection, oldCollection) {
console.log('Kolekcja zmodyfikowana');
console.log('Liczba elementów:', newCollection.length);
// Reaguj na dodanie/usunięcie elementów
if (newCollection.length > oldCollection.length) {
console.log('Dodano element');
}
});
// $watchGroup - obserwowanie wielu wartości
$scope.price = 100;
$scope.quantity = 2;
$scope.discount = 0;
$scope.$watchGroup(['price', 'quantity', 'discount'], function(newValues, oldValues) {
var price = newValues[0];
var quantity = newValues[1];
var discount = newValues[2];
// Oblicz całkowitą cenę
$scope.total = (price * quantity) * (1 - discount / 100);
console.log('Całkowita cena:', $scope.total);
});
// Praktyczny przykład: auto-save
$scope.document = {
title: 'Nowy dokument',
content: ''
};
var saveTimeout;
$scope.$watch('document.content', function(newValue, oldValue) {
if (newValue !== oldValue) {
// Debounce - opóźnij zapis
$timeout.cancel(saveTimeout);
saveTimeout = $timeout(function() {
console.log('Auto-save dokumentu...');
// Tutaj wywołanie API do zapisu
}, 2000); // Zapisz 2 sekundy po ostatniej zmianie
}
});
// Deregistracja watchera - ważne dla performance
var deregister = $scope.$watch('someValue', function(newValue) {
// Jeśli spełniony warunek, przestań obserwować
if (newValue === 'STOP') {
console.log('Zatrzymanie watchera');
deregister(); // Usuń watcher
}
});
// Przykład integracji z biblioteką zewnętrzną
$scope.chartData = [10, 20, 30];
$scope.$watchCollection('chartData', function(newData) {
// Zaktualizuj wykres biblioteki zewnętrznej
updateExternalChart(newData);
});
function updateExternalChart(data) {
console.log('Aktualizacja wykresu z danymi:', data);
// Integracja z Chart.js, D3, itp.
}
});
// Przykład dyrektywy używającej $watch
app.directive('autoExpand', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// Watch na wartość textarea
scope.$watch(attrs.ngModel, function(newValue) {
// Automatycznie zwiększ wysokość
element[0].style.height = 'auto';
element[0].style.height = element[0].scrollHeight + 'px';
});
}
};
});
<!-- Użycie w widoku -->
<div ng-controller="WatchController">
<!-- Podstawowe watch na input -->
<div>
<label>Username:</label>
<input ng-model="username">
<span ng-show="!isValid" style="color: red;">
Min. 3 znaki
</span>
</div>
<!-- Watch na wielu polach -->
<div>
<input ng-model="user.firstName" placeholder="Imię">
<input ng-model="user.lastName" placeholder="Nazwisko">
<p>Pełne imię: {{fullName}}</p>
</div>
<!-- WatchGroup - kalkulator -->
<div>
<input type="number" ng-model="price" placeholder="Cena">
<input type="number" ng-model="quantity" placeholder="Ilość">
<input type="number" ng-model="discount" placeholder="Zniżka %">
<p>Całość: {{total | currency:'PLN'}}</p>
</div>
<!-- Auto-save z watch -->
<div>
<input ng-model="document.title">
<textarea ng-model="document.content" auto-expand></textarea>
<small>Dokument zapisywany automatycznie...</small>
</div>
</div>
Materiały
10. Jaka jest różnica między $scope.$apply a $scope.$digest?
Odpowiedź w 30 sekund: $scope.$digest uruchamia cykl wykrywania zmian tylko dla bieżącego scope i jego dzieci, podczas gdy $scope.$apply wykonuje $digest na $rootScope (czyli dla całej aplikacji) i dodatkowo opakowuje kod w try-catch. $apply jest bezpieczniejsze i zalecane do integracji kodu zewnętrznego z AngularJS.
Odpowiedź w 2 minuty: Główna różnica między $digest a $apply dotyczy zakresu sprawdzania zmian i obsługi błędów. $scope.$digest rozpoczyna cykl "dirty checking" od bieżącego scope i propaguje go w dół hierarchii do scope potomnych, ale nie sprawdza scope rodzica ani rodzeństwa. Z kolei $scope.$apply (lub $rootScope.$apply) zawsze uruchamia digest od scope głównego, obejmując całą aplikację.
$apply przyjmuje opcjonalną funkcję jako argument i wykonuje ją wewnątrz kontekstu AngularJS, automatycznie wywołując $digest po jej zakończeniu. Dodatkowo opakowuje kod w blok try-catch, przekazując ewentualne błędy do serwisu $exceptionHandler. To sprawia, że $apply jest bezpieczniejszym wyborem przy integrowaniu zewnętrznego kodu JavaScript z aplikacją AngularJS.
W większości przypadków nie powinno się ręcznie wywoływać tych metod - AngularJS automatycznie uruchamia digest po zdarzeniach takich jak ng-click, $http, $timeout. Ręczne wywołanie jest potrzebne głównie przy integrowaniu bibliotek zewnętrznych (jQuery plugins, WebSocket, vanilla JS events) lub callback'ów spoza ekosystemu AngularJS. Najlepszą praktyką jest używanie $apply z funkcją, a nie bezpośrednio $digest, aby uniknąć błędu "digest already in progress".
Przykład kodu:
var app = angular.module('digestApp', []);
app.controller('DigestController', function($scope, $timeout) {
$scope.counter = 0;
$scope.message = '';
// PROBLEM: Zdarzenie spoza AngularJS
document.getElementById('externalBtn').addEventListener('click', function() {
// Ta zmiana nie zostanie wykryta przez AngularJS!
$scope.counter++;
// UI nie zostanie zaktualizowane
});
// ROZWIĄZANIE 1: Użycie $apply z funkcją (ZALECANE)
document.getElementById('externalBtn').addEventListener('click', function() {
$scope.$apply(function() {
$scope.counter++;
$scope.message = 'Zaktualizowano przez $apply';
});
// $apply automatycznie wywołuje $digest po wykonaniu funkcji
});
// ROZWIĄZANIE 2: Użycie $apply bez funkcji (mniej bezpieczne)
document.getElementById('externalBtn').addEventListener('click', function() {
$scope.counter++;
$scope.$apply(); // Wywołuje digest dla całej aplikacji
});
// ROZWIĄZANIE 3: Użycie $digest (tylko lokalny scope)
document.getElementById('localBtn').addEventListener('click', function() {
$scope.counter++;
$scope.$digest(); // Tylko dla tego scope i dzieci
});
// Przykład integracji z setTimeout (natywny JS)
$scope.updateWithTimeout = function() {
// ZLE: setTimeout nie jest częścią AngularJS
setTimeout(function() {
$scope.message = 'To nie zadziała!';
// AngularJS nie wie o tej zmianie
}, 1000);
// DOBRZE: Użycie $timeout (serwis AngularJS)
$timeout(function() {
$scope.message = 'To zadziała!';
// $timeout automatycznie wywołuje $apply
}, 1000);
// ALTERNATYWA: Ręczne $apply
setTimeout(function() {
$scope.$apply(function() {
$scope.message = 'To też zadziała!';
});
}, 1000);
};
// Integracja z jQuery
$scope.integrateWithJQuery = function() {
// jQuery event spoza AngularJS
$('#someElement').on('customEvent', function(e, data) {
$scope.$apply(function() {
$scope.externalData = data;
});
});
};
// Przykład z WebSocket
var socket = new WebSocket('ws://example.com');
socket.onmessage = function(event) {
// Callback spoza kontekstu AngularJS
$scope.$apply(function() {
$scope.socketMessage = event.data;
console.log('Otrzymano wiadomość:', event.data);
});
};
// Zabezpieczenie przed błędem "digest already in progress"
$scope.safeApply = function(fn) {
var phase = $scope.$root.$$phase;
if (phase === '$apply' || phase === '$digest') {
// Jeśli digest już trwa, wykonaj tylko funkcję
if (fn && typeof fn === 'function') {
fn();
}
} else {
// Jeśli digest nie trwa, użyj $apply
$scope.$apply(fn);
}
};
// Użycie safeApply
$scope.updateSafely = function() {
externalLibrary.doSomething(function(result) {
$scope.safeApply(function() {
$scope.result = result;
});
});
};
// Demonstracja różnicy zakresów
$scope.parentValue = 'Rodzic';
$scope.digestVsApply = function(useDigest) {
$scope.parentValue = 'Zmieniony';
if (useDigest) {
// Digest sprawdzi tylko lokalny scope
$scope.$digest();
console.log('Użyto $digest - lokalny zakres');
} else {
// Apply sprawdzi cały scope hierarchy
$scope.$apply();
console.log('Użyto $apply - cała aplikacja');
}
};
});
// Przykład dyrektywy z integracją zewnętrzną
app.directive('externalSlider', function() {
return {
restrict: 'A',
scope: {
value: '=',
onChange: '&'
},
link: function(scope, element, attrs) {
// Inicjalizacja jQuery UI slider (przykład)
$(element).slider({
min: 0,
max: 100,
value: scope.value,
slide: function(event, ui) {
// Event spoza AngularJS - potrzebne $apply
scope.$apply(function() {
scope.value = ui.value;
scope.onChange({ value: ui.value });
});
}
});
// Watch na zmiany w AngularJS
scope.$watch('value', function(newValue) {
// Aktualizuj slider gdy wartość zmienia się w AngularJS
$(element).slider('value', newValue);
});
}
};
});
// Serwis z metodami asynchronicznymi
app.factory('externalService', function($rootScope) {
return {
fetchData: function(callback) {
// Symulacja AJAX spoza AngularJS (np. vanilla XMLHttpRequest)
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = function() {
// Callback spoza kontekstu AngularJS
$rootScope.$apply(function() {
callback(JSON.parse(xhr.responseText));
});
};
xhr.send();
},
// Lepsza praktyka: zwracaj Promise
fetchDataPromise: function() {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = function() {
$rootScope.$apply(function() {
resolve(JSON.parse(xhr.responseText));
});
};
xhr.onerror = function() {
$rootScope.$apply(function() {
reject(new Error('Request failed'));
});
};
xhr.send();
});
}
};
});
<!-- Demonstracja użycia -->
<div ng-controller="DigestController">
<h2>Counter: {{counter}}</h2>
<p>Message: {{message}}</p>
<!-- Przyciski zewnętrzne (vanilla JS) -->
<button id="externalBtn">External Update</button>
<button id="localBtn">Local Digest</button>
<!-- Przyciski AngularJS (automatyczny digest) -->
<button ng-click="counter = counter + 1">AngularJS Update</button>
<button ng-click="updateWithTimeout()">Timeout Update</button>
<!-- Demonstracja różnicy -->
<div>
<p>Parent Value: {{parentValue}}</p>
<button ng-click="digestVsApply(true)">Use $digest</button>
<button ng-click="digestVsApply(false)">Use $apply</button>
</div>
<!-- Zewnętrzny slider -->
<div external-slider value="sliderValue" on-change="handleChange(value)"></div>
<p>Wartość slidera: {{sliderValue}}</p>
</div>
Materiały
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
12. Jak unikać problemów z dziedziczeniem prototypowym w scope?
Odpowiedź w 30 sekund: Aby unikać problemów z dziedziczeniem prototypowym w AngularJS, należy zawsze stosować "dot rule" - przechowywać dane w obiektach zamiast prymitywów bezpośrednio na scope. Alternatywnie można używać controller as syntax, który naturalnie enkapsuluje dane w obiekcie kontrolera, eliminując problemy z shadowing.
Odpowiedź w 2 minuty: Problemy z dziedziczeniem prototypowym w AngularJS wynikają z faktu, że scope potomny dziedziczy właściwości rodzica przez prototyp JavaScript. Gdy przypisujesz wartość prymitywną (string, number, boolean) bezpośrednio na scope dziecka, tworzona jest nowa właściwość w scope dziecka, która "maskuje" (shadow) właściwość rodzica. To prowadzi do niespodziewanego zachowania, szczególnie w dyrektywach takich jak ng-if, ng-repeat, czy ng-switch, które tworzą nowy scope.
Najważniejsza zasada to "dot rule" - jeśli w wyrażeniu ng-model nie ma kropki, prawdopodobnie robisz coś źle. Zamiast bindować bezpośrednio do prymitywu ($scope.name), należy tworzyć obiekt kontenerowy ($scope.data.name). Obiekt jest przekazywany przez referencję, więc modyfikacje w scope dziecka wpływają na obiekt w scope rodzica.
Alternatywne rozwiązania to: używanie controller as syntax (automatycznie enkapsuluje dane), wykorzystanie ng-model z $parent (niezalecane, tworzy ścisłe powiązanie), stosowanie izolowanych scopów w dyrektywach, używanie serwisów do współdzielenia stanu między kontrolerami, lub wykorzystanie bindToController: true w dyrektywach. Warto też pamiętać, że niektóre dyrektywy (ng-if, ng-repeat) tworzą nowy scope, podczas gdy inne (ng-show, ng-hide) go nie tworzą.
Przykład kodu:
var app = angular.module('scopeProblemsApp', []);
// PROBLEM: Shadowing z prymitywami
app.controller('ProblemController', function($scope) {
// Prymityw bezpośrednio na scope
$scope.userName = 'Jan Kowalski';
$scope.isLoggedIn = false;
$scope.count = 0;
// W child scope (ng-if, ng-repeat) zmiany tworzą nową właściwość!
});
// ROZWIĄZANIE 1: Dot Rule - używaj obiektów
app.controller('DotRuleController', function($scope) {
// Wszystkie dane w obiekcie
$scope.data = {
userName: 'Jan Kowalski',
isLoggedIn: false,
count: 0
};
// Zagnieżdżone obiekty dla lepszej organizacji
$scope.user = {
profile: {
name: 'Jan',
email: 'jan@example.com'
},
settings: {
theme: 'dark',
notifications: true
}
};
$scope.updateUser = function() {
// Modyfikacja obiektu działa poprawnie w child scopes
$scope.data.userName = 'Anna Nowak';
};
});
// ROZWIĄZANIE 2: Controller As Syntax
app.controller('ControllerAsCtrl', function() {
var vm = this;
// Wszystkie dane automatycznie w obiekcie 'vm'
vm.userName = 'Jan Kowalski';
vm.isLoggedIn = false;
vm.count = 0;
vm.items = ['A', 'B', 'C'];
vm.increment = function() {
vm.count++; // Zawsze modyfikuje właściwą zmienną
};
});
// Demonstracja problemów i rozwiązań
app.controller('DemoController', function($scope) {
// Setup różnych scenariuszy
// Scenariusz 1: Problem z ng-if
$scope.primitive = 'początkowa wartość';
$scope.object = { value: 'początkowa wartość' };
// Scenariusz 2: Problem z ng-repeat
$scope.items = [
{ id: 1, selected: false },
{ id: 2, selected: false }
];
// Scenariusz 3: Formularz w ng-if
$scope.showForm = true;
$scope.formData = {
email: '',
password: ''
};
// Metody pomocnicze
$scope.checkPrimitive = function() {
console.log('Primitive:', $scope.primitive);
};
$scope.checkObject = function() {
console.log('Object:', $scope.object.value);
};
$scope.selectAll = function() {
// Działa poprawnie bo modyfikujemy obiekty
angular.forEach($scope.items, function(item) {
item.selected = true;
});
};
});
// Przykład dyrektywy z izolowanym scope
app.directive('userProfile', function() {
return {
restrict: 'E',
scope: {
user: '=', // Two-way binding do obiektu
onSave: '&' // Callback
},
template: `
<div class="profile">
<input ng-model="user.name">
<input ng-model="user.email">
<button ng-click="onSave({user: user})">Zapisz</button>
</div>
`,
controller: function($scope) {
// Izolowany scope eliminuje problemy z dziedziczeniem
$scope.$watch('user.name', function(newVal) {
console.log('Nazwa zmieniona na:', newVal);
});
}
};
});
// Dyrektywa z bindToController
app.directive('smartInput', function() {
return {
restrict: 'E',
scope: {
value: '=',
label: '@'
},
bindToController: true,
controllerAs: 'smart',
template: `
<label>{{smart.label}}</label>
<input ng-model="smart.value">
`,
controller: function() {
var smart = this;
// value jest dostępne bezpośrednio na kontrolerze
smart.$onInit = function() {
console.log('Początkowa wartość:', smart.value);
};
}
};
});
// Serwis do współdzielenia stanu (najlepsze dla shared data)
app.factory('sharedDataService', function() {
var service = {
data: {
currentUser: null,
settings: {}
},
setUser: function(user) {
service.data.currentUser = user;
},
getUser: function() {
return service.data.currentUser;
},
updateSetting: function(key, value) {
service.data.settings[key] = value;
}
};
return service;
});
// Kontrolery używające serwisu
app.controller('HeaderCtrl', function($scope, sharedDataService) {
$scope.shared = sharedDataService.data;
$scope.logout = function() {
sharedDataService.setUser(null);
};
});
app.controller('ProfileCtrl', function($scope, sharedDataService) {
$scope.shared = sharedDataService.data;
$scope.updateProfile = function(user) {
sharedDataService.setUser(user);
};
});
// Przykład najlepszych praktyk
app.controller('BestPracticesCtrl', function($scope) {
// 1. Używaj obiektów dla wszystkich danych
$scope.vm = {
// User data
user: {
name: '',
email: '',
age: null
},
// UI state
ui: {
loading: false,
error: null,
showDetails: false
},
// Form data
form: {
search: '',
filters: {
category: '',
priceRange: [0, 1000]
}
},
// List data
items: []
};
// 2. Metody modyfikujące dane zawsze przez obiekt
$scope.updateUser = function(field, value) {
$scope.vm.user[field] = value;
};
// 3. Toggle methods
$scope.toggleDetails = function() {
$scope.vm.ui.showDetails = !$scope.vm.ui.showDetails;
};
// 4. Obsługa list
$scope.addItem = function(item) {
$scope.vm.items.push(item);
};
$scope.removeItem = function(index) {
$scope.vm.items.splice(index, 1);
};
// 5. Reset form
$scope.resetForm = function() {
$scope.vm.form = {
search: '',
filters: {
category: '',
priceRange: [0, 1000]
}
};
};
});
// Utility funkcja do głębokiego kopiowania
app.factory('utilService', function() {
return {
deepCopy: function(obj) {
return JSON.parse(JSON.stringify(obj));
},
// Bezpieczne przypisanie wartości (unika shadowing)
safeSet: function(scope, path, value) {
var keys = path.split('.');
var current = scope;
for (var i = 0; i < keys.length - 1; i++) {
var key = keys[i];
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
};
});
<!-- PROBLEM: Primitive binding z ng-if -->
<div ng-controller="ProblemController">
<input ng-model="userName">
<p>Nazwa: {{userName}}</p>
<div ng-if="true">
<!-- To NIE zadziała poprawnie - tworzy nową zmienną w child scope -->
<input ng-model="userName">
<p>W ng-if: {{userName}}</p>
</div>
</div>
<!-- ROZWIĄZANIE: Dot rule -->
<div ng-controller="DotRuleController">
<input ng-model="data.userName">
<p>Nazwa: {{data.userName}}</p>
<div ng-if="true">
<!-- Działa poprawnie - modyfikuje obiekt rodzica -->
<input ng-model="data.userName">
<p>W ng-if: {{data.userName}}</p>
</div>
<!-- Zagnieżdżone obiekty -->
<div ng-if="user.profile">
<input ng-model="user.profile.name">
<input ng-model="user.settings.theme">
</div>
</div>
<!-- Controller As Syntax -->
<div ng-controller="ControllerAsCtrl as vm">
<input ng-model="vm.userName">
<p>Nazwa: {{vm.userName}}</p>
<div ng-if="true">
<!-- Zawsze działa poprawnie -->
<input ng-model="vm.userName">
<p>W ng-if: {{vm.userName}}</p>
</div>
<!-- ng-repeat również działa -->
<div ng-repeat="item in vm.items">
<input ng-model="vm.count">
<button ng-click="vm.increment()">+</button>
</div>
</div>
<!-- Demonstracja problemów -->
<div ng-controller="DemoController">
<!-- Problem z primitive -->
<div ng-if="true">
<input ng-model="primitive">
<button ng-click="checkPrimitive()">Sprawdź primitive</button>
</div>
<!-- Rozwiązanie z object -->
<div ng-if="true">
<input ng-model="object.value">
<button ng-click="checkObject()">Sprawdź object</button>
</div>
<!-- Problem z ng-repeat i checkboxy -->
<div>
<button ng-click="selectAll()">Zaznacz wszystkie</button>
<div ng-repeat="item in items">
<!-- Działa bo item jest obiektem -->
<input type="checkbox" ng-model="item.selected">
Item {{item.id}}: {{item.selected}}
</div>
</div>
<!-- Formularz w ng-if -->
<div ng-if="showForm">
<input ng-model="formData.email" placeholder="Email">
<input ng-model="formData.password" type="password">
<p>Email: {{formData.email}}</p>
</div>
</div>
<!-- Dyrektywa z izolowanym scope -->
<user-profile
user="currentUser"
on-save="saveUser(user)">
</user-profile>
<!-- Dyrektywa z bindToController -->
<smart-input
value="myValue"
label="Podaj wartość">
</smart-input>
<!-- Współdzielenie danych przez serwis -->
<div ng-controller="HeaderCtrl">
<p>Zalogowany: {{shared.currentUser.name}}</p>
<button ng-click="logout()">Wyloguj</button>
</div>
<div ng-controller="ProfileCtrl">
<input ng-model="shared.currentUser.name">
<button ng-click="updateProfile(shared.currentUser)">Zapisz</button>
</div>
<!-- Best practices example -->
<div ng-controller="BestPracticesCtrl">
<!-- User form -->
<div class="user-form">
<input ng-model="vm.user.name" placeholder="Imię">
<input ng-model="vm.user.email" placeholder="Email">
<input ng-model="vm.user.age" type="number" placeholder="Wiek">
</div>
<!-- Search with filters -->
<div class="search">
<input ng-model="vm.form.search" placeholder="Szukaj...">
<select ng-model="vm.form.filters.category">
<option value="">Wszystkie</option>
<option value="tech">Technologia</option>
<option value="food">Jedzenie</option>
</select>
<button ng-click="resetForm()">Reset</button>
</div>
<!-- Conditional content -->
<div ng-if="vm.ui.showDetails">
<p>Szczegóły: {{vm.user.name}}</p>
<input ng-model="vm.user.email">
</div>
<button ng-click="toggleDetails()">
{{vm.ui.showDetails ? 'Ukryj' : 'Pokaż'}} szczegóły
</button>
<!-- Loading state -->
<div ng-show="vm.ui.loading">Ładowanie...</div>
<div ng-show="vm.ui.error" class="error">{{vm.ui.error}}</div>
</div>
Materiały
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:
14. Jak utworzyć własną dyrektywę w AngularJS?
Odpowiedź w 30 sekund:
Własną dyrektywę tworzymy używając metody .directive() na module, podając nazwę dyrektywy (w camelCase) i funkcję fabryczną, która zwraca obiekt definicji dyrektywy (DDO - Directive Definition Object). Obiekt ten określa właściwości takie jak restrict, template, scope, link i inne.
Odpowiedź w 2 minuty:
Tworzenie własnej dyrektywy w AngularJS składa się z kilku kroków. Najpierw rejestrujemy dyrektywę w module używając metody .directive(). Nazwa dyrektywy powinna być w formacie camelCase (np. mojaDyrektywa), ale w HTML używamy formatu kebab-case (np. moja-dyrektywa). Funkcja fabryczna zwraca obiekt konfiguracyjny (DDO), który definiuje zachowanie dyrektywy.
Obiekt DDO może zawierać wiele właściwości: restrict określa typ dyrektywy (E, A, C, M), template lub templateUrl definiuje HTML dyrektywy, scope określa czy dyrektywa ma własny scope i jakie dane z niego pobiera, link zawiera funkcję do manipulacji DOM, controller definiuje logikę biznesową, compile to zaawansowana funkcja do transformacji szablonu, oraz require określa zależności od innych dyrektyw.
Najważniejsze jest zrozumienie scope - możemy użyć scope rodzica (domyślnie), utworzyć nowy scope dziedziczący (scope: true), lub utworzyć izolowany scope (scope: {}). Izolowany scope jest zalecany dla komponentów wielokrotnego użytku, ponieważ zapobiega niezamierzonym interakcjom z zewnętrznym scope.
Funkcja link jest miejscem, gdzie wykonujemy manipulacje DOM, dodajemy event listenery i obserwujemy zmiany w scope. Otrzymuje ona parametry: scope (dane dyrektywy), element (jqLite/jQuery wrapped element), oraz attrs (atrybuty elementu).
Przykład kodu:
// Prosta dyrektywa wyświetlająca licznik
angular.module('myApp', [])
.directive('licznik', function() {
return {
restrict: 'E',
// Izolowany scope z bindowaniem
scope: {
wartoscPoczatkowa: '=',
krok: '@'
},
template: `
<div class="licznik">
<button ng-click="zmniejsz()">-</button>
<span>{{wartosc}}</span>
<button ng-click="zwieksz()">+</button>
</div>
`,
link: function(scope, element, attrs) {
// Inicjalizacja
scope.wartosc = scope.wartoscPoczatkowa || 0;
var krok = parseInt(scope.krok) || 1;
// Metody
scope.zwieksz = function() {
scope.wartosc += krok;
};
scope.zmniejsz = function() {
scope.wartosc -= krok;
};
// Stylowanie elementu
element.css({
'display': 'inline-block',
'padding': '10px',
'border': '1px solid #ccc'
});
}
};
});
// Bardziej zaawansowana dyrektywa z kontrolerem
angular.module('myApp')
.directive('listaZadan', function() {
return {
restrict: 'E',
scope: {
zadania: '='
},
templateUrl: 'lista-zadan.html',
controller: function($scope) {
$scope.dodajZadanie = function(nazwa) {
if (nazwa) {
$scope.zadania.push({
nazwa: nazwa,
ukonczone: false
});
$scope.noweZadanie = '';
}
};
$scope.usunZadanie = function(index) {
$scope.zadania.splice(index, 1);
};
$scope.przelaczStatus = function(zadanie) {
zadanie.ukonczone = !zadanie.ukonczone;
};
},
link: function(scope, element, attrs) {
// Dodatkowa logika DOM jeśli potrzebna
element.addClass('lista-zadan-container');
}
};
});
Szablon HTML (lista-zadan.html):
<div>
<input ng-model="noweZadanie" placeholder="Nowe zadanie">
<button ng-click="dodajZadanie(noweZadanie)">Dodaj</button>
<ul>
<li ng-repeat="zadanie in zadania">
<input type="checkbox"
ng-model="zadanie.ukonczone"
ng-change="przelaczStatus(zadanie)">
<span ng-class="{przekreslone: zadanie.ukonczone}">
{{zadanie.nazwa}}
</span>
<button ng-click="usunZadanie($index)">Usuń</button>
</li>
</ul>
</div>
Użycie:
<licznik wartosc-poczatkowa="10" krok="5"></licznik>
<lista-zadan zadania="mojeZadania"></lista-zadan>
Materiały:
15. Czym jest funkcja link w dyrektywie i do czego służy?
Odpowiedź w 30 sekund: Funkcja link to miejsce w dyrektywie, gdzie wykonujemy manipulacje DOM, rejestrujemy event listenery i konfigurujemy interakcje z użytkownikiem. Otrzymuje ona parametry scope, element i attrs, umożliwiając dostęp do danych dyrektywy, elementu DOM i jego atrybutów.
Odpowiedź w 2 minuty: Funkcja link jest sercem większości dyrektyw w AngularJS i wykonuje się po tym, jak szablon dyrektywy został skompilowany i połączony ze scope. Jest to właściwe miejsce do wykonywania operacji związanych z DOM, takich jak dodawanie event listenerów, manipulacja stylami, integracja z zewnętrznymi bibliotekami (np. jQuery plugins), oraz konfiguracja zachowań interaktywnych.
Funkcja link otrzymuje trzy główne parametry: scope - obiekt scope dyrektywy (może być scope rodzica, nowy scope lub izolowany scope), element - element DOM opakowany w jqLite (lub jQuery jeśli jest dostępny), oraz attrs - obiekt zawierający znormalizowane atrybuty elementu. Opcjonalnie może otrzymać czwarty parametr controller, jeśli dyrektywa wymaga kontrolera innej dyrektywy.
Istnieją dwa typy funkcji link: pre-link i post-link. Pre-link wykonuje się przed kompilacją elementów potomnych, podczas gdy post-link (domyślna) wykonuje się po kompilacji wszystkich elementów potomnych. Post-link jest wykonywany od najgłębszych dzieci do rodzica, co gwarantuje, że wszystkie elementy potomne są już gotowe.
W funkcji link należy unikać ciężkich operacji i złożonej logiki biznesowej - te powinny znajdować się w kontrolerze lub serwisach. Link powinien koncentrować się na operacjach DOM i integracjach wymagających bezpośredniego dostępu do elementów. Pamiętaj też o czyszczeniu event listenerów i timers podczas niszczenia scope używając scope.$on('$destroy', ...).
Przykład kodu:
// Przykład dyrektywy z funkcją link wykonującą różne operacje DOM
angular.module('myApp', [])
.directive('automatyczneFokus', function($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// Ustawienie focus po renderowaniu
$timeout(function() {
element[0].focus();
});
}
};
})
.directive('sledzMysz', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var pozycja = { x: 0, y: 0 };
// Rejestracja event listenera
element.on('mousemove', function(event) {
pozycja.x = event.clientX;
pozycja.y = event.clientY;
// Aktualizacja scope wymaga $apply
scope.$apply(function() {
scope.pozycjaMysz = pozycja;
});
});
// Czyszczenie przy niszczeniu dyrektywy
scope.$on('$destroy', function() {
element.off('mousemove');
});
}
};
})
.directive('wykresSlupkowy', function() {
return {
restrict: 'E',
scope: {
dane: '=',
wysokosc: '@'
},
template: '<div class="wykres"></div>',
link: function(scope, element, attrs) {
var wysokosc = parseInt(scope.wysokosc) || 200;
var kontener = element.find('div');
// Funkcja renderująca wykres
function rysujWykres(dane) {
kontener.empty();
if (!dane || !dane.length) return;
var maxWartosc = Math.max.apply(null, dane.map(d => d.wartosc));
var szerokoscSlupka = 100 / dane.length;
dane.forEach(function(item, index) {
var wysokoscSlupka = (item.wartosc / maxWartosc) * wysokosc;
var slupek = angular.element('<div class="slupek"></div>');
slupek.css({
'width': szerokoscSlupka + '%',
'height': wysokoscSlupka + 'px',
'background-color': item.kolor || '#3498db',
'float': 'left',
'margin': '0 2px'
});
kontener.append(slupek);
});
}
// Nasłuchiwanie zmian w danych
scope.$watch('dane', function(noweDane) {
if (noweDane) {
rysujWykres(noweDane);
}
}, true); // Deep watch dla tablic/obiektów
// Cleanup
scope.$on('$destroy', function() {
kontener.empty();
});
}
};
})
.directive('wymaganyKontroler', function() {
return {
restrict: 'A',
require: 'ngModel', // Wymaga dyrektywy ngModel
link: function(scope, element, attrs, ngModelCtrl) {
// Dostęp do kontrolera ngModel jako czwarty parametr
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
var minLength = parseInt(attrs.wymaganyKontroler);
return value && value.length >= minLength;
};
}
};
});
Przykład pre-link i post-link:
angular.module('myApp')
.directive('prePostLink', function() {
return {
restrict: 'E',
template: '<div>Dyrektywa z pre i post link</div>',
compile: function(element, attrs) {
console.log('Compile');
// Zwracamy obiekt z pre i post funkcjami link
return {
pre: function(scope, element, attrs) {
console.log('Pre-link - przed kompilacją dzieci');
},
post: function(scope, element, attrs) {
console.log('Post-link - po kompilacji dzieci');
}
};
}
};
});
Użycie:
<input type="text" automatyczne-fokus>
<div sledz-mysz>Pozycja: {{pozycjaMysz.x}}, {{pozycjaMysz.y}}</div>
<wykres-slupkowy dane="daneWykresu" wysokosc="300"></wykres-slupkowy>
<input type="text" ng-model="haslo" wymagany-kontroler="8">
Materiały:
16. Jaka jest różnica między compile a link w dyrektywie?
Odpowiedź w 30 sekund: Funkcja compile wykonuje się raz podczas transformacji szablonu DOM i służy do manipulacji szablonem przed jego klonowaniem. Funkcja link wykonuje się dla każdej instancji dyrektywy po sklonowaniu szablonu i służy do operacji na konkretnych instancjach elementów oraz rejestracji event listenerów.
Odpowiedź w 2 minuty: Różnica między compile a link jest fundamentalna dla zrozumienia cyklu życia dyrektywy w AngularJS. Proces kompilacji przebiega w dwóch fazach: compile (transformacja szablonu) i link (połączenie z scope).
Funkcja compile wykonuje się tylko raz dla każdego wystąpienia dyrektywy w kodzie źródłowym, przed jakimkolwiek klonowaniem szablonu. Otrzymuje element i atrybuty (bez scope!), i służy do transformacji szablonu DOM, która będzie współdzielona przez wszystkie instancje. Jest to miejsce na optymalizacje, takie jak przygotowanie struktur DOM, które będą wielokrotnie klonowane. Compile zwraca funkcję link lub obiekt z funkcjami pre-link i post-link.
Funkcja link wykonuje się dla każdej sklonowanej instancji elementu, po połączeniu z konkretnym scope. To tutaj rejestrujemy event listenery, obserwujemy zmiany w scope, i wykonujemy operacje specyficzne dla danej instancji. Link otrzymuje scope, element i atrybuty, co pozwala na interakcję z danymi i zachowaniem specyficznym dla instancji.
W praktyce większość dyrektyw używa tylko funkcji link, ponieważ compile jest potrzebny tylko w przypadkach, gdy potrzebujemy manipulować szablonem przed jego klonowaniem (np. w ng-repeat) lub gdy chcemy zoptymalizować dyrektywy, które będą używane wiele razy. Jeśli zdefiniujemy zarówno compile jak i link, funkcja link zostanie zignorowana - compile musi zwrócić funkcję link.
Przykład kodu:
// Przykład showing różnicy między compile a link
angular.module('myApp', [])
.directive('przykladCompile', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
console.log('Compile wykonuje się raz');
// Transformacja szablonu - wykonana tylko raz
element.append('<p>Dodane w compile</p>');
// Zwracamy funkcję link (lub obiekt z pre/post)
return function linkFn(scope, element, attrs) {
console.log('Link wykonuje się dla każdej instancji');
// Operacje specyficzne dla instancji
scope.liczba = Math.random();
};
},
template: '<div>Szablon</div>'
};
})
.directive('bezCompile', function() {
return {
restrict: 'E',
// Bez compile - tylko link (najczęstszy przypadek)
link: function(scope, element, attrs) {
console.log('Tylko link - wykonuje się dla każdej instancji');
},
template: '<div>Prosty szablon</div>'
};
})
// Zaawansowany przykład: optymalizacja dla ng-repeat
.directive('optymalizowanaLista', function() {
return {
restrict: 'E',
compile: function(tElement, tAttrs) {
// tElement i tAttrs to "template" element i atrybuty
console.log('Compile: przygotowanie szablonu');
// Przygotowanie struktury DOM raz dla wszystkich instancji
var szablon = angular.element('<div class="item"></div>');
szablon.append('<span class="label"></span>');
szablon.append('<span class="value"></span>');
tElement.append(szablon);
// Zwracamy funkcje pre-link i post-link
return {
pre: function preLink(scope, iElement, iAttrs) {
console.log('Pre-link: przed kompilacją dzieci');
// iElement i iAttrs to "instance" element i atrybuty
},
post: function postLink(scope, iElement, iAttrs) {
console.log('Post-link: po kompilacji dzieci');
// Bindowanie danych do konkretnej instancji
scope.$watch('dane', function(noweDane) {
if (noweDane) {
iElement.find('.label').text(noweDane.etykieta);
iElement.find('.value').text(noweDane.wartosc);
}
});
}
};
}
};
})
// Przykład kiedy używać compile: dyrektywa modyfikująca szablon
.directive('powtorzElement', function() {
return {
restrict: 'A',
compile: function(element, attrs) {
var liczbaPowtorzen = parseInt(attrs.powtorzElement) || 2;
var oryginalnyElement = element.clone();
// Klonujemy element w fazie compile (przed bindowaniem)
for (var i = 1; i < liczbaPowtorzen; i++) {
element.after(oryginalnyElement.clone());
}
// Nie potrzebujemy link function w tym przypadku
return function link(scope, element, attrs) {
// Opcjonalna logika link
};
}
};
})
// Porównanie wydajności: compile vs link
.directive('wydajnoscCompile', function($compile) {
return {
restrict: 'E',
scope: {
items: '='
},
compile: function(element, attrs) {
// Przygotowanie szablonu raz
var szablon = '<div class="item">{{item.nazwa}}</div>';
var skompilowany = null;
return function link(scope, element, attrs) {
// Kompilacja szablonu tylko raz
if (!skompilowany) {
skompilowany = $compile(szablon);
}
scope.$watchCollection('items', function(items) {
element.empty();
angular.forEach(items, function(item) {
var nowyScope = scope.$new();
nowyScope.item = item;
var nowyElement = skompilowany(nowyScope);
element.append(nowyElement);
});
});
};
}
};
});
Przykład ilustrujący kolejność wykonania:
angular.module('myApp')
.directive('rodzic', function() {
return {
restrict: 'E',
template: '<div>Rodzic: <dziecko></dziecko></div>',
compile: function() {
console.log('1. Compile rodzica');
return {
pre: function() {
console.log('2. Pre-link rodzica');
},
post: function() {
console.log('5. Post-link rodzica');
}
};
}
};
})
.directive('dziecko', function() {
return {
restrict: 'E',
template: '<span>Dziecko</span>',
compile: function() {
console.log('3. Compile dziecka');
return function link() {
console.log('4. Post-link dziecka');
};
}
};
});
// Wynik w konsoli:
// 1. Compile rodzica
// 3. Compile dziecka
// 2. Pre-link rodzica
// 4. Post-link dziecka
// 5. Post-link rodzica
Użycie:
<!-- Przykład compile wykonującego się raz -->
<div ng-repeat="i in [1,2,3]">
<przyklad-compile></przyklad-compile>
</div>
<!-- Compile: 1 raz, Link: 3 razy -->
<!-- Powtórzenie elementu w compile -->
<div powtorz-element="3">Tekst</div>
<!-- Utworzy 3 identyczne elementy -->
Materiały:
17. Czym jest izolowany scope w dyrektywie i jak go zdefiniować?
Odpowiedź w 30 sekund:
Izolowany scope to oddzielny scope dyrektywy, który nie dziedziczy właściwości z scope rodzica, zapewniając enkapsulację i wielokrotne użycie komponentu. Definiujemy go używając scope: {} i określamy bindowania: @ (string), = (two-way binding), & (funkcja), oraz < (one-way binding).
Odpowiedź w 2 minuty: Izolowany scope jest kluczową funkcją umożliwiającą tworzenie prawdziwie wielokrotnie używalnych komponentów w AngularJS. W przeciwieństwie do domyślnego dziedziczenia scope, izolowany scope tworzy całkowicie nowy scope, który nie ma dostępu do właściwości scope rodzica, chyba że są one jawnie przekazane przez atrybuty.
Izolowany scope definiujemy ustawiając właściwość scope na obiekt (zamiast true lub false). Klucze tego obiektu to nazwy właściwości w scope dyrektywy, a wartości określają typ bindowania: @ (at) - jednokierunkowe bindowanie string, wartość atrybutu jest interpolowana i przekazana jako string; = (equals) - dwukierunkowe bindowanie, zmiany w dyrektywie i rodzicu są synchronizowane; & (ampersand) - bindowanie funkcji, pozwala dyrektywie wywołać wyrażenie w kontekście scope rodzica; < (less than, dodane w AngularJS 1.5) - jednokierunkowe bindowanie obiektów, dyrektywa otrzymuje wartość, ale zmiany nie są propagowane do rodzica.
Izolowany scope zapewnia enkapsulację, co oznacza, że dyrektywa nie może przypadkowo modyfikować scope rodzica, i ułatwia testowanie komponentów w izolacji. Jest to best practice dla komponentów wielokrotnego użytku, szczególnie tych, które mogą być używane w różnych kontekstach aplikacji.
Można również używać aliasów w bindowaniu, np. scope: { lokalnyNazwa: '@atrybutNazwa' }, co pozwala na różne nazwy w scope dyrektywy i atrybutach HTML. Od AngularJS 1.5, zaleca się używanie komponentów zamiast dyrektyw dla większości przypadków użycia UI, ponieważ komponenty mają domyślnie izolowany scope i prostszą składnię.
Przykład kodu:
angular.module('myApp', [])
// Przykład wszystkich typów bindowania
.directive('profilUzytkownika', function() {
return {
restrict: 'E',
scope: {
// @ - bindowanie string (jednokierunkowe)
imie: '@',
// = - bindowanie dwukierunkowe (two-way)
uzytkownik: '=',
// & - bindowanie funkcji
onZapisz: '&',
// < - bindowanie jednokierunkowe (one-way, od AngularJS 1.5)
ustawienia: '<'
},
template: `
<div class="profil">
<h3>{{imie}}</h3>
<input ng-model="uzytkownik.email">
<p>Motyw: {{ustawienia.motyw}}</p>
<button ng-click="zapisz()">Zapisz</button>
</div>
`,
link: function(scope, element, attrs) {
scope.zapisz = function() {
// Wywołanie funkcji rodzica z parametrami
scope.onZapisz({
uzytkownik: scope.uzytkownik
});
};
}
};
})
// Przykład z aliasami
.directive('kartaProduktu', function() {
return {
restrict: 'E',
scope: {
// Nazwa w scope: nazwa atrybutu
nazwaProduktu: '@nazwa',
cenaProduktu: '@cena',
daneProduktu: '=produkt',
akcjaKliknij: '&onKlik'
},
template: `
<div class="karta-produktu">
<h4>{{nazwaProduktu}}</h4>
<p>Cena: {{cenaProduktu}} PLN</p>
<p>Opis: {{daneProduktu.opis}}</p>
<button ng-click="kliknij()">Dodaj do koszyka</button>
</div>
`,
link: function(scope) {
scope.kliknij = function() {
scope.akcjaKliknij({
produkt: scope.daneProduktu
});
};
}
};
})
// Przykład bez izolowanego scope (dla porównania)
.directive('bezIzolacji', function() {
return {
restrict: 'E',
// Brak scope lub scope: false - dziedziczy scope rodzica
template: '<div>Wartość z rodzica: {{wartoscRodzica}}</div>',
link: function(scope) {
// Ma bezpośredni dostęp do scope rodzica
scope.wartoscRodzica = 'Mogę to zmienić!';
}
};
})
// Przykład z opcjonalnymi bindowaniami
.directive('opcjonalneBind', function() {
return {
restrict: 'E',
scope: {
wymagane: '=',
opcjonalne: '=?', // Znak ? oznacza opcjonalne
opcjonalnaFunkcja: '&?'
},
link: function(scope) {
// Sprawdzenie czy opcjonalne bindowanie istnieje
if (angular.isDefined(scope.opcjonalne)) {
console.log('Opcjonalne bindowanie dostarczone');
}
// Bezpieczne wywołanie opcjonalnej funkcji
if (scope.opcjonalnaFunkcja) {
scope.opcjonalnaFunkcja();
}
}
};
})
// Praktyczny przykład: komponent formularza
.directive('formularzKontakt', function() {
return {
restrict: 'E',
scope: {
tytulFormularza: '@tytul',
dane: '=',
onWyslij: '&',
opcjeKonfiguracji: '<konfig'
},
template: `
<form name="kontaktForm" ng-submit="wyslij()">
<h3>{{tytulFormularza}}</h3>
<input type="text"
ng-model="dane.imie"
placeholder="Imię"
required>
<input type="email"
ng-model="dane.email"
placeholder="Email"
required>
<textarea ng-model="dane.wiadomosc"
placeholder="Wiadomość"
required></textarea>
<button type="submit"
ng-disabled="kontaktForm.$invalid">
Wyślij
</button>
</form>
`,
link: function(scope) {
scope.wyslij = function() {
if (scope.dane.imie && scope.dane.email) {
scope.onWyslij({
dane: angular.copy(scope.dane)
});
// Reset formularza jeśli skonfigurowane
if (scope.opcjeKonfiguracji &&
scope.opcjeKonfiguracji.resetPoWyslaniu) {
scope.dane = {};
}
}
};
}
};
});
// Kontroler używający dyrektyw z izolowanym scope
angular.module('myApp')
.controller('GlownyController', function($scope) {
$scope.uzytkownik = {
email: 'jan@example.com'
};
$scope.ustawienia = {
motyw: 'ciemny'
};
$scope.produkt = {
opis: 'Wysokiej jakości produkt'
};
$scope.zapiszUzytkownika = function(uzytkownik) {
console.log('Zapisywanie:', uzytkownik);
};
$scope.dodajDoKoszyka = function(produkt) {
console.log('Dodano do koszyka:', produkt);
};
$scope.daneKontakt = {};
$scope.konfig = {
resetPoWyslaniu: true
};
$scope.wyslij = function(dane) {
console.log('Wysłano:', dane);
};
});
Użycie w HTML:
<div ng-controller="GlownyController">
<!-- Bindowanie @ (string) -->
<profil-uzytkownika
imie="Jan Kowalski"
uzytkownik="uzytkownik"
on-zapisz="zapiszUzytkownika(uzytkownik)"
ustawienia="ustawienia">
</profil-uzytkownika>
<!-- Użycie z aliasami -->
<karta-produktu
nazwa="Laptop"
cena="3500"
produkt="produkt"
on-klik="dodajDoKoszyka(produkt)">
</karta-produktu>
<!-- Formularz z konfiguracją -->
<formularz-kontakt
tytul="Skontaktuj się z nami"
dane="daneKontakt"
on-wyslij="wyslij(dane)"
konfig="konfig">
</formularz-kontakt>
</div>
Materiały:
18. Jak działa transclude w dyrektywach?
Odpowiedź w 30 sekund:
Transclude pozwala dyrektywie zachować i wstawić oryginalną zawartość elementu do szablonu dyrektywy. Włączamy go ustawiając transclude: true i używając dyrektywy ng-transclude w szablonie, aby określić miejsce wstawienia zawartości.
Odpowiedź w 2 minuty: Transclusion to mechanizm w AngularJS, który umożliwia dyrektywie opakowywanie i ponowne użycie zawartości dostarczonej przez użytkownika dyrektywy. Jest to kluczowe dla tworzenia komponentów typu "wrapper" lub "container", takich jak panele, dialogi, karty, czy zakładki, które mają własną strukturę, ale pozwalają na niestandardową zawartość wewnętrzną.
Aby włączyć transclusion, ustawiamy transclude: true w definicji dyrektywy. Następnie w szablonie dyrektywy umieszczamy dyrektywę ng-transclude w miejscu, gdzie ma pojawić się oryginalna zawartość. AngularJS automatycznie wyciąga zawartość z oryginalnego elementu i wstawia ją w miejsce oznaczone ng-transclude.
Istnieją dwa typy transclusion: podstawowy (true) - cała zawartość elementu jest transcludowana, oraz nazwany/wielokrotny (obiekt) - różne części zawartości mogą być transcludowane do różnych miejsc w szablonie. Nazwany transclusion używa składni transclude: { 'slotName': '?elementSelector' }.
Ważną cechą transclusion jest to, że transcludowana zawartość zachowuje swój oryginalny scope (scope rodzica dyrektywy), a nie scope dyrektywy. To oznacza, że wyrażenia w transcludowanej zawartości są ewaluowane w kontekście scope rodzica, co jest zazwyczaj pożądane zachowanie. W funkcji link możemy uzyskać dostęp do funkcji transclude jako piątego parametru, co daje większą kontrolę nad procesem.
Przykład kodu:
angular.module('myApp', [])
// Podstawowy przykład transclusion
.directive('mojaKarta', function() {
return {
restrict: 'E',
transclude: true,
scope: {
tytul: '@'
},
template: `
<div class="karta">
<div class="naglowek">
<h3>{{tytul}}</h3>
</div>
<div class="tresc" ng-transclude>
<!-- Zawartość zostanie wstawiona tutaj -->
</div>
<div class="stopka">
<button>Akcja</button>
</div>
</div>
`
};
})
// Nazwany (wielokrotny) transclusion
.directive('panelZakladek', function() {
return {
restrict: 'E',
transclude: {
'tytul': '?panelTytul', // Opcjonalny slot dla tytułu
'zakladki': 'panelZakladki', // Wymagany slot dla zakładek
'tresc': 'panelTresc' // Wymagany slot dla treści
},
template: `
<div class="panel">
<div class="panel-header">
<h2 ng-transclude="tytul">Domyślny tytuł</h2>
</div>
<div class="panel-tabs" ng-transclude="zakladki">
</div>
<div class="panel-content" ng-transclude="tresc">
</div>
</div>
`
};
})
// Użycie funkcji transclude w link
.directive('warunkonyTransclude', function() {
return {
restrict: 'E',
transclude: true,
scope: {
pokazZawartosc: '='
},
template: '<div class="kontener"></div>',
link: function(scope, element, attrs, ctrl, transcludeFn) {
// transcludeFn to funkcja transclusion
scope.$watch('pokazZawartosc', function(pokazac) {
var kontener = element.find('.kontener');
kontener.empty();
if (pokazac) {
// Ręczne użycie funkcji transclude
transcludeFn(function(clone, scope) {
kontener.append(clone);
});
}
});
}
};
})
// Praktyczny przykład: komponent dialogu
.directive('dialog', function() {
return {
restrict: 'E',
transclude: {
'naglowek': '?dialogNaglowek',
'tresc': 'dialogTresc',
'stopka': '?dialogStopka'
},
scope: {
widoczny: '=',
onZamknij: '&'
},
template: `
<div class="dialog-overlay" ng-show="widoczny">
<div class="dialog-box">
<div class="dialog-header">
<span ng-transclude="naglowek"></span>
<button class="close-btn" ng-click="zamknij()">×</button>
</div>
<div class="dialog-body" ng-transclude="tresc">
</div>
<div class="dialog-footer" ng-transclude="stopka">
<button ng-click="zamknij()">Zamknij</button>
</div>
</div>
</div>
`,
link: function(scope) {
scope.zamknij = function() {
scope.widoczny = false;
if (scope.onZamknij) {
scope.onZamknij();
}
};
}
};
})
// Zaawansowany przykład: accordion
.directive('accordion', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
template: '<div class="accordion" ng-transclude></div>',
controller: function($scope) {
var sekcje = [];
this.zarejestrujSekcje = function(sekcja) {
sekcje.push(sekcja);
};
this.zamknijInne = function(otwartaSekcja) {
angular.forEach(sekcje, function(sekcja) {
if (sekcja !== otwartaSekcja) {
sekcja.otwarta = false;
}
});
};
}
};
})
.directive('accordionSekcja', function() {
return {
restrict: 'E',
transclude: true,
require: '^accordion',
scope: {
tytul: '@'
},
template: `
<div class="accordion-sekcja">
<div class="accordion-tytul" ng-click="przelacz()">
{{tytul}}
<span>{{otwarta ? '▼' : '▶'}}</span>
</div>
<div class="accordion-tresc"
ng-show="otwarta"
ng-transclude>
</div>
</div>
`,
link: function(scope, element, attrs, accordionCtrl) {
scope.otwarta = false;
accordionCtrl.zarejestrujSekcje(scope);
scope.przelacz = function() {
scope.otwarta = !scope.otwarta;
if (scope.otwarta) {
accordionCtrl.zamknijInne(scope);
}
};
}
};
})
// Przykład pokazujący scope transclusion
.directive('demoScope', function() {
return {
restrict: 'E',
transclude: true,
scope: {
wartoscDyrektywy: '='
},
template: `
<div>
<p>Scope dyrektywy: {{wartoscDyrektywy}}</p>
<div ng-transclude>
<!-- Ta zawartość ma dostęp do scope rodzica,
nie scope dyrektywy -->
</div>
</div>
`
};
});
// Kontroler demonstracyjny
angular.module('myApp')
.controller('DemoController', function($scope) {
$scope.wartoscRodzica = 'Wartość z rodzica';
$scope.wartoscDyrektywy = 'Wartość dyrektywy';
$scope.pokazDialog = false;
$scope.otworzDialog = function() {
$scope.pokazDialog = true;
};
$scope.zamknijDialog = function() {
console.log('Dialog zamknięty');
};
});
Użycie w HTML:
<div ng-controller="DemoController">
<!-- Podstawowy transclusion -->
<moja-karta tytul="Moja Karta">
<p>Ta zawartość zostanie transcludowana</p>
<p>Może zawierać {{wartoscRodzica}}</p>
</moja-karta>
<!-- Nazwany transclusion -->
<panel-zakladek>
<panel-tytul>
<strong>Panel z zakładkami</strong>
</panel-tytul>
<panel-zakladki>
<button>Zakładka 1</button>
<button>Zakładka 2</button>
</panel-zakladki>
<panel-tresc>
<p>Treść panelu</p>
</panel-tresc>
</panel-zakladek>
<!-- Dialog z transclusion -->
<button ng-click="otworzDialog()">Otwórz Dialog</button>
<dialog widoczny="pokazDialog" on-zamknij="zamknijDialog()">
<dialog-naglowek>
<h2>Potwierdzenie</h2>
</dialog-naglowek>
<dialog-tresc>
<p>Czy na pewno chcesz kontynuować?</p>
</dialog-tresc>
<dialog-stopka>
<button ng-click="pokazDialog = false">Anuluj</button>
<button ng-click="pokazDialog = false">OK</button>
</dialog-stopka>
</dialog>
<!-- Accordion z zagnieżdżonym transclusion -->
<accordion>
<accordion-sekcja tytul="Sekcja 1">
<p>Zawartość sekcji 1</p>
</accordion-sekcja>
<accordion-sekcja tytul="Sekcja 2">
<p>Zawartość sekcji 2</p>
</accordion-sekcja>
<accordion-sekcja tytul="Sekcja 3">
<p>Zawartość sekcji 3</p>
</accordion-sekcja>
</accordion>
<!-- Demo scope transclusion -->
<demo-scope wartosc-dyrektywy="wartoscDyrektywy">
<p>Scope rodzica: {{wartoscRodzica}}</p>
<p>Próba dostępu do scope dyrektywy: {{wartoscDyrektywy}}</p>
<!-- wartoscDyrektywy będzie pusta, bo transcludowana
zawartość używa scope rodzica -->
</demo-scope>
</div>
Materiały:
19. Jakie są wbudowane dyrektywy AngularJS (ng-model, ng-repeat, ng-if)?
Odpowiedź w 30 sekund:
AngularJS oferuje wiele wbudowanych dyrektyw: ng-model tworzy dwukierunkowe bindowanie danych z polami formularza, ng-repeat iteruje po kolekcjach tworząc elementy DOM, ng-if warunkowo dodaje/usuwa elementy z DOM, ng-show/ng-hide warunkowe pokazują/ukrywają elementy, ng-click obsługuje zdarzenia kliknięcia, oraz wiele innych.
Odpowiedź w 2 minuty: AngularJS dostarcza bogaty zestaw wbudowanych dyrektyw, które znacząco upraszczają tworzenie dynamicznych aplikacji. Najważniejsze z nich to:
ng-model - tworzy dwukierunkowe bindowanie danych między polem formularza a właściwością scope. Zmiany w polu automatycznie aktualizują model, a zmiany w modelu aktualizują pole. Dodatkowo dostarcza walidację i stan formularza ($valid, $dirty, $touched).
ng-repeat - iteruje po tablicach lub obiektach, tworząc kopię elementu dla każdego elementu kolekcji. Automatycznie zarządza cyklem życia elementów DOM przy zmianach w kolekcji. Oferuje specjalne zmienne jak $index, $first, $last, $even, $odd. Można użyć track by do optymalizacji wydajności.
ng-if - warunkowo dodaje lub całkowicie usuwa element z DOM (nie tylko ukrywa). Gdy warunek jest false, element i jego scope są niszczone. Gdy staje się true, są tworzone od nowa. To ważne dla wydajności przy dużych strukturach DOM.
ng-show i ng-hide - warunkowo pokazują/ukrywają elementy używając CSS (display: none). W przeciwieństwie do ng-if, element pozostaje w DOM. Lepsze dla częstych przełączeń, ponieważ nie niszczą/tworzą elementów.
ng-click - binduje wyrażenie do zdarzenia click elementu. Podobnie działają ng-dblclick, ng-mouseenter, ng-mouseleave, etc.
ng-bind - jednoκierunkowe bindowanie danych do zawartości elementu (alternatywa dla {{}}). ng-bind-html pozwala na bindowanie HTML (wymaga $sce).
ng-class i ng-style - dynamicznie dodają klasy CSS i style inline.
ng-disabled, ng-readonly, ng-checked - warunkowo ustawiają atrybuty elementów formularza.
Przykład kodu:
angular.module('myApp', [])
.controller('DemoController', function($scope) {
// Dane dla ng-model
$scope.uzytkownik = {
imie: '',
email: '',
wiek: 0
};
// Dane dla ng-repeat
$scope.produkty = [
{ id: 1, nazwa: 'Laptop', cena: 3500, dostepny: true },
{ id: 2, nazwa: 'Mysz', cena: 50, dostepny: true },
{ id: 3, nazwa: 'Klawiatura', cena: 200, dostepny: false },
{ id: 4, nazwa: 'Monitor', cena: 800, dostepny: true }
];
// Dane dla ng-if i ng-show
$scope.pokazSzczegoly = false;
$scope.zalogowany = false;
// Funkcja dla ng-click
$scope.dodajProdukt = function() {
$scope.produkty.push({
id: $scope.produkty.length + 1,
nazwa: 'Nowy produkt',
cena: 0,
dostepny: true
});
};
$scope.usunProdukt = function(index) {
$scope.produkty.splice(index, 1);
};
$scope.przelaczWidocznosc = function() {
$scope.pokazSzczegoly = !$scope.pokazSzczegoly;
};
// Dane dla ng-class
$scope.statusy = [
{ tekst: 'Sukces', typ: 'sukces' },
{ tekst: 'Ostrzeżenie', typ: 'ostrzezenie' },
{ tekst: 'Błąd', typ: 'blad' }
];
// Dane dla ng-disabled
$scope.formularzZablokowany = false;
// Zaznaczone elementy dla ng-checked
$scope.zaznaczone = {
opcja1: true,
opcja2: false
};
});
Przykłady użycia w HTML:
<div ng-controller="DemoController">
<!-- ng-model: dwukierunkowe bindowanie -->
<h3>ng-model</h3>
<form name="formularz">
<input type="text"
ng-model="uzytkownik.imie"
placeholder="Imię"
required>
<input type="email"
ng-model="uzytkownik.email"
placeholder="Email"
required>
<input type="number"
ng-model="uzytkownik.wiek"
min="0" max="120">
<p>Witaj, {{uzytkownik.imie}}!</p>
<p>Formularz ważny: {{formularz.$valid}}</p>
</form>
<!-- ng-repeat: iteracja po kolekcji -->
<h3>ng-repeat</h3>
<button ng-click="dodajProdukt()">Dodaj produkt</button>
<ul>
<li ng-repeat="produkt in produkty track by produkt.id">
<!-- Specjalne zmienne ng-repeat -->
<span>{{$index + 1}}.</span>
<strong>{{produkt.nazwa}}</strong> - {{produkt.cena}} PLN
<!-- ng-if: warunkowo dodaje/usuwa z DOM -->
<span ng-if="produkt.dostepny" style="color: green;">
✓ Dostępny
</span>
<span ng-if="!produkt.dostepny" style="color: red;">
✗ Niedostępny
</span>
<!-- ng-click: obsługa zdarzeń -->
<button ng-click="usunProdukt($index)">Usuń</button>
<!-- ng-class: dynamiczne klasy -->
<span ng-class="{
'pierwszy': $first,
'ostatni': $last,
'parzysty': $even,
'nieparzysty': $odd
}">
Index: {{$index}}
</span>
</li>
</ul>
<!-- Filtrowanie w ng-repeat -->
<input ng-model="szukaj" placeholder="Szukaj produktu">
<ul>
<li ng-repeat="produkt in produkty | filter:szukaj | orderBy:'cena'">
{{produkt.nazwa}} - {{produkt.cena}} PLN
</li>
</ul>
<!-- ng-if vs ng-show -->
<h3>ng-if vs ng-show</h3>
<button ng-click="przelaczWidocznosc()">
Przełącz widoczność
</button>
<!-- ng-if: element usunięty z DOM gdy false -->
<div ng-if="pokazSzczegoly">
<p>Używam ng-if - jestem usunięty z DOM gdy ukryty</p>
</div>
<!-- ng-show: element ukryty przez CSS gdy false -->
<div ng-show="pokazSzczegoly">
<p>Używam ng-show - jestem w DOM, tylko ukryty</p>
</div>
<!-- ng-hide: odwrotność ng-show -->
<div ng-hide="!pokazSzczegoly">
<p>Używam ng-hide</p>
</div>
<!-- ng-class: dynamiczne klasy CSS -->
<h3>ng-class</h3>
<div ng-repeat="status in statusy">
<p ng-class="{
'alert-sukces': status.typ === 'sukces',
'alert-ostrzezenie': status.typ === 'ostrzezenie',
'alert-blad': status.typ === 'blad'
}">
{{status.tekst}}
</p>
<!-- ng-class z tablicą -->
<p ng-class="['bazowa-klasa', status.typ]">
{{status.tekst}}
</p>
<!-- ng-class z wyrażeniem -->
<p ng-class="'status-' + status.typ">
{{status.tekst}}
</p>
</div>
<!-- ng-style: dynamiczne style -->
<h3>ng-style</h3>
<div ng-style="{
'background-color': produkt.dostepny ? 'lightgreen' : 'lightcoral',
'padding': '10px',
'border': '1px solid black'
}" ng-repeat="produkt in produkty">
{{produkt.nazwa}}
</div>
<!-- ng-disabled, ng-readonly, ng-checked -->
<h3>Atrybuty formularza</h3>
<label>
<input type="checkbox" ng-model="formularzZablokowany">
Zablokuj formularz
</label>
<input type="text"
ng-model="uzytkownik.imie"
ng-disabled="formularzZablokowany"
placeholder="Pole może być zablokowane">
<input type="text"
ng-readonly="formularzZablokowany"
value="Pole tylko do odczytu"
placeholder="Pole może być tylko do odczytu">
<label>
<input type="checkbox"
ng-checked="zaznaczone.opcja1"
ng-model="zaznaczone.opcja1">
Opcja 1
</label>
<!-- ng-bind: alternatywa dla {{}} -->
<h3>ng-bind</h3>
<p>Używając {{}}: {{uzytkownik.imie}}</p>
<p>Używając ng-bind: <span ng-bind="uzytkownik.imie"></span></p>
<!-- ng-switch: warunkowe renderowanie -->
<h3>ng-switch</h3>
<select ng-model="wybranyWidok">
<option value="lista">Lista</option>
<option value="siatka">Siatka</option>
<option value="tabela">Tabela</option>
</select>
<div ng-switch="wybranyWidok">
<div ng-switch-when="lista">Widok listy</div>
<div ng-switch-when="siatka">Widok siatki</div>
<div ng-switch-when="tabela">Widok tabeli</div>
<div ng-switch-default>Wybierz widok</div>
</div>
<!-- ng-options: generowanie opcji select -->
<h3>ng-options</h3>
<select ng-model="wybranyProdukt"
ng-options="produkt.nazwa for produkt in produkty track by produkt.id">
<option value="">-- Wybierz produkt --</option>
</select>
<p>Wybrany: {{wybranyProdukt.nazwa}}</p>
</div>
Style CSS dla przykładów:
/* Klasy dla ng-class */
.alert-sukces { background-color: #d4edda; color: #155724; padding: 10px; }
.alert-ostrzezenie { background-color: #fff3cd; color: #856404; padding: 10px; }
.alert-blad { background-color: #f8d7da; color: #721c24; padding: 10px; }
.pierwszy { font-weight: bold; }
.ostatni { font-style: italic; }
.parzysty { background-color: #f0f0f0; }
.nieparzysty { background-color: #ffffff; }
Materiały:
20. Czym jest ng-class i ng-style oraz jak ich używać?
Odpowiedź w 30 sekund:
ng-class dynamicznie dodaje lub usuwa klasy CSS na podstawie wyrażeń, przyjmując string, tablicę lub obiekt z mapowaniem klasa-warunek. ng-style dynamicznie ustawia style inline, przyjmując obiekt z mapowaniem właściwość CSS-wartość. Oba pozwalają na reaktywne stylowanie elementów w odpowiedzi na zmiany danych.
Odpowiedź w 2 minuty:
ng-class i ng-style to potężne dyrektywy AngularJS do dynamicznego stylowania elementów w odpowiedzi na zmiany w aplikacji. Są kluczowe dla tworzenia responsywnych interfejsów użytkownika, które wizualnie reagują na stan aplikacji.
ng-class przyjmuje trzy formy wyrażeń: 1) String - proste wyrażenie zwracające nazwę klasy lub klasy oddzielone spacjami: ng-class="'klasa1 klasa2'" lub ng-class="wyrazenieZwracajaceString". 2) Tablica - lista wyrażeń, które są ewaluowane i łączone: ng-class="['klasa1', warunek ? 'klasa2' : '', zmienna]". 3) Obiekt (najczęściej używane) - mapa klucz-wartość, gdzie klucz to nazwa klasy, a wartość to wyrażenie boolean określające czy klasa powinna być zastosowana: ng-class="{ 'aktywny': jestAktywny, 'wybrany': jestWybrany }".
ng-style przyjmuje obiekt JavaScript, gdzie klucze to nazwy właściwości CSS (w formacie camelCase lub w cudzysłowach), a wartości to wyrażenia zwracające wartości CSS: ng-style="{ backgroundColor: kolor, 'font-size': rozmiar + 'px' }". Wszystkie właściwości są stosowane jako style inline.
Kluczowe różnice: ng-class modyfikuje atrybut class elementu i korzysta z zewnętrznych arkuszy stylów (lepsze dla wydajności i separacji concerns), podczas gdy ng-style dodaje style inline (wyższy priorytet CSS, ale gorsze dla konserwacji). ng-class jest preferowany dla predefiniowanych stanów wizualnych, a ng-style dla wartości obliczanych dynamicznie (np. pozycje, wymiary).
Obie dyrektywy są reaktywne - automatycznie aktualizują się gdy zmienią się dane w wyrażeniach. Można ich używać równocześnie na tym samym elemencie. Dla złożonych transformacji wizualnych, rozważ użycie animacji AngularJS (ngAnimate).
Przykład kodu:
angular.module('myApp', [])
.controller('StyleController', function($scope, $interval) {
// Dane dla ng-class
$scope.status = 'aktywny';
$scope.wiadomosci = [
{ typ: 'sukces', tresc: 'Operacja zakończona pomyślnie', przeczytana: false },
{ typ: 'ostrzezenie', tresc: 'Uwaga: niska pamięć', przeczytana: false },
{ typ: 'blad', tresc: 'Błąd połączenia z serwerem', przeczytana: true },
{ typ: 'info', tresc: 'Nowa aktualizacja dostępna', przeczytana: false }
];
$scope.przelaczStatus = function(wiadomosc) {
wiadomosc.przeczytana = !wiadomosc.przeczytana;
};
// Dane dla ng-style
$scope.postep = 0;
$scope.kolor = '#3498db';
$scope.rozmiarCzcionki = 16;
// Animowany pasek postępu
$interval(function() {
$scope.postep = ($scope.postep + 10) % 110;
}, 1000);
// Funkcja zwracająca obiekt stylów
$scope.pobierzStylePostępu = function() {
return {
width: $scope.postep + '%',
backgroundColor: $scope.pobierzKolorPostepu(),
transition: 'width 0.5s ease'
};
};
$scope.pobierzKolorPostepu = function() {
if ($scope.postep < 30) return '#e74c3c';
if ($scope.postep < 70) return '#f39c12';
return '#27ae60';
};
// Element do przeciągania
$scope.pozycja = { x: 0, y: 0 };
$scope.przeciagnij = function(event) {
$scope.pozycja.x = event.clientX;
$scope.pozycja.y = event.clientY;
};
// Karty produktów z różnymi stanami
$scope.produkty = [
{
nazwa: 'Laptop',
cena: 3500,
dostepnosc: 'dostepny',
promocja: true,
ocena: 4.5
},
{
nazwa: 'Smartfon',
cena: 2000,
dostepnosc: 'niska',
promocja: false,
ocena: 4.0
},
{
nazwa: 'Tablet',
cena: 1500,
dostepnosc: 'niedostepny',
promocja: true,
ocena: 3.5
}
];
$scope.wybranyProdukt = null;
$scope.wybierz = function(produkt) {
$scope.wybranyProdukt = produkt;
};
// Funkcja zwracająca klasy produktu
$scope.pobierzKlasyProduktu = function(produkt) {
return {
'produkt-dostepny': produkt.dostepnosc === 'dostepny',
'produkt-niska-dostepnosc': produkt.dostepnosc === 'niska',
'produkt-niedostepny': produkt.dostepnosc === 'niedostepny',
'produkt-promocja': produkt.promocja,
'produkt-wybrany': produkt === $scope.wybranyProdukt,
'produkt-wysoka-ocena': produkt.ocena >= 4.0
};
};
// Dynamiczne układy
$scope.uklad = 'lista';
$scope.rozmiarSiatki = 3;
});
Przykłady użycia w HTML:
<div ng-controller="StyleController">
<!-- ng-class: Forma obiektowa (najczęstsza) -->
<h3>ng-class - Forma obiektowa</h3>
<div ng-repeat="wiadomosc in wiadomosci"
ng-class="{
'wiadomosc-sukces': wiadomosc.typ === 'sukces',
'wiadomosc-ostrzezenie': wiadomosc.typ === 'ostrzezenie',
'wiadomosc-blad': wiadomosc.typ === 'blad',
'wiadomosc-info': wiadomosc.typ === 'info',
'wiadomosc-przeczytana': wiadomosc.przeczytana
}"
ng-click="przelaczStatus(wiadomosc)">
{{wiadomosc.tresc}}
</div>
<!-- ng-class: Forma tablicowa -->
<h3>ng-class - Forma tablicowa</h3>
<div ng-class="[
'bazowa-klasa',
status === 'aktywny' ? 'aktywny' : 'nieaktywny',
'dodatkowa-klasa'
]">
Status: {{status}}
</div>
<!-- ng-class: Forma stringowa -->
<h3>ng-class - Forma stringowa</h3>
<div ng-class="'status-' + status">
Dynamiczna klasa: status-{{status}}
</div>
<!-- ng-class: Z funkcją -->
<h3>ng-class - Z funkcją</h3>
<div ng-repeat="produkt in produkty"
class="karta-produktu"
ng-class="pobierzKlasyProduktu(produkt)"
ng-click="wybierz(produkt)">
<h4>{{produkt.nazwa}}</h4>
<p>Cena: {{produkt.cena}} PLN</p>
<p>Status: {{produkt.dostepnosc}}</p>
<span ng-if="produkt.promocja" class="etykieta-promocja">PROMOCJA</span>
</div>
<!-- ng-style: Podstawowe użycie -->
<h3>ng-style - Podstawowe użycie</h3>
<div class="kontrole">
<label>
Kolor: <input type="color" ng-model="kolor">
</label>
<label>
Rozmiar czcionki:
<input type="range" ng-model="rozmiarCzcionki" min="10" max="48">
{{rozmiarCzcionki}}px
</label>
</div>
<div ng-style="{
'background-color': kolor,
'font-size': rozmiarCzcionki + 'px',
'padding': '20px',
'border-radius': '8px',
'color': 'white',
'text-align': 'center'
}">
Dynamicznie stylowany tekst
</div>
<!-- ng-style: Pasek postępu -->
<h3>ng-style - Pasek postępu</h3>
<div class="pasek-postepu-tlo">
<div class="pasek-postepu" ng-style="pobierzStylePostępu()">
{{postep}}%
</div>
</div>
<!-- ng-style: Pozycjonowanie dynamiczne -->
<h3>ng-style - Pozycjonowanie</h3>
<div class="obszar-przeciagania"
ng-mousemove="przeciagnij($event)">
<div class="przeciagalny-element"
ng-style="{
left: pozycja.x + 'px',
top: pozycja.y + 'px',
transform: 'translate(-50%, -50%)'
}">
X: {{pozycja.x}}, Y: {{pozycja.y}}
</div>
</div>
<!-- Kombinacja ng-class i ng-style -->
<h3>Kombinacja ng-class i ng-style</h3>
<div ng-repeat="produkt in produkty"
ng-class="{
'karta': true,
'wybrany': produkt === wybranyProdukt
}"
ng-style="{
'border-left': '5px solid ' + (produkt.promocja ? '#e74c3c' : '#3498db'),
'opacity': produkt.dostepnosc === 'niedostepny' ? 0.5 : 1
}"
ng-click="wybierz(produkt)">
<h4>{{produkt.nazwa}}</h4>
<p>{{produkt.cena}} PLN</p>
</div>
<!-- Dynamiczne układy z ng-class -->
<h3>Dynamiczne układy</h3>
<div class="kontrole-ukladu">
<button ng-click="uklad = 'lista'"
ng-class="{'aktywny': uklad === 'lista'}">
Lista
</button>
<button ng-click="uklad = 'siatka'"
ng-class="{'aktywny': uklad === 'siatka'}">
Siatka
</button>
</div>
<div ng-class="{
'uklad-lista': uklad === 'lista',
'uklad-siatka': uklad === 'siatka'
}">
<div ng-repeat="produkt in produkty"
class="element-ukladu"
ng-style="uklad === 'siatka' && {
'width': (100 / rozmiarSiatki) + '%'
}">
{{produkt.nazwa}}
</div>
</div>
<!-- Warunkowe style z operatorem ternarnym -->
<h3>Style warunkowe</h3>
<div ng-repeat="produkt in produkty"
ng-style="{
'background-color': produkt.ocena >= 4.0 ? '#d4edda' : '#f8d7da',
'border': '2px solid ' + (produkt === wybranyProdukt ? '#007bff' : '#dee2e6'),
'padding': '15px',
'margin': '10px 0',
'cursor': 'pointer'
}"
ng-click="wybierz(produkt)">
<strong>{{produkt.nazwa}}</strong> - Ocena: {{produkt.ocena}}
</div>
</div>
Style CSS wspierające przykłady:
/* Klasy dla ng-class */
.wiadomosc-sukces {
background-color: #d4edda;
border-left: 4px solid #28a745;
padding: 10px;
margin: 5px 0;
}
.wiadomosc-ostrzezenie {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 10px;
margin: 5px 0;
}
.wiadomosc-blad {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
padding: 10px;
margin: 5px 0;
}
.wiadomosc-info {
background-color: #d1ecf1;
border-left: 4px solid #17a2b8;
padding: 10px;
margin: 5px 0;
}
.wiadomosc-przeczytana {
opacity: 0.6;
text-decoration: line-through;
}
.karta-produktu {
border: 2px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.produkt-wybrany {
border-color: #007bff;
box-shadow: 0 0 10px rgba(0,123,255,0.3);
}
.produkt-dostepny {
background-color: #f0fff0;
}
.produkt-niska-dostepnosc {
background-color: #fffef0;
}
.produkt-niedostepny {
background-color: #fff0f0;
opacity: 0.7;
}
.produkt-promocja::after {
content: '🔥';
margin-left: 10px;
}
.produkt-wysoka-ocena::before {
content: '⭐';
margin-right: 5px;
}
.pasek-postepu-tlo {
background-color: #ecf0f1;
border-radius: 10px;
overflow: hidden;
height: 30px;
}
.pasek-postepu {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
.obszar-przeciagania {
position: relative;
width: 100%;
height: 300px;
border: 2px dashed #ccc;
cursor: crosshair;
}
.przeciagalny-element {
position: absolute;
background-color: #3498db;
color: white;
padding: 10px;
border-radius: 5px;
pointer-events: none;
}
.uklad-lista .element-ukladu {
display: block;
width: 100%;
padding: 10px;
border-bottom: 1px solid #ddd;
}
.uklad-siatka {
display: flex;
flex-wrap: wrap;
}
.uklad-siatka .element-ukladu {
box-sizing: border-box;
padding: 10px;
border: 1px solid #ddd;
}
Materiały:
Serwisy i Fabryki
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziFiltry
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziRouting
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziFormularze i Walidacja
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziCykl życia i Wydajność
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziTestowanie
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziMigracja i Dobre Praktyki
Odpowiedź dostępna w pełnej wersji. Odblokuj 30 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi