Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Provide Component Lifecycle Hooks as Observables #10185

Open
Jeff-Cortese opened this issue Jul 20, 2016 · 40 comments
Open

Proposal: Provide Component Lifecycle Hooks as Observables #10185

Jeff-Cortese opened this issue Jul 20, 2016 · 40 comments
Assignees
Labels
area: core Issues related to the framework runtime core: lifecycle hooks cross-cutting: observables feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature freq2: medium
Milestone

Comments

@Jeff-Cortese
Copy link

I'm submitting a ...

[ ] bug report
[x] feature request
[ ] support request

Current behavior
Currently, component lifecycle hooks are handled by implementing one or more interfaces provided by angular. I find myself often wanting to subscribe to an observable until the component/directive is destroyed. To do this, I have to have a member for each subscription for later disposal in the ngOnDestroy() method.
Example:

/* imports */

@Directive({
  selector: '[onDrag]'
})
export class OnDrag implements OnInit, OnDestroy {
  dragStart: Subscription;
  dragEnter: Subscription;
  dragOver: Subscription;
  dragLeave: Subscription;

  constructor(private element: ElementRef) { }

  ngOnInit(): any {
    let elem = this.element.nativeElement;

    this.dragStart = Observable.fromEvent(elem, 'dragstart')
      .do(() =>{ /*do stuff*/ })
      .subscribe();

    this.dragEnter = Observable.fromEvent(elem, 'dragenter')
      .do(() =>{ /*do stuff*/ })
      .subscribe();

    this.dragOver = Observable.fromEvent(elem, 'dragover')
      .do(() =>{ /*do stuff*/ })
      .subscribe();

    this.dragLeave = Observable.fromEvent(elem, 'dragleave')
      .do(() =>{ /*do stuff*/ })
      .subscribe();

    // etc...
  }

  ngOnDestroy(): any {
    this.dragStart.unsubscribe();
    this.dragEnter.unsubscribe();
    this.dragOver.unsubscribe();
    this.dragLeave.unsubscribe();
  }
}

Desired behavior
My suggestion is to provide components with an observable for each lifecycle event (via injection perhaps?). The above example could turn into something like:

/* imports */

@Directive({
  selector: '[onDrag]'
})
export class OnDrag implements OnInit {
  constructor(
    private element: ElementRef,
    private lifecycle: ThingWithLifecycleObservablesForThisComponent
  ) { }

  ngOnInit(): any {
    let elem = this.element.nativeElement;

    Observable.fromEvent(elem, 'dragstart')
      .do(() =>{ /*do stuff*/ })
      .takeUntil(this.lifecycle.ngDestroy$)
      .subscribe();

    Observable.fromEvent(elem, 'dragenter')
      .do(() =>{ /*do stuff*/ })
      .takeUntil(this.lifecycle.ngDestroy$)
      .subscribe();

    Observable.fromEvent(elem, 'dragover')
      .do(() =>{ /*do stuff*/ })
      .takeUntil(this.lifecycle.ngDestroy$)
      .subscribe();

    Observable.fromEvent(elem, 'dragleave')
      .do(() =>{ /*do stuff*/ })
      .takeUntil(this.lifecycle.ngDestroy$)
      .subscribe();

    // etc...
  }
}

What is the motivation / use case for changing the behavior?
Ultimately, I would like a way to not have to hold onto subscriptions as class members (perhaps there is a better way to achieve this than my suggestion). It could be convenient/concise to be able to handle lifecycle events as observables within components. As a workaround currently I have been extending my components with a base class that provides (some) of these events:

import { OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';

export class ComponentLifeCycle implements OnInit, OnDestroy {
  protected ngInit$: Subject<any> = new Subject<any>();
  protected ngDestroy$: Subject<any> = new Subject<any>();

  ngOnInit(): any {
    this.ngInit$.next({});
  }

  ngOnDestroy(): any {
    this.ngDestroy$.next({});
  }
}

Please tell us about your environment:

  • Angular version: 2.0.0-rc.4
  • Browser: all
  • Language: all
@tbosch tbosch added area: core Issues related to the framework runtime feature Issue that requests a new feature labels Oct 4, 2016
@bobvanderlinden
Copy link

To get this further along, would it be possible to hook lifecycle events outside of a component? That would make it possible to create a third party library that provides observable events.

@sod
Copy link
Contributor

sod commented Sep 25, 2017

So there mere presence of a method ngOnDestroy lets angular call it upon component destruction.

How about the mere presence of a:

class MyComponent {
    public ngDestroy = new EventEmitter<void>();
}

and angular does componentInstance.ngDestroy.next(); upon destruction?

@bobvanderlinden
Copy link

Just to share my findings: I have tried to set ngOnDestroy inside the constructor, but this will not work. It seems to check whether the method is created using reflection metadata.

@Jeff-Cortese
Copy link
Author

@ohjames I hadn't considered AoT when I first wrote this issue. A year later, we are using AoT'ed builds in production at my work and have never hit the issue you're describing. I tested out that scenario by logging out emitted ngDestroy$ values without calling super.ngOnDestroy(), and it appears to be working, meaning the base class's ngOnDestroy() was not compiled out.

@bygrace1986
Copy link

bygrace1986 commented Oct 6, 2017

@sod this may be similar to what you wanted.

I put together an alternate approach using property decorators if anyone prefers that flavor over inheritance. I haven't tested it with AOT yet. If the angular team wrote it then they could get tighter integration without needing to hijack the life-cycle callback methods on the component.

https://stackoverflow.com/questions/40737752/anuglar2-lifecycle-events-as-rxjs-observable/46572739#46572739

life-cycle.decorator.ts

import { SimpleChanges } from '@angular/core';
import { Subject } from 'rxjs/Subject';

/**
 * Creates an observable property on an object that will
 * emit when the corresponding life-cycle event occurs.
 * The main rules are:
 * 1. Don't name the property the same as the angular interface method (i.e. ngOnInit).
 * 2. If a class inherits from another component where the parent uses this decorator
 *    and the child implements the corresponding interface then it needs to call the parent method.
 * @param {string} lifeCycleMethodName name of the function that angular calls for the life-cycle event
 * @param {object} target class that contains the decorated property
 * @param {string} propertyKey name of the decorated property
 */
function applyLifeCycleObservable(
    lifeCycleMethodName: string,
    target: object,
    propertyKey: string
): void {
    // Save a reference to the original life-cycle callback so that we can call it if it exists.
    const originalLifeCycleMethod = target.constructor.prototype[lifeCycleMethodName];

    // Use a symbol to make the observable for the instance unobtrusive.
    const instanceSubjectKey = Symbol(propertyKey);
    Object.defineProperty(target, propertyKey, {
        get: function() {
            // Get the observable for this instance or create it.
            return (this[instanceSubjectKey] || (this[instanceSubjectKey] = new Subject<any>())).asObservable();
        }
    });

    // Add or override the life-cycle callback.
    target.constructor.prototype[lifeCycleMethodName] = function() {
        // If it hasn't been created then there no subscribers so there is no need to emit
        if (this[instanceSubjectKey]) {
            // Emit the life-cycle event.
            // We pass the first parameter because onChanges has a SimpleChanges parameter.
            this[instanceSubjectKey].next.call(this[instanceSubjectKey], arguments[0]);
        }

        // If the object already had a life-cycle callback then invoke it.
        if (originalLifeCycleMethod && typeof originalLifeCycleMethod === 'function') {
            originalLifeCycleMethod.apply(this, arguments);
        }
    };
}

// Property Decorators
export function OnChangesObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngOnChanges', target, propertyKey);
}
export function OnInitObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngOnInit', target, propertyKey);
}
export function DoCheckObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngDoCheck', target, propertyKey);
}
export function AfterContentInitObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterContentInit', target, propertyKey);
}
export function AfterContentCheckedObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterContentChecked', target, propertyKey);
}
export function AfterViewInitObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterViewInit', target, propertyKey);
}
export function AfterViewCheckedObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterViewChecked', target, propertyKey);
}
export function OnDestroyObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngOnDestroy', target, propertyKey);
}

