-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move Kompakkt/Extender to this repository (#1)
* Create extender lib to later integrate Kompakkt/Extender in this workspace * Copy over extender library from Kompakkt/Extender * Remove temporary generic testing from extender * Add extender to lerna configuration
- Loading branch information
1 parent
d787f45
commit 08d0398
Showing
21 changed files
with
1,208 additions
and
17 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"extends": ["../.eslintrc.json"], | ||
"ignorePatterns": ["!**/*"], | ||
"overrides": [ | ||
{ | ||
"files": ["*.ts"], | ||
"extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], | ||
"rules": { | ||
"@angular-eslint/directive-selector": [ | ||
"error", | ||
{ | ||
"type": "attribute", | ||
"prefix": "lib", | ||
"style": "camelCase" | ||
} | ||
], | ||
"@angular-eslint/component-selector": [ | ||
"error", | ||
{ | ||
"type": "element", | ||
"prefix": "lib", | ||
"style": "kebab-case" | ||
} | ||
] | ||
} | ||
}, | ||
{ | ||
"files": ["*.html"], | ||
"extends": ["plugin:@nx/angular-template"], | ||
"rules": {} | ||
}, | ||
{ | ||
"files": ["*.json"], | ||
"parser": "jsonc-eslint-parser", | ||
"rules": { | ||
"@nx/dependency-checks": "error" | ||
} | ||
} | ||
] | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# Kompakkt.Extender | ||
|
||
<p align="center"> | ||
<img src="https://github.com/Kompakkt/Assets/raw/main/extender-logo.png" alt="Kompakkt ExtenderLogo" width="600"> | ||
</p> | ||
|
||
Extension system for modularizing instances of Kompakkt. | ||
|
||
## How to use Extender in Kompakkt Viewer or Repo | ||
|
||
Use the `provideExtender`-method in the `providers` array of your ApplicationConfig (`app.config.ts`): | ||
|
||
```ts | ||
import { ApplicationConfig } from '@angular/core'; | ||
import { HelloWorldPlugin, provideExtender } from '@kompakkt/extender'; | ||
import { routes } from './app.routes'; | ||
|
||
export const appConfig: ApplicationConfig = { | ||
providers: [ | ||
provideExtender({ | ||
// Array of Extender Plugins | ||
plugins: [new HelloWorldPlugin()], | ||
// Name of the ComponentSet, either 'viewerComponents' or 'repoComponents' | ||
componentSet: 'repoComponents', | ||
}), | ||
], | ||
}; | ||
``` | ||
|
||
## Creating a basic plugin and how to consume it | ||
|
||
### Plugin creating using factory pattern | ||
|
||
The Extender library provides a factory for creating plugin classes. | ||
The plugin requires some basic metadata, and can optionally receive components for the Repo and the Viewer, as well as a token which can be consumed as a `ProviderToken`, allowing to inject the plugin instance anywhere in the application. | ||
|
||
```ts | ||
import { createExtenderPlugin } from '../plugin-factory'; | ||
import { HelloWorldComponent } from './hello-world.component'; | ||
|
||
// Create a plugin class extending the factory class | ||
export class HelloWorldPlugin extends createExtenderPlugin({ | ||
// Required | ||
name: 'Hello World', | ||
description: 'Hello World plugin', | ||
version: '1.0.0', | ||
// Optional | ||
tokenName: 'HelloWorldPlugin', | ||
viewerComponents: { | ||
'hello-world': [HelloWorldComponent], | ||
}, | ||
repoComponents: { | ||
'hello-world': [HelloWorldComponent], | ||
}, | ||
}) { | ||
// Custom logic | ||
} | ||
``` | ||
|
||
### Consume plugin components using slots and ExtenderSlotDirective | ||
|
||
Notice the `slot` names like `hello-world` in the plugin definition? | ||
|
||
Using these slots, we can inject the components of the plugin anywhere in Kompakkt, by using the `ExtenderSlotDirective`. | ||
Simply import the `ExtenderSlotDirective`, add it to a components' imports and use it on any HTML-tag in the template with the slot name you wish to use. | ||
|
||
```ts | ||
import { ExtenderSlotDirective } from '@kompakkt/extender'; | ||
|
||
@Component({ | ||
/* ... */ | ||
imports: [ExtenderSlotDirective] | ||
}) | ||
``` | ||
|
||
```html | ||
<div extendSlot="hello-world"></div> | ||
``` | ||
|
||
### Passing data to components | ||
|
||
Each element with the `ExtenderSlotDirective` also has an input called `slotData`. | ||
Using this input, we can pass any data to our components. | ||
|
||
```html | ||
<div extendSlot="hello-world" [slotData]="{ name: 'World' }" ]></div> | ||
``` | ||
|
||
When creating a component, you can access the `slotData` and use type guards to validate it. | ||
|
||
```ts | ||
@Component({ | ||
selector: 'lib-hello-world', | ||
standalone: true, | ||
template: '<p>Hello {{ name() }}</p>', | ||
styles: '', | ||
}) | ||
export class HelloWorldComponent extends createExtenderComponent() { | ||
name = computed(() => { | ||
const slotData = this.slotData(); | ||
|
||
const isHelloWorldData = (obj: unknown): obj is { name: string } => { | ||
return typeof obj === 'object' && obj !== null && 'name' in obj; | ||
}; | ||
|
||
return isHelloWorldData(slotData) ? slotData.name : 'World'; | ||
}); | ||
} | ||
``` | ||
|
||
### Injecting plugin using the plugin token | ||
|
||
If a token was given to the factory, the plugin can be injected anywhere in the application. | ||
|
||
```ts | ||
import { Component, inject } from '@angular/core'; | ||
import { HelloWorldPlugin } from '@kompakkt/extender'; | ||
|
||
@Component({ | ||
/*...*/ | ||
}) | ||
class ExampleComponent { | ||
pluginReference = inject<HelloWorldPlugin>(HelloWorldPlugin.providerToken); | ||
} | ||
``` |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"$schema": "../node_modules/ng-packagr/ng-package.schema.json", | ||
"dest": "../dist/extender", | ||
"lib": { | ||
"entryFile": "src/index.ts" | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "@kompakkt/extender", | ||
"license": "AGPL-3.0-only", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/Kompakkt/Extender.git" | ||
}, | ||
"version": "0.0.9", | ||
"peerDependencies": { | ||
"@angular/common": "^17.3.0", | ||
"@angular/core": "^17.3.0" | ||
}, | ||
"dependencies": { | ||
"tslib": "^2.3.0" | ||
}, | ||
"sideEffects": false | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "extender", | ||
"$schema": "../node_modules/nx/schemas/project-schema.json", | ||
"sourceRoot": "extender/src", | ||
"prefix": "lib", | ||
"tags": [], | ||
"projectType": "library", | ||
"targets": { | ||
"build": { | ||
"executor": "@nx/angular:package", | ||
"outputs": ["{workspaceRoot}/dist/{projectRoot}"], | ||
"options": { | ||
"project": "extender/ng-package.json" | ||
}, | ||
"configurations": { | ||
"production": { | ||
"tsConfig": "extender/tsconfig.lib.prod.json" | ||
}, | ||
"development": { | ||
"tsConfig": "extender/tsconfig.lib.json" | ||
} | ||
}, | ||
"defaultConfiguration": "production" | ||
}, | ||
"lint": { | ||
"executor": "@nx/eslint:lint" | ||
} | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export { | ||
ExtenderOptions, | ||
PLUGIN_COMPONENT_SET, | ||
PLUGIN_MANAGER, | ||
provideExtender, | ||
} from './lib/extender'; | ||
export { createExtenderComponent, createExtenderPlugin } from './lib/factory'; | ||
export { ExtenderSlotDirective } from './lib/slot.directive'; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { | ||
EnvironmentProviders, | ||
InjectionToken, | ||
Type, | ||
makeEnvironmentProviders, | ||
} from '@angular/core'; | ||
import { ExtenderPluginManager } from './manager'; | ||
import { ExtenderProviderPlugin } from './provider'; | ||
|
||
export type ExtenderOptions<T> = { | ||
componentSet: 'viewerComponents' | 'repoComponents'; | ||
plugins: ExtenderProviderPlugin[]; | ||
services: Record<string, Type<T>>; | ||
}; | ||
|
||
export const PLUGIN_MANAGER = new InjectionToken<ExtenderPluginManager<unknown>>( | ||
'KOMPAKKT_EXTENDER_PLUGIN_MANAGER', | ||
); | ||
|
||
export const PLUGIN_COMPONENT_SET = new InjectionToken<ExtenderOptions<unknown>['componentSet']>( | ||
'KOMPAKKT_EXTENDER_PLUGIN_COMPONENT_SET', | ||
); | ||
|
||
export const provideExtender = <T>({ | ||
componentSet, | ||
plugins, | ||
services, | ||
}: ExtenderOptions<T>): EnvironmentProviders => { | ||
return makeEnvironmentProviders([ | ||
{ | ||
provide: PLUGIN_MANAGER, | ||
useFactory: () => new ExtenderPluginManager<T>(plugins, services), | ||
}, | ||
{ | ||
provide: PLUGIN_COMPONENT_SET, | ||
useValue: componentSet, | ||
}, | ||
...plugins | ||
.filter(p => !!(p.constructor as any)?.providerToken) | ||
.map(p => { | ||
const provide = (p.constructor as any).providerToken; | ||
return { provide, useValue: p }; | ||
}), | ||
]); | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { Directive, InjectionToken, Type, input, output } from '@angular/core'; | ||
import { ExtenderPluginManager } from './manager'; | ||
import { ExtenderAddonProviderPlugin } from './provider'; | ||
|
||
@Directive() | ||
class ExtenderAddonProviderPluginBase {} | ||
|
||
export const createExtenderPlugin = (options: { | ||
name: string; | ||
description: string; | ||
version: `${number}.${number}.${number}`; | ||
viewerComponents?: Record<string, Type<ExtenderPluginBaseComponent>[]>; | ||
repoComponents?: Record<string, Type<ExtenderPluginBaseComponent>[]>; | ||
tokenName?: string; | ||
}) => { | ||
const providerToken = options.tokenName | ||
? new InjectionToken<ExtenderAddonProviderPlugin>( | ||
`KOMPAKKT_EXTENDER_PLUGIN_${options.tokenName}`, | ||
) | ||
: undefined; | ||
|
||
return class extends ExtenderAddonProviderPluginBase implements ExtenderAddonProviderPlugin { | ||
readonly type = 'addon-provider' as const; | ||
readonly tokenName = options.tokenName; | ||
readonly name = options.name; | ||
readonly description = options.description; | ||
readonly version = options.version; | ||
readonly viewerComponents = options.viewerComponents ?? {}; | ||
readonly repoComponents = options.repoComponents ?? {}; | ||
|
||
static readonly providerToken = providerToken; | ||
}; | ||
}; | ||
|
||
@Directive({}) | ||
export class ExtenderPluginBaseComponent { | ||
readonly slotData = input<unknown>(); | ||
readonly event = output<Event>(); | ||
readonly pluginManager = input<ExtenderPluginManager<unknown>>(); | ||
} | ||
|
||
export const createExtenderComponent = () => { | ||
return class extends ExtenderPluginBaseComponent {}; | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Injector, Type, inject, runInInjectionContext } from '@angular/core'; | ||
import { ExtenderPluginBaseComponent } from './factory'; | ||
import { | ||
ExtenderAddonProviderPlugin, | ||
ExtenderDataProviderPlugin, | ||
ExtenderLoginProviderPlugin, | ||
ExtenderProviderPlugin, | ||
} from './provider'; | ||
|
||
export class ExtenderPluginManager<T> { | ||
readonly plugins: ExtenderProviderPlugin[] = []; | ||
readonly serviceMap = new Map<string, Type<T>>(); | ||
readonly injectedServicesMap = new Map<string, T>(); | ||
|
||
#injector = inject(Injector); | ||
|
||
constructor(plugins: ExtenderProviderPlugin[], services: Record<string, Type<T>>) { | ||
this.plugins.push(...plugins); | ||
for (const [key, service] of Object.entries(services)) { | ||
this.serviceMap.set(key, service); | ||
} | ||
|
||
runInInjectionContext(this.#injector, () => { | ||
for (const [key, service] of this.serviceMap) { | ||
const injected = inject(service); | ||
this.injectedServicesMap.set(key, injected); | ||
} | ||
}); | ||
} | ||
|
||
get dataProvider(): ExtenderDataProviderPlugin | undefined { | ||
return this.plugins.find( | ||
(plugin): plugin is ExtenderDataProviderPlugin => plugin.type === 'data-provider', | ||
); | ||
} | ||
|
||
get loginProviders(): ExtenderLoginProviderPlugin[] { | ||
return this.plugins.filter( | ||
(plugin): plugin is ExtenderLoginProviderPlugin => plugin.type === 'login-provider', | ||
); | ||
} | ||
|
||
get addonProviders(): ExtenderAddonProviderPlugin[] { | ||
return this.plugins.filter( | ||
(plugin): plugin is ExtenderAddonProviderPlugin => plugin.type === 'addon-provider', | ||
); | ||
} | ||
|
||
public getComponentsForSlot(slot: string, componentSet: 'viewerComponents' | 'repoComponents') { | ||
return new Map<ExtenderAddonProviderPlugin, Type<ExtenderPluginBaseComponent>[]>( | ||
this.addonProviders.map(p => [p, p?.[componentSet]?.[slot] ?? []] as const), | ||
); | ||
} | ||
|
||
public findAddonForComponent( | ||
slot: string, | ||
componentSet: 'viewerComponents' | 'repoComponents', | ||
component: Type<ExtenderPluginBaseComponent>, | ||
): ExtenderAddonProviderPlugin | undefined { | ||
return this.addonProviders.find(p => | ||
p?.[componentSet]?.[slot]?.find(c => c.name === component.name), | ||
); | ||
} | ||
} |
Oops, something went wrong.