30 martie 2020

Angular - ziua 7 - Pipes

Pipes
- transforma output
- lista de pipe-uri oferite de Angular se poate consulta aici

// Html: 
<p> {{ username | uppercase }} </p>
// alte exemple de pipes: lowercase, dateTime, date, etc.
<p> {{ user.date | date:'fullDate' }} </p>

Pipe-uri inlantuite:
<p> {{ user.date | date:'fullDate' | uppercase }} </p>

Pipe-uri personalizate:
@Pipe({
  name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
  transform(value: any) {
    return value.substr(0, 10) + '...';
  }
}

// se adauga in app.module.ts -> declarations

// Html:
<p> {{ name | shorten  }} </p>

Parametrizare:
@Pipe({
  name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
  transform(value: any, limit: number) { // se pot adauga mai multe filtre, ca parametri
    if (value.length > limit) {
      return value.substr(0, limit) + '...';
    }
    return value;
  }
}

// Html:
<p> {{ name | shorten:5  }} </p>

Generare sablon pentru un pipe personalizat:
> ng generate pipe

!!! Filtrul, odata aplicat, nu este rulat din nou daca se schimba datele (Array sau Object), din motive de performanta. 
Pentru a forta sa se aplica filtrul, se adauga:
@Pipe({
  name: 'shorten',
  pure: false
})

Pipe asincron
appStatus = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('stable');
  } , 2000);
});

// Html
<h2> {{ appStatus }} </h2> // va afisa [object Object], nu se uita daca obiectul se transforma dupa 2 sec

Pentru update dupa 2 secunde (merge si pentru Observables):
<h2> {{ appStatus | async }} </h2>

24 martie 2020

Angular - ziua 6 - Formulare

Formulare - 2 abordari:
- bazata pe template - Angular deduce obiectul formularului, din DOM
- reactiva - formularul este creat in mod programatic si este sincronizat cu DOM-ul

TEMPLATE

In app.module.ts, @NgModule -> imports -> FormsModule

<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  <input type="text" id="username" name="username" ngModel>
</form>

In TypeScript:

onSubmit (form: NgForm) { // NgForm este un formular creat automat de Angular
     console.log(form.value['username']);
    // sau form.value.username
}

Altfel - daca vrei sa accesezi formularul nu numai la completarea lui, ci si inainte:
@ViewChild('f') signupForm: NgForm;
// ngModel aplicat pe tag-ul html iti da voie sa recuperezi obiectul in TypeScript
// onSubmit() poate deveni acum fara parametru de intrare

Cateva din alte proprietati furnizate de NgForm:
- dirty: daca formularul are vreun camp schimbat
- enabled, disabled
- valid, invalid
- touched: daca a fost facut click pe formular

Validare input:
- adaugare required in fisierul html, pe input-ul dorit
- alti validatori: email, lista completa aici => validitatea se va reflecta in atributele din NgForm
- Angular adauga dinamic clase css pe input-uri care reflecta starea, folosindu-le putem stiliza input-urile pentru a avertiza utilizatorul:

input .ng-invalid .ng-touched {
    border: 1px solid red;
}

Afisare mesaje de eroare:
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
    <input type="text" id="myemail" name="myemail" ngModel #email="ngModel">
</form>
<span class="help-block" *ngIf="!email.valid && email.touched">Please enter valid email</span>

Inserare valori initiale:
defaultQuestion='pet';

<select id='secret' [ngModel]="defaultQuestion" name="secret">
  <option value="pet">What was your first pet?</option>
  <option value="teacher">What was your first teacher?</option>
</select>


Valori predefinite in TypeScript

@ViewChild('f') myForm: NgForm;
this.myForm.setValue ({
  username: 'myChoice',
  secret: 'pet'
  // trebuiesc adaugate toate campurile
});

// suprascrie doar campurile care ne intereseaza:
this.myForm.form.patchValue({
  userData: {
    username: 'something'
  }
});

Curatare formular: this.myForm.reset();

Two-way binding:
<textarea name="questionAnswer" rows="3" [(ngModel)]="answer"/>
<p> Your reply: {{answer}} </p>
// unde answer trebuie sa fie declarat ca empty string in TypeScript


ngModelGroup - pentru a se aplica la mai mult de un singur camp

<div ngModelGroup="userData" #userData="ngModelGroup">
<!-- campuri -->
</div>
<p *ngIf="!user.Data.valid && userData.touched">User data invalid!</p>


