Angular Injectable service that is dependant on DOM element - javascript

I am just grasping Angular (6) framework, and it's quirks and features.
And there is this one thing, that I would need, but as it seems, it goes against Angular paradigms - a service, that needs a reference to DOM node.
More specifically - Esri Map component.
In it's constructor, it requires a DOM node - a place where the map will live.
Since the Map is going to be the center thing in the application, it would be used by multiple components throughout the app. And for that, I would want it in a service. Question is - how would I do that? I made solution to this, but I would like to validate it with more experienced Angular devs.
So I have a MapComponent that would be like a singleton component - included only once in the App.
export class MapComponent implements OnInit {
#ViewChild('map') mapElement:ElementRef;
constructor(
private renderer2: Renderer2,
private mapService: MapService) { }
ngOnInit() {
// This is where I pass my DOM element to the service, to initialize it
this.mapService.init(this.mapElement.nativeElement);
}
}
And my mapService, that I would reference throughout other services
#Injectable()
export class MapService {
private isInit:boolean = false;
private map: __esri.Map = null;
private mapView: __esri.MapView = null;
init(domElement: ElementRef) {
if (this.isInit) return Promise.resolve(); // A guard?
loadModules(["esri/Map", "esri/views/MapView", "esri/widgets/ScaleBar"])
.then(([Map, MapView, ScaleBar]) => {
this.map = new Map();
this.mapView = new MapView({
map: this.map,
container: domElement // This is where I need DOM element
});
})
.catch(err => {
// handle any errors
console.error(err);
})
.then(() => {
this.isInit = true;
});
}
}
This does work, I just wonder, would this be a correct way to do it.
I require these map objects to be accessible through other components, to add/remove/change map layers, draw/render geometries and other map things.

Related

How to use one function for different BehaviorSubjects

I used to have one banner at the top of the page for all events in my app (like some Errors, Warnings, and Success) and used for that BehaviorSubject.
For example:
in the main app.component.html file I had:
<baner [alerts]="alerts$ | async"></baner>
and alerts get from bannerService:
ngOnInit(): void { this.alerts$ = this.bannerService.alerts$; }
the service looks next:
alertSub$ = new BehaviorSubject<string>('');
alerts$ = this.alertSub$.asObservable();
showWarning(message: string): void {
const newAlert = { message, type: 'Warning' };
this.alertSub$.next([...this.alertSub$.getValue(), newAlert]);
setTimeout(() => this.dismiss(newAlert), 500);
}
dismiss(alert): void {
const updatedAlerts = this.alertSub$.getValue().filter(alertSub => alertSub !== alert);
this.alertSub$.next(updatedAlerts);
}
...and so on...
So when I wanted to add some warning, I called this.bannerService.showWarning('some msg') and everything was fine.
But now I need to add a banner inside another component for its own warnings, and it should be independent. This means that global warnings would be still on the top of the app, but warnings of this component are only inside the component.
I understand, that I should create a new BehaviorSubject, but how to re-use all functions correctly?
For now, I've added to all functions a parameter, that pass proper BehaviorSubject, but in that case, I need to make changes in all places, where bannerService was used.
Service with my new changes:
alertSub$ = new BehaviorSubject<string>('');
alerts$ = this.alertSub$.asObservable();
componentSub$ = new BehaviorSubject<string>('');
componentAlerts$ = this.componentSub$.asObservable();
showWarning(message: string, banner: BehaviorSubject<string>): void {
const newAlert = { message, type: 'Warning' };
banner.next([...banner.getValue(), newAlert]);
setTimeout(() => this.dismiss(newAlert, banner), 500);
}
dismiss(alert, banner: BehaviorSubject<string>): void {
const updatedAlerts = banner.getValue().filter(alertSub => alertSub !== alert);
banner.next(updatedAlerts);
}
...and so on...
Would be really grateful for any idea, on how to use old functions for different BehaviorSubjects.
I think it can be a bit easier than that. Your baner component is responsible of rendering the messages, right? What if you modify this component to take in two instances of bannerService instead of just one? Let's suppose this is our BannerComponent:
export class BannerComponent implements OnInit {
bannerService: BannerService;
constructor(
#Host() #Optional() parentBannerService: BannerService,
#Inject() globalBannerService: BannerService
) {
this.bannerService = parentBannerService ?? globalBannerService;
}
This allows us to ask the injector for an (optional) instance of BannerService that is provided by the parent component (the component that renders the BannerComponent component).
In case we don't have such a thing, we still want the BannerService to be injected from somewhere, hence the second parameter, globalBannerService.
Now all that is left for us to do, is to provide a BannerService instance from our custom component that displays the banner:
#Component({
selector: 'app-component-with-its-own-banner',
// template, css, etc
providers: [BannerService]
})
export class ComponentWithItsOwnBanner {
// ...
}
The template of this component also includes the banner component selector:
<baner [alerts]="bannerService.alerts$ | async"></baner>
Everything else can stay exactly the same. You don't need to create any additional behavior subjects.

