L'obbietto di questa serie di questa categoria è creare un workspace completo con le seguenti componenti:

  • Angular come framework base
  • Ionic come toolkit per l'interfaccia web e lo sviluppo dell'app
  • Cordova come container per la creazione dell'app per Andoid & IOs
  • Nx Workspace per la gestione di due librerie interne e due progetti separati per web e app
  • Redux come libreria per la gestione dello store e dei servizi

Sviluppando con queste tecnologie è indispensabile dare i nomi e le giuste caratteristiche ai componenti:

  • Typescript è il linguaggio di programmazione usato 
  • Angular è il framework
  • Ionic è un Toolkit con il quale sviluppare applicazione native per mobile e applicazioni web.
  • Capacitor o Cordova sono i container con cui generare le app compilabili negli SDK di IOS e Andoid 
  • Ionic Native, Auth Connect, Secure Storage, Identity Vault sono plugin o componenti aggiuntivi di Ionic.

In internet è difficile trovare un esempio completo che crei un progetto completo e questa serie di articoli vorrebbe creare un completo e semplice esempio di tutte le componenti. Anche se si stratta di un progetto di esempio verranno introdotte in maniera completa i componenti per la gestione delle credenziali e dell'autenticazione.

In questa serie di articoli si daranno per scontate le nozioni base di programmazione Angular e Typescript, si utilizzerà il tema base di Ionic senza badare molto all'aspetto grafico dell'app che comunque può essere personalizzato con i CSS. 

L'intero progetto (funzionante) è disponibile al GIT pubblico:

https://github.com/alnao/AngularIonicReduxWorkspaceWebAndAppExample

Prima di iniziare bisogna aver ben configurato il sistema di sviluppo, oltre a Visual Studio Code è necessario avere installato Node ed NPM/NVM. Prima di tutto bisogna cercare Node e/o NVM, che deve essere almeno alla versione 12. Su GNU Linux basta lanciare i comandi:

$ nvm list-remote
$ nvm install v16.13.1
$ nvm ls
$ nv --version

in particolare questo ultimo comando deve ritornare almeno la versione 12. Poi per creare il workspace bisogna lanciare i comandi di installazione dei pacchetti

$ npm install angular-cli -g
$ npm update angular-cli -g
$ npm install nx -g
$ npm install @nrwl/angular -g
$ npm install @nxtend/capacitor -g
$ npx create-nx-workspace frontend --preset=empty
$ cd frontend
$ npm install --save redux @angular-redux/store
$ npm install --save-dev --exact @nxtend/ionic-angular
$ npm install -g schematics
$ npm install -g httpclient
$ npm install -g @nrwl/schematics

Con questi comandi abbiamo scaricato tutte le librerie necessarie se non presenti e poi creato il workspace con il comando "npx", questo va a creare automaticamente una serie di file json di configurazione che poi verranno usati dai compilatori in automatico.

Quando si lavora con i pacchetti NPM e con i moduli Angular e Ionic bisogna sempre ricordare che le librerie sono in continuo aggiornamento e può succedere che ci siano incompatibilità tra le versioni. Il progetto è stato pensato nel 2021 con le versioni 11 di Angular, 4 di Ionic e 10 di Redux ma le recenti versioni hanno cambiato un po' di cose e bisogna armarsi di pazienza per aggiornare il progetto. In particolare il processo di installazione e creazione del progetto potrebbe andare in errore perché la libreria

@nxtend/ionic-angular

è stata sostituita dalla

@nxext/ionic-angular

come descritto dal sito ufficiale. Inoltre con la versione 15 di Angular la libreria

@angular-redux/store

potrebbe generare errore di compatibilità quindi è necessario forzare l'installazione con il comando

npm install --legacy-peer-deps

in questo modo si potrà installare i pacchetti NPM di Angular 15 con Redux 10. Un altro aggiornamento importante è stato il saldo di Ionic dalla versione 4 alla versione 6, saltando la 5 per pigrizia. In caso di creazione di nuovo progetto si può seguire la guida in linea nel sito ufficiale.

Mentre per aggiornare il progetto ionic e nx bisogna aggiornare a mano il pacakge.json e lanciare il comando di migrazione

npx nx migrate --run-migrations
nx migrate latest

Per poi lanciare il solito comando

npm install --legacy-peer-deps

per installare tutte le versioni dei pacchetti NPM ricordando che purtroppo la versione più recente di Redux è incompatibile con Angular 15 per ora. Una ultima cosa da notare e di cui bisogna prestare attenzione è l'aggiornamento della versione di Typescript, necessaria per il salto da Angular 11 alla versione 15, il cambiamento di versione impone di usare la recente versione 4.8.2 del linguaggio di programmazione, questo cambiamento potrebbe generare qualche problema ed errori di compilazione se il codice scritto non è compatibile con le ultime regole del linguaggio.

All'interno del nostro workspace ci saranno due applicazioni: una web e una app per smartphone, queste si possono creare con i comandi seguenti.

Per creare ed eseguire il progetto web basta lanciare i comandi :

$ nx generate @nxtend/ionic-angular:application exampleweb
$ nx format
$ nx serve exampleweb

Per creare ed eseguire il progetto app smartphone basta lanciare il comando

$ nx generate @nxtend/ionic-angular:app exampleapp
$ nx format
$ nx serve exampleapp --port 4201

Su alcune versioni possono comparire due errori:

Il primo erorre può sorgere in fase di compilazione (al serve) e riguarda la vesione di typescript

The Angular Compiler requires TypeScript >=4.0.0 and <4.2.0 but 4.4.4 was found instead.

errore risolvibile con la forzatura della versione di Typescript con il comando

$ npm i -D typescript@4.0.5
$ npm install

Il secondo errore riguarda l'assenza della libreria "@nrwl/angular/generators" e si risolve modificando/aggiungendo le dipendenze al progetto modificando il pacakge.json con

