RxJS: Mimic Angulars #Input() binding? - javascript

Im pretty new to Angular 8 and RxJS and stumbled upon an issue:
I make an angular app which relies heavily on an externally loaded THREE.js scene. Services handle those scenes.
So most of the time there is No HTML Template (only maintaining scene via js object) => no bindings ?
So I was thinking... is there a way to use Rxjs subjects/observables to achieve something like Input() binding?
thats what i basically want to
this.sub = myService.watch('window.deviceorientation')
.subscribe({next(x => { if(x) this.sub.unsubscribe; doStuff();})})
I want to continously check a certain object (any really), be notified as soon as it exists (or instantly if it already does). I bet there is some weird combination of RxJS Operators which can do exactly this?
(So basically its a little bit like AngularJS scope.$watch but to keep performance I'd of course clean up subscriptions.)

You can use the fromEvent rxjs creation utility to achieve the desired effect.
import { fromEvent } from 'rxjs';
orientation$ = fromEvent(window, 'deviceorientation');
const subscription = orientation$.subscribe((event)=>{
//do stuff
});
// when disponse is required
subscription.unsubscribe();

So I worked my way around this.
I created a root-level component that allows me to watch for specific changes on scope:
//HTML
<app-change-detector [watch]="window.anyObject" (onChange)="anyRootService.onObjectChanged($event)">
//TS
#Component({
selector: 'app-change-detector'
})
export class ChangeDetectorComponent implements OnChanges {
#Input() watch: any;
#Output() onChange = new EventEmitter<any>();
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.watch) {
this.onChange.emit(changes.watch.currentValue);
}
}
}

Related

Trigger a function once 2 inputs are set and then afterwards if either of the value changes in Angular

I have a component with 2 inputs (or mor) and I want to:
Trigger a method X the first time, when both value are set and exists
Trigger a method X each time, if either one of the both value changes
<some-cmp [itemid]="activeItemId$ | async" [userId]="activeUserId$ | async"></some-cmp>
Both values can change at any time, so I figured using rxjs to build a stream lets me control everything. My current solution seems a bit hacky and is difficult to test. I use 2 BehaviourSubjects and combineLatest with a debounceTime.
#Input() set itemId (id){this.itemId$.next(id)};
#Input() set userId (id){this.userId$.next(id)};
itemId$ = new BehaviourSubject$(null);
userId$ = new BehaviourSubbject$(null);
ngOnInt(){
combineLatest([
this.itemId$.pipe(filter(item=>item!===null)),
this.userId$.pipe(filter(item=>item!===null))
]).pipe(
debounceTime(10),
switchMap(...)
).subscribe(...)
}
So my question are
Is there a more elegant way to achieve this behavior?
Is there a way to avoid the debounceTime, which makes testing difficult?
The debounceTime is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
You are right in using combineLatest, it will only emit the first time after each source has emitted once and then will emit any time any source emits.
Is there a way to avoid the debounceTime. [It] is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
Maybe debounceTime isn't necessary due to the initial behavior of combineLatest; it will not emit the first time until all sources emit. However if you typically receive subsequent emissions from both sources that occur within a short timeframe, use of debounceTime may be an appropriate optimization.
Is there a more elegant way to achieve this behavior?
I think your code is fine. However, it may not be necessary to use BehaviorSubject since you aren't really using the default value. You could use plain Subject or ReplaySubject(1).
You could assign the result of your combineLatest to another variable and subscribe to that inside ngOnInit or use the async pipe in the template:
#Input() set itemId (id){ this.itemId$.next(id) };
#Input() set userId (id){ this.userId$.next(id) };
itemId$ = new Subject<string>();
userId$ = new Subject<string>();
data$ = combineLatest([
this.itemId$.pipe(filter(i => !!i)),
this.userId$.pipe(filter(i => !!i))
]).pipe(
debounceTime(10),
switchMap(...)
);
ngOnInit() {
this.data$.subscribe(...);
}
Angular provides ngOnChanges hook which can be used in this scenario. It'll trigger ngOnChanges method whenever there's a change in any of the inputs of the component.
Below is an example of how this can be achieved:
export class SomeComponent implements OnChanges {
#Input() itemId: any;
#Input() userId: any;
ngOnChanges(changes: SimpleChanges) {
const change = changes.itemId || changes.userId;
if (change && change.currentValue !== change.previousValue) {
this.doSomething();
}
}
private doSomething() {
// Your logic goes here
}
}
Your HTML will now look clean and you can get rid of async as well:
<some-cmp [itemid]="itemId" [userId]="userId"></some-cmp>

