Skip to content

Instantly share code, notes, and snippets.

@asdacap
Created March 20, 2018 09:38
Show Gist options
  • Save asdacap/666af575b2fcbe09cf180bd54dfc3c28 to your computer and use it in GitHub Desktop.
Save asdacap/666af575b2fcbe09cf180bd54dfc3c28 to your computer and use it in GitHub Desktop.
Easy Angular RxJS view binder/resolver.
import {
Component, ComponentFactoryResolver, Directive, Input, OnDestroy, OnInit, TemplateRef,
ViewContainerRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { ReplaySubject } from 'rxjs/ReplaySubject';
/**
* Do you love reactive streams? But hate subscribing to them, setting them to a local
* variable in your Angular component, and then making sure its unsubscribed later?
*
* Well, then this directive will reduce your boilerplate.. a bit. Its like the async
* pipe operator, but in a form of a structural directive allowing you to access the
* resulting data from the observable. And of course, when your observable send more
* data it will rerender it. In another word, you can do something like:
*
* <ng-template #customLoader>
* user is loading....
* </ng-template>
* <ng-container *appBindObservable="let user of user$; loadingTemplate: customLoader">
* <dl>
* <dt>Name: </dt>
* <dd>{{user.name}}</dd>
* <dt>Address: </dt>
* <dd>{{user.address}}</dd>
* </dl>
* </ng-container>
*
* where `user$` is an observable that results in the user object. When the observable
* is not resolved yet, the template specified by `loadingTemplate` will be used.
* If `loadingTemplate` is not specified, it will render 'Loading...'.
*/
@Component({
template: `Error loading resource: {{error.message}}`
})
export class BindObservableErrorComponent {
@Input()
error: Error = null;
constructor() { }
}
@Component({
template: `Loading...`
})
export class BindObservableLoadingComponent {
constructor() {
}
}
class BindObservableContext<T> {
constructor(public $implicit: T) {}
}
@Directive({
selector: '[appBindObservable][appBindObservableOf]'
})
export class BindObservableDirective<T> implements OnDestroy, OnInit {
observableSubject = new ReplaySubject<Observable<T>>(1);
subscription = new Subscription();
loadingTemplate: TemplateRef<{}>;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver
) {
}
ngOnInit(): void {
this.renderLoading();
this.subscription = this.observableSubject
.distinctUntilChanged()
.flatMap((it) => it)
.distinctUntilChanged()
.subscribe((it) => {
this.renderItem(it);
}, (err) => {
console.error(err);
this.renderError(err);
});
}
@Input()
set appBindObservableOf(ob: Observable<T>) {
this.observableSubject.next(ob);
}
@Input()
set appBindObservableLoadingTemplate(template: TemplateRef<{}>) {
this.loadingTemplate = template;
}
renderLoading() {
if (this.loadingTemplate) {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.loadingTemplate, {});
} else {
const componentFactory = this.componentFactoryResolver.resolveComponeimport {
Component, ComponentFactoryResolver, Directive, Input, OnDestroy, OnInit, TemplateRef,
ViewContainerRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { ReplaySubject } from 'rxjs/ReplaySubject';
/**
* Do you love reactive streams? But hate subscribing to them, setting them to a local
* variable in your Angular component, and then making sure its unsubscribed later?
*
* Well, then this directive will reduce your boilerplate.. a bit. Its like the async
* pipe operator, but in a form of a structural directive allowing you to access the
* resulting data from the observable. And of course, when your observable send more
* data it will rerender it. In another word, you can do something like:
*
* <ng-template #customLoader>
* user is loading....
* </ng-template>
* <ng-container *appBindObservable="let user of user$; loadingTemplate: customLoader">
* <dl>
* <dt>Name: </dt>
* <dd>{{user.name}}</dd>
* <dt>Address: </dt>
* <dd>{{user.address}}</dd>
* </dl>
* </ng-container>
*
* where `user$` is an observable that results in the user object. When the observable
* is not resolved yet, the template specified by `loadingTemplate` will be used.
* If `loadingTemplate` is not specified, it will render 'Loading...'.
*/
@Component({
template: `Error loading resource: {{error.message}}`
})
export class BindObservableErrorComponent {
@Input()
error: Error = null;
constructor() { }
}
@Component({
template: `Loading...`
})
export class BindObservableLoadingComponent {
constructor() {
}
}
class BindObservableContext<T> {
constructor(public $implicit: T) {}
}
@Directive({
selector: '[appBindObservable][appBindObservableOf]'
})
export class BindObservableDirective<T> implements OnDestroy, OnInit {
observableSubject = new ReplaySubject<Observable<T>>(1);
subscription = new Subscription();
loadingTemplate: TemplateRef<{}>;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver
) {
}
ngOnInit(): void {
this.renderLoading();
this.subscription = this.observableSubject
.distinctUntilChanged()
.flatMap((it) => it)
.distinctUntilChanged()
.subscribe((it) => {
this.renderItem(it);
}, (err) => {
console.error(err);
this.renderError(err);
});
}
@Input()
set appBindObservableOf(ob: Observable<T>) {
this.observableSubject.next(ob);
}
@Input()
set appBindObservableLoadingTemplate(template: TemplateRef<{}>) {
this.loadingTemplate = template;
}
renderLoading() {
if (this.loadingTemplate) {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.loadingTemplate, {});
} else {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(BindObservableLoadingComponent);
this.viewContainer.clear();
this.viewContainer.createComponent(componentFactory);
}
}
renderItem(it: T | 'loading') {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef, new BindObservableContext(it));
}
renderError(err: Error) {
this.viewContainer.clear();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(BindObservableErrorComponent);
const component = this.viewContainer.createComponent(componentFactory);
(component.instance as BindObservableErrorComponent).error = err;
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}ntFactory(BindObservableLoadingComponent);
this.viewContainer.clear();
this.viewContainer.createComponent(componentFactory);
}
}
renderItem(it: T | 'loading') {
this.viewContainer.clear();
this.viewContainer.createEmbeddedView(this.templateRef, new BindObservableContext(it));
}
renderError(err: Error) {
this.viewContainer.clear();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(BindObservableErrorComponent);
const component = this.viewContainer.createComponent(componentFactory);
(component.instance as BindObservableErrorComponent).error = err;
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment