From 964c1665a15c3bd14b67e36f165c2bf3e09761b2 Mon Sep 17 00:00:00 2001 From: Daniele Morosinotto Date: Fri, 15 Sep 2023 18:14:35 +0200 Subject: [PATCH 1/6] [feat] add call apply Pipes + tests and docs #35 --- docs/astro.config.mjs | 1 + docs/src/content/docs/utilities/call-apply.md | 52 +++++++ libs/ngxtension/call-apply/README.md | 3 + libs/ngxtension/call-apply/ng-package.json | 5 + libs/ngxtension/call-apply/project.json | 33 ++++ .../call-apply/src/call-apply.spec.ts | 146 ++++++++++++++++++ libs/ngxtension/call-apply/src/call-apply.ts | 44 ++++++ libs/ngxtension/call-apply/src/index.ts | 1 + tsconfig.base.json | 3 +- 9 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 docs/src/content/docs/utilities/call-apply.md create mode 100644 libs/ngxtension/call-apply/README.md create mode 100644 libs/ngxtension/call-apply/ng-package.json create mode 100644 libs/ngxtension/call-apply/project.json create mode 100644 libs/ngxtension/call-apply/src/call-apply.spec.ts create mode 100644 libs/ngxtension/call-apply/src/call-apply.ts create mode 100644 libs/ngxtension/call-apply/src/index.ts diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 8aa32393..f1b0f2fc 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -50,6 +50,7 @@ export default defineConfig({ { label: 'resize', link: '/utilities/resize' }, { label: 'createEffect', link: '/utilities/create-effect' }, { label: 'ifValidator', link: '/utilities/if-validator' }, + { label: 'call apply Pipes', link: '/utilities/call-apply' }, ], }, ], diff --git a/docs/src/content/docs/utilities/call-apply.md b/docs/src/content/docs/utilities/call-apply.md new file mode 100644 index 00000000..b89f812f --- /dev/null +++ b/docs/src/content/docs/utilities/call-apply.md @@ -0,0 +1,52 @@ +--- +title: call apply Pipes +description: ngxtension/call-apply +--- + +`callPipe` and `applyPipe` are simple standalone pipes that simplify the calling of a PURE functions passing params to it, they take advantage of the "memoization" offerd by pure pipes in Angular, and enforces that you use them only with PURE functions (aka if you use this inside the body function they throws error!) + +```ts +import { CallPipe, ApplyPipe } from 'ngxtension/if-validation'; +``` + +## Usage + +Both `CallPipe` and `ApplyPipe` need a PURE function or method to invoke (aka you can't use `this` in the function body), the difference between the two is only in that invocation order and that `|call` is sutable only for funciton with 1-param, instead `|apply` works for function with any number of params. + +```ts +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { CallPipe, ApplyPipe } from 'ngxtension/call-apply'; + +@Component({ + selector: 'my-app', + standalone: true, + imports: [CallPipe, ApplyPipe], + template: ` + + + call UTC: {{ now | call : ISOFormat }} + with apply: {{ ISOFormat | apply : now }} +

{{ join | apply : 'Hello' : ' world' : '!' }}

