Setting milestones

Tracking time is important. I don't know how you feel about time, but I really suck at organizing my time. Although a lot of people ask me how I manage to do so many things, I believe I'm actually very bad at managing how I get these things done. If I were a better organizer, I could get things done with much less energy involved.

One thing that always helps me organize myself is to break things down into smaller work packages. Users that organize themselves with our task management application can already do this by creating tasks in projects. While a project is the overall goal, we can create smaller tasks to achieve this goal. However, sometimes we tend to lose sight of the overall goal when we're only focused on tasks.

Milestones are a perfect glue between projects and tasks. They make sure that we bundle tasks together into larger packages. This will help us a lot in organizing our tasks, and we can look at milestones of the project to see the overall project health. However, we can still focus on tasks when we work in the context of a milestone.

In this section, we will create the necessary components in order to add basic milestone functionality to our application.

To implement milestone functionality in our application, we will stick to the following design decisions:

  • Milestones should be stored on the project level, and tasks can contain an optional reference to a project milestone.
  • To keep things simple, the only interaction point with milestones should be on task level. Therefore, creation of milestones will be done on task level, although the created milestones will be stored on project level.
  • Milestones currently only consist of a name. There are a lot more to milestones that we can potentially build into our system, such as deadlines, dependencies, and other nice things. However, we will stick to the bare minimum, which is a milestone name.

Creating an autocomplete component

In order to keep the management of milestones simple, we will create a new user interface component to deal with the design concerns that we listed. Our new autocomplete component will not only display possible values to select from, but it will also allow us to create new items. We can then simply use this component on our ProjectTaskDetail component in order to manage milestones.

Let's look at the Component class of our new autocomplete component that we will create in the lib/ui/auto-complete/auto-complete.js file:

import {Editor} from '../editor/editor';

@Component({
  selector: 'ngc-auto-complete',
  …
  directives: [Editor]
})
export class AutoComplete {
  @Input() items;
  @Input() selectedItem;
  @Output() selectedItemChange = new EventEmitter();
  @Output() itemCreated = new EventEmitter();
  …
}

Once again, our Editor component can be reused to create this higher component. We're lucky that we created such a nice component, as this saved us a lot of time throughout this project.

Let's look at the input and output properties of the AutoComplete component in more detail:

  • items: This is where we expect an array of strings. This will be the list of items a user can choose from when typing into the editor.
  • selectedItem: This is when we make the selected item an input property to actually make this component pure, and we can rely on the parent component to set this property right.
  • selectedItemChange: This output property will emit an event if the selected item was changed. As we create a pure component here, we somehow need to propagate the event of an item that was selected in the autocomplete list.
  • itemCreated: This output property will emit an event if a new item was added to the autocomplete list. Updating the list of items and changing the component items input property will still be the responsibility of the parent component.

Let's add more code to our component. We use an Editor component as main input source. While our users will type into the editor, we filter the available items using the text input of the editor. Let's create a filterItems for this purpose:

filterItems(filter) {
  this.filter = filter || '';
  this.filteredItems = this.items
    .filter(
      (item) => item
        .toLowerCase()
        .indexOf(this.filter.toLowerCase().trim()) !== -1)
    .slice(0, 10);
  this.exactMatch = this.items.includes(this.filter);
}

The filterItems method has a single parameter, which is the text that we want to use in order to search for relevant items in our list.

Let's look at the content of the method in more detail:

  • For later use in our template, we will set aside the filter query that was used the last time this method was called
  • In the filteredItems member variable, we will store a filtered version of the item list by searching for text occurrences of the filter string
  • As a last step, we also store the information if the search query resulted in an exact match of an item in our list

Now, we need to make sure that if the items or selectedItem input properties change, we also execute our filter method again. For this, we simply implement the ngOnChanges lifecycle hook:

ngOnChanges(changes) {
  if (this.items && this.selectedItem) {
    this.filterItems(this.selectedItem);
  }
}

Let's now see how we deal with the events provided by the Editor component:

onEditModeChange(editMode) {
  if (editMode) {
    this.showCallout = true;
    this.previousSelectedItem = this.selectedItem;
  } else {
    this.showCallout = false;
  }
}

If the editor changes to edit mode, we want to save the previously selected item. We'll need this if the user decides to cancel his edits and switch back to the previous item. Of course, this is also the point where we need to display the autocomplete list to the user.

On the other hand, if the edit mode is switched back to read mode, we want to hide the autocomplete list again:

onEditableInput(content) {
  this.filterItems(content);
}

The editableInput event is triggered by our editor on every editor input change. The event provides us with the text content that was entered by the user. If such an event occurs, we need to execute our filter function again with the updated filter query:

onEditSaved(content) {
  if (content === '') {
    this.selectedItemChange.next(null);
  } else if (content !== this.selectedItem && 
             !this.items.includes(content)) {
    this.itemCreated.next(content);
  }
}