"dependencies": {
"@nrwl/angular": "^12.0.0",
"@nxtend/capacitor": "^12.0.0",

e poi lanciando il comando

$ npm install

comando che andrà ad aggiornare tutte le librerie del workspace risolvendo i problemi di dipendenze.

Nel nostro workspace andremo a creare due librerie: la prima libreria sarà dedicata ai servizi e redux (central) mentre la seconda sarà dedicata ai componenti grafici condivisi tra app e web (custom). Per la creazione si usano i comandi:

$ nx generate @nrwl/angular:library example-custom-lib
$ nx format
$ nx generate @nrwl/angular:component EchoComponent --project example-custom-lib
$ nx g @nrwl/angular:ngrx app --module=apps/exampleweb/src/app/app.module.ts --root
$ nx g @nrwl/angular:lib example-central-lib
$ nx format

Per la creazione del primo servizio/componente si usano i comandi

$ nx g @nrwl/angular:ngrx echo --module=libs/example-central-lib/src/lib/example-central-lib.module.ts --directory +state/echo --no-interactive
$ nx generate @nrwl/angular:service services/echo/echo --project=example-central-lib --skip-import

Per usare il componente custom nelle applicazioni web bisogna prima importare i moduli delle librerie. Quindi bisogna modificare il file home.module.ts del progetto ExampleWeb aggiungendo le import

import { ExampleCentralLibModule } from '@frontend/example-central-lib';
import { ExampleCustomLibModule } from '@frontend/example-custom-lib';
...
imports: [ExampleCustomLibModule,ExampleCentralLibModule,

e poi aggiungere al file home.page.html il tag

<frontend-echo-component></frontend-echo-component>

in questo modo nella pagina dell'applicazione web verrà caricato e visualizzato il componente presente nella libreria Custom. 

Nel progetto ExampleApp bisogna modificare il file app.module.ts (modulo generico) aggiunto negli import

StoreModule.forRoot({}),
EffectsModule.forRoot(),

Nel file folder.module.ts gli stessi import aggiunti in precedenza nel home del progetto web e poi bisona aggiungere  nel file folfer.page.ts il tag 

<frontend-echo-component></frontend-echo-component>

in questo modo anche nella "pagina" della app verrà visualizzato il componente presente nella libreria Custom.

Questa tecnica verrà usata ogni volta che si crea un componente usato sia nella parte web sia nella parte app.

In questo progetto sarà usato una API di esempio "annotazioni" sviluppata nel progetto AWS nella sezione micro-servizi, in questi articoli sarà sviluppato il frontend di quella sezione. In una prima fase sarà sviluppata la funzionalità aperta a tutti, in una seconda fase del progetto sarà introdotta anche la autenticazione e la gestione delle autorizzazioni. In una terza sezione ci saranno altre evoluzioni ancora non dettagliate.

Il servizio di annotazioni prevede una API di TIPO REST esposte con i metodi GET, POST e PUT come standard HTML e segue il formato standad che ormai tutti i sistemi di backend attuali hanno. Per creare su un progetto angular un servizio si usa il comando

$ ng generate service annotazioni

Mentre in un workspace NX con Redux i comandi da eseguire sono

$ nx g @nrwl/angular:ngrx annotazioni --module=libs/example-central-lib/src/lib/example-central-lib.module.ts --directory +state/annotazioni --no-interactive
$ nx generate @nrwl/angular:service services/annotazioni --project=example-central-lib --skip-import

Con questi comandi abbiamo generato tutta una serie di file pronta per l'uso del NX e Redux, in particolare i file sono:

libs/example-central-lib/src/lib/+state/annotazioni/annotazioni.actions.ts
libs/example-central-lib/src/lib/+state/annotazioni/annotazioni.effects.ts
libs/example-central-lib/src/lib/+state/annotazioni/annotazioni.models.ts
libs/example-central-lib/src/lib/+state/annotazioni/annotazioni.reducer.ts
libs/example-central-lib/src/lib/+state/annotazioni/annotazioni.selectors.ts
libs/example-central-lib/src/lib/services/annotazioni.service.ts

Nota: in questo elenco non sono presenti anche i file spec per i test unit che in questo momento non riguardano questi articoli. Nei prossimi articoli andremo ad analizzare ogni file e andremo a creare i metodi specifici per il recupero dell'elenco delle annotazioni.

Lo scopo di questi file è essere indipendenti dalla piattaforma chiamata quindi sarà possibile usare lo stesso componente da web ma anche da app usando lo stesso backend.

Come visto nell'articolo precedente sono creati sei file dall'architettura redux, in questo articolo andremo a sviluppare ogni file dettagliando la funzionalità. Il file service deve contenere la chiamata la servizio che, per ora e solo per mostrare come esempio, possiamo scrivere con il comando

export class AnnotazioniService {
constructor(private httpClient: HttpClient) {}
get(): Observable<any> {
return this.httpClient.get('<urlAPI>');
}
}

Nota: per importare il modulo HttpClientModule bisogna censire la riga sul file example-central-lib.module.ts

import { HttpClientModule } from '@angular/common/http';

Il file models deve conterene il modello dati chiamate entity come interfacce typescript, per esempio

export interface AnnotazioniEntity {
id: string | number; // Primary ID
nome: string;
descrizione: string;
tipo : string;
}

da notare che deve essere l'unico punto dove il modello dati è rappresentato e in nessun altro punto questo schema deve essere replicato.

Nel file action devono essere censite tutte le "azioni" logiche che è possibile compiere sul componente, di solito e di default sono tre: init, success e failue. Di default vengono create

export const loadAnnotazioniInit = createAction("[Annotazioni Page] Init");
export const loadAnnotazioniSuccess = createAction("[Annotazioni/API] Load Annotazioni Success",props<{ annotazioni: AnnotazioniEntity[] }>());
export const loadAnnotazioniFailure = createAction( "[Annotazioni/API] Load Annotazioni Failure", props<{ error: any }>());

Da notare che nell'azione success è presente un array di annotazioni in output. Se il plugin genera un metodo generico con il nome init deve essere eliminato per evitare metodi doppi tra classi.

Nel file effects è censito la logica di buisness del componente: cioè dove all'action init corrispondere la chiamata al servizio e poi la gestione della risposta. Tipicamente il framework crea un componente vuoto dove bisogna aggiungere la chiamata al servizio per poi salvare i dati nella action. Per esempio

loadAnnotazioni$ = createEffect(() =>
this.actions$.pipe (
ofType(AnnotazioniActions.loadAnnotazioniInit),
fetch({
run:(action) => this.service.get().pipe(map((response) =>
AnnotazioniActions.loadAnnotazioniSuccess(response) )
)
,onError: (action, error) =>
AnnotazioniActions.loadAnnotazioniFailure({ error })
})
)
);

In questo file abbiamo scritto la logica di eseguire (con run) il service e si lanciano le azioni di success in caso di risposta o failure in caso di errore.

Nel file reducer vengono già creati i metodi necessari al funzionamento dello store e non devono essere aggiunti metodi se non sono necessarie logiche particolari. Bisona però modificare il nomde dell'interfaccia se questa viene creata come "State", per esempio cambiandola in AnnotazioniState e bisogna cambiare in tutti i metodi del file. Se nel file è presente un metodo generico "recuder" è obbligatorio cambiare i nomi in un nome unico, per esempio "annotazioniReducerMethod". Cambiando questo metodo bisogna anche cambiare il modulo globale "example-central-lib.module.ts" con il nome del nuovo metodo.

Nel file selectors sono già stati creati di default i metodi che servono alla nostra causa: il metodo getAllAnnotazioni recupera tutti gli elementi dallo store, in caso di oggetti più complessi sarà necessario implementare un metodo in questa classe per il recupero dei dati dallo store. Nel selectors è già presente un metodo getAnnotazioniLoaded che ritorna il valore booleano che indica se i dati sono stati caricati nello state, in questo modo il componente grafico potrà gestire la logica di caricamento dei dati. Se nel file sono presenti i metodi generici getSelectedId e getSelected si possono commentare o bisogna modificarne il nome. Se nel reducer si sono modificati i nomi di oggetti e metodi, nella classe selectos bisogna impostare gli stessi nomi.

Il cambio di nomi è necessario perché con più componenti i nomi sarebbero doppi all'interno della libreria e si generano errori di compilazione visibili in fase di avvio di una delle due applicazioni.

Per poter usare questo servizio bisogna costruire un componente, cosa che verrà fatta nel prossimo articolo.

Per visualizzare i dati in un componente grafico, bisogna creare un componente grafico che effettui la chiamata la componente redux, per come è costruita la architettura il componente redux si trova nella libreria principale mentre il componente grafico sarà posizionato nella libreria custom, i due progetti web poi dovranno importare ed usare il componente. Per comodità andremo a generare sia il modulo sia il componente grafico con i comandi

nx generate @nrwl/angular:module annotazioni --export=true --routing --project=exampleweb
nx generate @nrwl/angular:component annotazioni --export=true --routing --project=exampleweb

dall'output si possono vedere i file generati.

Nel file typescript si devono recuperare dallo state le due variabili list e isLoaded

export class AnnotazioniComponent implements OnInit {
//costruttore con l'accesso allo store e al routing
constructor(private store: Store, private router: Router) { }
//proprietà del componente che richiano i selectors
list$: Observable<any> = this.store.pipe(select(getAllAnnotazioni));
isLoaded$: Observable<any> = this.store.pipe(select(getAnnotazioniLoaded));
//bottone che esegue il dispatch della action init
buttonLoadAnnotazioni() {
this.store.dispatch(loadAnnotazioniInit());
}
ngOnInit(): void {} //non modificato
}

Mentre nel componente html basta visualizzare un bottone e poi la lista quando caricata

<button ion-button clear icon-left (click) ="buttonLoadAnnotazioni()" *ngIf="!(isLoaded$ | async)" >
<ion-icon name='beer' is-active="false"></ion-icon>Carica annotazioni
</button>
<ng-container *ngIf="isLoaded$ | async">
<div *ngFor="let el of list$ | async ; index as i">
<div>{{el.nome}}</div>
</div>
</ng-container>

Prima di procedere con la compilazione bisogna però eseguire alcuni import necesari:

  • nel annotazioni.modules.ts aggiungere la import di IonicModule
  • nel home.module.ts aggiungere la import AnnotazioniModule
  • nel home.page.html aggiungere la visualizzazione del <frontend-annotazioni></frontend-annotazioni>

Una volta lanciato il progetto con il comando

nx serve exampleweb

si può vedere nella pagina principale il bottone e al click dovrebbe comparire la lista delle annotazioni presenti se il backend è disponibile e contiene dati. Per vedere lo state e come funziona bisogna usare il browser chrome con installata la estensione "Redux DevTools" che mette a disposizione un tool molto utile.

Accedendo con il tasto F12 al tool per gli sviluppatori compare infatti un tab ulteriore "Redux" e al suo interno si vedono nell'elenco di sinistra l'elenco delle action eseguire mentre sulla destra, per ogni azione, lo state corrispettivo.

Questo tool è molto utile per eseguire debug e vedere i dati caricati.

Dopo aver creato il primo componente base per la visualizzazione dei dati ritornati dal servizio di elenco gestito dal redux nello store, bisogna andare a gestire visualizzare i dati con i tag specifici nel ionic.

Nel precedente articolo i dati sono stati visualizzati i dati con dei tag DIV ma questo deve essere evitato in un progetto Ionic: bisogna infatti usare sempre e solo i tag che la libreria ionic mette a disposizione, questo per poi poter compilare l'app per gli smartphone.  Per generare il componente nel progetto app basta lanciare i comandi

$ nx generate @nrwl/angular:module annotazioni --export=true --routing --project=exampleapp
$ nx generate @nrwl/angular:component annotazioni --export=true --routing --project=exampleapp

Successivamente dovete gestire la rotta per accedere al componente e dovete copiare il codice così da poter provare il componente nel progetto app. Poi conviene togliere il bottone e gestire il caricamento dal OnInit del compomente, lascio al lettore questo come esercizio. Ricordo solo che bisogna aggiungere al modulo creato l'import del IonicModule.

La grafica della lista deve essere gestita con ion-list e ion-item, tag specifici per visualizzare i dati in maniera standard:

<ion-content [fullscreen]="true">
<ion-header >
<ion-toolbar color="primary">
<ion-title size="large">Lista annotazioni</ion-title>
</ion-toolbar>
</ion-header>
<ion-list *ngIf="isLoaded$ | async">
<ion-item *ngFor="let el of list$ | async ; index as i">
<ion-label>{{el.nome}}</ion-label>
</ion-item>
</ion-list>
</ion-content>

In questo breve esempio viene visualizzato in titolo come toolbar e poi la lista con gli item per ogni elemento ritornato dal servizio. Nei precedenti articoli verrà introdotto il componente di dettaglio e modifica dei dati.

 

Per visualizzare il dettaglio di una annotazione bisogna creare un nuovo compomente con il comando:

nx generate @nrwl/angular:module annotazione-dettaglio --export=true --routing --project=exampleweb
nx generate @nrwl/angular:component annotazione-dettaglio --export=true --routing --project=exampleweb

E aggiungere la rotta nel app-routing oppure al modulo specifico

{
path: 'annotazione-dettaglio/:id',
component : AnnotazioneDettaglioComponent
}

Poi dal componente di elenco bisogna aggiungere il router link, per esempio:

<ion-item *ngFor="let el of list$ | async ; index as i"
routerDirection="root"
[routerLink]="['/','annotazione-dettaglio', el.id ]"
lines="none" detail="true"
>

Nel nuovo componente si può benissimo utilizzare lo stesso codice del componente di elenco ma con la differenza che bisogna eseguire un filtro per visualizzare solo l'elemento richiesto, per esempio usando il pipe e il filter previsti dal linguaggio di programmazione:

import { map } from 'rxjs/operators';
...
export class AnnotazioneDettaglioComponent implements OnInit {
constructor(
private store: Store,
private router: Router,
private route: ActivatedRoute
) { }
list$: Observable<AnnotazioniEntity[]> = this.store.pipe(select(getAllAnnotazioni))
.pipe(map(annotazioni => annotazioni
.filter( annotazione => annotazione.id===this.route.snapshot.params.id))
);
isLoaded$: Observable<any> = this.store.pipe(select(getAnnotazioniLoaded));

ngOnInit(): void { }
}

Per quanto riguarda la grafica si può creare una toolbar semplice con un bottone di ritorno e poi visualizzare la lista dei campi come item, graficamente lascio la libertà al lettore di usare tutte le proprietà del tag item, per esempio è possibile mostrare un header con il link per tornare alla lista e poi una lista di item per visualizzare una lista di campi:

<ion-content [fullscreen]="true" *ngIf="isLoaded$ | async">
<ion-header >
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button defaultHref="home"></ion-back-button>
</ion-buttons>
<ion-title size="large">Dettaglio annotazione</ion-title>
</ion-toolbar>
</ion-header>
<ion-list *ngFor="let el of list$ | async ; index as i">
<ion-item>
<h2>{{el.nome}}</h2>
</ion-item>
<ion-item >
{{el.descrizione}}
</ion-item>
<ion-item >
<ion-label color="medium" >Stato</ion-label>
<ion-label slot="end">{{el.stato}}</ion-label>
</ion-item>
</ion-list>
</ion-content>

La modifica di un elemento sfrutterà gli elementi già creati nei precedenti articoli: basta infatti aggiungere poco codice al componente di dettaglio per avere un form di modifica funzionante e solido. Verranno usati le librerie standard dei form reattivi di Angular già descritti in altri articoli mentre i metodi di validazione dei dati verrà solo introdotta a livello di esempio. 

Dopo aver importato le classi della libreria nei moduli del progetto e del componente, basta aggiungere poco codice al componente di dettaglio: per prima cosa bisogna definire il form

noteForm = newFormGroup({
id:newFormControl(),
nome:newFormControl(),
descrizione:newFormControl(),
tipo :newFormControl(),
stato :newFormControl(),
fase :newFormControl()
});
per seconda cosa bisogna dichiarare le validazioni nel metodo onInit
 
this.noteForm = this.formBuilder.group({
id :['', [Validators.required]],
nome :['', [Validators.required]],
descrizione:[],
tipo:[],
stato:[],
fase:[]
});

per terza cosa bisogna eseguire la substribe al componente observable così da definire la regola di caricamento dei dati

constmyObserver = {
next: (list: AnnotazioniEntity[]) => {
this.noteForm.controls['id'].setValue(list[0].id);
this.noteForm.controls['nome'].setValue(list[0].nome);
this.noteForm.controls['descrizione'].setValue(list[0].descrizione);
this.noteForm.controls['tipo'].setValue(list[0].tipo);
this.noteForm.controls['stato'].setValue(list[0].stato);
this.noteForm.controls['fase'].setValue(list[0].fase);
}
,error: (err: Error) => console.error('Observer got an error: ' + err)
,complete: () => {},
};
this.list$.subscribe(myObserver);

per quarta, e ultima cosa, bisogna visualizzare il form dopo il caricamento dei dati:

<ion-content [fullscreen]="true" *ngIf="isLoaded$ | async">
<ion-list*ngFor="let el of list$ | async ; index as i"[formGroup]="noteForm">
<ion-item>
<ion-labelposition="stacked"color="medium">Id</ion-label>
<ion-inputtype="text"id="id"name="id"formControlName="id"></ion-input>
</ion-item>
<ion-item>
<ion-labelposition="stacked"color="medium">Nome</ion-label>
<ion-inputtype="text"id="nome"name="nome"formControlName="nome"></ion-input>
</ion-item>

Per salvare le modifiche eseguite nel componente bisogna eseguire tre passi: recuperare i dati dal form, inviarli al servizio ed eseguire il servizio rest. Per semplicità andremo a sviluppare i componenti al contrario partendo dal servizio per terminare con il form. Per prima cosa bisogna implementare il servizio, per praticitià dell'esempio viene usato il service già creato dove basta aggiungere un metodo put()

put(id : any,element : AnnotazioniEntity): Observable<AnnotazioniEntity> {
return this.httpClient.put<AnnotazioniEntity>(`${this.urlApi}?id=${id}`,element);
}

Per seconda cosa bisogna creare i componenti redux per eseguire il passaggio di dati dal form al servizio, quindi bisogna crea un nuovo componente

$ nx g @nrwl/angular:ngrx updateannotazione --module=libs/example-central-lib/src/lib/example-central-lib.module.ts --directory +state/annotazioni --no-interactive

Il file models deve contenere il modello dati chiamate entity come interfacce typescript, si può eliminare quella appena creata e usare direttamente la annotazioni creata per l'elenco oppure, come in questo esempio, si può impostare che il modello appena creato estenda la classe dell'elenco:

export interface UpdateannotazioneEntity extends AnnotazioniEntity { }

Nel file action devono essere censite tutte le "azioni" logiche che è possibile compiere, come fatto in precedenza avremmo tre costanti: init, success e error

export const loadUpdateannotazioneInit = createAction(
"[Updateannotazione Page] Init",
props<{id: any; element: UpdateannotazioneEntity;}>() );
export const loadUpdateannotazioneSuccess = createAction(
"[Updateannotazione/API] Load Updateannotazione Success",
props<{ updateannotazione: UpdateannotazioneEntity[] }>() );
export const loadUpdateannotazioneFailure = createAction(
"[Updateannotazione/API] Load Updateannotazione Failure",
props<{ error: any }>() );

Da notare che nella action init abbiamo indicato due parametri che sono i parametri che dal form verranno passati al servizio. Questo passaggio di informazioni si effettua nella classe Effects, per esempio:

export class UpdateannotazioneEffects {
init$ = createEffect(() =>
this.actions$.pipe(
ofType(UpdateannotazioneActions.loadUpdateannotazioneInit),
fetch({
run:(action) => this.service.put(action.id,action.element).pipe(map((response) =>
UpdateannotazioneActions.loadUpdateannotazioneSuccess (
{updateannotazione: [action.element]}) ) )
,onError: (action, error) =>
UpdateannotazioneActions.loadUpdateannotazioneFailure({ error })
})
)
);
constructor(private actions$: Actions, private service: AnnotazioniService) {}
}

Nel file reducer andiamo a modificare l'interfaccia da State a UpdateannotazioneState. Nel file selectors sono già stati creati di default e non sarà necessario creare altri metodi visto che il componente esegue solo passaggio di dati dal form al service. Se nel file sono presenti i metodi generici getSelectedId e getSelected si possono commentare o bisogna modificarne il nome.

Come ultimo passaggio bisogna creare il metodo che dal Form si esegua la action di init per la chiamata al servizio e la gestione del valore di ritorno. Nella toolbar del componente di dettaglio basta aggiungere la chiamata al metodo:

<ion-buttons slot="end" >
<ion-button [disabled]="!noteForm.valid" (click)="onSubmit()" color="dark" outline round small>
Salva <ion-icon name="save-outline"></ion-icon>
</ion-button>

Il metodo di submit deve chiamare la action di esecuzione del servizio:

onSubmit() {
const id=this.noteForm.value.id;
//form --> UpdateannotazioneEntity with the same attribute
let element : UpdateannotazioneEntity= this.noteForm.value ;
//dispach init action to call service
this.store.dispatch( loadUpdateannotazioneInit({id,element}) );
//substribe to success
this.actionListener$.pipe(skip(1) // optional: skips initial logging done by ngrx
).pipe(ofType(loadUpdateannotazioneSuccess),
).subscribe((action) => {
console.log("success");
this.store.dispatch(loadAnnotazioniInit()); //to reload all list
this.router.navigate(["annotazioni"]); //route to the list
}
);
}

Da notare che al componente abbiamo dovuto aggiungere nel costruttore

private actionListener$: ActionsSubject

per poter eseguire il subscribe sull'azione di ritorno e gestire il ritorno alla lista dopo la modifica di un elemento. Nota: alcune guide suggeriscono di eseguire il subscribe sul metodo getUpdateannotazioneLoaded, cioè di gestire il ritorno dal selector ma personalmente preferisco gestire la substribe dell'action che è una soluzione molto più elegante e pratica.

Per l'inserimento dei un elemento non andremmo a costruire un nuovo componente ma useremo quello di aggiornamento usando come convenzione che l'id zero corrisponde al nuovo elemento. Per questo nel componente di elenco basta aggiungere il bottone per accedere al componente di modifica con il valore id a zero:

<ion-buttons slot="primary">
<ion-button [routerLink]="['/','annotazione', '0' ]" color="dark" outline round small>
New
<ion-icon slot="end" name="create"></ion-icon>
</ion-button>
</ion-buttons>

Successivamente, per praticità, basta definire nel observable la regola

if (this.route.snapshot.params.id != '0'){
... codice già presente
this.elemento=list[0];
}else{
this.noteForm.controls['id'].setValue("0");
}

Da notare che dobbiamo aggiungere un elemento come proprietà del componente, così facendo sarà possibile visualizzare i campi nuovi nel componente html solo se si è in fase di modifica evitando di visualizzarli nel caso in cui il componente sia caricato per la creazione di un nuovo elemento, nel codice bisogna aggiungere delle condizioni sui campi item:

<ion-item *ngIf="route.snapshot.params.id != 0">
<ion-label position="stacked" color="medium" >Id</ion-label>
<ion-input type="hidden" id="id" name="id" formControlName="id"></ion-input>
{{noteForm.controls['id'].value}}
</ion-item>

mentre per le date di creazione si può usare il pipe-date standard di Angular

<ion-item *ngIf="route.snapshot.params.id != 0">
<ion-label position="stacked" color="medium" >Data inserimento</ion-label>
<ion-label slot="end">{{element.datainserimento | date : 'dd/MM/yy HH:mm'}}</ion-label>
</ion-item>

Inoltre, nel metodo onSubmit, bisogna aggiungere la valorizzazione dei campi non modificabili dall'utente in pagina, per esempio:

if (id==0){
element.datainserimento=""+Date.now();
element.utenteinserimento="Utente";
}
element.datamodifica=""+Date.now();
element.utentemodifica="Utente"

Per ultima cosa nell'oggetto service si va a modificare il metodo per il salvataggio per definire quale metodo usare: post per gli elementi nuovi e put per gli elementi già esistenti con id diverso da zero

putPost(id : any,element : AnnotazioniEntity): Observable<AnnotazioniEntity> {
if (id==0)
return this.httpClient.post<AnnotazioniEntity>(`${this.urlApi}`,element);
else
return this.httpClient.put<AnnotazioniEntity>(`${this.urlApi}?id=${id}`,element);
}

 

E' possibile inserire velocemente la classica barra di ricerca nel componente che visualizza l'elenco degli elementi. Per questo basta inserire il tag nativo di ionic e configurare il metodo di filtro

<ion-searchbar showCancelButton="never" color="medium" 
animated placeholder="Ricerca" (keyup)="filtra($event)" ></ion-searchbar>

Poi nel componente delle annotazioni basta definire il metodo che esegue il filtro sull'elenco ritornato

filtra( el : any){
const value=el.target.value;
this.list$ = this.store.pipe(select(getAllAnnotazioni)).pipe(
map(items => items.filter(
item => item.nome.toLowerCase().indexOf(value) > -1 )
));
}

Da tenere presente che è meglio, per pulizia del codice, modificare la lista come array di entity:

list$: Observable<AnnotazioniEntity[]> = this.store.pipe(select(getAllAnnotazioni));

e questa modifica risulta necessaria se si esegue l'operazione filter come descritta sopra.

Sarebbe possibile usare anche i FormControl che sono stati introdotti nel componente di modifica di un elemento ma, per una semplice barra di ricerca, è sconsigliato l'uso di un componente così articolato.

All'interno dei form è possibile impostare delle Validatori in modo tale da evitare che il form venga inviato con dati indesiderati che potrebbero generare errori al servizio di backend. I validatori si possono definire nel formBuilder nel metodo onInit del componente, un esempio completo è

this.noteForm = this.formBuilder.group({
id :['', [Validators.required]],
nome :['', Validators.compose([
Validators.maxLength(25),
Validators.minLength(5),
Validators.required
])],
descrizione:['',[ Validators.maxLength(250)]],
tipo:['',[Validators.maxLength(250)]],
stato:['',[Validators.pattern('^attivo$|^bloccato$') ]],
fase:['',[Validators.maxLength(250)]],
datainserimento:[],utenteinserimento:[],datamodifica:[],utentemodifica:[]
});

dove al nome viene impostata lunghezza minima e massima mentre sugli altri solo la lunghezza massima.
Inoltre per lo stato è stato impostato un sistema di validazione semplice per permettere solo due valori, ovviamente questo esempio può essere evoluto con altre logiche. Per esempio un semplice validatore per un input di tipo mail per il controllo della presenza di chiocciola e del formato potrebbe essere:

Validators.pattern('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$')

Inoltre è possibile sviluppare dei validatori custom sia a livello di singolo input sia a livello di form coinvolgendo più input. Un classico esempio è il validatore in un form con password e conferma password per verificare che le due password inserite siano identiche:

this.registerForm = formBuilder.group({
'email': ['', Validators.compose([Validators.required, Validators.minLength(3),
Validators.required, Validators.maxLength(25), EmailValidator.checkEmail])],
'password': ['', [Validators.required, Validators.minLength(5), Validators.maxLength(45)]],
're_password': ['', [Validators.required]]
},
{ 'validator': PasswordValidator.isMatching }
);
export class PasswordValidator {
static isMatching(group: FormGroup){
var firstPassword = group.controls['password'].value;
var secondPassword = group.controls['re_password'].value;
if ((firstPassword && secondPassword) && (firstPassword != secondPassword)) {
group.controls['re_password'].setErrors({"pw_mismatch": true});
return { "pw_mismatch": true };
} else{
return null;
}
}
}

Ionic automaticamente evidenzia gli errori di un input se questo non supera le validazioni ma è possibile anche visualizzare i messaggi di errori in maniera esplicita aggiungendo una icona nella label visualizzata:

<ion-label position="stacked" color="medium" >Nome 
<ion-icon *ngIf="noteForm.controls['nome'].invalid" name="alert-circle-outline" color="danger">
</ion-icon>
</ion-label>

Esistono moltissimi plugin per la visualizzazione di spinner all'interno delle pagine, il metodo più semplice e nativo è usare il ion-spinner messo a disposizione da ionic. Il componente grafico può essere posizionato all'interno di una card, per esempio

<ion-card *ngIf="showSpinner" color="medium" ><ion-card-content color="medium" >
<h1 class="ion-text-center">
<ion-spinner slot="center" name="bubbles" [paused]="false" color="dark"></ion-spinner>
Salvataggio in corso
<ion-spinner slot="center" name="bubbles" [paused]="false" color="dark"></ion-spinner>
</h1>
</ion-card-content></ion-card>

Mentre gli altri componenti grafici devono essere disablitati, per esempio la ion-list del form

<ion-list [formGroup]="noteForm" *ngIf="!showSpinner">

e il bottone di salvataggio

<ion-button [disabled]="!noteForm.valid || showSpinner" 

Nel typescript bisogna definire una proprietà showSpinner valorizzata a false di default

this.showSpinner=true;
this.actionListener$.pipe(skip(1)
).pipe(ofType(loadUpdateannotazioneSuccess),
).subscribe((action) => {
this.showSpinner=false;

Comunque in internet è possibile trovare moltissimi altri esempi di effetti ma non sono standard ionic.

Oltre ad aver creato lo spinner singolo per la procedura di salvataggio, è possibile spostare il componente nella libreria custom in modo da poterlo usare diversi punti dell'app e anche nella parte web, questo proprio è lo scopo della libreria custom finalizzata a contenere tutti i componenti condivisi e usati più volte. Per creare un componente basta infatti usare il comando

nx generate @nrwl/angular:component Spinner --project example-custom-lib

Poi bisogna ricordarsi di aggiungere al file modules della custom-lib l'import dello IonicModule e l'export del componente appena creato SpinnerComponent. Il codice di esempio del componente:

<ion-card color="medium" ><ion-card-content color="medium" >
<h1 class="ion-text-center">
<ion-spinner slot="center" name="bubbles" [paused]="false" color="dark"></ion-spinner>
{{testo}}
<ion-spinner slot="center" name="bubbles" [paused]="false" color="dark"></ion-spinner>
</h1>
</ion-card-content></ion-card>

Mentre nel file typescript bisogna impostare le due variabili di input

@Input() testo : string;
@Input() paused: boolean;

Nel applicativo, per esempio nel Annotazioni.modules.ts, bisogna importare il modulo generale ExampleCustomLibModule e poi si può usare il nuovo componente nella lista, per esempio:

<frontend-spinner *ngIf="!(isLoaded$ | async)" [testo]="spinnerMessage"></frontend-spinner>

oppure nel componente di modifica si può sostituire l'esempio precedente con il più semplice codice di esempio:

<frontend-spinner *ngIf="showSpinner" [testo]="spinnerMessage"></frontend-spinner>

L'autofocus è una piccola chicca che si può aggiungere al proprio sito e alla propia app ionic con pochissime righe di codice. Nel componente dell'elenco basta aggiungere il ViewChild definito da Angular e nel metodo onInit è possibile fare il focus nell'input

@ViewChild('autofocus', { static: false }) searchbar: IonSearchbar;
ngOnInit(): void {
//...
setTimeout(() => this.searchbar.setFocus(), 300);
}

in questo esempio è usato il metodo javasript setTimeout per aspettare che il componente venga reindirizzato dal browser. Bisogna comunque ricordarsi di aggiungere il "#autofocus" al tag ion-searchbar in modo da collegare il ViewChild con il tag

<ion-searchbar .... (keyup)="filtra($event)" #autofocus ></ion-searchbar>

in internet si possono trovare tanti altri esempi anche più complessi ma alla fine tutti eseguono questa logica.

Alternativa al componente custom spinner creato in altri articoli è possibile usare il LoadingController messo a disposizione dalla libreria Ionic, personalmente non la uso perchè non mi piace molto ma è una questione di gusti. Per usarla basta dichiarala nel costruttore del componente:

constructor(private loadingController: LoadingController){}

e poi creare due metodi per l'inizio e la fine della visualizzazione dello spinner-loading

async loadingShow() { //https://ionicframework.com/docs/api/loading
const loading = await this.loadingController.create({
message: 'Loading...',
duration: 3000000,
spinner: 'circles'
});
loading.present();
console.log("loadingShow present");
}
async loadingHide(){
console.log("loadingHide");
const popover = await this.loadingController.getTop();
if (popover)
await popover.dismiss(null);
else
setTimeout(() => {this.loadingHide()}, 1000);
}

per richiamarlo basta attivarlo nel metodo onOnit o nel metodo specifico di partenza:

ngOnInit(): void {
this.loadingShow();

e poi nella gestione della risposta si può sottoscrivere alla action di success per nascondere lo spinner:

this.actionListener$.pipe(skip(1) //optional: skips logging done by ngrx
).pipe(ofType( loadAnnotazioniSuccess ),
).subscribe((action) => {
this.loadingHide();
})
));

Il motivo principale per cui è conveniente usare Ionic e NgRx è creare un workspace è che le applicazioni create con Ionic possono essere compilate per creare della app smartphone. La creazione dell'app in realtà è molto semplice: nell'app viene incluso in piccolo web server così da poter esporre una web-view dove l'applicazione web risulta navigabile con la UI simile ad una reale app così che l'utente non si rende nemmeno conto che si tratta di una web-application. La compilazione avviene tramite una libreria ufficiale capacitor, in molte guide si possono trovare anche informazioni riguardanti cordova che però non sarà usato in questi esempi.

Per procedere con la compilazione per i dispositivi Android è indispensabile che sia presente nel sistema il Android Studio che deve essere configurato prima di eseguire le compilazioni, il capacitor infatti crea un workspace per Andoid Studio che potrà gestire l'app in maniera veloce per chi non conosce l'ambiente di sviluppo per Andoid.

Per procedere con la compilazione per i dispositivi Apple (iPhone e iPad) è indispensabile che sia presente nel sistema il Xcode e quindi è indispensabile che venga eseguito in un sistema MacOS.

Nei successivi articoli sarà esposta la compilazione per Android mentre per i dispositivi Apple rimando alla guida ufficiale disponibile nel sito ufficiale di Capacitor.

Per la visualizzazione degli errori è possibile usare il componente AlertController di Ionic, infatti basta aggiungere al costruttore due controlli:

import { ActionsSubject } from '@ngrx/store';
import { AlertController } from '@ionic/angular';
constructor(
private store: Store,
private router: Router,
private actionListener$: ActionsSubject,
private alertController: AlertController
){ }

E poi nel metodo onInit è possibile eseguire il substribe della action Failuer, in caso di errore infatti viene eseguito il codice indicato e richiamata una funzione specifica.

ngOnInit(): void {
//...
this.actionListener$.pipe(skip(1) // optional: skips initial logging done by ngrx
).pipe(ofType(loadAnnotazioniFailure),
).subscribe((action) => {
console.log("Errore:" + action.error.message);
this.alertError(action.error.message , "" );
});
}

Nel metodo specifico per gestire l'errore basta creare un alert con tutte le proprietà previste e definire un metodo handler per eseguire il routing in modo da tornare nella home dopo il click del bottone dell'alert.

async alertError(message:string, routerTo: string) {
const alert = await this.alertController.create({
cssClass: 'basic-alert',
header: 'Alert', //subHeader: 'Errore',
message: message,
buttons: [{text:'OK'
,handler: (el) => { this.router.navigate([routerTo]); }
}]
});
await alert.present();
}

Per tutte le caratteristiche degli alert si può controllare la documentazione ufficiale

I progetti angular nativamente hanno una propria gestione per differenti ambienti con la definizione di file environments in typescript per la memorizzazione il recupero delle informazioni. Nel caso di workspace NG con più applicazioni conviene usare un unico punto per i file environments, nel nostro caso è consigliato usare la cartella che si trova nella cartella apps/exampleapp/enviroments. In questo caso basta osservare i due file create di default, uno per lo sviluppo e uno per la produzione, il comando per la compilazione per la produzione

nx build exampleapp --configuration=production

contiene proprio il parametro che indica al compilatore di usare il file environments giusto, regola che viene descritta nel file workspace.json nella sezione di compilazione dell'app. Una volta impostati i due file, all'interno delle classi service basta recuperare il file dalla corretta cartella:

import { environment } from '../../../../../apps/exampleapp/src/environments/environment';
...
urlApi: string= environment.urlApi;

E' consigliato aggiungere il file environments.prod.ts all'interno del file gitignore così le informazioni di produzione non vengono propagate nei repository Git. Da notare che questo passo è indispensabile per la compilazione per i dispositivi mobili in quanto le versioni app dovranno puntare ad un url non locale ma ad un servizio API esposto in internet per esempio su un cloud AWS.

Per creare l'app per i dispositivi con il sistema operativo android bisogna avere il progetto pronto e il programma Andoid Studio installato correttamente nel PC. Il progetto angular ionic deve essere compilato con il comando:

$ nx build exampleapp

Successivamente bisogna entrare nel progetto dell'app e installare il capacitor

$ cd ./apps/exampleapp
$ npx ionic
$ npm i -g @ionic/cli
$ npm install capacitor
$ npm install @capacitor/android
$ npm install --save @capacitor/core @capacitor/cli

Dopo di che si può lanciare il comando per la configurazione

$ npx cap init

E si DEVE modificare il file di configurazione xx.yy.ts impostando il nome dell'app ed è importamente il permesso di navigare dove bisogna impostare i server IP delle API consumate dell'app, senza questa configurazione l'app non riuscirà a raggiungere le API.

import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'it.alnao',
appName: 'alnaoIonic',
webDir: '../../dist/apps/exampleapp/',
bundledWebRuntime: false,
plugins : {
SplashScreen:{
launchShowDuration:0
}
},
cordova : {},
server:{
allowNavigation : ['18.161.111.62']
}
};
export default config;

Infine bisogna lanciare il capacitor che prepara il workspace per Andoid studio e poi lancia il programma alla giusta posizione:

$ npx cap add android
$ npx cap open android

Una volta aperto il workspace su Andoid studio, se il processo è corretto, comparirà il messaggio di conferma di compilazione del progetto. Poi sarà possibile lanciare l'app nel simulatore di dispositivi con la freccia play se all'interno del programma è impostato un dispositivo da emulare.

Ultimo passo da fare è provare l'app e soprattutto il corretto funzionamento delle chiamate API. Da notare che conviene posizionare la cartella android nel gitignore altrimenti verrebbe versionato tutto il workspace che risulta molto voluminoso.

Per installare l'app in un dispositivo con sistema operativo Android, bisogna generare un pacchetto APK e poi installarlo nel dispositivo, i pacchetti APK contengono tutti i file dell'applicazione, questi sono i pacchetti che si possono scaricare dal Play Store ufficiale oppure possono essere scaricati da diversi siti e conservati come file veri e propri. Per generare il pacchetto APK dal proprio Android Studio basta selezionare la voice "Build APK" dalla vice di menù principale "Build". A questo punto nella struttura del progetto comparirà il file all'interno della cartella target.

Il file APK deve essere inviato a qualsiasi dispositivo in qualsiasi modo, sia come allegato ad una mail, sia come file condiviso con le app come WhatUp/Telegram, sia copiato con programmi di gestione del filesystem del dispositivo. Dal dispositivo basta infatti aprire il file APK avviando l'installazione dell'applicazione.

Questa versione base dell'app non è autenticata e controllata dal PlayStore quindi il dispositivo segnalerà alcuni messaggi che il creatore dell'app non è confermato. Alcune versioni di Andoid bloccano tutte le installazioni non autenticate per sbloccare l'installazione basta selezionare l'opzione "Installa app sconosciute" dalla voce "App con accesso speciale" dal menù Privacy delle impostazioni del dispositivo. Altre versioni possono bloccare le origini sconosciute per (giuste) ragioni di sicurezza e per sbloccare questo controllo si deve selezionare l'opzione "Origini sconosciute" nella voce "Sicurezza" delle impostazioni del dispositivo.

Per quanto riguarda il rilascio su PlayStore e sulle varie proprietà avanzate dell'app come la gestione dell'icona, rimando alla documentazione di Andoid Studio e dello sviluppo per Andoid, argomento che non viene trattato da queste guide.

Capacitor è la più famosa libreria per il creazione di app mobile usando i servizi del dispositivo in maniera veloce e semplice, questa libreria è studiata proprio per Ionic anche se è possibile usare capacitor con altre librerie come React o progetti Angular puri. L'installazione e l'inizializzazione, già vista nei precedenti articoli, si esegue con i comandi:

$ npm install @capacitor/core
$ npm install @capacitor/cli --save-dev
$ npx cap init

questo va a creare il file di configurazione base capacitor.config.ts. Un semplice uso è l'interfacciamento con la videocamera del dispositivo, per installare il plugin dedicato è:

$ npm install @capacitor/camera
$ npm install @ionic/pwa-elements

per usare il plugin della camera si deve abilirare nel file main.js

import { defineCustomElements } from '@ionic/pwa-elements/loader';
defineCustomElements(window);

un bottone per richiamare un metodo

<div class="card-container">
<button class="card card-small" (click)="captureImage()">
<span>Capture image</span>
</button>
</div>
<img [src]="image" *ngIf="image" [style.width]="'300px'">

e poi definire un metodo nel componente per l'immagine

import { Component } from '@angular/core';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
@Component({
selector: 'app-root',
templateUrl: './app.component.html', styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'angularCapacitor';
image = '';
async captureImage() {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
source: CameraSource.Prompt,
resultType: CameraResultType.Base64
});
if (image) {
this.image = `data:image/jpeg;base64,${image.base64String}`!;
}
}
}

Tuttavia bisogna ricordarsi che nel progetto dell'app (su IOS o su Androd Studio) bisogna abilitare l'uso dell'app alla camera modificando il file android/app/src/main/AndroidManifest.xml aggiungendo i tag:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

In questa serie di articoli verrà introdotto un metodo classico e funzionante per la gestione delle credenziali e i componenti di login. Grazie all'architettura angular sarà possibile impostare una guardia per bloccare path e componenti in caso di navigazione senza credenziali. Nativamente sarà usato un token JWT ormai standard per quanto riguarda le credenziali in applicazioni web, infatti la prima cosa da fare è installare la libreria standard angular

npm install @auth0/angular-jwt

In questo primo esempio verrà usato lo storage standard di Ionic per poi passare ad un componente redux-rx, se non presente è necessario installare anche la libreria ionic store:

npm install @ionic/storage-angular

Dopo di che è neccessirio creare un componente Login e un service usato per questo primo esempio senza redux. Le rotte vanno modificate aggiungendo la rotta per la login e la regola di esecuzione della classe Guard che gestirà le regole di attivazione dei componenti:

const routes: Routes = [
{ path : 'login', component: LoginComponent},
{ path: 'home',
loadChildren: () =>
import('./home/home.module').then((m) => m.HomePageModule),
canActivate: [AuthGuard] },
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'annotazioni', component : AnnotazioniComponent ,
canActivate: [AuthGuard] },
{ path: 'annotazione/:id',component : AnnotazioneComponent,
canActivate: [AuthGuard]}
];