Butoane radio
<div class="radio" *ngFor="let gender of genders">
  <label>
    <input type="radio" name="gender" [value]="gender" ngModel> {{ gender }}
  </label>
</div>

ABORDAREA REACTIVA

- nu mai este nevoie de import FormsModule, se inlocuieste cu ReactiveFormsModule

signupForm: FormGroup;

ngOnInit() {
  this.signupForm = new FormGroup({
    'username': new FormControl(null),
    'email': new FormControl(null),
    'gender': new FormControl('male')
  });
}

onSubmit() { }

// Html:
<form (ngSubmit)="onSubmit()" [formGroup]="signupForm">
    <input type="text" id="username" formControlName="username">
    <input type="text" id="email" formControlName="email">
    <input type="radio" id="gender" formControlName="gender">
</form>

Validare prin metoda reactiva:  

this.signupForm = new FormGroup({
    'username': new FormControl(null, Validators.required), // referinta la metoda de validare
    'email': new FormControl(null, [Validators.required, Validators.email]),
    'gender': new FormControl('male')
});

Afisarea validarilor
<span *ngIf="!signupForm.get('username').valid && signupForm.get('username').touched">Please enter a valid username!</span>

Grupare reactiva
- elementele se aseaza intr-un <div formGroupName="userData">
- se actualizeaza span cu noua cale catre elementul validat:
<span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched">Please enter a valid username!</span>


FormArray









<div formArrayMode="hobbies">
  <h4>Your hobbies</h4>
  <button type="button" (click)="onAddHobby()">Add hobby</button>
  <div class="form-group" *ngFor="let hobbyControl of getControls(); let i = index">
    <input type="text" [formControlName]="i">
  </div>
</div>

// TypeScript:
ngOnInit() {
  this.signupForm = new FormGroup({
    'username': new FormControl(null),
    'email': new FormControl(null),
    'gender': new FormControl('male'),
    'hobbies': new FormArray([])
  });
}

getControls() {
  return (<FormArray>this.signupForm.get('hobbies')).controls;
}
onAddHobby() {
  const control = new FormControl(null, Validators.required);
  (<FormArray>this.signupForm.get('hobbies')).push(control); // explicit casting
}

Creare propriii validatori

forbiddenNames(control: FormControl): {(s: string): boolean} { // format output: {'mesaj de eroare': true}
  if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
    return {'name is forbidden': true};
  }
  return null; // validation is successful
}

this.signupForm = new FormGroup({
    'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
    'email': new FormControl(null, [Validators.required, Validators.email]),
    'gender': new FormControl('male'),
    'age': new FormControl(null, Validators.pattern(/^[1-9]+[0-9]+$'/))
});
// binding-ul este pt a ne asigura ca this in metoda este ceea ce trebuie sa fie

Angular adauga erorile pe fiecare Control -> errors:
<span *ngIf="signupForm.get('userData.username').errors['name is forbidden']">This name is invalid!</span>

Validatori asincroni

forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
  const promise = new Promise<any>((resolve, reject) => {
    setTimeout(() -> {
      if (control.value === 'someAddress@yahoo.com') {
        resolve({'emailIsForbidden': true});
      } else {
        resolve(null);
      }
    }, 1500); // simulare asincronicitate
  });
  return promise;
}

// Html:
this.signupForm = new FormGroup({
    'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
    'email': new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails.bind(this)), // al 3-lea arg este rezervat pt validatori asincroni
    'gender': new FormControl('male')
});

// TypeScript:
this.signupForm.valueChanged.subscribe(value => console.log(value));
this.signupForm.statusChanged.subscribe(status=> console.log(status)); // VALID, INVALID, PENDING (async)

setValue() si patchValue() sunt in continuare disponibile pe obiectul FormGroup.
reset() de asemenea.

19 martie 2020

Angular - ziua 5 - Observables

Observable - o sursa de date; declansat de un eveniment creat de utilizator; declansat de Http requests; poate fi declansat programatic;
--------
-- Observable --> .... stream .... -> Observer
--------
Observer-ul poate sa prelucreze datele, erorile, sau sa coordoneze terminarea, in fiecare dintre aceste actiuni se poate scrie cod (asincron) care sa fie executat.

