Skip to content

Commit

Permalink
Feature/context disposer (#5)
Browse files Browse the repository at this point in the history
* Update roadmap

* Add how ContextDisposerDirective works

* Add specs and class for ContextDisposerDirective

* Import/export ContextDisposerDirective

* Update roadmap with disposer directive

* Update version number

* Fix ContextProviderComponent import

* Avoid calls to syncProperties on provider reset

* Add integration test for ContextDisposerDirective

* Fix change detection problem on embedded view

* Fix view duplication

* Update documentation and revise title
  • Loading branch information
armanozak authored and mehmetakifalp committed Apr 12, 2019
1 parent 1a798b9 commit 84f5566
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 17 deletions.
88 changes: 83 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# ngx-context (a.k.a. Angular Context)
# Angular Context (ngx-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,21 +8,22 @@
<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

Data-binding and input properties are great. However, working with them has some challenges:

- Passing properties through several layers of the component tree is known as prop-drilling and it is time consuming and difficult and error prone to maintain.
- Passing properties through several layers of the component tree is known as prop-drilling and it is time consuming and error prone to maintain.
- The intermediary components become bloated with properties and methods just to pass data from parent to child and vice versa.
- 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 All @@ -302,3 +378,5 @@ There are several issues which are simply not addressed yet or impossible with c
- [x] CI integrations

- [ ] Benchmarks

- [ ] Optimization
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.markForCheck();
}

ngOnChanges() {
this.ngOnDestroy();
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

0 comments on commit 84f5566

Please sign in to comment.