Questo esempio di gestione delle credenziali è sviluppato con un normale servizio angular con il salvataggio delle informazioni nello store di angular-ionic. La classe deve implementare 4 metodi loadStoredToken per la creazione dello store, login per eseguire il servizio di verifica delle credenziali e poi due metodi di appoggio getUser e logOut per cancellare le credenziali presenti nello store locale. Definizione della classe e costruttore con l'injection dei servizi:

const helper=new JwtHelperService();
const TOKEN_KEY='jwt-token';
@Injectable({
providedIn: 'root'
})
export class AuthService {
public user: Observable<any>;
private userData = new BehaviorSubject(null);

constructor(private storage: Storage, private http: HttpClient,
private plt: Platform, private router: Router) {
this.loadStoredToken();
}

Metodo loadStoredToken per creare lo store

  loadStoredToken() {
let platformObs = from(this.plt.ready());

this.user = platformObs.pipe(
switchMap(() => {
return from(this.storage.get(TOKEN_KEY));
}),
map(token => {
if (token) {
let decoded = helper.decodeToken(""+token);
this.userData.next(decoded);
return true;
} else {
return null;
}
})
);
}

Metodo login. In questo esempio non esegue nessun servizio e le credenziali vengono verificate nel codice e la api chiamata è una mock disponibile in internet. In un caso pratico il metodo deve essere cambiato con una chiamta http ad un servizio reale che ritorna il codice del JWT.