Angular 9 call function when two components loaded

I am trying to have two components, <app-map> and <app-markers-list>.
<app-map> loads Google Maps API and displays a map on the page. It emits a mapLoaded event in ngAfterViewInit()
#Output() mapLoaded: EventEmitter<boolean> = new EventEmitter();
<app-markers-list> loads a list of markers via Angular's HttpClient. It emits a markersLoaded event at the end of HttpClient.get().subscribe() Observable.
#Output() markersLoaded: EventEmitter<boolean> = new EventEmitter();
How do I catch these two events at once so I can call another component's function that will populate the map with the markers?
I think there is a simple solution.
#Output()
allLoaded = new EventEmitter();
oneLoaded = false;
ngAfterViewInit() {
this.emitAllLoaded();
}
yourFunctionWhereHttpClientGetLocated() {
this.http.get(..).subscribe(() => {
this.emitAllLoaded();
});
}
emitAllLoaded() {
if (oneLoaded) this.allLoaded.emit();
oneLoaded = true;
}
You might don't need to use Observable or Subject
I am assuming you are trying to capture events from <app-map> and <app-markers-list> in a parent component.
You can capture as we generally do, put binding in parent template as below:
<app-map (mapLoaded)="mapLoaded($event)"></app-map>
<app-markers-list (markersLoaded)="markersLoaded($event)"></app-markers-list>
Now you can manage these events in parent component. You can use subjects or observables and observable operators (combineLatest, of). Please find the code below how you can use it.
import { combineLatest, of } from 'rxjs';
// this will be your parent component.
export class ParentComponent {
mapLoaded = new Subject()
mapLoaded$ = this.mapLoaded.asObservable();
markerListLoaded = new Subject()
markerListLoaded$ = this.markerListLoaded.asObservable();
constructor() {
const combinedValues = combineLatest(mapLoaded$, markerListLoaded$);
combinedValues.subscribe((value) => {
// Here you can write code when you receive notification from both the events.
})
}
mapLoaded(mapLoadedData) {
this.mapLoaded$.next('map loaded successfully');
}
markersLoaded(markersLoadedData) {
this.markerListLoaded$.next('markers loaded successfully');
}
}

Is dependeny injection possible when using the new operator?

Actually I want to implement some sort of strategy pattern. More precisely, I want to decide on runtime which class to instanciate.
Of course this is pretty simple.
if(...) {
this.service = new ServiceA();
} else {
this.service = new ServiceB();
}
But what if ServiceA and ServiceB use dependency injection in their classes? Do I have to pass those instances or is there a nicer way to let nest inject them automatically?
You can inject both services and then assign them to your variable dynamically:
service: Service;
constructor(private serviceA: ServiceA, private serviceB: ServiceB) {}
dynamicMethod() {
if (...) {
this.service = this.serviceA;
} else {
this.service = this.serviceB;
}
}
If the condition can be evaluated on startup (e.g. environment variabels) you can use a custom provider instead.

How to Access the Component Inside of Annotation in Angular2?

This is just me trying to get an understanding of the lifecycle and try and perform some logic before loading the component so I found the CanActivate Annotation.
I have a function in the Component that I want to call so I need the Component; I have injected it... it seems overly complex.
// HomeComponent Component
#Component({
selector: 'HomeComponent',
template: '<h2>HomeComponent Us</h2>'
})
#CanActivate((next,prev) => {
let injector: any = Injector.resolveAndCreate([HomeComponent]);
let theComponent: HomeComponent = injector.get(HomeComponent);
return theComponent.canActivate(next,prev)
})
class HomeComponent {
data = {}
myresolver: any
constructor() {
console.log("in constructor", this.data)
}
setData(data: any) {
this.data = data
}
canActivate(nextInstr, currInstr) {
console.log("in my resolver");
var that = this;
return new Promise(function(resolve, reject) {
setTimeout(function() {
var data = { data: "Aaron" };
that.setData(data)
resolve(data);
}, 2000)
});
}
}
In fact you can't since processing within the annotation is called right before instantiating (or not) the component.
I guess that you want to use the resolve feature of Angular1 router into Angular2. In fact, such feature isn't supported yet. For more details you can have a look at this issue in the Angular github:
https://github.com/angular/angular/issues/4015
The two following links give you some workarounds for your use case:
Angular 2 - equivalent to router resolve data for new router
Using Resolve In Angular2 Routes
Hope it helps you,
Thierry

Angular 2.x watching for variable change

I'm migrating from angular 1.x to 2.x but my brains still think in angular 1.x so sorry for silly questions.
What I need is to take some action when one of my scope variables component properties changes. I found a solution but I think there should be better solution
export class MyApp {
router: Router;
location: Location;
fixed: boolean = true;
private set isFixed(value:boolean) {
this.fixed = value;
//TODO: look here
console.log('isFixed changed', value);
}
private get isFixed():boolean {
return this.fixed;
}
constructor(router: Router, location: Location) {
this.router = router;
this.location = location;
}
}
Look at the line console.log('isFixed changed', value); It's what I need and it's working. But I made it by declaring getter and setter, but isn't there a better solution to watch variables? Like in angular 1.x was $scope.$watch?
I think my component code should look like
export class MyApp {
router: Router;
location: Location;
isFixed: boolean = true;
//TODO: $watch for isFixed change {
console.log('isFixed changed', value);
// }
constructor(router: Router, location: Location) {
this.router = router;
this.location = location;
}
}
You might want to implement the OnChanges interface and implement the ngOnChanges() method.
This method is called whenever one of the components input or output binding value changes.
See also https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
Dart code example
#Input() bool fixed;
#override
void ngOnChanges(Map<String, SimpleChange> changes) {
print(changes);
}
You might find this answer to Delegation: EventEmitter or Observable in Angular2 helpful (worked for me).
Essentially you could use a BehaviorSubject, which allows you to set an initial value for the property you're interested in, then subscribe to changes to that property wherever that service is injected.
e.g.
export class SomeService {
private fixed = new BehaviorSubject<boolean>(true); // true is your initial value
fixed$ = this.fixed.asObservable();
private set isFixed(value: boolean) {
this.fixed.next(value);
console.log('isFixed changed', value);
}
private get isFixed():boolean {
return this.fixed.getValue()
}
constructor(router: Router, location: Location) {
this.router = router;
this.location = location;
}
}
Then in a class (e.g. Component) that's interested in the fixed value:
export class ObservingComponent {
isFixed: boolean;
subscription: Subscription;
constructor(private someService: SomeService) {}
ngOnInit() {
this.subscription = this.someService.fixed$
.subscribe(fixed => this.isFixed = fixed)
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Update value:
export class Navigation {
constructor(private someService: SomeService) {}
selectedNavItem(item: number) {
this.someService.isFixed(true);
}
}
To auto get updated value by this service
NOTE: I tested it in Angular 9
my service file
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class SharedService {
private fixed= new BehaviorSubject<boolean>(false);
fixed$ = this.fixed.asObservable();
constructor() {}
updateFixedValue(value: boolean) {
this.fixed.next(value);
console.log('fixed changed', value);
}
}
Now you can get value in any component (within ngOnInit or anywhere you want) like below
NOTE: this value will be change automatically after update
this.sharedService.fixed$.subscribe(val=>{ this.isFixed = val; });
and you can update or set new value from any component like below
this.sharedService.updateFixedValue(your_boolean_value);
Thanks, I hope it's work for you.
See Angular2 Component Interaction (has code examples).
The short answer to your question is that it really just depends on what you are trying to do. Even then, there are multiple ways to do what you want to do even if it's not really intended for it. So, I think it's best if you just take a few minutes to look at their documentation about Component Interaction and Forms.
My personal preference is to use events when a property has changed. The ngOnChanges event can be used for this but I prefer to work with #Input and #Output, and form value changed events (Angular2 Forms).
Hope this helps and gives you a direction you want to take.
I think it is possible to use simple getter and setter for this purpose.
class Example {
private _variable: string = "Foo";
set variable(value: string) {
this._variable = value;
console.log("Change detected: ", this.variable);
}
get variable(): string {
return this._variable;
}
}
let example = new Example();
console.log(example.variable);
example.variable = "Bar";
console.log(example.variable);
And output will be:
Foo
Change detected: Bar
Bar

Categories

Resources