Created
March 20, 2018 09:38
-
-
Save asdacap/666af575b2fcbe09cf180bd54dfc3c28 to your computer and use it in GitHub Desktop.
Easy Angular RxJS view binder/resolver.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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