Angular Behavior Subject in service is getting messed up when initializing two of the same components

601 Views Asked by At

I created a component which is called sidebar. That component has let's say 2 inputs, sideBarMode and sideBarSide. In the app.component I add this component like this <sidebar [sideBarMode]="'sliding'" [sideBarSide]="'right'"></sidebar>.

When I use the above as one tag, it works as expected. However, when I do something like

<div>
<sidebar [sideBarMode]="'sliding'" [sideBarSide]="'right'"></sidebar>
</div>

<div>
<sidebar [sideBarMode]="'docked'" [sideBarSide]="'left'"></sidebar>
</div>

They are rendered but with the same classes, although if I will console log the values in ngOnInit which are being passed, they show correctly. E.G one div should show sliding as a class, and the other one as docked. Instead, they show as docked both.

This is the HTML template.

<div class="container-fluid">
<div [ngClass]="barSliding ? 'sliding' : 'sidebar-docked'">
    <div class="row k-sidebar {{theme}}"
        [ngClass]="[sidebarLeft ? 'position-left' : 'position-right', barSliding ? 'sliding-content-bar' : 'sidebar-docked', isDesktopWidth.value ? 'largeView' : 'mobileView']"
        [@slideAnimation]= 'animationState'
        [@dockMobileSidebar]= 'animationStateMobile'
        [ngStyle]="{'width.px': isDesktopWidth.value ? sideBarWidth : '' }"
        >
        <div class="col-md-12">
            <ng-container *ngIf="sidebarTemplate">
                <ng-container *ngTemplateOutlet="sidebarTemplate">

                </ng-container>
            </ng-container>

            <ng-container *ngIf="!sidebarTemplate">
                <p>This is a paragraph, no template has been passed</p>
            </ng-container>
        </div>
</div>
</div>

This is how I check the mode, wether it is docked or not in TS file and I call this method from the ngOnInit.

 private checkSidebarMode(mode) {
console.log('checkSidebarMode ', mode);
if (mode === 'sliding') {
  this.sidebarService.sideBarSliding.next(true);
  console.log('checkSidebarMode ', this.sidebarService.sideBarSliding.getValue());
  this.sideBarDock.next(false);
} else if (mode === 'docked' && this.sidebarService.innerWidth.getValue() <= 767) {
  this.sideBarDock.next(false);
  this.sidebarService.sideBarSliding.next(true);
} else if (mode === 'docked' && this.sidebarService.innerWidth.getValue() > 767) {
  this.sideBarDock.next(true);
  this.sidebarService.sideBarSliding.next(false);
  this.sidebarService.animationState.next('docked');
  console.log('checkSidebarMode ', this.sidebarService.sideBarSliding.getValue());
}
  }

As you can see, if it is sliding, I set the observable to true, and then In the ngOninit I assign that value to barSliding, which is used in the HTML template to determine the class name.

The docked one is correct, however, the sliding one is getting the docked properties...why?

enter image description here

1

There are 1 best solutions below

2
On

As your BehaviorSubject is in the sidebarService, this one is probably providedIn: 'root' which makes it a singleton. Thus, your BehaviorSubject is the same for the two instances of your component.

You have multiple solutions:

  • declare a BehaviorSubject directly in your component instead of using a shared BehaviorSubject in a service. I would update it by calling next() in a ngOnChanges method as soon as the mode input changes.
@Component({...})
export class MyComponent implements OnInit {
    @Input() mode: 'sliding' | 'docked' = 'sliding';

    private subject = new BehaviorSubject<string>(null);

    ngOnChanges() { 
        // here you should check that 'mode' changed before calling next()
        this.subject.next(this.mode);
    }
}
  • use a factory method in your service that returns a new BehaviorSubject instance to be used by your component. Maybe there is no real benefit over the first solution if it's a simple BehaviorSubject with no logic attached.
// service 

@Injectable()
export class MyService {

    public getNewBehaviorSubject() {
        return new BehaviorSubject<String>(null);
    }
}

// component

@Component({...})
export class MyComponent implements OnInit {
    @Input() mode: 'sliding' | 'docked' = 'sliding';

    private subject = this.myService.getNewBehaviorSubject();

    ngOnChanges() { 
        // here you should check that 'mode' changed before calling next()
        this.subject.next(this.mode);
    }
}

But this make really sense only if the BehaviorSubject you return from the service must use other information from the service (other data, static variables like size of the screen, etc).

  • make your service not a singleton by providing it at component's level instead of module or root level. But you may prefer the other solutions as having non-singletons services may be a little more confusing and not exactly what you want. More info on this Bear in mind that Angular's services are designed to be singletons by default. It's the most common use case.

One question you should ask yourself is the following: does this behavior should be shared among components or known by other component? In this case you can delegate it to a global singleton service. Example: every component may need to know at one point if the app's main sliding menu is opened or not. Or, does this behavior is specific to a component's instance? In this case the state should be stored in the component instance itself. Example: I have two collapsible elements, each must keep track of either it is collapsed or not, but no other component needs to know about this state.

There are many strategies you can adopt, and there is no answer that can fit all situations.