Adding content to our page components

Now that we have our routing all sorted, we are ready to start adding some functionality to our pages. As well as adding content, we are going to start adding some polish to our application by making use of Angular validation to provide instant feedback to our users. The component that we are going to start with is the AddTask component. Without the ability to add tasks, we aren't going to be able to display any, so let's give ourselves the opportunity to start adding some todo tasks.

Before I start adding in user interface elements, I like to make sure that I have as much of the logic in place behind the component as possible. Once this is in place, actually adding the user interface becomes straightforward. In some cases, this will mean that I have decided on UI constraints before I have even considered how the particular piece of display should be shown, or what control to use to show it. With this in mind, we know that one of the things that makes up our todo item is DueDate. If we think about this for a moment, we realize that it makes no sense for us to create a task that has a due date that has already passed. To that end, we are going to set the earliest date that a task can end as being today's date. This will be used as a constraint against whatever control we use to choose the date:

EarliestDate: Date;
ngOnInit() {
this.EarliestDate = new Date();
}

We have three things that we are going to be capturing from the user in order to create our todo task. We need to capture the title, the description, and the date the task is due. This tells us that we are going to need three items to act as our model:

Title: string;
Description?: string;
DueDate: Date;

This is all we need on the model side of our add task component, but we are missing the ability to actually save anything over to our GraphQL server. Before we can start talking to our server, we need to bring support for Apollo into our component. This is as simple as adding a reference to it in our constructor:

constructor(private apollo: Apollo) { }

The operation we are going to perform must match with what our resolver is expecting. This means that types must match exactly and our GraphQL must be well-formed. Since the task we are going to perform is an add operation, we are going to call the method that we use to add the data, Add:

Add(): void {
}

The add operation is going to trigger the Add mutation on the resolver we created on the server. We know that this accepts a TodoItemInput instance, so we need to transform our client-side model into a TodoItemInput instance, as follows:

const todo: ITodoItemInput = new TodoItemInput();
todo.Completed = false;
todo.Id = Guid.create.toString();
todo.CreationDate = new Date();
todo.Title = this.Title;
todo.Description = this.Description;
todo.DueDate = this.DueDate;

There is a bit in the preceding snippet that is unfamiliar to us, namely the Guid.create.toString() call. This command is responsible for creating a unique identifier known as a Globally Unique Identifier (GUID). A GUID is a 128-bit number that is externally represented in string and number format, which generally looks something like this—a14abe8b-3d9b-4b14-9a66-62ad595d4582. Since GUIDs are mathematically based to guarantee uniqueness, rather than having to call out to a central repository to get a unique value, they are quick to generate. Through the use of a GUID, we have given our todo item a unique value. We could have done this at the server if we needed to, but I chose to generate the entirety of the message on the client.

In order to use a GUID, we will use the guid-typescript component:

npm install --save guid-typescript

We can now put the code in place to transfer the data over to the GraphQL server. As I mentioned previously, we are going to be using the Add mutation, which tells us that we are going to be calling mutate on our apollo client:

this.apollo.mutate({
... logic goes here
})

The mutation is a specialist form of string that is covered by gql. If we can see what the entirety of this code looks like, we will be able to break it down immediately after:

this.apollo.mutate({
mutation: gql`
mutation Add($input: TodoItemInput!) {
Add(TodoItem: $input) {
Title
}
}
`, variables: {
input: todo
}
}).subscribe();

We already knew that we were going to call a mutation, so our mutate method accepts a mutation as MutationOption.

One of the parameters we can supply to MutationOption is FetchPolicy, which we could use to override the default options we set up when we created our Apollo link earlier.

The mutation uses gql to create the specially formatted query. Our query is broken down into two parts: the string text that tells us what the query is and any variables that we need to apply. The variables section creates an input variable that maps onto TodoItemInput, which we created previously. This is represented by $ inside our gql string, so any variable name must have a matching $variable in the query. When the mutation has completed, we tell it that we want the title back. We don't actually have to bring any values back, but when I was debugging earlier on, I found it useful to use the title to check whether we were getting a response from the server.

