7

Angular 4. Github source

I have a menu which is filled by a web service. The web service is in taskService, but is not necessary now.

ngOnInit() {
    this.getTasks();
    }
    getTasks(): void {
        this.taskService.getTasks()
            .subscribe(Tasks => this.tasks = Tasks);
    } 

When you click on a task it loads a page, a different component, with a form ready to update the data. It is also made by a web service and works fine. The problem is that after update the task, it is not reflected in the task menu

I am importing this:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';

and adding this to the constructor:

private cdRef: ChangeDetectorRef

And this is my best approach to the detectChanges() function, after update the data with the save() function

  this.taskService.updateTask(task, id)
          .subscribe(
              this.Ref.detach();
              setInterval(() => {
                this.Ref.detectChanges();
              }, 5000);
      );

This is the html from the menu to print the tasks:

   <li *ngFor="let task of tasks" class="d-inline-block col-md-12">
        <a routerLink="/task/{{task.id}}" > {{task.title}}</a>
        <!-- <span class="close big"></span> -->
        <button class="close big" title="delete task"
        (click)="delete(task)">x</button>
    </li>

And this is the form that updates the task

<form (ngSubmit)="save(taskName.value, taskBody.value)" #taskForm="ngForm" class="example-form">
  <mat-form-field class="example-full-width">
    <label>Task Name</label>
    <input matInput [(ngModel)]="task.name" #taskName name="name">
  </mat-form-field>

  <mat-form-field class="example-full-width">
    <textarea matInput [(ngModel)]="task.body" #taskBody name="body"></textarea>

  </mat-form-field>
  <button type="submit" class="btn btn-success" >Save</button>
</form>

Both are in different components.

I have tried to follow this tutorial, but I'm stuck, I don't know how to use the ChangeDetectorRef.