  login(credentials: {email: string, pw: string }) {
// TODO
if (credentials.email != 'alnao@alnao.it' || credentials.pw != 'bello') {
console.log("Credenziali errate");
return of(null);
}
console.log("Credenziali giuste");
return this.http.get('https://randomuser.me/api/').pipe(
take(1),
map(res => {
// Extract the JWT, here we just fake it
return `xxxxxxxxxx`;
}),
switchMap(token => {
let decoded = helper.decodeToken(token);
this.userData.next(decoded);
let storageObs = from(this.storage.set(TOKEN_KEY, token));
console.log("Credenziali salvate token",this.userData.getValue());
return storageObs;
})
);
}

Metodi di appoggio per il Guard e metodo di logout richiamabile da qualsiasi componente

  getUser() {
console.log("getUser: ",this.userData.getValue())
return this.userData.getValue();
}

logout() {
console.log("Logout");
this.storage.remove(TOKEN_KEY).then(() => {
this.router.navigateByUrl('/login');
this.userData.next(null);
});
}

}

La classe AuthGuard è necessaria al fine di bloccare gli url a quegli utenti che non hanno effettuato la login, come da standard Angular questa classe viene poi censita nel file delle rotte come visto in precedente articolo.

In questo semplice esempio la classe implementa la canActivate che verifica se l'utente esiste, se non esiste si crea un messaggio di errore e, grazie al routing, si naviga al componente di login

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate{

constructor(private router: Router, private auth: AuthService
, private alertCtrl: AlertController) {
}

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
console.log("canActivate" , this.auth.user)
return this.auth.user?.pipe(
take(1),
map(user => {
if (!user) {
this.alertCtrl.create({
header: 'Unauthorized',
message: 'You are not allowed to access that page.',
buttons: ['OK']
}).then(alert => alert.present());
this.router.navigateByUrl('/login');
return false;
} else {
return true;
}
})
)
}
}