Example Usage
I implement OnInit here to demonstrate that they work together but you could subscribe to onInit instead

import { Component, OnInit, Input, SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import {
    OnChangesObservable,
    OnInitObservable,
    DoCheckObservable,
    AfterContentInitObservable,
    AfterContentCheckedObservable,
    AfterViewInitObservable,
    AfterViewCheckedObservable,
    OnDestroyObservable
 } from './life-cycle.decorator';
import { MyService } from './my.service'

@Component({
    template: ''
})
export class TestDecoratorComponent implements OnInit {

    @OnChangesObservable
    onChanges: Observable<SimpleChanges>;
    @OnInitObservable
    onInit: Observable<void>;
    @DoCheckObservable
    doCheck: Observable<void>;
    @AfterContentInitObservable
    afterContentInit: Observable<void>;
    @AfterContentCheckedObservable
    afterContentChecked: Observable<void>;
    @AfterViewInitObservable
    afterViewInit: Observable<void>;
    @AfterViewCheckedObservable
    afterViewChecked: Observable<void>;
    @OnDestroyObservable
    onDestroy: Observable<void>;

    @Input()
    input: string;

    constructor(private myService: MyService) {
    }

    ngOnInit() {
        this.myService.takeUntil(this.onDestroy).subscribe(() => {});
        this.onChanges
            .map(x => x.input)
            .filter(x => x != null)
            .takeUntil(this.onDestroy)
            .subscribe((change: SimpleChange) => {
            });
    }
}

@insidewhy
Copy link

insidewhy commented Oct 9, 2017

@bygrace1986 Yeah that definitely won't work with AoT. The AoT compiler needs to be able to statically infer the existence of lifecycle methods in the component, it can't infer these from code run as part of a decorator.

In general with Angular 2/4, a million things are trivial to do with decorators, but barely any of these will work once you factor in AoT.

@bygrace1986
Copy link

@ohjames your right. I'd rather use the decorators to insert the hooks but I couldn't find any hacky way to make it work so I reverted to using a base class.

@sandangel
Copy link

any chance this proposal will be implemented?

@insidewhy
Copy link

insidewhy commented Oct 11, 2017

Without this proposal there is no way to include lifecycle events as part of rxjs pipelines or to hook into lifecycle events without abusing inheritance.

At my consultancy we're using Angular 2/4 with rxjs heavily. This issue together with the lack of observables to monitor @Input makes us question if VueJS or cycle.js would be a better fit for us given our love of rxjs.

There's this uncomfortable middle ground with Angular 4 where most things are trivial to do with observables and yet other things that should be trivial are obstructed by brick walls.

@mlc-mlapis
Copy link
Contributor

@ohjames ... for monitoring @Input() you can use setter because it is invoked also when there is a change.

@insidewhy
Copy link

@mlc-mlapis Yep, that's precisely the approach I'm using in my angular utility library https://github.com/ohjames/observable-input. Having to define a setter + Observable member for every @Input() is a bit too much boilerplate for me.

@fxck fxck mentioned this issue Nov 13, 2017
lacolaco added a commit to lacolaco/angular that referenced this issue Nov 29, 2017
lacolaco added a commit to lacolaco/angular that referenced this issue Nov 29, 2017
lacolaco added a commit to lacolaco/angular that referenced this issue Nov 29, 2017
lacolaco added a commit to lacolaco/angular that referenced this issue Nov 29, 2017
@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@smnbbrv
Copy link

smnbbrv commented Mar 29, 2018

Binding to #23038 for the sake of discussion...

@MickL
Copy link

MickL commented Apr 1, 2019

I was just about to request the same feature. The following code is duplicate code in most of my components:

@Component()
export class PaymentMethodsLogosComponent implements OnDestroy {
   destroy$ = new Subject();

   ngOnDestroy() {
      this.destroy$.next();
      this.destroy$.complete();
   } 
}

It would be nice to just be able to use takeUntil(this.destroy$) in every component and service.

@stupidawesome
Copy link

stupidawesome commented May 26, 2019

Those interested observable lifecycle hooks should check out this small library I'm working on called NgObservable that does just that.

It includes:

  • Lifecycle operators for RxJS.
  • AoT compatible Lifecycle method decorators.
  • Easy subscription management with automatic cleanup.
  • Typed changes for ngOnChanges
  • And much more

It's still in the works but I'm keen to get feedback. You can grab it on NPM.

@alxhub alxhub moved this from Inbox to Needs Project Proposal in Feature Requests Sep 30, 2021
@alxhub alxhub moved this from Needs Project Proposal to Proposed Projects in Feature Requests Sep 30, 2021
@alxhub alxhub moved this from Proposed Projects to Needs Project Proposal in Feature Requests Sep 30, 2021
@alxhub alxhub moved this from Needs Project Proposal to Proposed Projects in Feature Requests Oct 13, 2021
@robounohito
Copy link

robounohito commented May 15, 2022

Is this stale? because the idea is really awesome :) we could've used Angular entirely in declarative way... lifecycle methods are really oop way, they are obvious candidates to be observables...

