Subscribing in angular service constructor brakes my drag and drop functionality - javascript

I have a drag-and-drop.service that depends on lists.service.
The d&d service looks like this:
#Injectable({
providedIn: "root"
})
export class DragAndDropService {
private lists: any[] = null;
private dropListSubject = new BehaviorSubject<IDropListState>({
Lists: this.lists,
IsOpen: false,
Delay: 0
});
constructor(private ls: ListsService) {
// TODO: Subscription breakes the drag and drop functionallity
this.ls.onListsChanged().subscribe(x => {
this.lists = x;
console.log(x);
this.emitDropListState();
});
}
onDropListStateChange() {
return this.dropListSubject.asObservable();
}
open() {
this.emitDropListState(true);
}
close(delay: number) {
this.emitDropListState(false, delay);
}
add(id: string) {
this.ls.addNewList(id);
this.emitDropListState();
}
remove(listId: string) {
this.ls.deleteList(listId);
this.emitDropListState();
}
emitDropListState(isOpen = false, delay = 0) {
this.dropListSubject.next({
Lists: this.lists,
IsOpen: isOpen,
Delay: delay
});
}
}
the lists.service looks like this:
#Injectable({
providedIn: "root"
})
export class ListsService {
private listsFromStorage: BehaviorSubject<any[]>;
private lists: any[];
private STORAGE_LIST_NAME = "lists";
constructor() {
this.lists = JSON.parse(
window.localStorage.getItem(this.STORAGE_LIST_NAME)
);
this.listsFromStorage.asObservable();
this.listsFromStorage = new BehaviorSubject<any[]>(this.lists);
}
onListsChanged() {
return this.listsFromStorage.asObservable();
}
deleteList(listName: string) {
this.lists = this.lists.filter(x => {
return x.Name !== listName.trim();
});
this.saveAndEmmit();
}
addNewList(id: string) {
//TODO: List adding todo
console.log("TODO: Add List: id-", id);
this.saveAndEmmit();
}
saveAndEmmit() {
window.localStorage.setItem(
this.STORAGE_LIST_NAME,
JSON.stringify(this.lists)
);
this.listsFromStorage.next(this.lists);
}
}
NOW: When i use the angular material d&d stuff, this error is thrown:
When I remove the subscription in the d&d service, it doesn't throw the error.
Is this approach reasonable or how else can I imitate this behaviour ?
Thanks in advance!

Related

How to hold alert box on the page in case of reloading or changing route?