When the editSaved event is triggered by our editor, we need to decide whether we should do either of the following:

  • Emit an event using the selectedItemChange output property if the saved content is an empty string to signal the removal of a selected item to the parent component
  • Emit an event using the itemCreated output property if valid content is given and our list does not include an item with that name to signal an item creation:
    onEditCanceled() {
      this.selectedItemChange.next(this.previousSelectedItem);
    }

On the editCanceled event of the Editor component, we want to switch back to the previous selected item. For this, we can simply emit an event using the selectedItemChange output property and the previousSelectedItem member that we put aside after the editor was switched into edit mode.

These are all the binding functions that we will use to wire up our editor and in order to attach the autocomplete functionality to it.

There are two more rather simple methods that we will create before we take a look at the template of our autocomplete component:

selectItem(item) {
  this.selectedItemChange.next(item);
}

createItem(item) {
  this.itemCreated.next(item);
}

We will use these two for the click actions in the autocomplete callout from our template. Let's take a look at the template so that you can see all the code that we just created in action:

<ngc-editor [content]="selectedItem"
            [showControls]="true"
            (editModeChange)="onEditModeChange($event)"
            (editableInput)="onEditableInput($event)"
            (editSaved)="onEditSaved($event)"
            (editCanceled)="onEditCanceled($event)"></ngc-editor>

First, the Editor component is placed and all necessary bindings to the handler methods that we created in our Component class are attached.

Now, we will create the autocomplete list that will be displayed as a callout to the user right next to the editor input area:

<ul *ngIf="showCallout" class="auto-complete__callout">
  <li *ngFor="let item of filteredItems"
      (click)="selectItem(item)"
      class="auto-complete__item"
      [class.auto-complete__item--selected]="item === selectedItem">{{item}}</li>
  <li *ngIf="filter && !exactMatch"
      (click)="createItem(filter)"
      class="auto-complete__item auto-complete__item--create">Create "{{filter}}"</li>
</ul>

We rely on the showCallout member set by the onEditModeChange method of our Component class to signal if we should display the autocomplete list or not.

We then iterate over all filtered items using the NgFor directive and render the text content of each item. If one of the items gets clicked on, we will call our selectItem method with the concerned item as the parameter value.

As the last list element, after the repeated list items, we conditionally display an additional list element in order to create a nonexisting milestone. We only display this button if there's a valid filter already and if there's no exact match of the filter to an existing milestone:

Creating an autocomplete component

Our milestone component plays nicely together with the editor component using a clean composition

Now that we are all done with our autocomplete component, the only thing left to do in order to manage project milestones is to make use of it in the ProjectTaskDetails component.

Let's open the Component class located in lib/project/project-task-details/project-task-details.js and apply the necessary modifications:

import {AutoComplete} from '../../ui/auto-complete/auto-complete';

@Component({
  selector: 'ngc-project-task-details',
  …
  directives: […, AutoComplete]
})
export class ProjectTaskDetails {
  constructor(@Inject(forwardRef(() => Project)) project, {
    …
    this.projectChangeSubscription = this.project.document.change.subscribe((data) => {
      …
      this.projectMilestones = data.milestones || [];
    });
  }
  …
  onMilestoneSelected(milestone) {
    this.task.milestone = milestone;
    this.project.document.persist();
  }

  onMilestoneCreated(milestone) {
    this.project.document.data.milestones = this.project.document.data.milestones || [];
    this.project.document.data.milestones.push(milestone);
    this.task.milestone = milestone;
    this.project.document.persist();
  }
  …
}

In the subscription to project changes, we now also extract any preexisting project milestones and store them in a projectMilestones member variable. This makes it easier to reference in the template.

The onMilestoneSelected method will be bound to the selectItemChange output property of the AutoComplete component. We use the emitted value of the AutoComplete component to set our tasks milestone and persist the LiveDocument project using its persist method.

The onMilestoneCreated method will be bound to the itemCreated output property of the AutoComplete component. On such an event, we add the created milestone to the projects milestone list as well as assign the current task to the created milestone. After updating the LiveDocument data, we use the persist method to save all changes.

Let's look into lib/project/project-task-details/project-task-details.html to see the necessary changes in our template:

…
<div class="task-details__content">
  …
  <ngc-auto-complete [items]="projectMilestones"
                     [selectedItem]="task?.milestone"
               (selectedItemChange)="onMilestoneSelected($event)"
               (itemCreated)="onMilestoneCreated($event)">
  </ngc-auto-complete>
</div>

Besides the output property bindings that you're already aware of, we also create two input bindings for the items and selectedItem input properties of the AutoComplete component.

This is already it. We created a new UI component that provides autocompletion and used that component to implement milestone management on our tasks.

Isn't it nice how easy it suddenly seems to implement new functionality when using components with proper encapsulation? The great thing about component-oriented development is that your development time for new functionality decreased with the amount of reusable components that you already created.

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

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