@LanderBeeuwsaert
Copy link

I am of the same opinion, this would decrease complexity and boilerplate much more than standalone components do (if using rxjs of course)

@avchugaev
Copy link

avchugaev commented Jun 24, 2022

The new inject API opens new possibilities for better code reuse.

Here's how a typical implementation of OnDestroy looks like:

export class DemoComponent implements OnDestroy {
  private readonly destroy$ = new Subject<void>();
  
  // use it like `takeUntil(this.destroy$)`...
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

But folks want to type fewer lines of code, and the new inject() API allows them to do it by implementing a shorthand function once...

export function useDestroy() {
  // Some alternative needed, because it's not a CDR's responsibility 
  const viewRef = inject(ChangeDetectorRef) as ViewRef;
  const destroy$ = new Subject<void>();
  
  viewRef.onDestroy(() => {
    destroy$.next();
    destroy$.complete();
  });
  
  return destroy$.asObservable();
}

... and use it as a one-liner everywhere:

export class DemoComponent {
  private readonly destroy$ = useDestroy(); // less boilerplate
  
  // use it like `takeUntil(this.destroy$)`...
}

Could you consider adding an API to make it possible?

@why520crazy
Copy link
Contributor

why520crazy commented Jun 25, 2022

@avchugaev If possible, whether ViewRef can be inject directly and provide more public APIs of lifecycle?

export function useDestroy() {
  const viewRef = inject(ViewRef);
  const destroy$ = new Subject<void>();
  
  viewRef.onDestroy(() => {
    destroy$.next();
    destroy$.complete();
  });
  
  return destroy$.asObservable();
}

ViewRef can expose more lifecycle hooks like ngAfterViewInit ngOnChanges etc...

function useSome() {
 const viewRef = inject(ViewRef);

 viewRef.ngAfterViewInit(() => {
    // do somethings
 });

 viewRef.ngOnChanges((changes: SimpleChanges) => {
     // do somethings
 });
}

If so, Angular will become more powerful and flexible, I'm looking forward to the official team considering of this capability.

@yjaaidi
Copy link
Contributor

yjaaidi commented Jul 4, 2022

I mistakenly created a duplicate here #46692

This could look something like this:

interface LifecycleHooks {
  init$: Observable<void>;
  changes$: Observable<SimpleChanges>;
  destroy$: Observable<void>;
  ...
}

and could be implemented by ViewRef if it becomes injectable or maybe another service for better tree-shaking and separation of concerns.

This could allow use cases like this one:

@Component()
class MyCmp {
  constructor(injector: Injector) {
    useLowPriorityStrategy(injector);
  }
}

function useLowPriorityStrategy(injector) {

  const cdr = injector.get(ChangeDetectorRef);
  const { init$, changes$, destroy$ } = injector.get(LifecycleHooks);

  cdr.detach();

  merge(init$, changes$)
    .pipe(
      debounceTime(50),
      takeUntil(destroy$)
    )
    .subscribe(() => cdr.markForCheck());

}

Here is a working example (hacked using a decorator): https://stackblitz.com/edit/angular-lifecycle-hooks-service?file=apps%2Flifecycle%2Fsrc%2Fapp%2Fapp.component.ts

@avchugaev
Copy link

avchugaev commented Jul 5, 2022

@yjaaidi +1 from me, I see it just like you described, but with a shorter injectable name:

const { init$, changes$, destroy$ } = inject(Lifecycle);

@DmitryEfimenko
Copy link

I really like conversations moving in this direction! Thanks, @yjaaidi.
However, the amount of boilerplate to get it all wired up is too much