We are using the ` backtick because this lets us spread our input over multiple lines.

The mutate method is triggered from the call to subscribe. If we fail to supply this, our mutation will not run. As a convenience, I also added a Reset method so that we can clear values away from the UI when the user finishes. I did this so that the user would be able to immediately enter new values:

private Reset(): void {
this.Title = ``;
this.Description = ``;
this.DueDate = null;
}

That is the logic inside our component taken care of. What we need to do now is add the HTML that will be displayed in the component. Before we add any elements to our component, we want to display the card that will contain our display. This will be centered vertically and horizontally in the display. This is not something that comes naturally to Material, so we have to supply our own local styling. We have a couple of other styles that we are going to set as well, to fix the size of the text area and the width of the card, and to set how we display form fields to make sure each one appears on its own line.

Initially, we will set up a style to center the card. The card will be displayed inside a div tag, so we will apply the styling to the div tag, which will center the card inside it:

.centerDiv{
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

Now, we can style the Material card and form fields:

.mat-card {
width: 400px;
}
.mat-form-field {
display: block;
}

Finally, we are going to set the height of the textarea tag that the user will use to enter their description to 100 pixels:

textarea {
height: 100px;
resize: vertical;
}

Getting back to our display, we are going to set up the container for our card so that it is centered:

<div class="centerDiv" layout-fill layout="column" layout-align="center none">
.... content here
</div>

We have reached a point where we want to start leveraging the power of Angular to control the validation of the user input. In order to start treating user input as though it's all related, we are going to put the input parts of our display inside an HTML form:

<form name="form" (ngSubmit)="f.form.valid && Add()" #f="ngForm">
.... the form content goes here.
</form>

We need to break this form statement down a bit. We will start by working out what #f="ngForm" actually does. This statement assigns the ngForm component to a variable called f. When we use ngForm, we are referring to the component inside FormsModule (make sure that it's registered inside the app.module imports section). The reason that we do this is because this assignment means that we have access to properties of the component itself. The use of ngForm means that we are working with the top-level form group so that we can do things such as track whether or not the form is valid.

We can see this inside ngSubmit, where we are subscribing to the event that tells us that the user has triggered the form submission, which results in the validation being checked; when the data is valid, this results in triggering the Add method. With this in place, we don't have to directly call Add when the Save button is clicked because the submit event will take care of this for us.

There is a short-circuit logic in play with ngSubmit. In other words, if the form is not valid, then we won't call the Add method.

We are now ready to add the card itself. This lives entirely inside our form. The title section is placed inside a mat-card-title section and our buttons are situated inside the mat-card-actions section, which aligns the buttons at the bottom of the card. As we just covered, we aren't supplying a click event handler to our Save button because the form submission will take care of this:

<div layout="row" layout-align="center none">
<mat-card>
<mat-card-title>
<span class="mat-headline">Add ToDo</span>
</mat-card-title>
<mat-card-content>

.... content here.

<mat-card-content>
<mat-card-actions>
<button mat-button class="btn btn-primary">Save</button>
</mat-card-actions>
</mat-card>
</div>

We are ready to start adding the fields so that we can tie them back to the fields in our underlying model. We will start with the title as the description field largely follows this format as well. We will add the field and its related validation display in first, and then we will break down what is happening:

<mat-form-field>
<input type="text" matInput placeholder="Title" [(ngModel)]="Title" name="title" #title="ngModel" required />
</mat-form-field>
<div *ngIf="title.invalid && (title.dirty || title.touched)" class="alert alert-danger">
<div *ngIf="title.errors.required">
You must add a title.
</div>
</div>

The first part of our input element is largely self-explanatory. We created it as a text field and used matInput to hook the standard input so that it can be used inside mat-form-field. With this, we can set the placeholder text to something appropriate.

I opted to use [(ngModel)] instead of [ngModel] because of the way binding works. With [ngModel], we get one-way binding so that it changes flow from the underlying property through to the UI element that displays it. Since we are going to be allowing the input to change the values, we need a form of binding that allows us to send information back from the template to the component. In this case, we are sending the value back to the Title property in the element.

The name property must be set. If it is not set, Angular throws internal warnings and our binding will not work properly. What we do here is set the name and then use # with the value set in the name to tie it to ngModel. So, if we had name="wibbly", we would have #wibbly="ngModel" as well.

Since this field is required, we simply need to supply the required attribute, and our form validation will start working here.

Now that we have the input element hooked up to validation, we need some way of displaying any errors. This is where the next div statement comes in. The opening div statement basically reads as if the title is invalid (because it is required and has not been set, for instance), and it has either had a value changed in it or we have touched the field by setting focus to it at some point, then we need to display internal content using the alert and alert-danger attributes.

As our validation failure might just be one of several different failures, we need to tell the user what the problem actually was. The inner div statement displays the appropriate text because it is scoped to a particular error. So, when we see title.errors.required, our template will display the You must add a title. text when no value has been entered.

We aren't going to look at the description field because it largely follows the same format. I would recommend looking at the Git code to see how that is formatted.

We still have to add the DueDate field to our component. We are going to use the Angular date picker module to add this. Effectively, the date picker is made up of three parts.

We have an input field that the user can type directly into. This input field is going to have a min property set on it that binds the earliest date the user can select to the EarliestDate field we created in the code behind the component. Just like we did in the title field, we will set this field to required so that it will be validated by Angular, and we will apply #datepicker="ngModel" so that we can associate the ngModel component with this input field by setting the name with it:

<input matInput [min]="EarliestDate" [matDatepicker]="picker" name="datepicker" placeholder="Due date"
#datepicker="ngModel" required [(ngModel)]="DueDate">

The way that we associate the input field is by using [matDatepicker]="picker". As part of our form field, we have added a mat-datepicker component. We use #picker to name this component picker, which ties back to the matDatepicker binding in our input field:

<mat-datepicker #picker></mat-datepicker>

The final part that we need to add is the toggle that the user can press to show the calendar part on the page. This is added using mat-datepicker-toggle. We tell it what date picker we are applying the calendar to by using [for]="picker":

<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>

Right now, our form field looks like this:

<mat-form-field>
<input matInput [min]="EarliestDate" [matDatepicker]="picker" name="datepicker" placeholder="Due date"
#datepicker="ngModel" required [(ngModel)]="DueDate">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>

All that we are missing now is the validation. Since we have already defined that the earliest date we can choose is today, we don't need to add any validation to that. We have no maximum date to worry about, so all we need to do is check that the user has chosen a date:

<div *ngIf="datepicker.invalid && (datepicker.dirty || datepicker.touched)" class="alert alert-danger">
<div *ngIf="datepicker.errors.required">
You must select a due date.
</div>
</div>

So, we have reached the point where we can add tasks to our todo list and they will be saved to the database, but that isn't much use to us if we can't actually view them. We are now going to turn our attention to the AllTasksComponent and OverdueTasksComponent components.

Our AllTasksComponent and OverdueTasksComponent components are going to display the same information. All that differs between the two is the GQL call that is made. Because they have the same display, we are going to add a new component that will display the todo information. AllTasksComponent and OverdueTasksComponent will both use this component:

ng g c components/Todo-Card

Just like in our add task component, TodoCardComponent is going to start off with an EarliestDate field and the Apollo client being imported:

EarliestDate: Date;
constructor(private apollo: Apollo) {
this.EarliestDate = new Date();
}

We have reached the point where we need to consider what this component is actually going to be doing. It will receive a single ITodoItem as input from either AllTasksComponent or OverdueTasksComponent, so we will need a means for the containing component to be able to pass this information in. We will also need a means to notify the containing component of when the todo item has been deleted so that it can be removed from the tasks being displayed (we will just do this on the client side rather than triggering a requery via GraphQL). Our UI will add a Save button when the user is editing the record, so we are going to need some way to track that the user is in the edit section.

With those requirements for the component, we can add in the necessary code to support this. First, we are going to address the ability to pass in a value to our component as an input parameter. In other words, we are going to add a field that can be seen and has values set on it by using data binding by the containers. Fortunately, Angular makes this a very simple task. By marking a field with @Input, we expose it for data binding:

@Input() Todo: ITodoItem;

That takes care of the input, but how do we let the container know when something has happened? When we delete a task, we want to raise an event as output from our component. Again, Angular makes this simple by using @Output to expose something; in this case, we are going to expose EventEmitter. When we expose this to our containers, they can subscribe to the event and react when we emit the event. When we create EventEmitter, we are going to create it to pass the Id of our task back, so we need EventEmitter to be a string event:

@Output() deleted: EventEmitter<string> = new EventEmitter<string>();

With this code in place, we can update our AllTasksComponent and OverdueTasksComponent templates that will hook up to our component:

<div fxLayout="row wrap" fxLayout.xs="column" fxLayoutWrap fxLayoutGap="20px grid" fxLayoutAlign="left">
<atp-todo-card
*ngFor="let todo of todos"
[Todo]="todo"
(deleted)="resubscribe($event)"></atp-todo-card>
</div>

Before we finish adding the logic to TodoCardComponent, let's get back to AllTasksComponent and OverdueTasksComponent. Internally, these are both very similar, so we will concentrate on the logic in OverdueTasksComponent.

It shouldn't come as a shock now that these components will accept an Apollo client in the constructor. As we saw from ngFor previously, our component will also expose an array of ITodoItem called todos, which will be populated by our query:

todos: ITodoItem[] = new Array<ITodoItem>();
constructor(private apollo: Apollo) { }

You may notice, from looking at the code in the repository, that we have not added this code into our component. Instead, we are using a base class called SubscriptionBase that provides us with a Subscribe method and a resubscribe event.

Our Subscribe method is generic accepts either OverdueTodoItemQuery or TodoItemQuery as the type, along with a gql query, and returns an observable that we can subscribe to in order to pull out the underlying data. The reason we have added the base class goes back to the fact that AllTasksComponent and OverdueTasksComponent are just about identical, so it makes sense to reuse as much code as possible. The name that is sometimes given to this philosophy is Don't Repeat Yourself (DRY):

protected Subscribe<T extends OverdueTodoItemQuery | TodoItemQuery>(gqlQuery: unknown): Observable<ApolloQueryResult<T>> {
}

All this method does is create a query using gql and set fetch-policy to no-cache to force the query to read from the network rather than relying on the cache set in app-module. This is just another way of controlling whether or not we read from the in-memory cache:

return this.apollo.query<T>({
query: gqlQuery,
fetch-policy: 'no-cache'
});

We extend from a choice of two interfaces because they both expose the same items but with different names. So, OverdueTodoItemQuery exposes OverdueTodoItems and TodoItemQuery exposes TodoItems. The reason that we have to do this, rather than using just one interface, is because the field must match the name of the query. This is because Apollo client uses this to automatically map results back.

The resubscribe method is called after the user clicks the delete button in the interface (we will get to building up the UI template shortly). We saw that our resubscribe method was wired up to the event and that it would receive the event as a string, which would contain the Id of the task we want to delete. Again, all we are going to do to delete the record is find the one with the matching Id, and then splice the todos list to remove it:

resubscribe = (event: string) => {
const index = this.todos.findIndex(x => x.Id === event);
this.todos.splice(index, 1);
}

Going back to OverdueTasksComponent, all we need to do is call subscribe, passing in our gql query and subscribing to the return data. When the data comes back, we are going to populate our todos array, which will be displayed in the UI:

ngOnInit() {
this.Subscribe<OverdueTodoItemQuery>(gql`query ItemsQuery {
OverdueTodoItems {
Id,
Title,
Description,
DaysCreated,
DueDate,
Completed
}
}`).subscribe(todo => {
this.todos = new Array<ITodoItem>();
todo.data.OverdueTodoItems.forEach(x => {
this.todos.push(x);
});
});
}
A note on our subscription—as we are creating a new list of items to display, we need to clear this.todos before we start pushing the whole list back into it.

With AllTasksComponent and OverdueTasksComponent complete, we can turn our attention back to TodoCardComponent. Before we finish off adding the component logic, we really need to take a look at the way the template is created. A large part of the logic is similar to the add task UI logic, so we aren't going to worry about how to hook up to a form or add a validation. The things I want to concentrate on here relate to the fact that the task component will display differently when the user is in edit mode, as opposed to a read-only or label-based version. Let's start by looking at the title. When the task is in read-only mode, we are just going to display the title in span, like this:

<span>{{Todo.Title}}</span>

When we are editing the task, we want to show input elements and validation, as follows:

<mat-form-field>
<input type="text" name="Title" matInput placeholder="Title" [(ngModel)]="Todo.Title" #title="ngModel"
required />
</mat-form-field>
<div *ngIf="title.invalid && (title.dirty || title.touched)" class="alert alert-danger">
<div *ngIf="title.errors.required">
You must add a title.
</div>
</div>

We do this by using a neat trick of Angular. Behind the scenes, we are maintaining an InEdit flag. When that is false, we want to display the span. If it is true, we want to display a template in its place that contains our input logic. To do this, we start off by wrapping our span inside a div tag. This has an ngIf statement that is bound to InEdit. The ngIf statement contains an else clause that picks up the template with the matching name and displays this in its place:

<div *ngIf="!InEdit;else editTitle">
<span>{{Todo.Title}}</span>
</div>
<ng-template #editTitle>
<mat-form-field>
<input type="text" name="Title" matInput placeholder="Title" [(ngModel)]="Todo.Title" #title="ngModel"
required />
</mat-form-field>
<div *ngIf="title.invalid && (title.dirty || title.touched)" class="alert alert-danger">
<div *ngIf="title.errors.required">
You must add a title.
</div>
</div>
</ng-template>

Other fields are displayed in a similar way. There is one more point of interest in the way we display the read-only fields. DueDate needs to be formatted in order to be displayed as a meaningful date rather than as the raw date/time that is saved in the database. We use | to pipe DueDate into a special date formatter that controls how the date is displayed. For instance, March 21, 2018 would be displayed as Due: Mar 21st, 2019 using the following date pipe:

<p>Due: {{Todo.DueDate | date}}</p>

Please take the time to review the rest of todo-card.component.html. Swapping templates is heavily done, so it is a good way to review how to make the same UI serve two purposes.

In the component itself, we have three operations left to look at. The first one that we will cover is the Delete method, which is triggered when the user presses the delete button on the component. This is a simple method that calls the Remove mutation, passing the Id across to be removed. When the item has been removed from the server, we call emit on our deleted event. This event passes the Id back to the containing component, which results in this item being removed from the UI:

Delete() {
this.apollo.mutate({
mutation: gql`
mutation Remove($Id: String!) {
Remove(Id: $Id)
}
`, variables: {
Id: this.Todo.Id
}
}).subscribe();
this.deleted.emit(this.Todo.Id);
}

The Complete method is just as simple. When the user clicks the Complete link, we call the Complete query, which passes across the current Id as the matching variable. As we could be in edit mode at this point, we call this.Edit(false) to switch back to read-only mode:

Complete() {
this.apollo.mutate({
mutation: gql`
mutation Complete($input: String!) {
Complete(Id: $input)
}
`, variables: {
input: this.Todo.Id
}
}).subscribe();
this.Edit(false);
this.Todo.Completed = true;
}

The Save method is very similar to the Add method in the add task component. Again, we need to switch back from edit mode when this mutation finishes:

Save() {
const todo: ITodoItemInput = new TodoItemInput();
todo.Completed = false;
todo.CreationDate = new Date();
todo.Title = this.Todo.Title;
todo.Description = this.Todo.Description;
todo.DueDate = this.Todo.DueDate;
todo.Id = this.Todo.Id;
this.apollo.mutate({
mutation: gql`
mutation Update($input: TodoItemInput!) {
Update(TodoItem: $input)
}
`, variables: {
input: todo
}
}).subscribe();

this.Edit(false);
}

At this point, we have a fully functioning client- and server-based GraphQL system.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.189.177