Exemple: la un route: ActivatedRoute => route.params este un Observable, este un stream de parametri care ofera un nou parametru de fiecare data cand se schimba URL-ul
- la observabile poti doar sa faci subscribe(...)
- rolul lor cel mai important este sa emita date

Construirea unui observabil de la 0

import { interval } from 'rxjs';

// intr-o clasa:
ngOnInit() {
    interval(1000).subscribe(count => { console.log(count); } ); // primim un eveniment in fiecare secunda
}

!!! Trebuie facut unsubscribe daca observabilul nu mai este de interes (salvare Subscription intr-o variabila, si apelarea unsubscribe() in onDestroy()). Altfel, la reinitializarea componentei, se va adauga de fiecare data un nou observabil pe langa cele deja existente.
!!! Exceptie de la aceasta regula fac toti observabilii furnizati de Angular (cum ar fi route.params si altii) pentru care nu trebuie sa se apeleze unsubscribe().

Construire cu adevarat de la 0:

import { Observable } from 'rxjs';

ngOnInit() {
    const customIntervalObservable = Observable.create(observer => { 
        let count = 0;
        setInterval(() => { observer.next(count); count++; }, 1000) ;
    });

    // folosire:
    customIntervalObservable.subscribe(data => { console.log(data); });
}

// 1000 este timeout-ul
// observer.next() emite o noua valoare; se poate folosi alternativ observer.error() sau observer.complete()

Tratarea erorilor -> observer.error(new Error(' etc '));
- observabilul moare dupa ce emite o eroare (nu mai e nevoie de unsubscribe), totusi nu e acelasi lucru cu terminarea
- prindere: customIntervalObservable.subscribe(data => { console.log(data); , error => { console.error("Caught it"); } );

Terminarea unui observabil -> observer.complete();
- prindere: customIntervalObservable.subscribe(data => { console.log(data); , error => { console.error("Caught it"); }, () -> { console.log('do some cleanup'); } );
- nu este nevoie de unsubscribe()

Operatori - ajuta la construirea unor pasi prin care vrei sa obtii datele
- exemple: map, filter, expand, etc.

import { map } from 'rxjs/operators';

// in ngOnInit()
customIntervalObservable.pipe( map((data: number) => { return 'Round ' + (data + 1); }) ).subscribe(....);

customIntervalObservable.pipe( filter(...), map(...) ).subscribe(....);

Subiecti - un fel de Observable, dar sunt mai activi pentru ca se poate chema metoda next() pe ei din exterior (pe cand in observabile, next() se poate apela doar din interior)
- fiind tot niste observabile, se poate apela in continuare subscribe() si, de asemenea, unsubscribe()
- se recomanda folosirea subiectilor in detrimentul EventEmitter-ilor (mai putin cand obiectul este si un Output())
- subiectii sunt pentru comunicarea intre componente prin servicii, prin mecanisme la care se face subscribe() manual.

// in user.service.ts
import { Subject } from 'rxjs';

activatedEmitter = new Subject<boolean>(); // putea fi EventEmitter<boolean>

// Html:
<button (click)="onActivated()" />

// Ts:
onActivate() {
    this.userService.activatedEmitter.next(true); // era emit(true) pt EventEmitter
}

---
Sursa: Udemy, section 13+14

17 martie 2020

Angular - ziua 4 - Routing

Routing - functionalitate oferita de Angular pentru a da impresia mai multor pagini pe care  poate naviga un utilizator. In realitate este o singura pagina pe care se plimba mai multe componente.

Pasii pentru inregistrarea rutelor:

1. Rutele trebuie declarate in app.module.ts, exemplu:

const appRoutes = Routes [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent }
]

2. In app.module.ts -> @NgModule -> imports, se adauga: RouterModule.forRoot(appRoutes)

3. In fisierul html se adauga: <router-outlet></router-outlet> in locul componentelor care fusesera adaugate folosind selectori (adica in exemplu, in locul lui <app-home> si <app-users>).

La acest moment, accesarea paginii de baza (http://localhost:4200/ si http://localhost:4200/users va duce utilizatorul catre cele 2 componente, dar nu exista navigatie in interior).

4. Adaugarea navigatiei:
- actualizare cu <a href="/"> sau <a href="/users" in html => dezavantaj ca se va face refresh la pagina de fiecare data cand se acceseaza acele link-uri
- inlocuire <a href> cu <a routerLink="/users"> pentru a scapa de refresh. Alternativa este <a [routerLink]="['/users']">

Alternativ (primii 2 pasi):
1. Outsourcing - se creeaza un nou modul "app-routing.module.ts", in care se declara const appRoutes ca la pct.1 (dar in afara  definitiei clasei). Clasa va fi adnotata cu:
@NgModule({
  imports: [RouterModule.forRoot(appRoutes)],
  exports: [RouterModule]
})
2. In app.module.ts -> @NgModule -> imports, se adauga AppRoutingModule

Tips<li routerLinkActive="active"> marcheaza ruta activa (via Bootstrap). Se adauga pe <li> care detine link-ul pe care s-a navigat.

Rutare programatica
Se injecteaza Router => se adauga Router in constructor, dupa care se poate folosi intr-o metoda din Typescript cu sintaxa: this.router.navigate(['/users']);

Cai relative: in constructor se injecteaza in plus route: ActivatedRoute
this.router.navigate(['/users'], {relativeTo: this.route});

Cai dinamice:
const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent }
    { path: 'users/:id', component: UsersComponent }
]
Acum se poate accesa un anume utilizator cu id=3 la http://localhost:4200/users/3:
- id este un camp dintr-o variabila user definita in UsersComponent.
- acestei componente i se injecteaza in constructor route: ActivatedRoute
- id-ul se poate obtine apoi prin this.routes.snapshot.params['id'];
Se pot adauga si alte elemente dinamice:  { path: 'users/:id/:name', component: UsersComponent }