  • Use component class-level decorator: @LinkLifecycleHooks()
  • Add it to component providers: provideLifecycleHooks()
  • Finally, inject it via injectLifecycleHooks()

I understand this is a demo. Perhaps the official solution would be lighter?

Also, I really like that the proposed solution includes getInputChange function. Absolutely needed for a complete story.

So far I prefer the solution provided by ngx-observable-lifecycle since it requires less boilerplate. Although it seems that it may have some troubles running with ng 13 (based on the open issues).

@antischematic
Copy link

antischematic commented Jul 29, 2022

Observable lifecycle hooks are possible in Angular 14 by creating a second component.

class LifecycleObserver {
   onInit = new Subject()
   afterViewInit = new Subject()
   onDestroy = new Subject()
}

@Component({ template: `` })
class Lifecycle extends LifecycleObserver {
   ngOnInit() {
      super.onInit.next()
   }
   ngAfterViewInit() {
      super.afterViewInit.next()
   }
   ngOnDestroy() {
      super.onDestroy.next()
   }
}

function createLifecycleObservable(type) {
   const viewContainerRef = inject(ViewContainerRef)
   const componentRef = viewContainerRef.createComponent(Lifecycle)
   // keep the DOM clean
   componentRef.location.nativeElement.remove()
   
   return componentRef.instance
}

Then using it

@Component({
   template: `{{ result | json }}`
})
export class MyComponent {
   http = inject(HttpClient)
   lifecycle = createLifecycleObservable()
   result