I need to maintain an alert box on the Registration page indicating the user has registered successfully. However, by redirecting to the Login form this box disappears, because the page refreshes.
I utilize the Alert component to manage this scenario. All of the features work flawlessly but this problem really makes me confused. I shared my code and hope you assist me in getting to the root of this predicament.
alert.component.ts
import { Component, OnInit, OnDestroy, Input } from '#angular/core';
import { Router, NavigationStart } from '#angular/router';
import { Subscription } from 'rxjs';
import { Alert, AlertType } from 'src/app/_models/alert';
import { AlertService } from 'src/app/_services/alert.service';
#Component({ selector: 'alert',
templateUrl: 'alert.component.html',
styleUrls: ['./alert.component.scss'] })
export class AlertComponent implements OnInit, OnDestroy {
#Input() id = 'default-alert';
#Input() fade = true;
alerts: Alert[] = [];
alertSubscription: Subscription;
routeSubscription: Subscription;
constructor(private router: Router, private alertService: AlertService) { }
ngOnInit() {
// subscribe to new alert notifications
this.alertSubscription = this.alertService.onAlert(this.id)
.subscribe(alert => {
// clear alerts when an empty alert is received
if (!alert.message) {
// filter out alerts without 'keepAfterRouteChange' flag
this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);
// remove 'keepAfterRouteChange' flag on the rest
this.alerts.forEach(x => delete x.keepAfterRouteChange);
return;
}
// add alert to array
this.alerts.push(alert);
setTimeout(() => this.removeAlert(alert), 5000);
});
// clear alerts on location change
this.routeSubscription = this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this.alertService.clear(this.id);
}
});
}
ngOnDestroy() {
// unsubscribe to avoid memory leaks
this.alertSubscription.unsubscribe();
this.routeSubscription.unsubscribe();
}
removeAlert(alert: Alert) {
// check if already removed to prevent error on auto close
if (!this.alerts.includes(alert)) return;
if (this.fade) {
// fade out alert
this.alerts.find(x => x === alert).fade = true;
// remove alert after faded out
setTimeout(() => {
this.alerts = this.alerts.filter(x => x !== alert);
}, 250);
} else {
// remove alert
this.alerts = this.alerts.filter(x => x !== alert);
}
}
cssClass(alert: Alert) {
if (!alert) return;
const classes = ['toast'];
const alertTypeClass = {
[AlertType.Success]: 'toast-success',
[AlertType.Error]: 'toast-error',
[AlertType.Info]: 'toast-info',
[AlertType.Warning]: 'toast-warning'
}
classes.push(alertTypeClass[alert.type]);
if (alert.fade) {
classes.push('fade');
}
return classes.join(' ');
}
}
alert.service.ts
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Alert, AlertType } from '../_models/alert';
#Injectable({ providedIn: 'root' })
export class AlertService {
private subject = new Subject<Alert>();
private defaultId = 'default-alert';
// enable subscribing to alerts observable
onAlert(id = this.defaultId): Observable<Alert> {
return this.subject.asObservable().pipe(filter(x => x && x.id === id));
}
// convenience methods
success(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Success, message }));
}
error(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Error, message }));
}
info(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Info, message }));
}
warn(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Warning, message }));
}
// main alert method
alert(alert: Alert) {
alert.id = alert.id || this.defaultId;
this.subject.next(alert);
}
// clear alerts
clear(id = this.defaultId) {
this.subject.next(new Alert({ id }));
}
}
This is a piece of code in which an alert message is called (It should be noted that the keepAfterRouteChange is set to True):
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.form.invalid) {
return;
}
this.loading = true;
this.accountService
.register(this.form.value)
.pipe(first())
.subscribe((data) => {
this.loading = false;
this.submitted = false;
if (data.hasError) {
this.alertService.error(data.errorMessage);
} else {
this.alertService.success('Registration successfully completed.', {
keepAfterRouteChange: true,
});
localStorage.setItem('regCount',JSON.parse(localStorage.getItem('regCount')) + 1);
this.router.navigate(['/login']).then(() => {
window.location.reload();
});
}
},
() => {
this.loading = false;
this.submitted = false;
this.alertService.error('Something went wrong.');
});
}
Your problem probably comes from window.location.reload(); when window is reloaded all components and services are flushed. Find other ways to clear services if that's the point this line. Or find other way to store info that alert should be showing (e.g storing the need to show an alert with info and duration in SessionStorage or LocalStorage) - which doesn't seem like a good idea though. Normally we want to avoid reloading windows - for the same reason, losing all data and forcing the client to reload all resources.

Observable/BehaviorSubject not working in different modules of Angular