Simple state management in Angular?

I'm learning Angular from the very beginning, and coming from React, I can't find a way to manage a simple state in Angular. It's a simple todo app, and my strategy is to create a Todos array in todo.service.ts
#Injectable()
export class TodoService {
todos: Todo[] = [
{ id: 0, title: 'Cooking', completed: false }
]
nextId: number = 1;
constructor() { }
public getTodos(): Todo[] {
return this.todos;
}
public addTodo(title: string): any {
this.newTodo = new Todo({ id: this.nextId, title, completed: false })
this.todos.push(this.newTodo);
this.newTodo = new Todo();
this.nextId++
}
public deleteTodo(chosenTodo: Todo): any {
this.todos = this.todos.filter(todo => todo.id !== chosenTodo.id)
}
}
Now on the ListingComponent, I want to get all Todos, and then pass it down to ListingItemComponent, as such
todos: Todo[] = []
todoTitle: string = '';
constructor(private todoService: TodoService) { }
ngOnInit(): void {
this.todos = this.todoService.getTodos();
this.todoTitle = 'Do Something';
}
addTodo() {
this.todoService.addTodo(this.todoTitle);
}
<div *ngFor="let todo of todos">
<todo-listing-item [todo]="todo"></todo-listing-item>
</div>
/* This just adds a todo with title 'Do Something' for testing */
<button (click)="addTodo()">Add Todo</button>
Finally in ListingItemComponent
#Input() todo: Todo
constructor(private todoService: TodoService) { }
ngOnInit(): void {
}
deleteTodo(): void {
this.todoService.deleteTodo(this.todo)
}
<h1>{{todo.title}}</h1>
<button (click)="deleteTodo()">Delete Todo</button>
Now the component has no error, and I made sure to declare everything, but it's not working. Clicking the button won't change anything. After digging in, I changed the ListingComponent into "let todo of todoService.getTodos()", and remove the todos in the ts file along with the ngOnInit, and somehow it works. My guessing is that the ListingComponent doesn't know that the todos array has changed, and so it's not rerendering?
So if I was to initialize the todos in the ngOnInit like I did before, do I need some other life-cycle to listen to the change of todos?
If many components need the todos array, should I write ngOnInit(): void {
this.todos = this.todoService.getTodos();} on every single one of them? And if one component changes their todos, can other components listen and rerender as well?
Thank you for reading. In React I would just create a global state, along with the functions, and pass it as props down to every child. But I can't wrap my head around managing state in Angular. Is it because I'm thinking in React, while I should not?
Let me first say that I'm a huge fan of NGRX and the pattern, and I suggest you make this part of your learning path. However, if you're looking for simple state management than a simple Observable will do the trick.
I created a stack overflow to illustrate the technique using the code you provided:
https://stackblitz.com/edit/angular-ivy-kfw1sd. In this demonstration, I'm leveraging BehaviorSubject which is a special type of observable.
In closing, do your best to avoid .subscribe() and use the async pipe instead.
Hope this helps,
Isaac
If you want to have a simple global state, you can start using Shared Services, obviously it will be only for learning purposes, because if you want to use in a real project, you can use Shared Services, but also, you would need Facade or other Design Patterns.
Main advantage using Facade is that you can migrate to NgRx easier.
I have an example using API + State + Base Architecture: https://github.com/cmandamiento/angular-architecture-base
Feel free to reach me out.
Indeed you do not always need to add another Library to manage state in Angular. Angular comes with RxJS out of the box...
All the well known state management libraries use RxJS/BehaviorSubject internally. This article shows how to write a simple DIY state management Class which can be extended by Angular services.
Simple yet powerful state management in Angular with RxJS
This solution is also based on RxJS/BehaviorSubject.
Maybe it can be useful for you. The code example in the article is a Todo App :)
Disclaimer: I'm the author of the library
ng-simple-state
Simple state management in Angular with only Services and RxJS
See the stackblitz demo.
See the source code.

Call method after view (DOM) is updated (in Angular 2)