Il componente grafico della login è ovviamente indispensabile per il nostro caso d'uso e ovviamente tutte le app e tutti i siti con aree riservate devono averne una, qui si riporta un semplice esempio grafico:

<ion-header>
<ion-toolbar color="primary">
<ion-title>Annotazioni Auth</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-row>
<ion-col size="12" size-sm="10" offset-sm="1" size-md="8"
offset-md="2" size-lg="6" offset-lg="3" size-xl="4" offset-xl="4">
<ion-card>
<ion-card-header>
<ion-card-title class="ion-text-center">Your Account</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item lines="none">
<ion-label position="stacked">Email</ion-label>
<ion-input type="email" placeholder="Email" name="email"
[(ngModel)]="credentials.email"></ion-input>
</ion-item>
<ion-item lines="none">
<ion-label position="stacked">Password</ion-label>
<ion-input type="password" placeholder="Password"
name="password" [(ngModel)]="credentials.pw">
</ion-input>
</ion-item>
<ion-button (click)="login()" expand="block">Login</ion-button>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-content>

Il codice del componente è molto semplice e, per questo esempio, si crea un oggetto con un valore delle credenziali predefinito per velocizzare la navigazione, ovviamente in un caso reale questo non deve essere sviluppato. Il componente esegue la chiamata al servizio di login con una subscrive e al ritorno si esegue il routing verso il componente home oppure si visualizza un messaggi di errore nel caso di credenziali non verificate dal service.

export class LoginComponent implements OnInit {
credentials = {
email: 'alnao@alnao.it',
pw: 'bello'
};
constructor(private auth: AuthService,private router:
Router,private alertCtrl: AlertController) {
}
ngOnInit() {
}
login() {
this.auth.login(this.credentials).subscribe(async res => {
if (res) {
this.router.navigateByUrl('/home');
} else {
const alert = await this.alertCtrl.create({
header: 'Login Failed',
message: 'Wrong credentials.',
buttons: ['OK']
});
await alert.present();
}
});
}
}

Il Logout è un componente necessario quando si vuole gestire creare un sito con delle credenziali interne, in questo esempio non serve un componente dedicato si può usare il componente principale Home e aggiungerci un bottone nella header-bar:

<ion-buttons slot="primary">
<ion-button (click)="this.logout()">
Logout <ion-icon slot="end" name="log-out" ></ion-icon>
</ion-button>
</ion-buttons>

E un semplice metodo del corrispettivo typescript:

export class HomePage {
constructor(private auth: AuthService) {}
logout(){ console.log("Logout"); this.auth.logout();}
}

Con questo semplice esempio si conclude l'esempio della base delle credenziali usando lo storage di Ionic. Chi prova questo esempio noterà un problema enorme: le credenziali rimangono salvate anche quando si chiude il browser, quindi dopo aver fatto la login se si chiude e riapre il browser l'accesso sarà ancora disponibile visto che il tipo di Local storage usato è persistente. Lo Ionic Local Storage è una tecnica semplice ed efficace per gestire lo storage all'interno dei progetti Ionic, in questo semplice esempio è stato usato a titolo divulgativo ma è da evitare quando si usa redux e il suo storage interno.

Lo Ionic Local Storage è basato su SQLite, un semplice software pubblico e gratuito che salva i dati come coppia di chiave e valore all'interno del browser con il limite di 5Mb esistono anche altri sistemi come WEBSQL e LocalStorage di Chrome.

Per gestire lo storage con SQLite è disponibile il plugin dedicato cordova-sqlite-storage plugin, indispensabile per chi vuole creare app con un proprio storage interno, non è questo il caso delle credenziali e di questi esempi. Questo tipo di storge può essere usato se si vuole salvare nel dispositivo o nel browser qualcosa di persistente ma non è una buona idea nel caso di credenziali o dati che possono cambiare nel tempo e che sono tornati da servizi o API.

L'obbiettivo è gestire le credenziali con Redux e il suo storage che si adatta perfettamente all'esigenza di salvare il token jwt temporaneamente e distruggere questa informazione alla chiusura del browser o alla chiusura dell'applicazione nel caso di smartphone. Prima di tutto bisogna separare il service e lo store quindi bisogna creare un componente auth nella libreria con il comando:

nx g @nrwl/angular:ngrx auth --module=libs/example-central-lib/src/lib/example-central-lib.module.ts --directory +state/auth --no-interactive

La classe entity deve contenere username e password di invio e il token di ritorno:

export interface AuthEntity {
id: string | number | null; // Primary ID
username: string ;
password: string | null;
towenJwt: string | null;
}

Alla classe action andiamo a modificare l'init di default aggiugendo l'entity in input in modo da usarlo come parametro in ingresso

export const loadAuthInit = createAction("[Auth Page] Init"
,props<{id: any; element: AuthEntity;}>() ) ;

Il metodo effects è un po' complicato perchè bisogna chiamare la API e ritornare il token ritornato dal servizio, in questo semplice esempio invece le credenziali vengono verificate dal javascript e il token è fisso, può essere creato dal sito ufficiale di Jwt. Il codice della classe effects:

export class AuthEffects {
init$ = createEffect(() =>
this.actions$.pipe(
ofType(AuthActions.loadAuthInit),
fetch({
run: (action) => {
//TODO chiamata alla API che ritorna se auth, qui esempio con PWD statica
if (action.element.username==='alnao@alnao.it'
&& action.element.password==='bello'){
let authel : AuthEntity = {id:0,password:'',
username : action.element.username,
towenJwt : 'ey.....VZ5U'
};
let auth: AuthEntity[]=[];
auth[0]=authel
return AuthActions.loadAuthSuccess({ auth });
}
console.error("Error", "Username o password errate");
return AuthActions.loadAuthFailure({ error:"Username o password errate" });
},
onError: (action, error) => {
console.error("Error", error);
return AuthActions.loadAuthFailure({ error });
},
})
)
);
}

Nella classe selectors basta creare due metodi getUtente e IsUserLogged usati per poter permettere ai componenti grafici di recuperare tale informazione:

export const getUtente = createSelector(getAuthState,
(state: AuthState) => selectAll(state)[0]);
export const isUserLogged = createSelector(getAuthState,
(state: AuthState) => selectAll(state)[0]?.towenJwt!==null
&& selectAll(state)[0]!== undefined
);

inoltre bisogna cambiare alcuni nomi di default che potrebbero creare contrasto con altri come "getSelectedId" e "getSelected" nel selector e "reducer" nella classe reducer, inoltre bisogna cambiare "State" in "AuthState" nella classe reducer e in tutti i punti dove è referenziata.

Modificando il tipo di storage usato per il token bisogna cambiare anche la guardia che deve andare a recuperare l'informazione dell'utente grazie al metodo isUserLogged creato specifico nella classe selectors.

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
let isLogged$ = this.store.pipe(select(isUserLogged));
return isLogged$.pipe( map( (value) => {
if (!value){
this.alertCtrl.create({
header: 'Unauthorized',
message: 'You are not allowed to access that page.',
buttons: ['OK']
}).then(alert => alert.present());
this.router.navigateByUrl('/login');
}
return value;
}));
}

Inoltre il componente di login deve essere modificato per eseguire la action loadAuthInit e aspettare la risposta del success o del failer tramite le sottoscrizione alle action

login() {
this.store.dispatch( loadAuthInit(
{id:0,element:{id:0,towenJwt:null,
username:this.credentials.email,
password:this.credentials.pw}})
);
//substribe to success or error message
// optional: skips initial logging done by ngrx
this.actionListener$.pipe(skip(1)
).pipe(ofType(loadAuthSuccess),
).subscribe((action) => {
console.log("success");
this.router.navigateByUrl('/home');
});
// optional: skips initial logging done by ngrx
this.actionListener$.pipe(skip(1)
).pipe(ofType(loadAuthFailure),
).subscribe((action) => {
console.log("Error: ",action);
this.alertCtrl.create({
header: 'Unauthorized',
message: 'Error '+ action.error,
buttons: ['OK']
}).then(alert => alert.present());
});
);
}

Un token Jwt possiede, al suo interno, l'informazione della scadenza del token stesso quindi è possibile bloccare la navigazione dell'utente che ha un token scaduto, per impedirgli di chiamare servizi e API con un token scaduto quindi ricevere un brutto errore. Per installare la libreria si usa npm:

npm install @auth0/angular-jwt

E poi si deve modificare la classe Guard per verificare il token dell'utente

export class AuthGuard implements CanActivate{
constructor(private router: Router, private store: Store,
private alertCtrl: AlertController) { }
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
let utente$ = this.store.pipe(select(getUtente));
return utente$.pipe( map( (ut) => {
let utenteAutenticato: boolean =
(ut!== undefined && ut.towenJwt!==null && ut.towenJwt!=='');
if (utenteAutenticato){
const jwtService = new JwtHelperService();
utenteAutenticato = !jwtService.isTokenExpired(ut.towenJwt);
//console.log(!jwtService.isTokenExpired(ut.towenJwt));
}
if (!utenteAutenticato){
this.alertCtrl.create({
header: 'Unauthorized',
message: 'You are not allowed to access that page.',
buttons: ['OK']
}).then(alert => alert.present());
this.router.navigateByUrl('/login');
}
//console.log("utenteAutenticato" + utenteAutenticato);
return utenteAutenticato;
}));
}
}