I have 1 feature module (Fund module) that displays fund data in one of its components and an AppModule that displays Advisor data. Fund data and Advisor data have one to many relationship.
Fund component gets data from services defined in the AppModule.
Data are passed to the Fund module using BehaviorSubject as data might change if the user updates or modifies the data in AppModule.
The subscription of the data in not working correctly. The fund module only displays the value (10) that the BehaviorSubject is initialized with. It doesn't display the updated value (100), nor does the subscription work properly to display the values second time.
Here's the code:
Service in AppModule:
test = new BehaviorSubject<number>(10);
getTest(): Observable<number> {
return this.test.asObservable();
}
public updateLocalData(sleeves: Sleeve[]): void {
.... update logic
this.test.next(100);
}
FundDataComponent in Fund Module
changeDetection: ChangeDetectionStrategy.OnPush,
ngOnInit(): void {
this.test = this.service.getTest().subscribe((number) => {
this.number = number;
console.log(number); // get's called only once to display 10
});
}
Create a model - resource.ts:
import { BehaviorSubject, Observable } from 'rxjs';
import { refCount, publishReplay } from 'rxjs/operators';
export class StreamResource {
private loading = false;
private behaviorSubject: BehaviorSubject<any>;
public readonly obs: Observable<any>;
constructor(defaultValue?: any) {
this.behaviorSubject = new BehaviorSubject(defaultValue);
this.obs = this.behaviorSubject.asObservable().pipe(publishReplay(1), refCount());
}
request(method: any): void {
if (method && !this.loading) {
this.loading = true;
method().toPromise().then((data: any) => {
if (data) { this.update(data); }
this.loading = false;
});
}
}
getValue() {
return this.behaviorSubject.getValue();
}
update(data: any) {
this.behaviorSubject.next(data);
}
refresh() {
const data = this.getValue();
if (data) { this.update(data); }
}
}
Create a StreamService
import { StreamResource } from '../models/resource';
public test = new StreamResource(10);
...
getTest() {
this.test.request(() => this.testService.getTest());
}
How to use?
constructor(private streamService: StreamService) { }
ngOnInit() {
this.streamService.test.obs.subscribe((data: any) => {
if (!data) {
return this.streamService.getTest();
} else {
this.test = data;
}
});

Push on an array when listening to an event

I am working with Ionic and I want to push an array of an object, when an event is emitted.
I have this
export class PublicationService {
constructor(
private storage: Storage
){}
private addPublicationSubject = new BehaviorSubject<PublicationModel>(new PublicationModel());
data = this.addPublicationSubject.asObservable();
publishData(data: PublicationModel) {
this.addPublicationSubject.next(data);
}
}
Here, event is emitted
savePublication() {
this.newPublication.id_pub = 1;
this.newPublication.textPub = this.f.text.value;
this.newPublication.user_sender = 'Juance';
this.newPublication.datetime = "asd";
this.pubService.publishData(this.newPublication);
}
And on my home page the event is listen (in ngOnInit)
// Variable defined in the component
publications: PublicationModel[] = [];
//ngOnInit
this.pubService.data.subscribe((data) => {
if (data != null) {
console.log(data);
this.publications.push(data);
}
});
Now my problem is: when I try to push the data into the array it tells me it cannot read property of null (this.publications).
When entering the subscribe of the event, it does not take the variable as defined in the component. Any ideas?
EDIT:
My component HomePage
#Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {
viewAddPublication: boolean;
publications: PublicationModel[] = [];
countLikePub: number;
addPublication: any;
constructor(
private storage: Storage,
private navCtrl: NavController,
private pubService: PublicationService) {
this.publications = new Array<PublicationModel>();
}
ngOnInit() {
this.publications = [];
this.pubService.data.subscribe((data) => {
if (data != null) {
console.log(data);
this.setData(data);
}
}
);
this.viewAddPublication = false;
this.countLikePub = 0;
this.storage.get('publications').then((val) => {
this.publications = val;
});
}
setData(data) {
this.publications.push(data);
}
goToAddPub() {
this.navCtrl.navigateForward('/add-publication', {
animated: true,
animationDirection: "forward",
});
}
public likedPost(event) {
console.log();
let like = (document.getElementById(event.target.id) as HTMLElement);
like.style.color = '#0277bd';
this.countLikePub++;
}
debug mode in chrome
I need a way to push an array in real time and this is the only way I could think of, the other is to use Socket.io
I think maybe you are setting publications to null because of this function:
this.storage.get('publications').then((val) => {
this.publications = val;
});
You could change it a little bit to make sure publications are still an array
this.storage.get('publications').then((val) => {
this.publications = val || [];
});
I added this.publications = val || []; which is creating an empty array if val is not defined

How to solve time raise problems in Angular?

I am writing an Angular Service to prove a Users permissions. In the constructor I want to get the current logged in user from an API. The current User which is created is used in other methods of this Service. This Methods are called from components to check which things can be shown and so on.
The problem is that the methods in the service are called faster than the current user is available.
Are there any possibilities solving this issue?
permission.service.ts
#Injectable()
export class PermissionService {
currUser = {
'id': "",
'permission': ""
};
apiService: AlfrescoApiService;
authService: AuthenticationService;
constructor(apiService: AlfrescoApiService, authService: AuthenticationService) {
this.apiService = apiService;
this.authService = authService;
this.init();
}
init() {
let userId: string = this.authService.getEcmUsername();
this.currUser.id = userId;
//API call
this.apiService.sitesApi.getSiteMember(SITENAME, userId).then(resp => {
this.currUser.permission = resp.entry.role;
})
}
isSiteManager(): boolean {
console.log(this.currUser.permission, this.currUser);
if(this.currUser.permission === "SiteManager"){
return true;
}else{
return false;
}
}
}
method call
export class AppLayoutComponent {
constructor(permissionService:PermissionService) {
permissionService.isSiteManager();
}
}
output in Google Chrome
{id: "admin", permission: ""}
id: "admin"permission: "SiteManager"
You should use promise in your getEcmUsername() handle this; after that you can code like this
`
this.authService.getEcmUsername().then((userID) => {
this.apiService.sitesApi.getSiteMember(SITENAME, userId).then(resp => {
this.currUser.permission = resp.entry.role;
})
});
`
In my opinion better solution is to use Observable here and rxjs. In service you can create Subject and subscribe it inside your compnent, to be sure data is already there. E.g.:
#Injectable()
export class PermissionService {
public Subject<bool> userFetched= new Subject<bool>();
currUser: IUser = {
'id': "",
'permission': ""
};
apiService: AlfrescoApiService;
authService: AuthenticationService;
constructor(apiService: AlfrescoApiService, authService: AuthenticationService) {
this.apiService = apiService;
this.authService = authService;
this.init();
}
init() {
let userId: string = this.authService.getEcmUsername();
this.currUser.id = userId;
//API call
this.apiService.sitesApi.getSiteMember(SITENAME, userId).subscribe((data:IUser)=>
{
this.user=data;
this.userFetched.next(true);
})
}
isSiteManager(): boolean {
console.log(this.currUser.permission, this.currUser);
if(this.currUser.permission === "SiteManager"){
return true;
}else{
return false;
}
}
}
After that in your component:
export class AppLayoutComponent {
constructor(permissionService:PermissionService) {
permissionService.userFetched.subscribe((data)=>{
permissionService.isSiteManager();
});
}
}
It's better approach. You need to consider if better is Subject or BehaviourSubject.
Thanks to everybody who answered. I have found a solution. I have changed the isSiteManager() method to a method which checks all four permissiontypes. This method is executed in the then() block and affects four variables for each permissiontype. These variables i can reach from other components.
Looks like this:
#Injectable()
export class PermissionService {
isSiteManager: boolean;
isSiteConsumer: boolean;
isSiteContributor: boolean;
isSiteCollaborator: boolean;
userId : string;
constructor(private apiService: AlfrescoApiService, private authService: AuthenticationService) {
this.init();
}
init() {
this.isSiteCollaborator = false;
this.isSiteConsumer = false;
this.isSiteContributor = false;
this.isSiteManager = false;
this.userId = localStorage.USER_PROFILE;
//proof permission of user
this.apiService.sitesApi.getSiteMember(SITENAME, this.userId).then(resp=>{
if(resp.entry.role === "SiteManager"){
this.isSiteManager = true;
}else if(resp.entry.role === "SiteConsumer"){
this.isSiteConsumer = true;
}else if(resp.entry.role === "SiteContributor"){
this.isSiteContributor = true;
}else{
this.isSiteCollaborator = true;
}
});
}
}
Now i can ask for the variables in other components like this:
export class AppLayoutComponent {
constructor(private permissionService : PermissionService) {
if(permissionService.isSiteManager){
console.log("You are Boss!");
}
}
}
You should call your service method synchrony. To do so, you have to map your response from the service:
your component code:
constructor() {
...
permissionService.isSiteManager().map(
response => {
isManager = response;
}
);
}
Something like this.
To call map operator import it before:
import 'rxjs/add/operator/map';

Typescript thinks response array is an object

I have an Observable stream that obviously outputs an array into the subscribe block. I am trying to assign the response to an array variable to render the autocompletion results. Everything works fine except for the typescript error.
Autocomplete Component:
#Component({
selector: 'auto-complete',
styleUrls: ['auto-complete.component.scss'],
template: `
<div class="ac-container">
<div class="ac-input">
<input type="text" [(ngModel)]="query" (keyup)="filter()">
</div>
<div class="ac-results" *ngIf="results.length > 0">
<ul *ngFor="let item of results">
<li>
<a (click)="select(item)">{{item}}</a>
</li>
</ul>
</div>
</div>
`
})
export class AutoCompleteComponent {
#Input() fn: Function;
public query = '';
public results = [];
filter() {
let value = Observable
.from(this.query)
.throttleTime(20)
.map(value => value)
.distinctUntilChanged()
.map(search => this.fn(search))
.switch()
.subscribe(response => {
this.results = response;
}, error => {}
);
}
}
Parent Component:
#Component({
selector: 'auto-completor',
template: `<auto-complete [fn]="apiSearch"></auto-complete>`
})
export class AppComponent implements OnInit {
public results: any;
constructor(
private service: AppService
) {}
public apiSearch(term) {
return this.service.getSearchData(term);
}
ngOnInit() {
this.apiSearch = this.apiSearch.bind(this);
}
}
Error:
IED Error Indication:
I wish I could show examples of things I tried but all I did was googled. I have no idea. Thanks.
Edit / Additions
Fake DB/Http Response
import { Injectable } from '#angular/core';
#Injectable()
export class Database {
private FAILURE_COEFF = 10;
private MAX_SERVER_LATENCY = 200;
private getRandomBool(n) {
var maxRandomCoeff = 1000;
if (n > maxRandomCoeff) n = maxRandomCoeff;
return Math.floor(Math.random() * maxRandomCoeff) % n === 0;
}
public getSuggestions(text) {
var pre = 'pre';
var post = 'post';
var results = [];
if (this.getRandomBool(2)) {
results.push(pre + text);
}
if (this.getRandomBool(2)) {
results.push(text);
}
if (this.getRandomBool(2)) {
results.push(text + post);
}
if (this.getRandomBool(2)) {
results.push(pre + text + post);
}
return new Promise((resolve, reject) => {
var randomTimeout = Math.random() * this.MAX_SERVER_LATENCY;
setTimeout(() => {
if (this.getRandomBool(this.FAILURE_COEFF)) {
reject();
} else {
resolve(results);
}
}, randomTimeout);
});
}
}
App Service Converting the promise response to Observable
export class AppService {
constructor(
private database: Database
) {}
public getSearchData(term) {
return Observable.defer(() => {
return Observable.fromPromise(this.database.getSuggestions(term)
.then(function(res) {
return res;
})
);
})
}
}
The problem is that the Observable is not typed, because your function fn has no call signature. I cannot check it at the moment, but if you would give fn a lambda expression call signature, the observable will probably take over it's types, enabling you to assign it to results.
#Input() fn: (string) => string[];
Alternatively, you could type results as any, but that's just a quick and very dirty workaround to remove types altogether.

Categories