Assume that I have the following directive:
import {Directive, Input, OnChanges } from '#angular/core'
#Directive({
selector: 'select[my-select]'
})
export class NbSelect implements OnChanges {
#Input() ngModel;
constructor() {
}
ngOnChanges(changes) {
doSomeStuffAfterViewIsUpdated();
}
doSomeStuffAfterViewIsUpdated() {
console.log(jQuery('select[my-select]').val()); // this should write the new value
}
}
I'm using it in somewhere in a template: ...<select my-select [(ngModel)]="someReference">...
At some point I change the value of someReference in a component: someReference = "newValue"
What is happening now: The doSomeStuffAfterViewIsUpdated function is called before the DOM is updated. Which means the console shows the old value of the select.
What I want: The doSomeStuffAfterViewIsUpdated function should be called after the DOM is updated by angular, so the console shows the "newValue".
What should I use instead of ngOnChanges to make it work like I want?
Please note that the important thing here is that the code should run after the DOM update. The question is not about how can I access the new value of the element.
Thx!
I'm a bit late to the party, but since I had a similar problem and a probable solution, I thought I'd share:
What you're looking for is MutationObservers (CanIUse);
I solved a very similar issue with a directive similar to this (simplified for... simplicity? :P):
ngOnChanges() {
this.observer = new MutationObserver(() => { this.methodToCall() });
this.observer.observe(this.elRef.nativeElement, this.observerOptions); // See linked MDN doc for options
}
private methodToCall(): void {
this.observer.disconnect();
}
ngOnDestroy() {
if(this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
The MutationObserver callback will be triggered when either an attribute, children, etc is modified in the DOM (or added, deleted, etc)
It is possible you won't need the ngOnChanges at all depending on your use case and it would make the code a bit cleaner too.

Angular 2: How to detect changes in an array? (#input property)

I have a parent component that retrieves an array of objects using an ajax request.
This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an #input property and they display the content properly. Everything as expected.
The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.
I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.
I've read something about DoCheck but I'm not sure how it works.
OnChanges Lifecycle Hook will trigger only when input property's instance changes.
If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = iterableDiffers.find([]).create(null);
}
ngDoCheck() {
let changes = this.iterableDiffer.diff(this.inputArray);
if (changes) {
console.log('Changes detected!');
}
}
If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).
Visit this post for more information: Detect changes in objects inside array in Angular2
You can always create a new reference to the array by merging it with an empty array:
this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [].concat(this.yourArray);
The code above will change the array reference and it will trigger the OnChanges mechanism in children components.
Read following article, don't miss mutable vs immutable objects.
Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.
I use redux store in one of my projects to avoid this kind of issues.
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
You can use IterableDiffers
It's used by *ngFor
constructor(private _differs: IterableDiffers) {}
ngOnChanges(changes: SimpleChanges): void {
if (!this._differ && value) {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
}
}
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}
It's work for me:
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {
#Input() changeArray: MyClassArray[]= [];
private differ: IterableDiffers;
constructor(private differs: IterableDiffers) {
this.differ = differs;
}
ngDoCheck() {
const changes = this.differ.find(this.insertedTasks);
if (changes) {
this.myMethodAfterChange();
}
}
This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant.
When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.
Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)
#Pipe({
name: 'arrayChangeDetector',
pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
private differ: IterableDiffer<any>;
constructor(iDiff: IterableDiffers) {
this.differ = iDiff.find([]).create();
}
transform(value: any[]): any[] {
if (this.differ.diff(value)) {
return [...value];
}
return value;
}
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>
For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.
https://stackblitz.com/edit/array-value-changes-not-detected-ang-8
Solutions include:
NgDoCheck
Using a Pipe
Using Immutable JS NPM github

Add element with RouterLink dynamically