All'interno del token JWT può essere inserite informazioni dell'utente, infatti chi esegue l'a autenticazione e successiva creazione del token può (dovrebbe) inserire anche le regole di autorizzazione, cioè l'elenco delle attività che quell'utente è abilitato ad eseguire, per esempio un utente non amministratore non dovrà avere la regola ADMIN. Queste regole sono sempre personalizzate e non esiste un vero standard per i nomi e per le regole stesse, quindi ogni progetto ha regole diverse ma che si basano sempre sulla proprietà "role" all'interno del token. Nel nostro esempio si può limitare le rotte agli utenti che hanno la regola "ANNOTAZIONI", in questo modo gli utenti che non sono abilitati a questa regola non potranno navigare all'interno di quei componenti grafici.

Per limitare le rotte alle regole specifiche basta aggiungere la proprietà roles alle rotte nel app.routing.ts o dove sono definite le rotte:

{
path: 'annotazioni'
,component : AnnotazioniComponent
,canActivate: [AuthGuard]
,data : {roles : ['ANNOTAZIONI']}
},
{
path: 'annotazione/:id'
,component : AnnotazioneComponent
,canActivate: [AuthGuard]
,data : {roles : ['ANNOTAZIONI']}
}

Poi bisogna modificare il service di guardia RouteGuardService aggiungendo il recupero delle rotte e la validazione che in quel token sia presente la regola necessaria per eseguire quella rotta:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
let utente$ = this.store.pipe(select(getUtente));
return utente$.pipe( map( (ut) => {
let utenteAutenticato: boolean =
(ut!== undefined && ut.towenJwt!==null && ut.towenJwt!=='');
if (utenteAutenticato){
const jwtService = new JwtHelperService();
utenteAutenticato = !jwtService.isTokenExpired(ut.towenJwt);
//console.log(!jwtService.isTokenExpired(ut.towenJwt));
if (utenteAutenticato){//verifico autorizzazioni con il ruolo
if ( route.data.roles == null || route.data.roles.lenght === 0) {
utenteAutenticato=true; //non ci sono regole quindi OK
}else{
console.log(ut.towenJwt);
const ruoli = jwtService.decodeToken(ut.towenJwt)['role'];
console.log(ruoli);
if (ruoli!==undefined && ruoli.some(
r => route.data.roles.includes(r.authority))){
utenteAutenticato=true;//autorizzato
}else{
utenteAutenticato=false;//non autorizzato
console.log("Regola non rispetta " + route.data.roles);
}
}
}
}
...
return utenteAutenticato;
}));

Da notare che questo è uno strato di sicurezza non indispensabile in quanto il backend dovrebbe eseguire la validazione del ruolo indipendentemente dal frontend ma è sempre buona norma bloccare i componenti agli utenti non abilitati. Questo esempio si limita a gestire le rotte, nel prossimo esempio vedremo come visualizzare bottoni o link solo agli utenti con l'autorizzazione per eseguire quella specifica azione.

Una applicazione web che gestisce un token JWT può essere raffinata con una tecnica che permette di nascondere o mostrare elementi grafici a seconda delle regole autorizzative presenti nel token JWT dell'utente loggato. Il caso d'uso più semplice è la visualizzazione del bottone "modifica elemento" solo per gli utenti che dispongono dei permessi di amministrazione all'interno del token ritornato in fase di login.
Nel componente typescript bisogna comunque impostare il servizio JWT e usare la select del selector del redux per recuperare il token.

ngOnInit(): void {
//console.log("ngOnInit");
let utente = this.store.pipe(select(getUtente)).subscribe( ut => {
const jwtService = new JwtHelperService();
const ruoli : any[]= jwtService.decodeToken(ut.towenJwt)['role'];
console.log(ruoli);
if ((ruoli!==undefined ) && (ruoli.some( r => 'ANNOTAZIONI2'===(r.authority) ) ))
this.visualizzaAnnotazioni=true;
});
}

visualizzaAnnotazioni: boolean=false;
Oppure è possibile definire un selector che richiami il servizio JwtHelperService

export const isUserEnabledRole = createSelector( getAuthState,
(state: AuthState, role : string) => {
let auth=false;
const ut=selectAll(state)[0];
const jwtService = new JwtHelperService();
const ruoli : any[]= jwtService.decodeToken(ut.towenJwt)['role'];
if ((ruoli!==undefined ) && (ruoli.some( r => r.authority === role ) )){
auth=true;
}
return auth;
});

E poi nel componente il typescript risulta molto più semplice e molto più elegante

visualizzaAnnotazioni: boolean=false; 
ngOnInit(): void {
this.store.pipe(select(isUserEnabledRole,'ANNOTAZIONI2')).subscribe(
val => this.visualizzaAnnotazioni=val
);
}

Il componente grafica poi può usare la condizione con ngIf per mostrare un tag o una sezione html.

Un sito che riceve un token JWT può utilizzare alcune informazioni per visualizzare i dati contenuti dentro al token stesso, ovviamente questo dipende al tipo di backend e se questo è in grado di salvare queste informazioni nel token, esattamente come i ruoli può essere salvato il nome dell'utente. Se tale dato è salvato nel token si può comodamente inserire un metodo selector per recuperare il dato al token già usato nei componenti redux di autenticazione:

export const getUtenteName = createSelector(  getAuthState,
(state: AuthState, role : string) => {
let auth=false;
const ut=selectAll(state)[0];
const jwtService = new JwtHelperService();
return jwtService.decodeToken(ut.towenJwt)['sub'];
}
);

E poi in qualsiasi componente grafico è possibile recuperare il dato grazie ad una subscribe del selector

this.store.pipe(select(getUtenteName)).subscribe(
val => this.userName=val
);

E poi visualizzare il nome in qualsiasi componente grafico

<ion-item *ngIf="userName!==''">
Bentornato {{userName}}
</ion-item>

Da notare che questo componente può essere sviluppato anche nella custom-lib e che può essere usato per qualsiasi informazione che il backend salva nel token, per esempio la data di ultimo accesso spesso viene salvata come proprietà del token e mostrata a video.

Il framework Angular permette di sviluppare classi interceptor per catturare qualsiasi chiamata alle API di un backend, questa tecnica è usata per aggiungere un token JWT nel caso di utente autenticato, questo permette di non dover impostare il token nelle classi service visto che spesso queste possono essere decine o migliaia in caso di un numero molto elevato di risorse API. La definizione della classe interceptor si deve censire nel modulo principale aggiungendo:

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { JwtInterceptor } from 'libs/example-central-lib/src/lib/services/auth.service';
...
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
],

e la classe JwtInterceptor implementa una interfaccia e deve descrivere il comportamento: per ogni chiamata http aggiunge il token se è presente un token, in questo esempio viene cercato dal selector del redux:

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private store: Store) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add auth header with jwt if user is logged in and request is to api url
const token$= this.store.pipe(select(getJwtToken ) );
let token = null;
token$.subscribe( v => token=v );
if (token && request.method!=='OPTION') {
request = request.clone({
setHeaders: { //...request.headers,
Authorization: `Bearer ${token}`
}
});
}
return next.handle(request);
}
}

Da notare che questa modifica può creare un errore di tipo CORS perchè, se il server gestire il cors bisogna aggiungere "Authorization" come tipo permesso, per esempio:

header('Access-Control-Allow-Headers: token, Content-Type, Authorization');

questo errore può essere subdolo e poco parlante nel frontend perché nemmeno dalle console dei browser è visibile il motivo di tale blocco CORS.

In questo gruppo di articoli saranno compresi tutti gli articoli che parlano di componenti grafici per l'app, facendo una veloce ma esaustiva analisi dei più comuni e più usati elementi grafici nelle "app" per Smartphone. Nel precedente gruppo del componente lista si è costruito qualcosa più adatto al web e basato su un semplice CRUD elementare (cioè una tabella lista e un dettaglio per la modifica dei dati) mentre in questi articoli si vuole costruire qualcosa di più evoluto dal punto di vista grafico.

Tutti gli esempi si basano solo ed esclusivamente su componenti Angular-Ionic e necessitano tutti dell'installazione della libreria base

npm install -g @ionic/cli

e poi per creare il progetto base si deve usare il comando

ionic start <nomeProgetto> --type=angular

Questi esempi invece saranno compresi nel progetto che già abbiamo con NgRx e Redux ma queste librerie non sono indispensabili.

Uno dei componenti più usati nelle applicazioni di questa generazione è lo skeleton: un elenco di persone o elementi con un avatar, un nome e una breve descrizione, al click/tap dell'elemento si accede ad un dettaglio, questo comportamento è molto usato nei social ma anche nelle app di tutti i giorni dove il concetto di persona può essere sostituito da qualsiasi lista.

Nel nostro esempio useremo la lista delle note che abbiamo già creato e usato in passato modificando solo la grafica e lasciando invariata la logica del servizio e dei componenti redux. Nel ciclo che visualizza gli item basta aggiungere un "ion-thumbnail" e un "ion-label", nel primo casi si visualizza l'avatar o l'icona mentre nel secondo elemento il testo

<ion-item *ngFor="let el of list$ | async ; index as i"
[routerLink]="['/','annotazione', el.id ]"
detail="true" routerLinkActive="active"
>
<ion-thumbnail slot="start">
<!-- alternaatica con IMG <img [src]="getImage(el.stato)" /> -->
<ion-icon [name]="getImage(el.stato)" size="large" size="large"></ion-icon>
</ion-thumbnail>
<ion-label>
<h3>Nome: {{ el.nome }}</h3>
<p>Tipo: {{ el.tipo }}</p>
<p>Fase: {{ el.fase }}</p>
</ion-label>
</ion-item>

In questo semplice esempio è stato creato un metodo getImage che ritorna il nome dell'icona per eseguire un mapping

getImage(value){
if (value=="attivo") { return "checkmark"; }
return "close";
}

Questo articolo è stato scritto dopo l'aggiornamento alla versione 6 di Ionic per usare proprio i componenti standard di questa versione che risultano molto più facili da usare rispetto alle precedenti versioni. Quindi per usare i tag qui descritti bisogna aggiornare il proprio framework come descritto nell'articolo dedicato. Per gestire le modali la versione sei di Ionic prevede un nuovo tag dedicato che semplifica di molto la vita del programmatore, rispetto alle precedenti versioni che imponevano l'uso di javascript e di molto più codice. In questo primo esempio di modale viene creato un blocco per visualizzare le informazioni generali dell'app, come componente informativo o disclaimer. Il primo passo da eseguire è creare il componente grafico con il comando

nx generate @nrwl/angular:component InformazioniGenerali --export=true --project=exampleapp --standalone

poi bisogna aggiungere nel file home.module.ts il componente appena creato nella sezione import e poi si potrà aggiungere il tag nel html del componente home.

<frontend-informazioni-generali #infoG></frontend-informazioni-generali>

Sempre nel componente home bisogna aggiungere il bottone per aprire la modale

<ion-button (click)="infoG.open()" tappable outline round small>
Info <ion-icon name="information-circle-outline"></ion-icon>
</ion-button>

e in questo caso conviene usare i riferimenti con il # per richiamare il metodo del componente.
Nel componente appena creato bisogna aggiungere una proprietà isOpen che andrà a gestire la visualizzazione del modulo e i metodi per modificare la proprietà

isOpen: boolean=false;
open(){this.isOpen=true;}
close(){this.isOpen=false;}

un semplice esempio di codice HTML per il componente è

<ion-modal *ngIf="isOpen" [isOpen]="isOpen">
<ion-header>
<ion-header [translucent]="true">
<ion-toolbar color="primary">
<ion-title>Informazioni generali</ion-title>
</ion-toolbar>
</ion-header>
</ion-header>
<ion-content class="ion-padding">
<ion-card>
<ion-card-header>
<ion-card-title>Applicazione di esempio</ion-card-title>
<ion-card-subtitle>realizzata da Alberto Nao</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
Applicazione di esempio realizzata con Ionic, Redux e NgRx.
</ion-card-content>
</ion-card>
<ion-button expand="block" (click)="close()">Chiudi</ion-button>
</ion-content>
</ion-modal>