   doSomething = this.lifecycle.onViewInit.pipe(
      mergeMap(() => this.http.get(endpoint)),
      takeUntil(this.lifecycle.onDestroy)
   ).subscribe(result => this.result = result)
}

I have a more concrete example here that allows hooks to be composed with nested objects.

@robounohito
Copy link

sure! - but from POV of a reactive programmer, that kind of low-level machinery should be incapsulated from us by framework, only observables exposed. (also there maybe can be some pitfalls related to memory management here too... this kind of stuff should be handled at framework level IMO... (otherwise idk why we need a framework at all :D ))

@fabioemoutinho
Copy link
Contributor

fabioemoutinho commented Aug 29, 2022

Personally I would love to see something like:

@ReactiveComponent({ ... }}
class MyReactiveComponent {
  @ReactiveInput() myInput$: Observable<number>;
  protected ngOnDestroy$: Observable<void>;
}

I don't know if this is achievable, but in this way you could have this be implemented only in @ReactiveComponent and it would not mess with current @Component API.
ngOnDestroy$ and other lifecycle observables would be implemented by the @ReactiveComponent. And maybe with the @ReactiveInput, we could finally have fully reactive components 🙌

@tayambamwanza
Copy link

I really hope this gets implemented along with observable inputs, ever since I realised this could be done I realised how much it would make the reactive authoring experience in Angular so much more DX friendly. Using the Decorator lifecycle hooks and even the inputs actually forces you out of getting the most from the experience, which is funny because I was introduced to reactivity by Angular and after understanding it better I realised Angulars implementation is missing a few key features to take it from ok to amazing.

I hope along with any other primitives they do lean into the strength of RXJS that many have come to love, I think the fact that many frameworks are developing reactive primitives which mimic rxjs in a more lightweight manner shows once again that Angular was actually ahead of it's time like with typescript.

@niklaas
Copy link

niklaas commented Oct 10, 2022

I really hope this gets implemented along with observable inputs

What do you mean by "observable inputs"? You can already pass in observables to a component using an input binding. Do you also want the subscription to be managed by the component automatically?

@tayambamwanza
Copy link

@niklaas Basically what I want is to be able to subscribe to the input rather than using ngOnChanges, then I could apply operators to the input directly. So the input itself would be of type observable and emit the values passed to it through that. Forgive me if my terminology is wrong but that should be enough to give you an idea.

@niklaas
Copy link

niklaas commented Oct 31, 2022

@niklaas Basically what I want is to be able to subscribe to the input rather than using ngOnChanges, then I could apply operators to the input directly. So the input itself would be of type observable and emit the values passed to it through that. Forgive me if my terminology is wrong but that should be enough to give you an idea.

That is, you don't mean passing in an observable explicitly, right? Because if you do, this is actually possible.

@tayambamwanza
Copy link

tayambamwanza commented Oct 31, 2022

@niklaas Basically what I want is to be able to subscribe to the input rather than using ngOnChanges, then I could apply operators to the input directly. So the input itself would be of type observable and emit the values passed to it through that. Forgive me if my terminology is wrong but that should be enough to give you an idea.

That is, you don't mean passing in an observable explicitly, right? Because if you do, this is actually possible.

Not my work btw it's from @tomastrajan

Ffiy2rDXwAI2RnR

@csisy
Copy link

csisy commented Jan 11, 2023

There was a PR mentioned in this issue #20682 (back from 2017) which provided a good solution - tho I understand why it wasn't merged. But since then, 5+ years passed. Is there any plan to introduce something similar? This issue is open for so long and we really need this feature, especially with the new inject feature introduced with v14.

@JeanMeche
Copy link
Member

This PR will land a takeUntilDetroyed operator ! (Target is V16)

@gavinsawyer
Copy link

gavinsawyer commented Aug 5, 2023

This would be useful for accessing child components' lifecycles as well. Currently, I can't find a use for ViewChild as it's easier to avoid implementing component lifecycle hooks by just nexting a subject:

Current approach

Defines a reactive value (map: Subject<google.maps.Map>) with no rules and uses the template to set its value:

<google-map
  height="100%"
  width="100%"
  [options]="mapOptions()"
  (mapInitialized)="map.next($event);"
  *ngIf="googleMapsAPILoaded()" />
import { CommonModule, isPlatformBrowser }                                                from "@angular/common";
import { HttpClient, HttpClientJsonpModule, HttpClientModule }                            from "@angular/common/http";
import { Component, computed, Inject, PLATFORM_ID, signal, Signal }                       from "@angular/core";
import { takeUntilDestroyed, toSignal }                                                   from "@angular/core/rxjs-interop";
import { GoogleMapsModule }                                                               from "@angular/google-maps";
import { APP_ENVIRONMENT }                                                                from "@heypoint/injection-tokens";
import { AppEnvironment }                                                                 from "@heypoint/types";
import { map, merge, Observable, Observer, startWith, Subject, switchMap, TeardownLogic } from "rxjs";


@Component({
  imports:     [
    CommonModule,
    GoogleMapsModule,
    HttpClientModule,
    HttpClientJsonpModule,
  ],
  selector:    "heypoint-map",
  standalone:  true,
  styleUrls:   [
    "./map.component.sass",
  ],
  templateUrl: "./map.component.html",
})
export class MapComponent {

  public readonly googleMapsAPILoaded: Signal<boolean>;
  public readonly map:                 Subject<google.maps.Map>;
  public readonly mapOptions:          Signal<google.maps.MapOptions>;

  private readonly mapGeolocationEngaged: Signal<boolean>;

