Creating Angular UI components and connecting them to the GraphQL AppSync API - Part 2
Tags: angular, graphql, awsamplify, awsappsync
Published: 2019-03-27 17:43:08

---
title: Creating Angular UI components and connecting them to the GraphQL AppSync API - Part 2
published: true
description: We're building a simple Todo-App, but with a setup in mind that is targeted towards a non-trivial multi-app, multi-team project. The AWS Amplify CLI will enable us to build a powerful GraphQL API with just a simple schema. In the second part of this series we are creating our UI with the help of Angular CLI.
tags: angular, graphql, awsamplify, awsappsync
cover_image: https://thepracticaldev.s3.amazonaws.com/i/9be1kmte96uunrsawzam.png
series: Angular with NX multi-app project powered by an AWS Amplify GraphQL Backend
---

In the [first part of the series](https://dev.to/beavearony/getting-started-with-a-angularnx-workspace-backed-by-an-aws-amplify-graphql-api---part-1-24m0) we setup a workspace, configured AWS cloud resources in a shared library, added Authentication to protect private data, generated typings for the GraphQL API, wrote a mutation helper wrapper function and discussed various topics around this. 
{% link beavearony/getting-started-with-a-angularnx-workspace-backed-by-an-aws-amplify-graphql-api---part-1-24m0 %}
We were busy, but until this point, we still haven't done much towards our UI. Let's create some components using the Angular CLI in this second part!

I will omit some styles for brevity reasons, but you can find them [on GitHub](https://github.com/BeaveArony/amplify-angular-nx-todo).

{% github BeaveArony/amplify-angular-nx-todo %}

## Building the UI

1. The todo-page that wraps the whole UI. The component, that will be imported by app-component and is going to be the only container/smart component of this lib
2. The form to create new todos
3. The filter component to switch visibility on the _completed_ property
4. The todo-list component with its todo-item components
5. A component to show error messages

```
ng g c --project=todo-ui --export  todo-page
ng g c --project=todo-ui -c=OnPush create-form
ng g c --project=todo-ui -c=OnPush todo-list
ng g c --project=todo-ui -c=OnPush todo-item
ng g c --project=todo-ui -c=OnPush filter-bar
ng g c --project=todo-ui -c=OnPush error-alert
```

I would like to style the app like the AWS authenticator component. To help with the styling, replace the content of `apps/todo-app/src/styles.css` with this:

```css
/* You can add global styles to this file, and also import other style files */
@import '~aws-amplify-angular/theme.css';

html,
body {
  height: 100%;
}
body {
  margin: 0;
  background-color: var(--color-accent-brown);
}

/* Amplify UI fixes */
.amplify-form-input {
  width: calc(100% - 1em);
  box-sizing: border-box;
}
.amplify-alert {
  margin-top: 2em;
}
.amplify-greeting > * {
  white-space: nowrap;
}
.amplify-greeting-flex-spacer {
  width: 100%;
}

.card,
.amplify-authenticator {
  width: var(--component-width-desktop);
  margin: 1em auto;
  border-radius: 6px;
  background-color: var(--color-white);
  box-shadow: var(--box-shadow);
}
.card-container {
  width: 400px;
  margin: 0 auto;
  padding: 1em;
}

@media (min-width: 320px) and (max-width: 480px) {
  .card,
  .amplify-authenticator {
    width: var(--component-width-mobile);
    border-radius: 0;
    margin: 0.5em auto;
  }
  .card-container {
    width: auto;
  }
  .amplify-alert {
    left: 4%;
  }
}
```

We are setting up html / body and card classes that look very similar to the Amplify Components styles. We are reusing the CSS variables imported earlier. The amplify-authenticator has rounded corners in the mobile view, so we are changing this in the media-query as well.

We are also fixing the styling of the html input tags, because they are extending too far to the right in mobile.

There are still some styling issues with the amplify angular authenticator component, but I think it's good enough for a simple drop in UI component to support the general sign-up/sign-in procedure.

## Todo-page

The todo-page component is the starting point of the todo-ui module. It is the only component exported, leaving all other components internal to this lib. We could introduce routing at a later stage and even lazy load this module for a snappier start of our app.

This component is also going to be the only container component handling the API communication and providing data to the other presentational components in this module. Since we haven't written any code in our components yet, we compose our view with what we got so far. All components were generated by the `ng generate` commands and can be referenced in our HTML markup.

Change `libs/todo-ui/src/lib/todo-page/todo-page.component.html` to this

```html
<div class="card">
  <div class="card-container">
    <my-create-form></my-create-form>
    <my-filter-bar></my-filter-bar>
  </div>
</div>
<my-error-alert></my-error-alert>
<div class="card">
  <div class="card-container"><my-todo-list></my-todo-list></div>
</div>
```

and import the todo-ui module in our root app module `apps/todo-app/src/app/app.module.ts` like this

```typescript
// ... other imports
import { TodoUiModule } from '@my/todo-ui';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppsyncModule, TodoUiModule],
  bootstrap: [AppComponent]
})
export class AppModule {}
```

To display the todo-page in the app change `apps/todo-app/src/app/app.component.html` to this

```html
<my-todo-page></my-todo-page>
```

So far this is just the starting point for our UI. You can run the app with `npm start` and glance on a basic skeleton.

## AWS Authenticator UI-Component

AWS Amplify provides several drop-in UI components for Angular/Ionic, React and Vue. We can choose between

- Authenticator
- Photo Picker
- Album
- Chatbot

To add the AWS Authenticator to our app change `apps/todo-app/src/app/app.component.html` to both include it and the todo-page component.

```html
<amplify-authenticator></amplify-authenticator>
<my-todo-page *ngIf="hydrated && isLoggedIn"></my-todo-page>
```

And the content of `apps/todo-app/src/app/app.component.ts`

```typescript
import { Component, OnInit } from '@angular/core';
import { AppSyncClient } from '@my/appsync';
import { AmplifyService } from 'aws-amplify-angular';

@Component({
  selector: 'my-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  hydrated = false;
  isLoggedIn = false;

  constructor(private amplifyService: AmplifyService) {}

  ngOnInit(): void {
    AppSyncClient.hydrated().then(client => (this.hydrated = !!client));
    this.amplifyService.authStateChange$.subscribe(authState => {
      if (authState.state === 'signedOut' && this.isLoggedIn) {
        AppSyncClient.clearStore().then(() =>
          console.log('User signed out, store cleared!')
        );
      }
      this.isLoggedIn =
        authState.state === 'signedIn' || authState.state === 'confirmSignIn';
    });
  }
}
```

The AppSyncClient persists its redux store, so we should render the todo-ui only when the store is rehydrated. AppSyncClient offers a `AppSyncClient.hydrated()` function we can use for this.
The AmplifyService offers an Observable `authStateChange$` we can subscribe to and only show the todo-ui when a user is logged In. To not leave any cached items around when a user signs out we clear the store on that event. The `<my-todo-page>` component is hidden while the client is not rehydrated or while no user is signed in with the `*ngIf` directive. The `<amplify-authenticator>` is showing a greeting message by default and a link to log out when a user is logged in, so we don't need to hide it.

We can configure this component [by passing in a config-object](https://aws-amplify.github.io/docs/js/angular#signup-configuration)

```html
<amplify-authenticator [signUpConfig]="signUpConfig"></amplify-authenticator>
```

or change the framework to Ionic and hide certain parts, like the SignOut link

```html
<amplify-authenticator
  framework="ionic"
  [hide]="['SignOut']"
></amplify-authenticator>
```

## Creating the presentational UI

We start with the presentational components, that do not use any API and get their data only through `@Input()` and emit events through `@Output()` using Angular's `EventEmitter`. Later we
connect them in the todo-page component.

### model

Create a file `libs/todo-ui/src/lib/model.ts` to keep types specific to this UI-Library. In our case we only type our Todo's visibility states.

```typescript
export type TodoFilterType = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED';
```

### create-form component

This create-form component emits a new todo name when the reactive form is submitted. We also disable the submit button, when the required text field is empty. Once we emitted a new todo, we clear the input field. Using the form tag together with the submit button gives us the advantage to just hit 'Enter' after typing our todo text.

`libs/todo-ui/src/lib/create-form/create-form.component.html`

```html
<form [formGroup]="form" (ngSubmit)="createTodo()">
  <input type="text" formControlName="todoName" required />
  <button type="submit" [disabled]="!form.valid">+</button>
</form>
```

`libs/todo-ui/src/lib/create-form/create-form.component.ts`

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Output
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'my-create-form',
  templateUrl: './create-form.component.html',
  styleUrls: ['./create-form.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CreateFormComponent {
  @Output() created = new EventEmitter<string>();
  form = new FormGroup({ todoName: new FormControl('') });

  createTodo() {
    const todoName = this.form.value.todoName.trim();
    if (todoName) {
      this.created.emit(todoName);
      this.form.setValue({ todoName: '' });
    }
  }
}
```

### filter-bar component

The filter-bar component holds a bar of toggle buttons to change the visibility of the todo-list. The selected button will be disabled and styled so that it looks active. We can use CSS variables from the Amplify Angular Theme for that.

`libs/todo-ui/src/lib/filter-bar/filter-bar.component.html`

```html
<nav>
  <button
    *ngFor="let btn of buttons"
    (click)="filter(btn.value)"
    [disabled]="currentFilter === btn.value"
  >
    {{ btn.name }}
  </button>
</nav>
```

`libs/todo-ui/src/lib/filter-bar/filter-bar.component.css`

```css
nav {
  margin-top: 0.5em;
  display: flex;
  justify-content: space-between;
}
button {
  border: 1px solid var(--color-blue);
  height: var(--button-height);
  width: 128px;
  color: var(--color-blue);
  text-transform: uppercase;
  cursor: pointer;
}

button:hover {
  background-color: var(--color-accent-blue);
}

button:disabled {
  background-color: var(--color-blue);
  color: white;
  border: none;
  cursor: auto;
}
```

`libs/todo-ui/src/lib/filter-bar/filter-bar.component.ts`

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Output
} from '@angular/core';
import { TodoFilterType } from '../model';

@Component({
  selector: 'my-filter-bar',
  templateUrl: './filter-bar.component.html',
  styleUrls: ['./filter-bar.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class FilterBarComponent {
  @Output() filtered = new EventEmitter<TodoFilterType>();

  currentFilter: TodoFilterType = 'SHOW_ALL';
  buttons: { value: TodoFilterType; name: string }[] = [
    { value: 'SHOW_ALL', name: 'All' },
    { value: 'SHOW_ACTIVE', name: 'Active' },
    { value: 'SHOW_COMPLETED', name: 'Completed' }
  ];

  filter(query: TodoFilterType) {
    this.currentFilter = query;
    this.filtered.emit(this.currentFilter);
  }
}
```

We emit a filter query string depending on the selected button. The parent todo-page component will listen to this and pass the query to its child todo-list component as Input, so it can filter the list on the _completed_ property.

### todo-item component

The todo-list renders a list of todo-item components. These items are responsible to display the provided input values. We use HTML entities as icons. When clicking on the delete icon we emit the `deleted` event and when clicked on the rest of this item a `toggled` event is emitted to change the _completed_ property of this todo item.

`libs/todo-ui/src/lib/todo-item/todo-item.component.html`

```html
<div class="item" *ngIf="todo">
  <div
    class="checkmark"
    [class.checked]="todo.completed"
    (click)="toggled.emit()"
  >
    {{ todo.completed ? '✔' : '🗸' }}
  </div>
  <div
    class="content"
    [ngStyle]="{ textDecoration: todo.completed ? 'line-through' : 'none' }"
    (click)="toggled.emit()"
  >
    {{ todo.name }}
  </div>
  <div class="delete" (click)="deleted.emit()">✗</div>
</div>
```

`libs/todo-ui/src/lib/todo-item/todo-item.component.ts`

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  Output
} from '@angular/core';
import { ListTodos_listTodos_items } from '@my/appsync';

@Component({
  selector: 'my-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoItemComponent {
  @Input() todo: ListTodos_listTodos_items;
  @Output() toggled = new EventEmitter<boolean>();
  @Output() deleted = new EventEmitter<boolean>();
}
```

### todo-list component

The todo-list component renders each todo item through the previously created todo-item component. To react on the Inputs to filter the todos we use getters and setters. Component life cycle hook `ngOnChanges` would also work here.

The visibility filter is the only client state in our app and is managed in our components. Apollo Client, which is used by AWSAppSyncClient under the hoods, keeps the data in a normalized redux store.  
We have many choices here, when we think about our architecture:

- Manage the state in an Angular provider using a Behavior Subject
- Write actions and reducers to extend the underlying redux store
- Use a separate state management library for our client state
- Extend Apollo Client to use apollo-link-state, which offers querying and mutating our local state with GraphQL operations
- Keep it in our components
- ...

Extending the underlying redux store is a little bit risky, since the AWSAppSyncClient can change the implementation internals at any time. We would get stuck with a certain version or need to refactor our client state.

For this simple app using a Behavior Subject is probably the easiest. For non-trivial apps I would use a state management library. Using apollo-link-state feels like the cleanest solution, having local and remote state in one place.

```html
<!-- libs/todo-ui/src/lib/todo-list/todo-list.component.html -->
<my-todo-item
  *ngFor="let todo of visibleTodos"
  [todo]="todo"
  (toggled)="toggled.emit(todo)"
  (deleted)="deleted.emit(todo)"
></my-todo-item>
<div *ngIf="!visibleTodos || visibleTodos?.length === 0">No todos</div>
```

```typescript
// libs/todo-ui/src/lib/todo-list/todo-list.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  Output
} from '@angular/core';
import { ListTodos_listTodos_items } from '@my/appsync';
import { TodoFilterType } from '../model';

@Component({
  selector: 'my-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class TodoListComponent {
  @Output() toggled = new EventEmitter<ListTodos_listTodos_items>();
  @Output() deleted = new EventEmitter<ListTodos_listTodos_items>();

  private _filter: TodoFilterType;
  private _todos: ListTodos_listTodos_items[];

  @Input()
  public get filter(): TodoFilterType {
    return this._filter;
  }
  public set filter(value: TodoFilterType) {
    this._filter = value;
    this.visibleTodos = this.getVisibleTodos(this.todos, value);
  }

  @Input()
  public get todos(): ListTodos_listTodos_items[] {
    return this._todos;
  }
  public set todos(value: ListTodos_listTodos_items[]) {
    this._todos = value;
    this.visibleTodos = this.getVisibleTodos(value, this.filter);
  }

  visibleTodos: ListTodos_listTodos_items[];

  private getVisibleTodos(
    todos: ListTodos_listTodos_items[],
    filter: TodoFilterType
  ) {
    switch (filter) {
      case 'SHOW_ALL':
        return todos;
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed);
      default:
        return todos;
    }
  }
}
```

Whenever either the filter query or the provided todos change, we calculate the visible todos and store them in _visibleTodos_.

## error-alert component

This reusable component receives an error message and emits an event if the close button is clicked.

```html
<div class="card" *ngIf="errorMessage">
  <div class="card-container">
    <div class="flex-row">
      <span class="icon">⚠</span>
      <div class="msg">{{ errorMessage }}</div>
      <a class="close" (click)="closed.emit()">×</a>
    </div>
  </div>
</div>
```

```css
.flex-row {
  display: flex;
  justify-content: space-between;
}
.close {
  color: var(--color-gray);
  font-size: 32px;
  cursor: pointer;
}
.msg {
  width: 100%;
  margin: 10px 2em;
}
.icon {
  color: var(--red);
  font-size: 32px;
  width: 38px;
}
```

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  Input,
  Output,
  EventEmitter
} from '@angular/core';

@Component({
  selector: 'my-error-alert',
  templateUrl: './error-alert.component.html',
  styleUrls: ['./error-alert.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ErrorAlertComponent {
  @Input() errorMessage: string | null = null;
  @Output() closed = new EventEmitter();
}
```

## Watching todos

Now that we have build our presentational components, we can come back to our wrapping component, that handles all the events, interacts with our API and displays possible errors.

```typescript
// imports ...
@Component({
  selector: 'my-todo-page',
  templateUrl: './todo-page.component.html',
  styleUrls: ['./todo-page.component.css'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class TodoPageComponent implements OnInit {
  todos: ListTodos_listTodos_items[];
  errorMessage: string | null = null;

  ngOnInit() {
    AppSyncClient.watchQuery<ListTodos, ListTodosVariables>({
      query: LIST_TODOS
    }).subscribe(todoList => {
      this.todos = todoList.data
        ? todoList.data.listTodos.items.sort((a, b) =>
            a.createdOnClientAt.localeCompare(b.createdOnClientAt)
          )
        : [];
    }, this.handleError);
  }

  handleError = (err: ApolloError) => {
    this.errorMessage = err.message;
  };
}
```

We listen to any local changes of our todos in _ngOnInit_ life cycle hook. Using _watchQuery()_ for this, informs our component of local operations like adding or removing items. To listen to remote changes, we would need to set up a GraphQL subscription. Fortunately _watchQuery()_ is generic so we can specify the previously generated query and variables types. This offers us complete IntelliSense in the subscribe block! If we receive data, we sort them on the client on the _createdOnClientAt_ property.

> _watchQuery()_ returns a zen-observable, not one from rxjs! So, we can't use the async pipe here. We could write a converter or simply use apollo-angular to do this for us.

We have to think about change detection here. If this component would use the OnPush ChangeDetectionStragy, our UI would not react to changes coming from this observable. We can either change to the Default strategy like above or inject _ChangeDetectorRef_ to invoke _markForCheck()_ in the subscribe block.

Something like this, focussing on the changes:

```typescript
import { ChangeDetectorRef } from '@angular/core';
// ...
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoPageComponent {
  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    AppSyncClient.watchQuery<ListTodos, ListTodosVariables>({
      query: LIST_TODOS
    }).subscribe(todoList => {
      // ...
      this.cd.markForCheck();
    });
  }
}
```

Angular's async pipe would handle this automatically for us!

Change one line in the html file to pass our todos to the todo-list component:

```html
<my-todo-list [todos]="todos"></my-todo-list>
```

Interact with the error-alert component by passing the error message and setting it to _null_ when the _closed_ event emits

```html
<my-error-alert
  (closed)="errorMessage = null"
  [errorMessage]="errorMessage"
></my-error-alert>
```

## Creating todos

To have items to display in our list, we need to be able to create a new todo. So, let's implement a handler in our component class and pass it the event payload of the _created_ event of the create-form component.

```html
<my-create-form (created)="create($event)"></my-create-form>
```

```typescript
// ...
export class TodoPageComponent {
  // ...
  create(name: string) {
    executeMutation<CreateTodoVariables, CreateTodo_createTodo>({
      mutation: CREATE_TODO,
      variablesInfo: {
        input: {
          name,
          completed: false,
          createdOnClientAt: new Date().toISOString()
        }
      },
      cacheUpdateQuery: [LIST_TODOS],
      typename: 'Todo'
    }).catch(this.handleError);
  }
}
```

Here happens quite a lot in a single statement. Let's break it down!

We invoke our generic helper function _executeMutation()_ and pass it an object. This object needs the following properties:

- **mutation**: The graphql mutation operation
- **variablesInfo**: The input object we want to send to our API
- **cacheUpdateQuery**: A list of queries we would like to be updated in our cache
- **typename**: The value of the _\_\_typename_ property

Since we add a new item to our list, we need to update the _LIST_TODOS_ query in the cache. It's as simple as adding it to the _cacheUpdateQuery_'s array. Doing this will invoke the watchQuery observable twice. First with an optimistic response using a temporary ID, like we would expect our API to respond if all goes well and finally with the actual result from the server.

If we would receive an error when trying to create the todo, the second call would not contain this new todo anymore, negating the optimistic UI update. Additionally, we are handling the error in the _catch_ part of the returned promise and displaying it with our error-alert component. This way the user gets a hint of what just happened, even though the error message is quite technical.

> Note, everything is type safe, even the _typename_ string!

## Updating a todo

To update the completed property of a todo, we listen to the _toggled_ event of the todo-list component. We pass the payload to the _toggle()_ function, destructuring it in the process.

```html
<my-todo-list [todos]="todos" (toggled)="toggle($event)"></my-todo-list>
```

```typescript
// ...
export class TodoPageComponent {
  // ...
  toggle({
    id,
    completed,
    createdOnClientAt,
    name
  }: ListTodos_listTodos_items) {
    executeMutation<UpdateTodoVariables, UpdateTodo_updateTodo>({
      mutation: UPDATE_TODO,
      variablesInfo: {
        input: {
          completed: !completed,
          id,
          createdOnClientAt,
          name
        }
      },
      cacheUpdateQuery: null,
      typename: 'Todo'
    }).catch(this.handleError);
  }
}
```

This looks very similar to the create mutation. We provide the updated item and this time we don't need to take care of updating the cache. Apollo Client is handling this automatically, so we can pass _null_ or an empty array to _cacheUpdateQuery_.

> _ListTodos_listTodos_items_ looks pretty weird as an input type. It's the type of one item of the list, but I guess that's the price we have to pay when this gets generated automatically!

## Deleting a todo

To delete a todo, we listen to the _deleted_ event of the todo-list component. We pass the payload to the _toggle()_ function, destructuring only the _id_.

```html
<my-todo-list
  [todos]="todos"
  (toggled)="toggle($event)"
  (deleted)="delete($event)"
></my-todo-list>
```

```typescript
// ...
export class TodoPageComponent {
  // ...
  delete({ id }: ListTodos_listTodos_items) {
    executeMutation<DeleteTodoVariables, DeleteTodo_deleteTodo>({
      mutation: DELETE_TODO,
      variablesInfo: {
        input: { id }
      },
      cacheUpdateQuery: [LIST_TODOS],
      typename: 'Todo'
    }).catch(this.handleError);
  }
}
```

Deleting an item from a list requires us to take care of the cache again.

## Handling the filter

The only thing left is listening to the filter-bar component and passing the value to the todo-list component. We start with _'SHOW_ALL'_, but don't do anything else with the filter variable.

```html
<my-filter-bar (filtered)="filter = $event"> </my-filter-bar>
<!-- ... -->
<my-todo-list
  [todos]="todos"
  [filter]="filter"
  (toggled)="toggle($event)"
  (deleted)="delete($event)"
></my-todo-list>
```

```typescript
filter: TodoFilterType = 'SHOW_ALL';
```

That's it. Please take a look at [this repo](https://github.com/BeaveArony/amplify-angular-nx-todo) to see the complete source code.

## Taking it for a spin

Let's run `npm start` to get a chance to admire our work. Here are a few things for you to play around with:

- Create at least two users to see how each one has its own todos
- Add new todos
- Try out the responsive UI
- Open the dev-tools and tick the _offline_ checkbox, do a few operations and finally go online again.
- Open the redux dev-tools (if installed) and inspect the _outbox_ of redux-offline, the normalized entities of apollo client's InMemoryCache and the rehydration actions of redux-persist
- Take a look at things stored in IndexedDB and the JWT in LocalStorage
- Set a breakpoint in _watchQuery()_ and observe the optimistic response in action
- Change the name variable to _null_ in _create()_ and enjoy the Undo taking place in the list paired with the splendid error message displayed
- Reload the app to appreciate the persisted cache
- Login to the AWS management console and analyze the DynamoDB Table entries created

## Recap

We created our UI with Angular components. The wrapping component orchestrates everything by connecting the presentational components, listening to changes in the cache and talking to the API using helpers we set up in [Part 1](https://dev.to/beavearony/getting-started-with-a-angularnx-workspace-backed-by-an-aws-amplify-graphql-api---part-1-24m0).

## Next

In [Part 3] (https://dev.to/beavearony/personal-growth-with-git-flow-and-aws-amplify-console-part-3-1f6c) we are going to explore how to add a CI/CD pipeline the easy way using the amplify console and even look at how to add a second frontend app.