Questo si deve intendere come semplice esempio per provare il componente base ma già si può vedere come deve essere usato: basta infatti creare una proprietà per la gestione dell'aperture e i metodi per gestire il comportamento da dentro il componente ma metodi anche richiamabili dall'esterno. Da notare che non è stata aggiunta nessuna rotta Angular visto che il componente è interno ad un altro componente e questo è un grande vantaggio rispetto ad altre tecniche e ad altre librerie che impongono la definizione di una rotta specifica per il contenuto della modale.

Il componente modale visto nel precedente articolo può essere usato anche per visualizzare il moda-handle, cioè la modale che compare nella parte bassa dello schermi che l'utente può espandere o nascondere con uno "swipe up". Per implementare una modale di questo tipo si usa la stessa tecnica di una modale normale e si devono aggiungere alcune proprietà che definiscono la posizione della modale.

<ion-modal
trigger="open-modal"
[initialBreakpoint]="0.25"
[breakpoints]="[0, 0.25, 0.5, 0.75]"
handleBehavior="cycle"
>

in particolare le proprietà initialBreakpoint e breakpoints definiscono la posizione nellos schermo e possono essere usate anche per visualizare all'alto l'elemento anche se è una pratica poco usata nelle app per smartphone, ancora meno nei siti web tradizionali. Altri esempi di modali con diverse applicazioni può essere trovato nella pagina della documentazione ufficiale.

Un esempio pratico di uso di modale è la creazione di una finestra per la selezione multipla, partendo da un elemento si usa una finestra modale per permettere agli utenti di eseguire un'operazione su più elementi della lista, per fare questo si usa la tecnica dell'input/output di Angular. Prima di tutti bisogna creare un componente modale, "selezione multipla" nel nostro esempio, e lo si importa nel componente lista

<frontend-selezione-multipla #select 
[data]="list$.pipe()"
(selectedToCaller)="selectedToCaller($event)"
>

come si vede da questo esempio ci sono i dati in input che passano dalla lista alla modale attraverso la proprietà "data" e il ritorno dalla modale al componente lista tramite un EventEmitter nella proprietà "selectedToCaller", questo evento scatena un metodo nel componente lista che deve gestire il ritorno, nel nostro esempio vengono cambiate le icone dello stato visualizzate. Nel componente modale oltre al titolo e al bottone chiusi si possono visualizzare tutti gli elementi della lista e, per ogni elementi, si visualizza una checkbox esposta da ionic, che userà un metodo specifico per aggiungere o rimuovere elementi dalla lista. Un bottone che permetterà l'emissione dell'evento verso il componente lista chiamante.

<ion-content class="ion-padding">
<ion-list *ngIf="data | async">
<ion-item *ngFor="let el of data | async ; index as i">
<ion-checkbox slot="start" [(ngModel)]="el.selected"
(ionChange)="itemSelected()" ></ion-checkbox>
<ion-label>{{ el.nome }}</ion-label>
</ion-item>
</ion-list>
<ion-button *ngIf="selected.length > 0" expand="block" (click)="confirm()">
Invia {{selected.length}} selezionati (
<span *ngFor="let item of selected; let last=last">
{{ item.nome }}{{ last ? "" : ", "}}
</span>)
</ion-button>
</ion-content>

Nel codice del componente modale vengono definiti questi componenti:

export class SelezioneMultiplaComponent implements OnInit {
//metodi per la gestione della visualizzazione del componente
isOpen: boolean=false;
open(){this.isOpen=true;}
close(){this.isOpen=false;}
confirm(){//emettitore di evento verso il chiamanete
this.selectedToCaller.emit(this.selected);
this.isOpen=false;
}
//proprietà di input e output
selected=[];
@Input() title='Selezione multipla';
@Input() data:Observable<AnnotazioniEntitySel[]> ;
datal:AnnotazioniEntitySel[]=[] ;
@Input() multiple = false;
@Output() selectedToCaller: EventEmitter<any> = new EventEmitter();
//costruttore e onInit che riceve la lista dal chiamante
constructor(){}
ngOnInit(): void {
this.data.subscribe(l => this.datal=l);
}
//alla selezione di un elemento aggiorno la lista dei selezionati
itemSelected(){
this.selected = this.datal.filter( (item) => item.selected );
}
}

Come si può notare da questo dice i metodi usati sono tre:

  • onInit che riceve la lista degli elementi dal componente chiamante
  • itemSelected che aggiorna la lista degli elementi selezionati
  • confirm che emette l'evento verso il chiamante

In alcuni esempi si possono trovare il secondo e il terzo metodo raggruppati in un unico metodo ma trovo molto più elegante separare il codice. Inoltre in questo esempio la lista dei selezionati viene ripetuta nel testo del bottone solo a titolo di esempio grazie proprio al secondo metodo indipendente dal terzo.

Spesso nelle applicazioni è necessario visualizzare messaggi all'interno di una app, nella parte alta o bassa dell'app un messaggio per qualche secondo che poi sparisce in maniera automatica, spesso usato per dare conferma ad un utente di una operazione appena conclusa. La libreria Ionic mette a disposizione il Toast, un componente facilmente importabile e usabile, basta infatti importare

import { ToastController } from '@ionic/angular';

e dichiarlo nel costruttore

constructor(private toastController: ToastController) {}

per poi poterlo usare per visualizzare mesaggi grazie al metodo create, per esempio:

async showToast() {
const toast = await this.toastController.create({
message: 'Salvataggio completato!',
duration: 3000,
cssClass: 'custom-toast',
position: 'top',
/*buttons: [{text: 'Dismiss',role: 'cancel'}],*/
});
await toast.present();
}

in questo semplice esempio un metodo a-scrincono visualizza il messaggio con alcune caratteristiche tra qui: la posizione, la durata, il messaggio e la classe css. E' possibile anche aggiungere dei bottoni personalizzati, per esempio:

buttons: [{
text: 'More Info',
role: 'info',
handler: () => { ... codice1 }
},{
text: 'Dismiss',
role: 'cancel',
handler: () => { ... codice2 }
}];

l'elenco completo di tutte le informazioni e delle personalizzazioni possibili è disponibile nella documentazione ufficiale.

La libreria mette a disposizione i checkbox con alcune proprietà personalizzabili per graficare al meglio i componenti. L'esempio base, già visto nel componente della selezione multipla è

<ion-checkbox slot="start" [(ngModel)]="el.selected" 
(ionChange)="itemSelected()" ></ion-checkbox>

con la possiblità di creare una proprietà per il valore delle seleziona e un metodo evento per cattura la modifica del'utente. Un altro caso d'uso è la creazione di radio per imporre all'utente di selezionare un valore tra un elenco prefissato, in alternativa del componente select simile nella finalità ma diverso nell'esposizione grafica.

<ion-list [inset]="true" ><ion-radio-group formControlName="tipo">
<ion-item>
<ion-label>Friends</ion-label>
<ion-radio value="friends" checked></ion-radio>
</ion-item>
<ion-item>
<ion-label>Family</ion-label>
<ion-radio value="family"></ion-radio>
</ion-item>
<ion-item>
<ion-label>Enemies</ion-label>
<ion-radio value="enemies" [disabled]="true"></ion-radio>
</ion-item>
</ion-radio-group></ion-list>

da notare che il form-control è stato assegnato al radio-group e nell'esempio è stato inserito un valore fisso disabilitato. I componenti alternativi all'uso dei radio sono: segment, toogle e select che saranno esposti nei prossimi articoli.

Il componente segment consiste nell'uso di bottoni per selezionare valori tra un elenco prefissato, il componente può prevedere una scollbar e ci possono essere valori disabilitati. Un classico esempio è mostrare un elemento che permetta all'utente tra due valori come attivo/disattivo.

<ion-segment formControlName="stato">
<ion-segment-button value="attivo">
<ion-label>Attivo</ion-label>
</ion-segment-button>
<ion-segment-button value="bloccato">
<ion-label>Bloccato</ion-label>
</ion-segment-button>
</ion-segment>

Eventualmente è possibile personalizzare la grafica dei componenti visto che la versione base di segment è graficamente poco accattivante, specialmente via browser e nei dispositivi Andoid. Per esempio si può aggiungere uno stile personalizzato del tipo