ValRob
  • 2,584
  • 7
  • 32
  • 40
  • Can you post your html file too? – Bunyamin Coskuner Dec 20 '17 at 19:35
  • done @BunyaminCoskuner – ValRob Dec 20 '17 at 19:40
  • I'm trying to understand why you would need to trigger `changeDetection` manually. Angular should be able to pick it up when you update `tasks`. Do you set `changeDetectionStrategy` in your `Component` declaration? – Bunyamin Coskuner Dec 20 '17 at 19:45
  • Have you confirmed that `this.tasks` in your subscribe callback is being redefined with the updated data from the form submission? – bazzells Dec 20 '17 at 19:46
  • @BunyaminCoskuner no, nothing of that, i have to it manually, cuz they are in different components. Look here is the full [project:](https://github.com/josuevalrob/tasks/tree/master/src/app) – ValRob Dec 20 '17 at 19:46
  • Oh, okay now I see the problem. I'll try to come up with a solution. – Bunyamin Coskuner Dec 20 '17 at 19:49
  • After running the `updateTask()` in the view.task component, I did a `console.log` there, in the menu component, to see if something change, but no, my console log show me old data. @bazzells – ValRob Dec 20 '17 at 19:49
  • Ok so after `updateTask()` finishes, it's returned to `save()` in your `ViewTaskComponent`, but there is no link to `this.task` in the subscription callback to `this.taskService.updateTask()`. Since `updateTask` uses a PATCH request, I'm assuming you're not getting the whole `Task` object back. So you won't be able to just say `this.task = valuePassedToSubscribeCallback`. Instead you can invoke `this.viewTask()` in that subscribe callback and fetch the entire updated `Task` object. – bazzells Dec 20 '17 at 20:07
  • @ValRob I posted a possible solution. Check it out. – Bunyamin Coskuner Dec 20 '17 at 20:18
  • thanks @BunyaminCoskuner i will test it now! – ValRob Dec 20 '17 at 20:42
  • where to I put the invoke `this.viewTask()` ? in the `save()` function, inside the `updatetask`? @bazzells – ValRob Dec 20 '17 at 20:57
  • Yup. Just think of it as the following flow: (1) submit form (2) make PATCH request (3) fetch updated data _once_ PATCH finishes. Only place to know PATCH has finished is inside the callback for the subscription to `updateTask`. – bazzells Dec 20 '17 at 21:01

2 Answers2

6

I've looked at your code. The problem is that the view-task.component updates your tasks, but the navigation.component is not notified about this transaction. I think BehaviorSubject might be just the thing for you.

You can read more about it here

I assume you would have single array of tasks throughout your application and you would display them on your navigation component.

Task.service.ts

export class TaskService {
     // behaviorSubject needs an initial value.
     private tasks: BehaviorSubject = new BehaviorSubject([]);
     private taskList: Task[];

     getTasks() {
         if (!this.taskList || this.taskList.length === 0) {
             this.initializeTasks();
         }

         return this.tasks.asObservable();
     }

     initializeTasks() {
          this.http.get('api/tasks')
              .subscribe(tasks => {
                   // next method will notify all of the subscribers
                   this.tasks.next(tasks);
              }, error => {
                   // proper error handling here
              });
     }

     updateTasks(task: Task) {
          this.http.post('api/updateTask')
              .subscribe(resp => {
                   // update your tasks array
                   this.tasks = ...
                   // and call next method of your behaviorSubject with updated list
                   this.tasks.next(this.tasks);
              }, error => {
                   // proper error handling here    
              });
     }
}

Navigation.component.ts

 export class NavigationComponent implements OnInit{
      tasks: Task[];
      constructor(private taskService: TaskService) {}

      ngOnInit() {
          // this method will be called every time behaviorSubject
          // emits a next value.
          this.taskService.getTasks()
              .subscribe(tasks => this.tasks = tasks);
      }
 }

View-task.component.ts

 export class ViewTaskComponent {
     constructor(private taskService: TaskService) {}

     updateTask(task: Task) {
         this.taskService.updateTask(task);
     }
 }

I haven't tried this code myself. However, I have implemented something similar on my application before. So when you try it and have a problem, let me know.

Bunyamin Coskuner
  • 8,719
  • 1
  • 28
  • 48
  • I have a diferente get http request, it give a problem: `initializeTasks(): Observable { const url = `${this.mainUrl}/tasks`; const returnGets = this.http.get(url) .subscribe (tasks => { this.tasks.next(tasks); }); return returnGets }` Said that the type **'subscription'** is not assignable to type **`'observable`'**. Property `'_isScalar'` is missing in the subcription – ValRob Dec 21 '17 at 09:51
  • You are defining your return type as `Observable`, but you are subscribing to the http request. If you want to return an Observable, you cannot subscribe to it. If you just want to get the data and assign it to some attribute, you should subscribe to it but can not define your return type as `Observable`. You should do one thing or the other. – Bunyamin Coskuner Dec 21 '17 at 09:56
  • Okay, thanks it works, but with a couple of things different: [service.ts](https://github.com/josuevalrob/tasks/blob/master/src/app/task.service.ts) , [view-task.ts](https://github.com/josuevalrob/tasks/blob/master/src/app/view-task/view-task.component.ts) and the [navitation.ts](https://github.com/josuevalrob/tasks/blob/master/src/app/navigation/navigation.component.ts). – ValRob Dec 21 '17 at 11:01
  • This is my `updatetask()` now: `updateTask (task: Task, id){ const url = ${this.mainUrl}/node/${id}; return this.http.patch(url, task, httpHaljson) .subscribe(resp => { this.initializeTasks() }); }` – ValRob Dec 21 '17 at 11:01
  • I'm glad it works, just one final note. You don't need to return `Subscription`, probably no one is gonna do anything with it – Bunyamin Coskuner Dec 21 '17 at 12:11
3

Angular isn't running changeDetection because you aren't asking it to. None of the instance variables in your ViewTaskComponent are being updated inside your save method.

In your code, after updateTask() finishes, it returns to save() in your ViewTaskComponent. But there is no link to this.task in the subscription callback to this.taskService.updateTask().

Since updateTask uses a PATCH request, I'm assuming you're not getting the whole Task object back. So you won't be able to just say this.task = valuePassedToSubscribeCallback.

Instead you can invoke this.viewTask() in that subscribe callback and fetch the entire updated Task object.

For example:

this.taskService.updateTask(task, id)
    .subscribe(
        // new line here to fetch the updated Task object
        this.viewTask()
    );

Note: I'd second @Bunyamin Coskuner's suggestion to utilize BehaviorSubject's (or even a standard Subject). If you're up for a refactor, they are a clean way to manage state within your services.

bazzells
  • 329
  • 2
  • 8
  • I dont know how to log this, but i got an error `StaticInjectorError[NavigationComponent]: NullInjectorError: No provider for NavigationComponent!` – ValRob Dec 20 '17 at 21:02
  • That's thrown because on line 32 of view-task.component.ts you've initialized your `NavigationComponent` private variable in your class. You shouldn't need to though. Is there a reason you have that in there? Try commenting it out at least and seeing if it runs. – bazzells Dec 20 '17 at 21:08
  • done, but it doesn't work. I don't get anything after `save()` function. :( – ValRob Dec 20 '17 at 21:18
  • Are you referring to the `NavigationComponent` not updating? In order for that to work I'd recommend implementing a `Subject` or `BehaviorSubject`. – bazzells Dec 20 '17 at 21:34