When I put an anchor element in somewhere in an Angular component like this:
<a [routerLink]="['/LoggedIn/Profile']">Static Link</a>
everything is working fine. When clicking the link, the Angular router navigates to the target component.
Now, I would like to add the same link dynamically. Somewhere in my app I have a "notification component", its single responsibility is to display notifications.
The notification component does something like this:
<div [innerHTML]="notification.content"></div>
Where notification.content is a public string variable in the NotificationComponent class that contains the HTML to display.
The notification.content variable can contain something like:
<div>Click on this <a [routerLink]="['/LoggedIn/Profile']">Dynamic Link</a> please</div>
Everything works fine and shows up on my screen, but nothing happens when I click the dynamic link.
Is there a way to let the Angular router work with this dynamically added link?
PS: I know about DynamicComponentLoader, but I really need a more unrestricted solution where I can send all kinds of HTML to my notification component, with all kind of different links.
routerLink cannot be added after the content is already rendered but you can still achieve the desired result:
Create a href with dynamic data and give it a class:
`<a class="routerlink" href="${someDynamicUrl}">${someDynamicValue}</a>`
Add a HostListener to app.component that listens for the click and uses the router to
navigate
#HostListener('document:click', ['$event'])
public handleClick(event: Event): void {
if (event.target instanceof HTMLAnchorElement) {
const element = event.target as HTMLAnchorElement;
if (element.className === 'routerlink') {
event.preventDefault();
const route = element?.getAttribute('href');
if (route) {
this.router.navigate([`/${route}`]);
}
}
}
}
routerLink is a directive. Directives and Components are not created for HTML that is added using [innerHTML]. This HTML is not process by Angular in any way.
The recommended way is to not use [innerHTML] but DynamicComponentLoaderViewContainerRef.createComponent where you wrap the HTML in a component and add it dynamically.
For an example see Angular 2 dynamic tabs with user-click chosen components
Since angular 9, AOT is the default recommended way to compile angular projects.
Unlike JIT, AOT doesn't hold an instance for the compiler at runtime, which means you can't dynamically compile angular code.
It's possible to disable AOT in angular 9, but it's not recommended as your bundle size will be bigger and your application slower.
The way I solve this is by adding a click listener at runtime using renderer api, preventing the default behavior of urls and calling angular router
import { Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '#angular/core';
import { Router } from '#angular/router';
#Directive({
selector: '[hrefToRouterLink]'
})
export class HrefToRouterLinkDirective implements OnInit, OnDestroy {
private _listeners: { destroy: () => void }[] = [];
constructor(private _router: Router,
private _el: ElementRef,
private _renderer: Renderer2) {
}
ngOnInit() {
// TODO how to guarantee this directive running after all other directives without setTimeout?
setTimeout(() => {
const links = this._el.nativeElement.querySelectorAll('a');
links.forEach(link => {
this._renderer.setAttribute(link, 'routerLink', link?.getAttribute('href'));
const destroyListener = this._renderer.listen(link, 'click',
(_event) => {
_event.preventDefault();
_event.stopPropagation();
this._router.navigateByUrl(link?.getAttribute('href'));
});
this._listeners.push({ destroy: destroyListener });
});
}, 0);
}
ngOnDestroy(): void {
this._listeners?.forEach(listener => listener.destroy());
this._listeners = null;
}
}
You can find an example here : https://stackblitz.com/edit/angular-dynamic-routerlink-2
Obviously the method explained above work for both JIT & AOT, but If you are still using JIT and want to dynamically compile component (which may help solve other problems) . You can find an example here :
https://stackblitz.com/edit/angular-dynamic-routerlink-1
Used resources :
https://stackoverflow.com/a/35082441/6209801
https://indepth.dev/here-is-what-you-need-to-know-about-dynamic-components-in-angular
Combining some of the other answers - I wanted this as a Directive so I could target specific elements that are being innerHTML'd, but to avoid using querySelector (etc) to keep everything Angulary.
I also found an issue with the approaches above, in that if the href is a full URL (i.e, https://www.example.com/abc) feeding that whole thing to the router would result in navigating to /https.
I also needed checks to ensure we only router'd hrefs that were within our domain.
#Directive({
selector: '[hrefToRouterLink]'
})
export class HrefToRouterLinkDirective {
constructor(private _router: Router){}
private _baseHref = quotemeta(environment.root_url.replace(`^https?://`, ''));
private _hrefRe: RegExp = new RegExp(`^(https?:)?(\\/+)?(www\\.)?${this._baseHref}`, `i`);
#HostListener('click', ['$event'])
onClick(e) {
// Is it a link?
if (!(e.target instanceof HTMLAnchorElement))
return;
let href: string = e.target?.getAttribute('href')
.replace(/(^\s+|\s+$)/gs, '');
// Is this a URL in our site?
if (!this._hrefRe.test(href))
return;
// If we're here, it's a link to our site, stop normal navigation
e.preventDefault();
e.stopPropagation();
// Feed the router.
this._router.navigateByUrl(
href.replace(this._hrefRe, '')
);
}
}
In the above environment.root_url describes our base domain, and quotemeta is a rough implementation of a Perl-ish quotemeta function just to escape special characters.
YMMV and I've definitely missed some edge cases, but this seems to work fine.

Categories

Resources