ion-segment{
--background: var(--ion-color-light, #FFFFFF);
border-radius: .5rem !important;
padding: 2px;
margin: 0;
}
ion-segment-button {
--indicator-box-shadow: transparent !important;
--background:white;
--color:var(--ion-color-secondary, #00000);
--background-checked: linear-gradient(to right,
var(--ion-color-dark, white) ,
var(--ion-color-medium, white)
)!important;
--color-checked: var(--ion-color-light, white);
border-radius: .5rem !important;
margin: 0;
}

Come si può vedere dall'esempio, l'aspetto grafico di questo tipo di componente può essere personalizzato sia con css ma anche con i scss messi a disposizione della libreria.

Il componente toggle permette di gestire un componente grafico booleano (si/no) o genericamente la selezione di due valori come (Attivo / Disattivo). Il tag è molto semplice e permette di impostare il valore iniziale checked e di gestire l'evento dimodifica, non ho ancora trovato il sistema con la proprietà formControlName. Un semplice esempio:

<ion-toggle slot="end" 
checked="{{noteForm.controls['stato'].value === 'attivo'}}"
(ionChange)="changeStatus($event)"
></ion-toggle>

In questo semplice evento il metodo per gestire il cambio di stato può essere

changeStatus(event){ //console.log(event);
this.noteForm.controls['stato'].setValue(event.detail.checked?'attivo':'bloccato');
}

che va a cambiare il valore del form con il setValue del singolo controllo, in questo modo è possibile valorizzare con un testo specifico, l'alternativa è salvare il valore booleano (true/false) e inviarlo al backend. I dettagli di tutte le caratteristiche e tutti i parametri per personalizzare questo tipo di componente sono disponibili nella documentazione ufficiale.

Il componente select permette di proporre una lista di valori all'utente e di selezionarne uno, come standard html viene usato il tag SELECT e ionic mette a disposizione una estensione evoluta e flessibile.
Esattamente come per il tag HTML standard, il tag select deve contenere la definizione della lista delle option selezionabili con la distinzione tra valore e etichetta. Un semplice esempio di uso di questo componente:

<ion-select placeholder="Select" formControlName="tipo">
<ion-select-option value="friends">Friends</ion-select-option>
<ion-select-option value="family">Family</ion-select-option>
<ion-select-option value="enemies">Enemies</ion-select-option>
</ion-select>

in questo caso la grafica sarà minimale ma efficace con una modale per permettere all'utente di selezionare il valore. E' possibile modificare l'interfaccia utente con una proprietà dedicata

interface="popover" 

per visualizzare le options come tooltip oppure

interface="action-sheet"

per visualizzare le options come modale in fondo alla pagina. Come ogni componente è possibile gestire gli eventi con i metodi:

(ionChange)="handleChange($event)"
(ionCancel)="pushLog('ionCancel fired')"
(ionDismiss)="pushLog('ionDismiss fired')"

I dettagli di tutte le caratteristiche e tutti i parametri per personalizzare questo tipo di componente sono disponibili nella documentazione ufficiale.

Il componente chip permette di visualizzare un complesso elemento che di solito viene usato per visualizzare i dati dell'utente o dei contatti. Può contenere alcune differenti elementi al suo interno come testi e icone. L'esempio più semplice è

<ion-chip>Default</ion-chip>

ma può essere personalizzato con le proprietà generiche disabled, outline, color, inoltre è possibile gestire la grafica con i css standard del framework. Un classico esempio di uso di questo componente è la visualizzazione del nome utente, un semplice avatar e una icona per eseguire il logout:

<ion-chip *ngIf="userName!==''" color="dark">
<ion-avatar>
<img alt="Silhouette of a person's head"
src="https://ionicframework.com/docs/img/demos/avatar.svg" />
</ion-avatar>
<ion-label>{{userName}}</ion-label>
<ion-icon name="log-out" (click)="this.logout()"></ion-icon>
</ion-chip>

I dettagli di tutte le caratteristiche e tutti i parametri per personalizzare questo tipo di componente sono disponibili nella documentazione ufficiale.

La libreria Ionic mette a disposizione un componente molto bello per la gestione delle date e dell'ora, tipicamente questo tipo di componente è molto delicato e quindi bisogna prestare sempre attenzione quando si usano componenti per la gestione e l'inserimento di questo tipo di valori. Il tag ha diverse proprietà che possono essere usate per gestire al meglio il componente, per esempio:

<ion-datetime id="descrizione" name="descrizione" 
formControlName="descrizione"
min="2021-01-01T00:00:00"
max="2023-31-12T23:59:59"
locale="it" [firstDayOfWeek]="1"
></ion-datetime>

In questo esempio è impostata una data minima, una data massima, un formato (locale) e la configurazione del primo giorno della settimana se diverso dalla domenica. Il formato con cui la data è salvata è quello ISO 8601, cioè quello standard usato da json e da tutti i framework javascript e typescript. Per maggiori informazioni basta guardare la documentazione ufficiale di json.

Esistono altre possilità come il visualizzare solo la data o solo l'orario, la selezione multipla.
Altri dettagli riguardo a questo tipo do componente può essere trovato nella documentazione ufficiale.

Nelle applicazioni e anche nelle pagine web può capitare di dover visualizzare un numero molto elevato di elementi, per questo si usa la tecnica dello scroll infinito cioè al caricamento si visualizza un numero limitato di elementi e poi si caricano gli altri elementi nel momento in cui l'utente ha scrollato fino alla fine della pagina, questa tecnica è molto usata nei social per visualizzare i contenuti dal più recente, man mano che l'utente visualizza contenuti, ne compaiono altri in basso perché vengono caricati i contenuti più datati che prima non erano visualizzati. Questa tecnica permette di costruire pagine snelle e volto veloci al primo caricamento e poi è l'utente ad eseguire il caricamento di altri contenuti in base al proprio comportamento.

Con la libreria Ionic questa tecnica è stata evoluta con la "ion-infinite-scroll" che permette di visualizzare il loader e richiamare un metodo solo quando l'utente scrolla in basso e visualizza la barra:

</ion-list>
<ion-infinite-scroll (ionInfinite)="onIonInfinite($event)">
<ion-infinite-scroll-content loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>

ovviamente è necessario che questa venga posizionata appena dopo una ion-list. Il metodo richiamato ha il campo di gestire l'evento di scroll, di solito viene richiamato un servizio che recupera e popola la lista, in questo semplice esempio invece viene usato un timeout per aggiungere un elemento vuoto alla lista.

onIonInfinite(ev) { //console.log(ev);
//a titolo di esempio eseguo un timeout per far sembrare passare un po' di tempo
setTimeout(() => {
this.aggiungiElemento(ev);
(ev as InfiniteScrollCustomEvent).target.complete();
console.log("onIonInfinite "+ ev.timeStamp);
}, 500);
}
aggiungiElemento(ev){
//nota: andrebbe eseguita il dispath di una azione
console.log("aggiungiElemento "+ ev.timeStamp);
const elementoVuoto : AnnotazioniEntity={nome:'added by inf', fase: ev.timeStamp}
as AnnotazioniEntity;
this.list$ = this.list$.pipe(
map ( li => {
li.push(elementoVuoto);
console.log("aggiungiElemento "+ li.length);
return li;
});
);
}

Da notare che se il browser o l'app ha già visualizzato tutti gli elementi della lista, non verrà visualizzato nulla. Maggiori dettagli si possono trovare nella documentazione ufficiale:

https://ionicframework.com/docs/api/infinite-scroll

La libreria Ionic mette a disposizione un tag specifico con il quale gestire i tab che tanto vengono usati nelle applicazioni per smartphone, in fase di creazione di un progetto mette anche a disposizione un archetipo specifico in modo da avere un progetto vuoto con un sistema a tab di esempio. Un esempio semplice con dei tab con le icone e il router di default è:

<ion-tabs>
<ion-router-outlet></ion-router-outlet>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home">
<ion-icon mode="md" name="home"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button tab="schedules" class="inner-left-btn">
<ion-icon name="calendar"></ion-icon>
<ion-label>Schedule</ion-label>
<ion-badge>6</ion-badge>
</ion-tab-button>
<ion-tab-button tab="settings">
<ion-icon name="settings"></ion-icon>
<ion-label>Settings</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>

La documentazione ufficiale in realtà non è molto ricca di dettagli in quanto la maggior personalizzazione è possibile grazie ai CSS. Per esempio è possibile personalizzare la grafica della barra con un css:

ion-tab-bar {
bottom: 60px;
position: relative;
--background: var(--ion-color-tertiary);
ion-tab-button {
--color: var(--ion-color-medium);
--color-selected: var(--ion-color-light);
&.tab-selected::before {
background-color: var(--ion-color-light);
}
}
}

Non esiste un componente ionic ufficiale per creare Grafici ma esistono delle librerie terze compatibiliti e molto facili da usare. La più semplice e comune è ChartJS installabile in un progetto Ionic con il comando:

npm install chart.js --save --legacy-peer-deps

Per creare un grafico basta creare un tag all'interno di una pagina assegnandogli un riferimento:

<canvas #graficoDiEsempioEl 
style="position: relative; height:20vh; width:40vw"></canvas>

e poi nel codice conviene definie un grafico per visualizzare i dati

//import { Chart, registerables } from 'chart.js';
@ViewChild('graficoDiEsempioEl') private graficoDiEsempioEl: ElementRef;
graficoDiEsempioChart: any;
ngAfterViewInit() {
Chart.register(...registerables); //necessario
this.graficoDiEsempioMethod();
}
graficoDiEsempioMethod() {
this.graficoDiEsempioChart = new Chart(this.graficoDiEsempioEl.nativeElement, {
type: 'bar', //line
data: {
labels:['GEN','FEB','MAR','APR','MAG','GIU', 'LUG','AGO','SET','OTT','NOV','DIC'],
datasets: [{
label: 'Valori',
data: [42, 59, 80, 81, 56, 55, 40, 10, 5, 50, 10, 42],
}]
}
});
}

in questo semplice esempio si è creata un grafico di tipo bar con dei valori fissi, è possibile creare grafici di vario tipo (bar, linee, area, ecc...). Nel sito della documentazione ufficiale è possibile trovare esempi di diversi tipi di grafici ed è possibile trovare una descrizione di tutte le proprietà che permettono di personalizzare graficamente e modificare il comportamento del grafico.

La libreria Ionic attualmente non mette a disposizione un componente specifico per la paginazione, ma si può usare uno strumento esterno come jw-angular-pagination mentre non si può usare il famoso ngx.pagination in quanto questo secondo componente non è compatibile con Ionic. 

Per l'installazione è necessario usare il comando npm

$ npm i jw-angular-pagination --force

L'import del modulo in un componente si usa con un semplice import 

import { JwPaginationModule } from 'jw-angular-pagination';
...
,JwPaginationModule

E poi nel componente basta inserire la gestione della paginazione con una lista items

itemlist = [];
currentPage = 1;
pageOfItems: Array<any>;
onChangePage(pageOfItems: Array<any>) {
if (pageOfItems!==null)
this.pageOfItems = pageOfItems;
}
ngOnInit(): void {
this.loadAnnotazioni();
this.actionListener$.pipe(skip(1) // optional: skips initial logging done by ngrx
).pipe(ofType( loadAnnotazioniSuccess ),
).subscribe((action) => {//console.log(action.annotazioni);
if (this.itemlist!==undefined)
this.itemlist=action.annotazioni;
})
);
}

Nel componente grafico la pagina è gestita dal tag specifico:

<ion-card *ngIf="isLoaded$ | async">
<jw-pagination [items]="itemlist" (changePage)="onChangePage($event)"
[pageSize]="2"></jw-pagination>
</ion-card>
<div class="pagination">pagination</div>

Ovviamente una volta implementato deve essere aggiunta la gestione della grafica grazie alle classi css messe a disposizione del componente usato.

I contenuti di AlNao.it potrebbero avere inesattezze o refusi. Non potrà in alcun caso e per qualsiasi motivo essere ritenuta responsabile di eventuali imprecisioni ed errori né di danni causati. Il sito web e tutte le informazioni ed i contenuti in esso pubblicati potranno essere modificati in qualsiasi momento e di volta in volta e senza preavviso. Poiché ogni materiale sarà scaricato o altrimenti ottenuto attraverso l’uso del servizio a scelta e a rischio dell’utente, ogni responsabilità per eventuali danni a sistemi di computer o perdite di dati risultanti dalle operazioni di scarico effettuato dall'utente, ricade sull'utente stesso e non potrà essere imputata ad AlNao.it che declina ogni responsabilità per eventuali danni derivanti dall'inaccessibilità ai servizi presenti sul sito o da eventuali danni causati da virus, file danneggiati, errori, omissioni, interruzioni del servizio, cancellazioni dei contenuti, problemi connessi alla rete, ai provider o a collegamenti telefonici e/o telematici, ad accessi non autorizzati, ad alterazioni di dati, al mancato e/o difettoso funzionamento delle apparecchiature elettroniche dell’utente stesso.

AlNao.it ha adottato ogni possibile accorgimento al fine di evitare che siano pubblicati, nel sito web, contenuti che descrivano o rappresentino scene o situazioni inappropriate o tali che, secondo la sensibilità degli utenti, possano essere ritenuti lesivi delle convinzioni civili, dei diritti umani e della dignità delle persone, in tutte le sue forme ed espressioni. In ogni caso non garantisce che i contenuti del sito web siano appropriati o leciti in altri Paesi, al di fuori dell’Italia. Tuttavia, qualora tali contenuti siano ritenuti non leciti o illegali in alcuni di questi Paesi, ti preghiamo di evitare di accedere al nostro sito e ove scegliessi, in ogni caso, di accedervi, ti informiamo che l’uso che deciderai di fare dei servizi forniti dal sito sarà di tua esclusiva e personale responsabilità. L’utente sarà esclusivo responsabile della valutazione delle informazioni e del contenuto ottenibile mediante il sito web. Il sito web e tutte le informazioni ed i contenuti in esso pubblicati potranno essere modificati in qualsiasi momento e di volta in volta e senza preavviso.

Le pagine di questo sito sono protette dal diritto d’autore (copyright). In particolare a norma della legge sul diritto d’autore e il contenuto del sito è protetto contro duplicazioni, traduzioni, inserimento o trasformazione dello stesso in altri media, incluso l’inserimento o la trasformazione con mezzi elettronici. La riproduzione e lo sfruttamento economico di tutto o di parte del contenuto di questo sito sono consentite solo a seguito del consenso scritto dell’avente diritto. Sia il contenuto che la struttura del sito sono protetti dal diritto d’autore. In particolare, la duplicazione di informazioni o dati, l’uso dei testi o di parte di essi o delle immagini contenute nel sito (eccetto per le foto ad uso stampa) è consentita solo previo consenso scritto dell’avente diritto. Anche le illustrazioni, a norma dell’art. 1 della legge 633/1941 – e successive modifiche e integrazioni – sono protette dal diritto d’autore. Il diritto di pubblicazione e riproduzione di questi disegni è di titolarità dell’avente diritto. Il diritto d’autore sui disegni rimane in vigore anche per i disegni automaticamente o manualmente aggiunti a un archivio. Nulla di quanto contenuto in questo sito vale come concessione a terzi dei diritti di proprietà industriale ed intellettuale indicati in questa sezione. Ci riserviamo tutti i diritti di proprietà intellettuale del sito web.

Marchi ed immagini riferibili a soggetti terzi utilizzati in questo sito non appartengono a AlNao.it e sono da ritenersi di proprietà esclusiva dei rispettivi titolari. Le denominazioni dei prodotti pubblicati su questo sito web o delle società menzionate, anche qualora non siano indicate con il simbolo identificativo della registrazione del marchio sono marchi di titolarità di terzi e sono protetti dalla legge sui marchi (D.lgs. 30/2005 e successive modifiche e integrazioni) e dalle norme in tema di concorrenza sleale. Qualsiasi riproduzione degli stessi è da ritenersi vietata ai sensi di legge. In particolare è espressamente vietato qualsiasi uso di questi marchi senza il preventivo consenso scritto del relativo titolare ed, in particolare, è vietato utilizzarli in modo da creare confusione tra i consumatori in merito all'origine dei prodotti o per finalità di sponsorizzazione, nonché in qualsiasi modo tale da svilire e discreditare il titolare del marchio. Tutti i diritti che non sono espressamente concessi sono riservati al titolare del marchio.

In aggiunta a quanto indicato in altre previsioni delle Condizioni Generali d’Uso, AlNao.it non potrà essere ritenuta in alcun caso responsabile di alcun danno derivante dall'utilizzo o dall'impossibilità di utilizzare il sito web, i contenuti, le informazioni o connessi alla qualità degli stessi.