+ + +
{{ join | apply : 41 : 1 }}
+ + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class App { + public now = new Date(42, 42, 42, 42, 42, 42, 42); //"1945-08-12T16:42:42.042Z" + public ISOFormat = (date: Date) => date.toISOString(); + public join(first: string, ...rest: string[]) { + return rest.reduce((a, b) => a + b, first); + } + public updateClock() { + this.now = new Date(); + return this.now; + } +} +``` diff --git a/libs/ngxtension/call-apply/README.md b/libs/ngxtension/call-apply/README.md new file mode 100644 index 00000000..697e9f22 --- /dev/null +++ b/libs/ngxtension/call-apply/README.md @@ -0,0 +1,3 @@ +# ngxtension/call-apply + +Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/call-apply`. diff --git a/libs/ngxtension/call-apply/ng-package.json b/libs/ngxtension/call-apply/ng-package.json new file mode 100644 index 00000000..b3e53d69 --- /dev/null +++ b/libs/ngxtension/call-apply/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/ngxtension/call-apply/project.json b/libs/ngxtension/call-apply/project.json new file mode 100644 index 00000000..2742f3eb --- /dev/null +++ b/libs/ngxtension/call-apply/project.json @@ -0,0 +1,33 @@ +{ + "name": "ngxtension/call-apply", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/ngxtension/call-apply/src", + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/ngxtension/jest.config.ts", + "testPathPattern": ["call-apply"], + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "libs/ngxtension/call-apply/**/*.ts", + "libs/ngxtension/call-apply/**/*.html" + ] + } + } + } +} diff --git a/libs/ngxtension/call-apply/src/call-apply.spec.ts b/libs/ngxtension/call-apply/src/call-apply.spec.ts new file mode 100644 index 00000000..44d3426e --- /dev/null +++ b/libs/ngxtension/call-apply/src/call-apply.spec.ts @@ -0,0 +1,146 @@ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ApplyPipe, CallPipe } from './call-apply'; + +describe(CallPipe.name, () => { + @Component({ + standalone: true, + template: ` +

{{ now | call : ISOFormat }}

+ {{ now | call : doSomething }} + `, + imports: [CallPipe], + }) + class Dummy { + now = new Date(42, 42, 42, 42, 42, 42, 42); + ISOFormat(d: Date) { + return d.toISOString(); + } + doSomething() { + return 42; + } + } + + @Component({ + standalone: true, + template: ` + {{ 'WILL FAIL' | call : notPureFn }} + `, + imports: [CallPipe], + }) + class FailDummy { + now = new Date(42, 42, 42, 42, 42, 42, 42); + notPureFn() { + this.now = new Date(); + return this.now.toISOString(); + } + } + + it('can call PURE function with a param', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elP = fixture.debugElement.query(By.css('p')); + expect(elP.nativeElement.textContent).toContain('1945-08-12T16:42:42.042Z'); + }); + it('can call PURE function without params', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elB = fixture.debugElement.query(By.css('b')); + expect(elB.nativeElement.textContent).toContain('42'); + }); + it('will fail if the function is NOT PURE (using this in the body)', () => { + expect(() => { + const fixture = TestBed.createComponent(FailDummy); + fixture.detectChanges(); + }).toThrowError( + `DON'T USE this INSIDE A FUNCTION CALLED BY | call OR | apply IT MUST BE A PURE FUNCTION!` + ); + }); +}); + +describe(ApplyPipe.name, () => { + @Component({ + standalone: true, + template: ` + {{ IamPure | apply }} +

{{ ISOFormat | apply : now }}

+ {{ doSomething | apply : 'Hello world' }} + {{ doSomething | apply : 'Prova' : 1 : 2 : 3 }} + + {{ doSomething | apply : 42 : 1 : 2 : 3 }} + `, + imports: [ApplyPipe], + }) + class Dummy { + now = new Date(42, 42, 42, 42, 42, 42, 42); + IamPure = () => 42; + ISOFormat(d: Date) { + return d.toISOString(); + } + doSomething(a: string, ...rest: number[]) { + return rest.reduce((a, b) => a + b, a); + } + } + + @Component({ + standalone: true, + template: ` + {{ notPureFn | apply }} + `, + imports: [ApplyPipe], + }) + class FailDummy { + now = new Date(42, 42, 42, 42, 42, 42, 42); + notPureFn() { + this.now = new Date(); + return this.now.toISOString(); + } + } + + it('can apply PURE function without params', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elI = fixture.debugElement.query(By.css('i')); + expect(elI.nativeElement.textContent).toContain('42'); + }); + it('can apply PURE function with a param', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elP = fixture.debugElement.query(By.css('p')); + expect(elP.nativeElement.textContent).toContain('1945-08-12T16:42:42.042Z'); + }); + it('can apply PURE function with lesser param', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elB = fixture.debugElement.query(By.css('b')); + expect(elB.nativeElement.textContent).toContain('Hello world'); + }); + it('can apply PURE function with more rest param', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elA = fixture.debugElement.query(By.css('a')); + expect(elA.nativeElement.textContent).toContain('Prova123'); + }); + it('can apply PURE function but it is NOT STRICT!', () => { + const fixture = TestBed.createComponent(Dummy); + fixture.detectChanges(); + + const elU = fixture.debugElement.query(By.css('u')); + expect(elU.nativeElement.textContent).toContain('48'); + }); + it('will fail if the function is NOT PURE (using this in the body)', () => { + expect(() => { + const fixture = TestBed.createComponent(FailDummy); + fixture.detectChanges(); + }).toThrowError( + `DON'T USE this INSIDE A FUNCTION CALLED BY | call OR | apply IT MUST BE A PURE FUNCTION!` + ); + }); +}); diff --git a/libs/ngxtension/call-apply/src/call-apply.ts b/libs/ngxtension/call-apply/src/call-apply.ts new file mode 100644 index 00000000..9aba066b --- /dev/null +++ b/libs/ngxtension/call-apply/src/call-apply.ts @@ -0,0 +1,44 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +const error_this = function () { + throw new Error( + `DON'T USE this INSIDE A FUNCTION CALLED BY | call OR | apply IT MUST BE A PURE FUNCTION!` + ); +}; +const NOTHIS = !('Proxy' in window) + ? Object.seal({}) + : new Proxy( + {}, + { + get: error_this, + set: error_this, + deleteProperty: error_this, + has: error_this, + } + ); + +@Pipe({ + name: 'call', + pure: true, + standalone: true, +}) +export class CallPipe implements PipeTransform { + transform(value: any, args?: Function): any { + if (typeof args !== 'function') + throw new TypeError('You must pass a PURE funciton to | call'); + return args?.call(NOTHIS, value); + } +} + +@Pipe({ + name: 'apply', + pure: true, + standalone: true, +}) +export class ApplyPipe implements PipeTransform { + transform(fn: Function, ...args: any[]): any { + if (typeof fn !== 'function') + throw new TypeError('You must use | apply on a PURE function'); + return fn.apply(NOTHIS, args); + } +} diff --git a/libs/ngxtension/call-apply/src/index.ts b/libs/ngxtension/call-apply/src/index.ts new file mode 100644 index 00000000..3027617d --- /dev/null +++ b/libs/ngxtension/call-apply/src/index.ts @@ -0,0 +1 @@ +export * from './call-apply'; diff --git a/tsconfig.base.json b/tsconfig.base.json index 99c31c2d..c89e51e3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,6 +20,7 @@ "ngxtension/assert-injector": [ "libs/ngxtension/assert-injector/src/index.ts" ], + "ngxtension/call-apply": ["libs/ngxtension/call-apply/src/index.ts"], "ngxtension/computed-from": [ "libs/ngxtension/computed-from/src/index.ts" ], @@ -30,12 +31,12 @@ "ngxtension/create-injection-token": [ "libs/ngxtension/create-injection-token/src/index.ts" ], + "ngxtension/if-validator": ["libs/ngxtension/if-validator/src/index.ts"], "ngxtension/inject-destroy": [ "libs/ngxtension/inject-destroy/src/index.ts" ], "ngxtension/repeat": ["libs/ngxtension/repeat/src/index.ts"], "ngxtension/resize": ["libs/ngxtension/resize/src/index.ts"], - "ngxtension/if-validator": ["libs/ngxtension/if-validator/src/index.ts"], "plugin": ["libs/plugin/src/index.ts"] } }, From 7c7b54f59e2b5077ccc68d02a6768979c646d56d Mon Sep 17 00:00:00 2001 From: Daniele Morosinotto Date: Fri, 15 Sep 2023 18:15:26 +0200 Subject: [PATCH 2/6] [chore] small fix to CONTRIBUTING.md --- CONTRIBUTING.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41a19f77..390087d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,15 @@ We will review your PR as soon as possible, and your contribution will be greatl Most likely, you'll need to create new secondary entry point to put the new utility in. To create entry point, use the following command: ```shell -pnx nx g local-plugin:entry-point --library=ngxtension --skip-module +pnpm nx g local-plugin:entry-point --library=ngxtension --skip-module +``` + +#### Please write some Tests + +Try to cover your new contrib with some tests and make it pass running: + +```shell +pnpm exec nx run ngxtension/:test ``` ## Development Setup From 1b168869229b59367c0d00333282093562daaaaf Mon Sep 17 00:00:00 2001 From: Daniele Morosinotto Date: Fri, 15 Sep 2023 19:23:39 +0200 Subject: [PATCH 3/6] [feat] call-apply Pipes fixed strict-type check --- CONTRIBUTING.md | 2 +- docs/src/content/docs/utilities/call-apply.md | 14 +++++--------- libs/ngxtension/call-apply/src/call-apply.spec.ts | 9 --------- libs/ngxtension/call-apply/src/call-apply.ts | 7 +++++-- 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 390087d3..6fb582fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ We will review your PR as soon as possible, and your contribution will be greatl Most likely, you'll need to create new secondary entry point to put the new utility in. To create entry point, use the following command: ```shell -pnpm nx g local-plugin:entry-point --library=ngxtension --skip-module +pnpm exec nx g local-plugin:entry-point --library=ngxtension --skip-module ``` #### Please write some Tests diff --git a/docs/src/content/docs/utilities/call-apply.md b/docs/src/content/docs/utilities/call-apply.md index b89f812f..aa436e8d 100644 --- a/docs/src/content/docs/utilities/call-apply.md +++ b/docs/src/content/docs/utilities/call-apply.md @@ -3,7 +3,7 @@ title: call apply Pipes description: ngxtension/call-apply --- -`callPipe` and `applyPipe` are simple standalone pipes that simplify the calling of a PURE functions passing params to it, they take advantage of the "memoization" offerd by pure pipes in Angular, and enforces that you use them only with PURE functions (aka if you use this inside the body function they throws error!) +`callPipe` and `applyPipe` are simple standalone pipes that simplify the calling of PURE functions passing params to it; they take advantage of the "memoization" offered by pure pipes in Angular, and ensure that you use them only with PURE functions (aka if you use this inside the body function they throw errors!) ```ts import { CallPipe, ApplyPipe } from 'ngxtension/if-validation'; @@ -11,7 +11,7 @@ import { CallPipe, ApplyPipe } from 'ngxtension/if-validation'; ## Usage -Both `CallPipe` and `ApplyPipe` need a PURE function or method to invoke (aka you can't use `this` in the function body), the difference between the two is only in that invocation order and that `|call` is sutable only for funciton with 1-param, instead `|apply` works for function with any number of params. +Both `CallPipe` and `ApplyPipe` need a PURE function or method to invoke (aka you can't use `this` in the function body), the difference between the two is only in that invocation order and that `|call` is suitable only for function with 1-param, instead `|apply` works for function with any number of params. ```ts import { Component, ChangeDetectionStrategy } from '@angular/core'; @@ -26,11 +26,7 @@ import { CallPipe, ApplyPipe } from 'ngxtension/call-apply'; call UTC: {{ now | call : ISOFormat }} with apply: {{ ISOFormat | apply : now }} -

{{ join | apply : 'Hello' : ' world' : '!' }}

- - -
{{ join | apply : 41 : 1 }}
- +

{{ join | apply : 'Hello' : 'world' : '!' }}

- {{ doSomething | apply : 42 : 1 : 2 : 3 }} `, imports: [ApplyPipe], }) @@ -128,13 +126,6 @@ describe(ApplyPipe.name, () => { const elA = fixture.debugElement.query(By.css('a')); expect(elA.nativeElement.textContent).toContain('Prova123'); }); - it('can apply PURE function but it is NOT STRICT!', () => { - const fixture = TestBed.createComponent(Dummy); - fixture.detectChanges(); - - const elU = fixture.debugElement.query(By.css('u')); - expect(elU.nativeElement.textContent).toContain('48'); - }); it('will fail if the function is NOT PURE (using this in the body)', () => { expect(() => { const fixture = TestBed.createComponent(FailDummy); diff --git a/libs/ngxtension/call-apply/src/call-apply.ts b/libs/ngxtension/call-apply/src/call-apply.ts index 9aba066b..ef4ff038 100644 --- a/libs/ngxtension/call-apply/src/call-apply.ts +++ b/libs/ngxtension/call-apply/src/call-apply.ts @@ -23,7 +23,7 @@ const NOTHIS = !('Proxy' in window) standalone: true, }) export class CallPipe implements PipeTransform { - transform(value: any, args?: Function): any { + transform(value: T, args?: (param?: T) => R): R { if (typeof args !== 'function') throw new TypeError('You must pass a PURE funciton to | call'); return args?.call(NOTHIS, value); @@ -36,7 +36,10 @@ export class CallPipe implements PipeTransform { standalone: true, }) export class ApplyPipe implements PipeTransform { - transform(fn: Function, ...args: any[]): any { + transform any>( + fn: TFunction, + ...args: Parameters + ): ReturnType { if (typeof fn !== 'function') throw new TypeError('You must use | apply on a PURE function'); return fn.apply(NOTHIS, args); From 07b55283714bb5c8cb716fd5ea94785edaa6158b Mon Sep 17 00:00:00 2001 From: Daniele Morosinotto Date: Fri, 15 Sep 2023 19:34:26 +0200 Subject: [PATCH 4/6] [feat] call-apply Pipes fixed call strict fn:1par --- libs/ngxtension/call-apply/src/call-apply.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ngxtension/call-apply/src/call-apply.ts b/libs/ngxtension/call-apply/src/call-apply.ts index ef4ff038..3b9cc7ae 100644 --- a/libs/ngxtension/call-apply/src/call-apply.ts +++ b/libs/ngxtension/call-apply/src/call-apply.ts @@ -23,7 +23,7 @@ const NOTHIS = !('Proxy' in window) standalone: true, }) export class CallPipe implements PipeTransform { - transform(value: T, args?: (param?: T) => R): R { + transform(value: T, args?: (param: T) => R): R { if (typeof args !== 'function') throw new TypeError('You must pass a PURE funciton to | call'); return args?.call(NOTHIS, value); From d500ced03b68a8aaf4ac9b466b41f461146f9c7c Mon Sep 17 00:00:00 2001 From: Daniele Morosinotto Date: Fri, 15 Sep 2023 20:22:53 +0200 Subject: [PATCH 5/6] [feat] call-apply fix failing test for timezone :( --- .../call-apply/src/call-apply.spec.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/libs/ngxtension/call-apply/src/call-apply.spec.ts b/libs/ngxtension/call-apply/src/call-apply.spec.ts index 0049cce1..fa0136b0 100644 --- a/libs/ngxtension/call-apply/src/call-apply.spec.ts +++ b/libs/ngxtension/call-apply/src/call-apply.spec.ts @@ -4,6 +4,7 @@ import { By } from '@angular/platform-browser'; import { ApplyPipe, CallPipe } from './call-apply'; describe(CallPipe.name, () => { + const NOW = new Date(42, 42, 42, 42, 42, 42, 42); @Component({ standalone: true, template: ` @@ -13,10 +14,7 @@ describe(CallPipe.name, () => { imports: [CallPipe], }) class Dummy { - now = new Date(42, 42, 42, 42, 42, 42, 42); - ISOFormat(d: Date) { - return d.toISOString(); - } + now = NOW; doSomething() { return 42; } @@ -30,7 +28,7 @@ describe(CallPipe.name, () => { imports: [CallPipe], }) class FailDummy { - now = new Date(42, 42, 42, 42, 42, 42, 42); + now = NOW; notPureFn() { this.now = new Date(); return this.now.toISOString(); @@ -42,7 +40,7 @@ describe(CallPipe.name, () => { fixture.detectChanges(); const elP = fixture.debugElement.query(By.css('p')); - expect(elP.nativeElement.textContent).toContain('1945-08-12T16:42:42.042Z'); + expect(elP.nativeElement.textContent).toContain(NOW.toISOString()); }); it('can call PURE function without params', () => { const fixture = TestBed.createComponent(Dummy); @@ -62,6 +60,7 @@ describe(CallPipe.name, () => { }); describe(ApplyPipe.name, () => { + const NOW = new Date(42, 42, 42, 42, 42, 42, 42); @Component({ standalone: true, template: ` @@ -73,7 +72,7 @@ describe(ApplyPipe.name, () => { imports: [ApplyPipe], }) class Dummy { - now = new Date(42, 42, 42, 42, 42, 42, 42); + now = NOW; IamPure = () => 42; ISOFormat(d: Date) { return d.toISOString(); @@ -91,7 +90,7 @@ describe(ApplyPipe.name, () => { imports: [ApplyPipe], }) class FailDummy { - now = new Date(42, 42, 42, 42, 42, 42, 42); + now = NOW; notPureFn() { this.now = new Date(); return this.now.toISOString(); @@ -110,7 +109,7 @@ describe(ApplyPipe.name, () => { fixture.detectChanges(); const elP = fixture.debugElement.query(By.css('p')); - expect(elP.nativeElement.textContent).toContain('1945-08-12T16:42:42.042Z'); + expect(elP.nativeElement.textContent).toContain(NOW.toISOString()); }); it('can apply PURE function with lesser param', () => { const fixture = TestBed.createComponent(Dummy); From 602c699c3e7d6a31fb40a6b107ea3e088715536f Mon Sep 17 00:00:00 2001 From: Daniele Morosinotto Date: Fri, 15 Sep 2023 20:32:42 +0200 Subject: [PATCH 6/6] [feat] call-apply Pipes fix tests --- libs/ngxtension/call-apply/src/call-apply.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/ngxtension/call-apply/src/call-apply.spec.ts b/libs/ngxtension/call-apply/src/call-apply.spec.ts index fa0136b0..6c4d6da6 100644 --- a/libs/ngxtension/call-apply/src/call-apply.spec.ts +++ b/libs/ngxtension/call-apply/src/call-apply.spec.ts @@ -15,6 +15,9 @@ describe(CallPipe.name, () => { }) class Dummy { now = NOW; + ISOFormat(d: Date) { + return d.toISOString(); + } doSomething() { return 42; }