  constructor(
    @Inject(APP_ENVIRONMENT) appEnvironment: AppEnvironment,
    @Inject(PLATFORM_ID)     platformId:     object,

    httpClient: HttpClient,
  ) {
    this
      .googleMapsAPILoaded = isPlatformBrowser(platformId) ? toSignal<boolean>(
        httpClient.jsonp(
          "https://maps.googleapis.com/maps/api/js?key=" + appEnvironment.firebase.apiKey,
          "callback",
        ).pipe<boolean, boolean, boolean>(
          map<unknown, boolean>(
            (): boolean => true,
          ),
          startWith<boolean>(false),
          takeUntilDestroyed<boolean>(),
        ),
        {
          requireSync: true,
        },
      ) : signal<boolean>(false);
    this
      .map = new Subject<google.maps.Map>();
  }

}

Proposed approach

Eliminates the use of subjects, integrating directly into the child component's lifecycle:

<google-map
  #googleMap
  height="100%"
  width="100%"
  [options]="mapOptions()"
  *ngIf="googleMapsAPILoaded()" />
import { CommonModule, isPlatformBrowser }                                       from "@angular/common";
import { HttpClient, HttpClientJsonpModule, HttpClientModule }                   from "@angular/common/http";
import { Component, computed, Inject, PLATFORM_ID, signal, Signal, ViewChild }   from "@angular/core";
import { takeUntilDestroyed, toSignal }                                          from "@angular/core/rxjs-interop";
import { GoogleMap, GoogleMapsModule }                                           from "@angular/google-maps";
import { APP_ENVIRONMENT }                                                       from "@heypoint/injection-tokens";
import { AppEnvironment }                                                        from "@heypoint/types";
import { map, merge, Observable, Observer, startWith, switchMap, TeardownLogic } from "rxjs";


// New rxjs-interop behavior:
import { AfterViewInit } from "@angular/core";
import { Subject }       from "rxjs";


const afterViewInitSubject: Subject<void> = new Subject<void>();
const afterViewInit: Observable<void> = afterViewInitSubject.asObservable(); // Just export this, or a function of it taking ComponentRef like takeUntilDestroyed.
// ----

@Component({
  imports:     [
    CommonModule,
    GoogleMapsModule,
    HttpClientModule,
    HttpClientJsonpModule,
  ],
  selector:    "heypoint-map",
  standalone:  true,
  styleUrls:   [
    "./map.component.sass",
  ],
  templateUrl: "./map.component.html",
})
export class MapComponent /* New rxjs-interop behavior: */ implements AfterViewInit /* ---- */ {

  public readonly googleMapsAPILoaded: Signal<boolean>;
  public readonly mapOptions:          Signal<google.maps.MapOptions>;

  private readonly map:                   Observable<google.maps.Map>;
  private readonly mapGeolocationEngaged: Signal<boolean>;

  @ViewChild("googleMap") private googleMap!: GoogleMap;

  constructor(
    @Inject(APP_ENVIRONMENT) appEnvironment: AppEnvironment,
    @Inject(PLATFORM_ID)     platformId:     object,

    httpClient: HttpClient,
  ) {
    this
      .googleMapsAPILoaded = isPlatformBrowser(platformId) ? toSignal<boolean>(
        httpClient.jsonp(
          "https://maps.googleapis.com/maps/api/js?key=" + appEnvironment.firebase.apiKey,
          "callback",
        ).pipe<boolean, boolean, boolean>(
          map<unknown, boolean>(
            (): boolean => true,
          ),
          startWith<boolean>(false),
          takeUntilDestroyed<boolean>(),
        ),
        {
          requireSync: true,
        },
      ) : signal<boolean>(false);
    this
      .map = afterViewInit
      .pipe<google.maps.Map>(
        switchMap<void, Observable<google.maps.Map>>(() => this.googleMap.mapInitialized.asObservable())
      );
  }

  // New rxjs-interop behavior:
  ngAfterViewInit() {
    afterViewInitSubject
      .next();
  }
  // ----

}

I left out declarations for mapOptions and mapGeolocationEngaged for brevity, which are reactive values that depend upon the output from mapInitialized. I'm using event listeners on the native Google Maps component to determine whether to keep updating mapOptions with the user's live location. If they swipe around or zoom, it pauses recentering the map from the geolocation stream.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: lifecycle hooks cross-cutting: observables feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature freq2: medium
Projects
Feature Requests
Proposed Projects
Development

Successfully merging a pull request may close this issue.