!!! Pentru actualizarea obiectului din Typescript cand se apeleaza un link asemanator cu rutare, pentru ca schimbarile sa se propage in template, se adauga: this.route.params.subscribe( (params: Params) -> { this.user.id = params['id']; });
!!! Pentru ca subscriptia sa nu traiasca la nesfarsit in ciuda distrugerii componentei din care face parte, se poate salva ca variabila, iar in ngOnDestroy() se va apela: this.subscription.unsubscribe();

Cai care contin parametri de tip query:
<a [routerLink]="['/users']" [queryParams]="{allowEdit: '1'}" fragment="loading" href="#"> // va duce catre http://localhost:4200/users?allowEdit=1#loading

Echivalent in Typescript: this.router.navigate(['/users'], {queryParams: {allowEdit: '1'}}, fragment: 'loading');

Accesul la parametrii de tip query (non-reactiv la schimbari):
- se injecteaza route: ActivatedRoute in constructor
- se acceseaza prin: this.route.snapshot.queryParams si this.route.snapshot.fragment
Reactiv: se apeleaza subscribe() pe cele 2 variabile, in ngOnInit().

Tips: const id = +this.route.snapshot.params['id']; // va cauza ca id-ul sa fie interpretat ca un numar si nu un string (default)

Nested routes:
const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent, children: [
        { path: ':id', component: UsersComponent }
    ]}
]

Pastrarea parametrilor query - this.router.navigate(['/users'], {relativeTo: this.route, queryParamsHandling: 'preserve'});  // optiuni: merge, preserve

Redirectari pentru pagini invalide - se adauga in app.module.ts:

const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent },
    { path: 'not-found', component: PageNotFoundComponent },
    { path: '**', redirectTo: '/not-found' }
]

'**' este wildcard pt orice altceva. Ordinea este importanta (redirectarea va fi intotdeauna ultima ruta adaugata)
Tips: { path: '', redirectTo: '/somewhere-else', pathMatch: 'full' } pentru ca '' sa nu mai faca match cu absolut orice

Route Guards - functionalitati care se executa inainte sa se incarce o ruta, sau imediat dupa parasirea ei.

export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
         return this.authService.isAuthenticated().then(
            (authenticated: boolean) => { if (authenticated) { return true; } else { router.navigate(['/']); } });
    }
}

Apoi,  pentru a proteja rutele:
const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', canActivate: [AuthGuard], component: UsersComponent } // va fi accesibila doar daca canActivate() din AuthGuard intoarce true
}

Protejarea rutelor-copii: AuthGuard va implementa si interfata CanActivateChild -> se suprascrie metoda canActivateChild cu aceiasi parametri, in care doar se cheama canActivate(...)

const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', canActivateChild: [AuthGuard], component: UsersComponent, children: [
        { path: ':id', component: UsersComponent }
    ]}
]

Transmiterea de date statice unei rute:

1. in declararea rutei se adauga:
const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent },
    { path: 'not-found', component: PageNotFoundComponent, data: {message: 'Page not found!'} },
    { path: '**', redirectTo: '/not-found' }
]

2. in TypeScript se injecteaza un ActivatedRoute, iar in ngOnInit() se adauga: this.errorMessage = this.route.snapshot.data['message'];
Eventual se poate inlocui cu urmatoarea linie, daca vrem sa fim la curent cu schimbarile:
this.route.data.subscribe( (data: Data) => { this.errorMessage = data['message']; } );

Transmiterea de date dinamice unei rute:

1. in declararea rutei se adauga:

const appRoutes : Routes [
    { path: '', component: HomeComponent },
    { path: 'users', component: UsersComponent, resolve: {detectie: DetectionResolver} }
]
// aici detectie este un keyword ales

2. se declara DetectionResolver ca implementand Resolve<MyDataType>, in care trebuie implementata metoda resolve:

resolve (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<MyDataType> | Promise<MyDataType> | MyDataType {
    // aici se poate folosi un serviciu injectat deja in constructor pentru a putea aduce date din exterior
    // se va returna obiectul MyDataType continand informatiile dorite (sau un Observable, sau un Promise)
}

3. in TypeScript-ul componentei, in care s-a injectat un route: ActivatedRoute, se poate acum obtine obiectul MyDataType cu informatiile dorite, folosind in cazul de mai sus: this.route.snapshot.data['detectie'];

---
Sursa: cap. 11-12 Udemy

16 martie 2020

Angular - ziua 3

Directiva:
- de atribute: afecteaza un singur element, cel pe care este pus
- structurale: afecteaza mai multe elemente din DOM, contine * (exemplu *ngIf, *ngFor) - o singura directiva structurala are voie sa existe pe un element

Exemplu in TypeScript:

@Directive({
    selector: '[appBasicHighlight]' // va fi recunoscut si fara paranteze patrate, ca un atribut
})
export class BasicHighlightDirective implements OnInit{
    constructor(private elementRef: ElementRef) {}

    ngOnInit(private elementRef: ElementRef) {
        this.elementRef.nativeElement.style.backgroundColor = 'green'; // setare stil
    }
}

- se adauga in app.module.ts -> declarations, numele BasicHighlightDirective
- utilizarea in Html: <p appBasicHighlight></p>


Renderer - o abordare mai buna pentru accesul la DOM, detalii despre api-ul Renderer2 aici

@Directive({
    selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit{
    constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

    ngOnInit() {
        this.renderer.setStyle(this.elementRef.nativeElement, 'background-color', 'blue');
    }
}

<p appBetterHighlight></p>


HostListener

@HostListener('mouseenter')  mouseover (eventData: Event){
     this.renderer.setStyle(...);
}
@HostListener('mouseleave')  mouseleave (eventData: Event){
     this.renderer.setStyle(...);
}

Alias-urile de la @HostListener sunt standard, specifica trigger-ul.


HostBinding - pentru scurtarea accesului la proprietati

@HostBinding('style.backgroundColor') backgroundColor: string; // alias-ul specifica proprietatea elementului (locatia din DOM)

@HostListener('mouseenter')  mouseover (eventData: Event){
     this.backgroundColor = 'blue'; // => se acceseaza mai usor stilul
}


Directiva ngSwitch

<div [ngSwitch]="value">
    <p *ngSwitchCase="5">Value is 5</p>
    <p *ngSwitchCase="10">Value is 10</p>
    <p *ngSwitchDefault>Value is default</p>
</div>


Serviciile - o clasa in Typescript, fara directive 

In componenta unde vrei sa folosesti un serviciu, se adauga tipul in @Component -> providers. Apoi serviciul trebuie injectat in constructor (nu se instantiaza): constructor(private service: MyService) {}
Instanta salvata din constructor va putea fi folosita oriunde in restul codului din acel ts.

Injectie la nivel de:
- AppModule: serviciul e vizibil in intreaga aplicatie. Se adauga in providers.
- AppComponent: serviciul e vizibil in toate componentele (dar nu in alte servicii)
- alta componenta: serviciul e vizibil in acea componenta si in toti copiii ei

@Injectable() - se adauga intr-un serviciu in care se poate injecta alt serviciu (cand te astepti sa se injecteze ceva).