Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/context disposer #5

Merged
merged 12 commits into from
Apr 12, 2019
80 changes: 78 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# ngx-context (a.k.a. Angular Context)

<p align="center">
<a href="https://travis-ci.org/ng-turkey/ngx-context"><img src="https://travis-ci.org/ng-turkey/ngx-context.svg?branch=master"/></a>
<a href="https://codeclimate.com/github/ng-turkey/ngx-context/maintainability"><img src="https://api.codeclimate.com/v1/badges/5115f5820cd9dfc5c409/maintainability" /></a>
Expand All @@ -7,11 +8,12 @@
<img src="https://img.shields.io/github/license/ng-turkey/ngx-context.svg" />
<a href="https://twitter.com/ngTurkiye"><img src="https://img.shields.io/twitter/follow/ngTurkiye.svg?label=Follow"/></a>
</p>

Angular Context is a library to bind data to deeply nested child components **without passing properties through other components or getting blocked by a router outlet**.

If you would like to have further information on why you need a library like this, you may find the [reasons](#reasons-to-use-this-library) below. Otherwise, skip to the [quickstart](#quickstart) or [usage](#usage) section.

Check [sample application](https://stackblitz.com/edit/ngx-context) out for a preview.
Check [demo application](https://stackblitz.com/edit/ngx-context) out for a preview.

## Reasons to Use This Library

Expand All @@ -21,7 +23,7 @@ Data-binding and input properties are great. However, working with them has some
- When a component is loaded via `router-outlet`, data-binding is not available and prop-drilling is no longer an option.
- Providing data through state management has its own caveat: Since connecting presentational (dumb) components directly to a specific state breaks their reusability, they have to be wrapped by container (smart) components instead and that usually is additional work.

This library is designed to improve developer experience by fixing all issues above. It provides context through dependency injection system behind-the-scenes and lets your deeply nested dumb components consume this context easily. It is conceptually influenced by [React Context](https://reactjs.org/docs/context.html), but differs in implementation and is 100% tailored for Angular.
This library is designed to improve developer experience by fixing all issues above. It provides context through dependency injection system behind-the-scenes and lets your deeply nested dumb components consume this context easily. It is inspired by [React Context](https://reactjs.org/docs/context.html), but differs in implementation and is 100% tailored for Angular.

![](./assets/context.svg)

Expand Down Expand Up @@ -272,6 +274,78 @@ Consumed property names can be mapped.

```

### ContextDisposerDirective

There are some cases where you will need the context on a higher level and end up putting properties on a middle component's class. For example, in order to make [reactive forms](https://angular.io/guide/reactive-forms) work, a `ContextConsumerComponent` will most likely be used and the consumed properties will have to be added to the wrapper component. This is usually not the preferred result. After all, we are trying to keep intermediary components as clean as possible. In such a case, you can use `ContextDisposerDirective` on an `<ng-template>` and make use of [template input variables](https://angular.io/guide/structural-directives#template-input-variable).

```HTML
<!-- disposer will dispose any property provided under context -->

<ng-template contextDisposer let-context>
<child-component [someProp]="context.someProp"></child-component>
</ng-template>

```

The name of specific props to be disposed can be set by `contextDisposer` input and it can take `string` or `Array<string>` values.

```HTML
<!-- disposer will dispose someProp and someOtherProp under context -->

<ng-template contextDisposer="someProp someOtherProp" let-context>
<child-component
[prop1]="context.someProp"
[prop2]="context.someOtherProp"
></child-component>
</ng-template>

```

— or —

```HTML
<!-- disposer will dispose someProp and someOtherProp under context -->

<ng-template contextDisposer="['someProp', 'someOtherProp']" let-context>
<child-component
[prop1]="context.someProp"
[prop2]="context.someOtherProp"
></child-component>
</ng-template>

```

Properties to dispose can be dynamically set.

```HTML
<!-- disposer will dispose properties defined by propertiesToDispose under context -->

<ng-template [contextDisposer]="propertiesToDispose" let-context>
<child-component
[prop1]="context.someProp"
[prop2]="context.someOtherProp"
></child-component>
</ng-template>

```

Disposed property names can be individually assigned to template input variables.

```HTML
<!-- disposer will dispose prop1 and prop2 -->

<ng-template
contextDisposer
let-prop1="someProp"
let-prop2="someOtherProp"
>
<child-component [prop1]="prop1" [prop2]="prop2"></child-component>
</ng-template>

```

Note: If you are wondering how you can implement reactive forms using Angular Context, please refer to the [demo application](https://stackblitz.com/edit/ngx-context).

## Caveats / Trade-offs

There are several issues which are simply not addressed yet or impossible with currently available tools.
Expand All @@ -287,6 +361,8 @@ There are several issues which are simply not addressed yet or impossible with c

- [x] Component and directive to consume context

- [x] Directive to dispose context

- [x] Test coverage

- [x] Documentation & examples
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-context",
"version": "1.0.0",
"version": "1.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
7 changes: 5 additions & 2 deletions src/lib/consumer.abstract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { filter, takeUntil } from 'rxjs/operators';
import { parseKeys } from './internals';
import { ContextProviderComponent } from './provider.component';
import { ContextMap } from './symbols';
Expand Down Expand Up @@ -46,7 +46,10 @@ export abstract class AbstractContextConsumer<T> implements OnChanges, OnDestroy

if (this.provider.provide.length)
this.provider.change$
.pipe(takeUntil(this.destroy$))
.pipe(
takeUntil(this.destroy$),
filter(key => !!key),
)
.subscribe(providerKey => this.syncProperties(consumed, providerKey));
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/context.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { NgModule } from '@angular/core';
import { ContextConsumerComponent } from './consumer.component';
import { ContextConsumerDirective } from './consumer.directive';
import { ContextDisposerDirective } from './disposer.directive';
import { ContextProviderComponent } from './provider.component';

@NgModule({
declarations: [
ContextConsumerComponent,
ContextConsumerDirective,
ContextDisposerDirective,
ContextProviderComponent,
],
exports: [
ContextConsumerComponent,
ContextConsumerDirective,
ContextDisposerDirective,
ContextProviderComponent,
],
})
Expand Down
89 changes: 89 additions & 0 deletions src/lib/disposer.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Directive,
EmbeddedViewRef,
Input,
Optional,
SkipSelf,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { parseKeys } from './internals';
import { ContextProviderComponent } from './provider.component';

@Directive({
selector: '[contextDisposer]',
})
export class ContextDisposerDirective {
private destroy$ = new Subject<void>();
private _dispose: string | string[] = '';
private view: EmbeddedViewRef<any>;

@Input('contextDisposer')
set dispose(dispose: string | string[]) {
this._dispose = dispose || '';
}
get dispose(): string | string[] {
return this._dispose;
}

constructor(
@Optional()
private tempRef: TemplateRef<any>,
@Optional()
private vcRef: ViewContainerRef,
@Optional()
@SkipSelf()
private provider: ContextProviderComponent,
) {}

private init(): void {
const disposed: string[] = parseKeys(this.dispose);

this.provider.reset$
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.ngOnChanges());

if (this.provider.provide.length)
this.provider.change$
.pipe(
takeUntil(this.destroy$),
filter(key => !!key),
)
.subscribe(providerKey => this.syncProperties(disposed, providerKey));
}

private reset(): void {
this.view = this.vcRef.createEmbeddedView(this.tempRef, new Context());
}

private syncProperties(disposed: string[], providerKey: string): void {
const key = this.provider.contextMap[providerKey] || providerKey;

if (disposed.length && disposed.indexOf(key) < 0) return;

const value = this.provider.component[providerKey];

this.view.context.$implicit[key] = value;
this.view.context[key] = value;
this.view.detectChanges();
}

ngOnChanges() {
this.destroy$.next();

this.reset();
if (this.provider && this.tempRef && this.vcRef) this.init();
}

ngOnDestroy() {
this.destroy$.next();

if (this.view) this.vcRef.clear();
}
}

export class Context {
$implicit: { [key: string]: any } = {};
}
4 changes: 3 additions & 1 deletion src/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-context",
"version": "1.0.0",
"version": "1.1.0",
"repository": {
"type": "git",
"url": "git+https://github.com/ng-turkey/ngx-context.git"
Expand All @@ -18,9 +18,11 @@
"prop-drilling",
"data-binding",
"property binding",
"event binding",
"utility",
"provider",
"consumer",
"disposer",
"router-outlet"
],
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { NgxContextModule } from './lib/context.module';
export { ContextConsumerComponent } from './lib/consumer.component';
export { ContextConsumerDirective } from './lib/consumer.directive';
export { ContextDisposerDirective } from './lib/disposer.directive';
export { ContextProviderComponent } from './lib/provider.component';
export { ContextMap } from './lib/symbols';
Loading