I want to isolate http interactions by creating data access objects from a class so that in a component I might simply get data like this:
// dashboard.component
import { AppUser } from './appuser.service'
export class DashboardComponent implements OnInit {
user: AppUser = new AppUser();
constructor() { }
ngOnInit() {
let id = JSON.parse(window.localStorage.getItem('session')).userId;
this.user.find(id) // 'find' is from base class
.subscribe(
// handle user data
);
}
}
I have defined a base class and a sub class like this:
// base-resource.service
import { HttpClient } from '#angular/common/http';
...
export class BaseResource {
private fullpath: string;
protected http: HttpClient;
constructor (path: string) {
this.fullpath = path;
}
find (id): Observable<Object> {
return this.http.get(this.fullpath + '/' + id); // this line throws Error!
}
}
// app-user.service
...
export class AppUser extends BaseResource {
constructor(data?) {
super('api/appusers');
}
}
However this generates an error: ERROR TypeError: Cannot read property 'get' of undefined from within the base class function.
My 'AppUser' instance is clearly inheriting find from 'BaseResource', but find is picking up the 'AppUser' instance as the value of this and http is not available. I have tried declaring http as public and private as well as protected, but that had no effect. I imagine I'm missing some bigger picture of how to extend classes.
As specifically as possible, i think my question is in how to abstract functions to a base class when they need access to the base class's context.
(using Angular 6.0.4)
EDIT
I updated the title as it became clear that this is a problem of instantiating the HttpClient service in a class.
The error is because nothing is instantiating HttpClient, so it is undefined when you come to use it.
You should inject HttpClient into AppUser, and pass it into BaseResource via the constructor
export class AppUser extends BaseResource {
constructor(HttpClient http) {
super(http, 'api/appusers');
}
}
And in base-resource.service
import { HttpClient } from '#angular/common/http';
...
export class BaseResource {
private fullpath: string;
protected http: HttpClient;
constructor (httpClient: HttpClient, path: string) {
this.fullpath = path;
this.http = httpClient;
}
find (id): Observable<Object> {
return this.http.get(this.fullpath + '/' + id); // this line throws Error!
}
}
Related
In an Angular 11 app, I have a simle service that mekes a get request and reads a JSON.
The service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Promo } from '../models/promo';
#Injectable({
providedIn: 'root'
})
export class PromoService {
public apiURL: string;
constructor(private http: HttpClient) {
this.apiURL = `https://api.url.com/`;
}
public getPromoData(){
return this.http.get<Promo>(`${this.apiURL}/promo`);
}
}
In the the component, I need to compare the array of products with the array of campaign products (included in the JSON mantioned above) and higlight the promoted products:
export class ProductCardComponent extends DestroyableComponent implements OnInit, OnChanges
{
public promoData: any;
public promoProducts: any;
public isPromoProduct: boolean = false;
public ngOnInit() {
this.getCampaignData();
}
public ngOnChanges(changes: SimpleChanges): void {
this.getCampaignData();
}
public getPromoData() {
this.promoService.getPromoData().pipe(takeUntil(this.destroyed$)).subscribe(data => {
this.promoData = data;
this.promoProducts = this.promoData.products;
let promoProduct = this.promoProducts.find((product:any) => {
return this.product.unique_identifier == product.unique_identifier;
});
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
}
});
}
}
In the component's html file (template), I have:
<span *ngIf="isPromoProduct" class="promo">Promo</span>
There are no compilation errors.
The problem
For a reason I have been unable to understand, the template does not react to the change of the variable isPromoProduct and the template is not updated, despite the fact that I call the function inside ngOnInit and ngOnChanges.
Questions:
Where is my mistake?
What is a reliable way to update the template?
subscribing to Observable inside .ts file it's mostly not a best practice.
try to avoid it by using async pipe of Angular.
you need to store the observable in the variable and not the data returned from the observable, for example:
// this variable holds the `observable` itself.
this.promoData$ = this.promoService.getPromoData()
and then in the template you can do it like this:
<div *ngIf="promoData$ | async as promoData">
here you can access the promoData
</div>
you can still use pipe() to map the data etc but avoid the subscribe()
The isPromoProduct boolean is not an input. The ngOnChanges gets triggered for changes on your properties that are decorated with the #Input decorator. For your particular case, you can inject the ChangeDetectorRef and trigger change detection manually:
constructor(private cdr: ChangeDetectorRef) {}
// ...
public getPromoData() {
this.promoService.getPromoData().subscribe(data => {
// ...
if (promoProduct) {
// Update boolean
this.isPromoProduct = true;
this.cdr.detectChanges();
}
});
}
You also don't need to manage httpClient subscriptions. The observables generated by a simple get or post request will complete after they emit the response of the request. You only need to explicitly manage the unsubscribe for hot observables (that you create from subjects that you instantiate yourself).
I tried to instantiate a javascript object in a typescript file, basically in an Angular Application but there is an error as object is not defined.
My JavaScript file:
import { EventEmitter } from 'events';
export default class Cursor extends EventEmitter {
constructor(el) {}
//some methods
}
My typescript file
First alternative :
declare var Cursor:any;
export class HomeComponent implements OnInit {
ngOnInit(): void {
const cursor = new Cursor();
console.log(cursor.emit('enter'))
}
}
=> ERROR ReferenceError: Cursor is not defined
Second alternative (importing js file)
import Cursor from '../utils/js/cursor';
export class HomeComponent implements OnInit {
ngOnInit(): void {
const cursor = new Cursor();
console.log(cursor.emit('enter'))
}
}
=> Property 'emit' does not exist on type 'Cursor'.
In that case there is an inheritance problem with class Cursor extending EventEmitter
Can someone give me an hand with that problem ?
Thanks
I'm trying to create a generic service but not a generic database repository.
I already have a generic repository for database logic. This works.
// entity.repository.ts
export abstract class EntityRepository<T extends Document> {
constructor(protected readonly entityModel: Model<T>) {}
async find(): { }
}
Elsewhere I have a UsersRepository that extends the EntityRepository. This works.
// users.repository.ts
#Injectable()
export class UsersRepository extends EntityRepository<UserDocument> {
constructor(#InjectModel(User.name) userModel: Model<UserDocument>) {
super(userModel)
}
}
I want to create a generic service that can take in different instances of the entity repository, so it can be a UsersRepository, CatRepository or DogRepository so it's defined generically here. I'm having trouble defining the type for this generic repository. The actual injected repository here will be an instance of entityRepository - for example, the UsersRepository above should be able to be injected in this EntityService.
// entity.service.ts
#Injectable()
export abstract class EntityService<T extends Document> {
constructor(
protected readonly entityRepostiory: EntityRepository<T>,
private readonly myOtherService1: MyOtherService1,
private readonly MyOtherService2: MyOtherService2,
)
public myMethod() {
// Property 'find' does not exist on type 'EntityRepository<T>'
const user = this.entityRepository.find({}); // <- Type error
}
}
And then I want to create an instance of the above base EntityService. Not sure how to type this out properly either
// cat.service.ts
#Injectable()
export class CatService extends EntityService<CatDocument> { // <- not sure about typing
constructor(
private readonly catRepository: CatRepository<CatDocument>, // <- ???
private readonly myOtherService1: MyOtherService1,
private readonly MyOtherService2: MyOtherService2,
) {
super(catRepository, myOtherService1, myOtherService2) // <- I need to call super on all 3 right?
}
}
Within my anguular app , i ve this service :
#Injectable()
export class myService{
myBehaviouSubject= new BehaviorSubject("");
setData(){
this.myBehaviouSubject.next("123");
}
}
Inside my app.component , i m able to get the value , but i want to keep it readonly or editable only inside the service itself , i want to prevent to push any data from component (.next('DATA'))
#Component({
})
export class AppComponent implements OnInit {
constructor(public myService : MyService) { }
getData(){
// GET VALUE
this.myService.myBehaviouSubject.value
}
unwantedMethodToSetValue(){
// SET VALUE -> i want to prevent this
this.myService.myBehaviouSubject.next("unwanted value")
}
}
Suggestions ?
You can keep the observable inside service only by declaring it as private field of a class.
#Injectable()
export class myService {
private myBehaviouSubject = new BehaviorSubject("");
// Use this observable inside the app component class.
myBehaviouSubjectObservable = myBehaviouSubject.asObservable();
setData() {
this.myBehaviouSubject.next("123");
}
}
#Component({
})
export class AppComponent implements OnInit {
constructor(public myService: MyService) {}
getData() {
// You can subscribe to observable and can get value here
this.myService.myBehaviouSubjectObservable.subscribe((value) => {
console.log(value);
})
}
unwantedMethodToSetValue() {
// SET VALUE -> you cannot do this here now.
this.myService.myBehaviouSubject.next("unwanted value")
}
}
Use property access modifiers:
#Injectable()
export class MyService{
private myValueSubject: BehaviorSubject<string> = new BehaviorSubject<string>("");
public readonly myValueObservable: Observable<string> = this.myValueSubject.asObservable();
public setData() {
this.myValueSubject.next("123");
}
public getData(): string {
return this.myValueSubject.value;
}
}
Instances of MyService will not have a publicly accessible subject.
I usually try to avoid a method like getData, favoring subscriptions to the related observable. If I ever find myself writing those kinds of methods, it's a warning flag to re-evaluate my architecture. If you just want to store a value and get/set it with methods, use a plain old private property. The entire purpose of the subject is defeated if you are only ever getting the value through a method like getData()
Check out the documentation for typescript classes, which discusses access modifiers: https://www.typescriptlang.org/docs/handbook/classes.html
The traditional answer : If you return the Subject as an observable, you disallow .next() calls.
But in your case, you also want direct access to the current value without subscribing, so you could add a getter for that too.
#Injectable()
export class myService{
private readonly myBehaviouSubject = new BehaviorSubject("");
setData(){
this.myBehaviouSubject.next("123");
}
public get myObservable$(): Observable<string>{
return this.myBehaviourSubject;
}
public get currentValue(): string{
return this.myBehaviourSubject.value;
}
}
https://stackblitz.com/edit/angular-protected-rxjs-subject
in this solution which I hope meet you needs:
be aware that there is no subscription
fetching updates handled manually
Property 'myBehaviourSubject' is private and only accessible
within class 'TestService'.
I have a shared service SharedService
#Injectable()
export class SharedService {
private title = new BehaviorSubject<string>("");
currentMessage = this.title.asObservable();
constructor() { }
setData(val: string) {
this.title.next(val);
}
}
I have a component, where I get new data
export class Fill implements OnInit {
public title;
constructor(public shared: SharedService) {
}
ngOnInit() {
this.shared.setData(this.title);
}}
And the component Info, where I want read new data
export class InfoComponent implements OnInit {
constructor(public shared: SharedService) {
this.title = ''; }
ngOnInit() {
console.log('i am title from info ')
this.shared.currentMessage.subscribe(title => this.title = title)
console.log(this.shared.currentMessage);
console.log(this.title);
}}
In both cases of console.log I get Objects, that contains information, that I need - but I can't retrieve value of it. So, on my console it look like
But if try something like
console.log(this.shared.currentMessage.source.source.value);
it says Property 'source' is protected and only accessible within class 'Observable' and its subclasses.
this.shared.currentMessage.value
this.title.value
doesn't work also...
How could I retrieve data (value) of one or another?
Yes. #wostex mentioned it correctly. Keep ngOninit out side of the constructor function and,
this.shared.currentMessage.subscribe(title => {
this.title = title;
console.log(this.title)
})
Update
I suspect currentMessage = this.title.asObservable(); is invalid line inside your SharedService Injectable.
You could write a method to expose this.title expose by currentMessage method like below
#Injectable()
export class SharedService {
private title = new BehaviorSubject<string>("");
//below line was wrong.
//currentMessage = this.title.asObservable();
currentMessage(){
return this.title.asObservable();
}
constructor() { }
setData(val: string) {
this.title.next(val);
}
}
//Usage
console.log(this.shared.currentMessage().getValue());
Yes, you should first take out ngOnInit from constructor function. I supposed you want to retrieve initial state of your BehaviourSubject, so then you could use .getValue() function on that Observable without subscribing to stream.
console.log(this.shared.currentMessage.getValue());
ngOnInit has to be Outside the constructor.
And as pointed by #micronkys
As you are subscribing to a observable the value of this.title is not available in the next line as the subscribing is async and so the title dosent get updated . when you log it.
try and use *ngIf = "title" in the template like this
<div *ngIf = "title">
{{title}}
</div>
UPDATE
this.shared.currentMessage.subscribe((title) => {
this.title = title;
console.log(this.title)
}
Please find a plunker for the problem hope you get the answer now