Search code examples
angularasynchronousangular5angular-services

Angular 5 | ASYNC, conflict in multiple data binding 1 CRUD service


I created a "Master CRUD Service".

Its failing when i use it in 2 different components to GET data from a passed URL and use | async

When data is retrieved in 1 component the other component changes to that stream

Each components has a declared Observable but with different names so why would this happen?

Component1 HTML

<kendo-grid [data]="component1| async" />

Component1 request

export class Component1 implements OnInit {
  public component1: Observable<GridDataResult>;
  public gridState: State = {
    skip: 0,
    take: 100
  };

  constructor(private fetchDataEditService: FetchDataEditService) {
  }

  public requestClick(): void {
    this.component1 = this.fetchDataEditService.map((data: any) =>
      (<GridDataResult>{
        data: data.results,
        total: data.count,
      })
    )

    this.fetchDataEditService.read(this.url, this.gridState);
  }

Component2 HTML

<kendo-grid [data]="component2| async" />

Component2 request

export class Component2 implements OnInit {
  public component2: Observable<GridDataResult>;
  public gridState: State = {
    skip: 0,
    take: 100
  };

  constructor(private fetchDataEditService: FetchDataEditService) {
  }

  public requestClick(): void {
    this.component2 = this.fetchDataEditService.map((data: any) =>
      (<GridDataResult>{
        data: data.results,
        total: data.count,
      })
    )

    this.fetchDataEditService.read(this.url, this.gridState);
  }

fetchDataEditService

@Injectable()
export class FetchDataEditService extends BehaviorSubject<any[]> {
  constructor(private http: HttpClient) {
    super([]);
  }

  public data: any[] = [];

  public read(url: string, state: DataSourceRequestState = {}) {

    const data = [];

    this.fetch('', url, state)
      .do((data: any) =>
        (<GridDataResult>{
          data: data.results,
          total: data.count,
        })
      )
      .subscribe((data: any) => {
        super.next(data);
      });

  };

  public save(url: string, state: DataSourceRequestState, data: any, isNew?: boolean) {
    const action = isNew ? CREATE_ACTION : UPDATE_ACTION;

    this.reset();

    this.fetch(action, url, state, data)
      .subscribe(() => this.read(url, state), () => this.read(url, state));
  }

  public remove(url: string, state: DataSourceRequestState, data: any) {
    this.reset();
    this.fetch(REMOVE_ACTION, url, state, data)
      .subscribe(() => this.read(url, state), () => this.read(url, state));
  }

  public resetItem(url: string, dataItem: any) {
    if (!dataItem) { return; }

    const originalDataItem = this.data.find(item => item.djangoid === dataItem.djangoid);

    Object.assign(originalDataItem, dataItem);

    super.next(this.data);
  }

  private reset() {
    this.data = [];
  }

  private fetch(action: string = '', url: string = '', state?: DataSourceRequestState, data?: any): Observable<GridDataResult> {

    const queryStr = `${toDataSourceRequestString(state)}`;

    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    }

    if (action == "") {
      return this.http
        .get<GridDataResult>(`${API_URL + url}/?${queryStr}`)
        .retry(3).pipe(catchError(this.handleError))
    }

    if (action == "create") {
      return this.http
        .post(API_URL + url + '/', data, httpOptions)
        .pipe(catchError(this.handleError))
    }

    if (action == "update") {
      alert
      return this.http
        .put(API_URL + url + '/' + data.djangoid + '/', data, httpOptions)
        .pipe(catchError(this.handleError))
    }

    if (action == "destroy") {
      return this.http
        .delete(API_URL + url + '/' + data.djangoid + '/', httpOptions)
        .pipe(
          tap(res => console.log(`deleted id=${data.djangoid}`)),
          catchError(this.handleError))
    }
    throw Error('Internal REST Command Error')

  }
  private handleError(error: Response) {
    console.error(error);
    return Observable.throw(error.json() || 'Server Error');
  }

}

Any Ideas

Thanks

EDIT 1 - In response to where is the service provided

I provide the service in a DataModule which i import into a CoreModule and finally import this into my AppModule

Data.module.ts

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';

import { FetchDataEditService } from './edit.service';
import { SeniorSandpitService } from './seniorsandpit.service';
import { UtilsService } from './utils.service';

const SERVICES = [
  FetchDataEditService,
  SeniorSandpitService,
  UtilsService,
];

@NgModule({
  imports: [
    CommonModule,
  ],
  providers: [
    ...SERVICES,
  ],
})
export class DataModule {
  static forRoot(): ModuleWithProviders {
    return <ModuleWithProviders>{
      ngModule: DataModule,
      providers: [
        ...SERVICES,
      ],
    };
  }
}

Core.Module.ts

import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { throwIfAlreadyLoaded } from './module-import-guard';
import { DataModule } from './data/data.module';
import { AnalyticsService } from './utils/analytics.service';

const NB_CORE_PROVIDERS = [
  ...DataModule.forRoot().providers,
  AnalyticsService,
];

@NgModule({
  imports: [
    CommonModule,
  ],
  exports: [
  ],
  declarations: [],
})
export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    throwIfAlreadyLoaded(parentModule, 'CoreModule');
  }

  static forRoot(): ModuleWithProviders {
    return <ModuleWithProviders>{
      ngModule: CoreModule,
      providers: [
        ...NB_CORE_PROVIDERS,
      ],
    };
  }
}

Solution

  • I think your problem lies in the fact that both of your components are sharing the exact same instance of your service. This normally wouldn't be a problem, however your service is a subject in itself, so any calls that result in data being broadcast out will be seen by both components. This is because services in Angular are singletons at the spot where they are provided (unless you register a factory to create them).

    I have a feeling you wouldn't see this behavior if you provided the service in the configuration object for each of your components as they would be separate subjects then.

    @Component({
        ...
        providers: [FetchDataEditService]
    })
    

    I would recommend taking a look at the Angular docs on Dependency Injection.