Files
phx-frontend-plugin-webcomp…/README.DE.md

25 KiB

PHX Frontend Plugin Demo

English version

Beispielprojekt, das zeigt, wie ein PHX-ERP-Frontend-Plugin als Angular-Web-Component mit @phx-erp/shared und @phx-erp/shared-ui erstellt wird.

Dieselbe Codebasis unterstützt zwei Arbeitsweisen:

Modus Zweck Typischer Befehl
Plugin host Ausführung in PHX als Custom Element yarn run plugin
Standalone client Lokale Entwicklung wie eine normale Angular-App yarn client

Inhaltsverzeichnis


Übersicht

Ein PHX-Frontend-Plugin ist eine Angular-Anwendung, die als Custom Element (Web Component) verpackt wird. PHX lädt es über ein Manifest, das auf die kompilierte main.js verweist und den Element-Tag-Namen deklariert.

┌─────────────────┐     manifest.json      ┌──────────────────┐
│   PHX host      │ ─────────────────────► │  main.js (+ deps) │
│  (ERP shell)    │     loads & registers  │  custom element   │
└────────┬────────┘                        └────────┬─────────┘
         │                                          │
         │  pluginServices (Apollo, notifications…) │
         │  hostInjector                            │
         └──────────────────────────────────────────┘

Was diese Demo enthält:

  • Custom-Element-Tag: frontend-plugin-demo
  • Beispiel-Routen: Hello World, Product View, Address List
  • GraphQL-Abfragen über den PHX-Host-Apollo (Production) oder einen lokalen Apollo-Client (Development)
  • Login-Flow für die eigenständige Entwicklung ohne vorkonfigurierten API-Key
  • PrimeNG- und Tailwind-Styling im PHX-Stil

Voraussetzungen

  • Node.js (LTS empfohlen)
  • Yarn v4
  • Eine laufende PHX-Instanz (für Plugin-Host-Tests und GraphQL-Schema/Codegen)

Für die lokale eigenständige Entwicklung benötigen Sie außerdem eines der folgenden:

  • Einen PHX-API-Benutzer-Token in Ihrer Development-Umgebung, oder
  • Gültige Anmeldedaten für den integrierten Login-Bildschirm (siehe Apollo, Auth Guard und Login)

Schnellstart

  1. Abhängigkeiten installieren:

    yarn install
    
  2. Lokale Development-Umgebung anlegen (optional, für den Standalone-Modus):

    cp src/environments/environment.example.ts src/environments/environment.development.ts
    

    Passen Sie apiUrl, wsUrl und optional apiKey in dieser Datei an. Die Datei ist gitignored.

  3. GraphQL-Typen generieren (erfordert PHX Admin API unter der in codegen.ts konfigurierten Schema-URL):

    yarn codegen
    
  4. In einem der Entwicklungsmodi unten starten.


Live-Demo

Um diese Demo in Ihrer eigenen Instanz zu nutzen, fügen Sie unter Admin → Custom Elements (https://your.phx.instance/admin/customElements) ein Custom Element mit folgender Manifest-URL hinzu:

https://gitea.phx-erp.de/api/v1/repos/PHXGMBH/phx-frontend-plugin-webcomponent-demo/raw/master/latest/manifest.json

Das Manifest verweist auf die kompilierte main.js im Verzeichnis latest/ dieses Repositorys.


Entwicklungsmodi

Standalone client (üblicher Angular-Workflow)

Startet die App mit der Development-Build-Konfiguration auf Port 4201 — Routing, Login und ein lokaler Apollo-Client funktionieren ohne PHX.

yarn client
# equivalent: ng serve --port 4201 --watch --configuration development

Öffnen Sie http://localhost:4201/.

Plugin-Host-Modus (innerhalb von PHX)

Baut mit der Production-Konfiguration (ohne Output Hashing) und stellt die Ausgabe mit CORS- und No-Cache-Headern bereit, damit PHX während der Entwicklung aktuelle Bundles laden kann.

yarn run plugin

Dies führt yarn build und yarn serve parallel aus:

Skript Funktion
yarn build Production-Watch-Build (in diesem Repo zusätzlich Sync nach latest/ für die gehostete Demo)
yarn serve Stellt dist/.../browser/ unter http://localhost:3223/ bereit

Verweisen Sie Ihre PHX-Instanz auf das lokale Manifest:

http://localhost:3223/manifest.json

(public/manifest.json wird in die bereitgestellte Ausgabe kopiert.)

Hinweis: Verwenden Sie yarn run plugin, nicht yarn plugin — Yarn behandelt plugin als eingebauten Befehl.

Terminal Keeper ist in .vscode/sessions.json vorkonfiguriert, um sowohl yarn run plugin als auch yarn client zu starten.

Weitere nützliche Befehle

yarn start          # ng serve with development configuration (default port 4200)
yarn codegen        # Regenerate GraphQL types

Projektstruktur

phx-frontend-plugin-demo/
├── latest/                    # Published build output (main.js, manifest.json)
├── public/
│   └── manifest.json          # Local manifest (path → localhost:3223)
├── scripts/
│   ├── copy-latest.mjs        # (this repo only) Sync dist/*.js → latest/ for hosted demo
│   └── serve-dist.mjs         # Static server for plugin-host dev
├── src/
│   ├── app/
│   │   ├── components/        # Demo pages (hello-world, product-view, …)
│   │   ├── login/             # Login form (standalone development)
│   │   ├── services/
│   │   │   ├── apollo.service.ts
│   │   │   └── phoenix-host-bridge.service.ts
│   │   ├── app.config.ts      # providePhoenixPluginWithPrimeNG, providers
│   │   ├── app.routes.ts
│   │   ├── apollo.provider.ts # Local Apollo (development only)
│   │   └── auth-guard.ts
│   ├── graphql/               # GraphQL documents for codegen
│   ├── environments/
│   │   ├── environment.ts              # Production defaults (used in PHX)
│   │   ├── environment.example.ts      # Template for local dev
│   │   └── environment.development.ts  # Local overrides (gitignored)
│   └── main.ts                # Registers the custom element
├── codegen.ts
├── serve.json                 # Cache headers for serve-dist.mjs
└── tailwind.config.js

Eigenes Plugin erstellen

Die folgenden Schritte führen durch die Erstellung eines Projekts, das dieser Demo ähnelt, mit Yarn v4, Angular 20, PrimeNG 20 und Tailwind CSS.

Ersetzen Sie Platzhalter wie *PROJECT-NAME*, *YOUR-TAG* und *YOUR-TOKEN* durch Ihre Werte.

1. Grundsetup

mkdir *PROJECT-NAME*
cd *PROJECT-NAME*
npx @angular/cli@20 new *PROJECT-NAME* --directory ./ --package-manager yarn --style scss --ssr false --routing --standalone
yarn add primeng@20 @primeng/themes@20 tailwindcss@3.4.17 postcss tailwindcss-primeui @angular/animations@20 @angular/elements@20
yarn add -D typescript@5.9.2 @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node concurrently
ng add apollo-angular

2. Tailwind CSS

Erstellen Sie tailwind.config.js im Projektroot. Diese Konfiguration ist an PHX angelehnt:

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: 'selector',
  content: ['./src/**/*.{html,ts,scss}'],
  theme: {
    extend: {
      animation: {
        fadein: 'fadein 0.5s ease-in-out',
        fadeout: 'fadeout 0.5s ease-in-out',
        fadeinleft: 'fadeinleft 0.5s ease-in-out',
        fadeinright: 'fadeinright 0.5s ease-in-out',
        fadeintop: 'fadeintop 0.5s ease-in-out',
        fadeinbottom: 'fadeinbottom 0.5s ease-in-out',
      },
      keyframes: {
        fadein: { '0%': { opacity: 0 }, '100%': { opacity: 1 } },
        fadeout: { '0%': { opacity: 1 }, '100%': { opacity: 0 } },
        fadeinleft: { '0%': { opacity: 0, transform: 'translateX(-100%)' }, '100%': { opacity: 1, transform: 'translateX(0)' } },
        fadeinright: { '0%': { opacity: 0, transform: 'translateX(100%)' }, '100%': { opacity: 1, transform: 'translateX(0)' } },
        fadeintop: { '0%': { opacity: 0, transform: 'translateY(-100%)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
        fadeinbottom: { '0%': { opacity: 0, transform: 'translateY(100%)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
      },
    },
  },
  plugins: [require('tailwindcss-primeui')],
};

Aufgrund eines bekannten Problems müssen diese Direktiven in den styles jeder Komponente (und in src/styles.scss für die lokale Entwicklung) ergänzt werden:

@tailwind base;
@tailwind components;
@tailwind utilities;

3. GraphQL Codegen

Erstellen Sie codegen.ts im Projektroot (passen Sie die Schema-URL an, falls Ihre PHX-Instanz woanders läuft):

import type { CodegenConfig } from '@graphql-codegen/cli';

const sharedConfig = {
  scalars: { DateTime: 'Date' },
  immutableTypes: false,
} as const;

const config: CodegenConfig = {
  overwrite: true,
  schema: 'http://localhost:3000/admin-api/schema.gql',
  documents: './src/graphql/*.ts',
  ignoreNoDocuments: true,
  generates: {
    './src/app/schema-types.ts': {
      plugins: ['typescript'],
      config: sharedConfig,
    },
    './src/app/generated.ts': {
      plugins: ['typescript-operations', 'typed-document-node'],
      config: {
        ...sharedConfig,
        importSchemaTypesFrom: './src/app/schema-types.ts',
      },
    },
  },
};

export default config;

In package.json ergänzen:

"codegen": "graphql-codegen"

Definieren Sie Queries und Mutations in src/graphql/ und führen Sie anschließend aus:

yarn codegen

Dies erzeugt src/app/schema-types.ts und src/app/generated.ts.

4. Environments

Verwenden Sie getrennte Environments, damit derselbe Build als PHX-Plugin (Production) oder als eigenständige Dev-App (Development) läuft.

src/environments/environment.interface.ts

export abstract class Environment {
  production: boolean = false;
  apiUrl: string | undefined;
  wsUrl: string | undefined;
  apiKey: string | undefined;
  serverUrl: string = '';
}

src/environments/environment.ts (Production — verwendet bei Einbettung in PHX)

import { Environment } from './environment.interface';

export const environment: Environment = {
  production: true,
  apiUrl: undefined,
  wsUrl: undefined,
  apiKey: undefined,
  serverUrl: '',
};

src/environments/environment.development.ts (lokale Entwicklung — in .gitignore aufnehmen)

import { Environment } from './environment.interface';

export const environment: Environment = {
  production: false,
  apiUrl: 'http://localhost:3000/admin-api',
  wsUrl: 'ws://localhost:3000/admin-api',
  apiKey: undefined, // or a PHX API user token; otherwise use the login route
  serverUrl: 'https://localhost:4200',
};

Fügen Sie fileReplacements zur Development-Build-Konfiguration in angular.json hinzu:

"development": {
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.development.ts"
    }
  ]
}

Tipp: Legen Sie environment.example.ts im Repository ab und kopieren Sie es lokal nach environment.development.ts, wie diese Demo es tut.

5. PHX-Bibliotheken

@phx-erp/shared und @phx-erp/shared-ui werden in der öffentlichen npm-Registry veröffentlicht.

Installieren Sie die Bibliotheken:

yarn add @phx-erp/shared @phx-erp/shared-ui

6. Plugin-Setup

Application config

Fügen Sie providePhoenixPluginWithPrimeNG in src/app/app.config.ts hinzu:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { providePhoenixPluginWithPrimeNG } from '@phx-erp/shared-ui';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    // stripTrailingSegments: child route segments appended by PHX deep links
    // so APP_BASE_HREF still matches the host mount path.
    ...providePhoenixPluginWithPrimeNG({
      stripTrailingSegments: ['*YOUR*', '*ROUTES*', '*HERE*'],
    }),
    provideRouter(routes),
  ],
};

Routing: Übergeben Sie Ihre Routen-Pfadsegmente in stripTrailingSegments, oder leiten Sie sie automatisch ab:

stripTrailingSegments: routes.map((r) => r.path!).filter((p) => (p?.length ?? 0) > 0)

Host bridge service

Erstellen Sie src/app/services/phoenix-host-bridge.service.ts:

import { Injectable, Injector, signal } from '@angular/core';
import type { IPluginServices } from '@phx-erp/shared-ui';

@Injectable({ providedIn: 'root' })
export class PhoenixHostBridgeService {
  private readonly _hostInjector = signal<Injector | null>(null);
  private readonly _pluginServices = signal<IPluginServices | null>(null);

  hostInjector(): Injector | null {
    return this._hostInjector();
  }

  setHostInjector(injector: Injector): void {
    this._hostInjector.set(injector);
  }

  pluginServices(): IPluginServices | null {
    return this._pluginServices();
  }

  setPluginServices(services: IPluginServices): void {
    this._pluginServices.set(services);
  }
}

Die Host Bridge speichert von PHX bereitgestellte Services (Apollo Client, Notification Service usw.), damit Ihre Komponenten sie im Production-Modus nutzen können.

Root component

Ihre Root-Komponente empfängt pluginServices und hostInjector von PHX und leitet sie an die Bridge weiter:

import { Component, effect, inject, Injector, input } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { PhoenixHostBridgeService } from './services/phoenix-host-bridge.service';
import { IPluginServices, syncPhoenixHostInjector } from '@phx-erp/shared-ui';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<router-outlet />`,
  styles: [`
@tailwind base;
@tailwind components;
@tailwind utilities;
`],
})
export class App {
  private readonly hostBridge = inject(PhoenixHostBridgeService);

  readonly pluginServices = input<IPluginServices>({});
  readonly hostInjector = input<Injector | undefined>(undefined);

  private readonly _syncHostInjector = syncPhoenixHostInjector(this.hostBridge, this.hostInjector);

  private readonly _syncPluginServices = effect(() => {
    this.hostBridge.setPluginServices(this.pluginServices());
  });
}

Bootstrap / custom element registration

Registrieren Sie in src/main.ts Ihr Plugin als Custom Element (ersetzen Sie *YOUR-TAG*):

import { appConfig } from './app/app.config';
import { App } from './app/app';
import { bootstrapPhoenixPluginCustomElement } from '@phx-erp/shared-ui';
import { environment } from './environments/environment';

bootstrapPhoenixPluginCustomElement(App, '*YOUR-TAG*', appConfig).then((app) => {
  if (!environment.production) {
    return app!.bootstrap(App);
  }
  return app;
});
  • bootstrapPhoenixPluginCustomElement — erstellt die Angular-Anwendung und registriert das Custom Element für PHX.
  • app.bootstrap(App) in Development — mountet zusätzlich die Root-Komponente, damit ng serve / der Standalone-Modus mit Routing funktioniert.

Der Custom-Element-Tag muss kleingeschrieben mit Bindestrichen sein (z. B. my-company-orders). Verwenden Sie denselben Tag in Ihrem Manifest.

7. In PHX registrieren

Manifest-Format

PHX lädt ein JSON-Manifest, das auf Ihr Entry-Skript verweist und den Custom-Element-Tag deklariert:

{
  "path": "https://example.com/path/to/main.js",
  "items": [
    {
      "tagName": "*YOUR-TAG*"
    }
  ]
}
Feld Beschreibung
path Absolute URL zu main.js (und Basis für Chunk-Auflösung)
items[].tagName In main.ts registrierter Custom-Element-Tag

Beispiel lokale Entwicklung (public/manifest.json in diesem Repo):

{
  "path": "http://localhost:3223/main.js",
  "items": [{ "tagName": "frontend-plugin-demo" }]
}

Gehostetes Beispiel (latest/manifest.json in diesem Repo):

{
  "path": "https://gitea.phx-erp.de/api/v1/repos/PHXGMBH/phx-frontend-plugin-webcomponent-demo/raw/master/latest/main.js",
  "items": [{ "tagName": "frontend-plugin-demo" }]
}

Registrierungsschritte

  1. Hosten Sie main.js (und ggf. weitere Chunks) unter einer URL, die aus den Browsern Ihrer Nutzer erreichbar ist — dieselben Netzwerkregeln wie für Ihre PHX-Instanz.
  2. Veröffentlichen Sie ein Manifest-JSON unter einer stabilen URL.
  3. In PHX: Admin → Custom Elements → Manifest-URL hinzufügen und Mount-Pfad oder Tag wählen.
  4. Ab- und wieder anmelden. Das Plugin ist verfügbar unter https://your.phx.instance/customElements/*PATH*.

Für die lokale Plugin-Host-Entwicklung siehe Plugin lokal bereitstellen.


Weitere Themen

Plugin lokal bereitstellen

Statischen Server installieren:

yarn add -D serve

scripts/serve-dist.mjs

import { spawn } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const port = process.env.PORT ?? '3223';

const serveBin = join(root, 'node_modules', '.bin', 'serve');
const child = spawn(serveBin, ['-l', port, '--cors', '--no-etag'], {
  stdio: 'inherit',
  shell: true,
  cwd: root,
});

child.on('exit', (code) => process.exit(code ?? 0));

serve.json (ersetzen Sie *PROJECT-NAME* durch Ihren Angular-Projektnamen aus angular.json)

{
  "public": "dist/*PROJECT-NAME*/browser",
  "headers": [
    {
      "source": "**/*.{js,mjs}",
      "headers": [
        { "key": "Cache-Control", "value": "no-store, no-cache, must-revalidate" }
      ]
    }
  ]
}

Die Bereitstellung aus dem Projektroot (nicht direkt aus dist/) stellt sicher, dass serve.json angewendet wird und verhindert veraltete gecachte main.js nach Rebuilds.

In package.json ergänzen:

"serve": "node ./scripts/serve-dist.mjs"

Eigener Port: PORT=8080 yarn serve.

Nur in diesem Repository: scripts/copy-latest.mjs kopiert gebaute JS-Dateien nach latest/, damit das Live-Demo-Manifest auf einen stabilen Pfad in Git verweisen kann. Für die lokale Plugin-Host-Entwicklung ist das nicht erforderlich.

Apollo, Auth Guard und Login

Im Production-Modus (eingebettet in PHX) übernimmt der Host die Authentifizierung. Nutzen Sie den Apollo Client aus IPluginServices über PhoenixHostBridgeService — ein separater Login-Flow ist nicht nötig.

Im Development-Modus (Standalone) stellen Sie einen eigenen Apollo Client und optional einen Login bereit.

Apollo provider

Erstellen Sie src/app/apollo.provider.ts (vollständige Implementierung in diesem Repository). Er:

  • verbindet HTTP- und WebSocket-Links mit environment.apiUrl / environment.wsUrl
  • sendet Authorization: Bearer … aus environment.apiKey oder localStorage
  • speichert Tokens aus dem Response-Header phoenix-auth-token

Nur in Development registrieren, z. B. in app.config.ts:

...(environment.production ? [] : [...apolloProvider()])

Apollo service abstraction

Ein kleiner Service wählt im Production-Modus den Host-Apollo-Client und im Development-Modus den lokalen Client:

@Injectable({ providedIn: 'root' })
export class ApolloService {
  private readonly _apollo = signal<Apollo>(undefined!);

  constructor(private readonly injector: Injector) {
    if (environment.production) {
      this._apollo.set(injector.get(PhoenixHostBridgeService)?.pluginServices()?.apollo!);
    } else {
      this._apollo.set(
        injector.get(PhoenixHostBridgeService)?.pluginServices()?.apollo ?? injector.get(Apollo)
      );
    }
  }

  apollo = () => this._apollo();
}

Wenden Sie dasselbe Muster auf andere Host-Services an (z. B. Notifications), wenn Sie Standalone-Fallbacks benötigen.

Login component

Für die Entwicklung ohne voreingestellten API-Key fügen Sie eine /login-Route hinzu, die die PHX-login-Mutation aufruft:

mutation Login($username: String!, $password: String!) {
  login(username: $username, password: $password) {
    ... on CurrentUser {
      id
      identifier
      channels { 
        id 
        token 
      }
    }
    ... on InvalidCredentialsError { 
      errorCode 
      message 
    }
    ... on NativeAuthStrategyError { 
      errorCode 
      message 
    }
    ... on EmailCodeAuthStrategyError { 
      errorCode 
      message 
    }
  }
}

Eine vollständige Formular-Implementierung finden Sie unter src/app/login/ in diesem Repo.

Auth guard

Nicht authentifizierte Nutzer nur im Development-Modus zum Login weiterleiten:

@Injectable()
export class AuthGuard implements CanActivate {
  private readonly router = inject(Router);

  canActivate(_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): boolean {
    const token = environment.apiKey ?? localStorage.getItem('api-key');
    if (!environment.production && !token) {
      this.router.navigate(['login'], {
        queryParams: { redirectTo: btoa(window.location.pathname + window.location.search) },
      });
    }
    return true;
  }
}

In app.routes.ts einbinden:

const canActivate = [environment.production ? () => true : AuthGuard];

export const routes: Routes = [
  { path: '', canActivate, component: HelloWorld },
  { path: 'product-view', canActivate, component: ProductView },
  { path: 'address-list', canActivate, component: AddressList },
  { path: 'login', component: Login },
];

package.json scripts

Empfohlene Skripte nach dieser Anleitung:

{
  "scripts": {
    "build": "ng build --watch --output-hashing none --configuration production",
    "serve": "node ./scripts/serve-dist.mjs",
    "plugin": "concurrently \"yarn build\" \"yarn serve\"",
    "client": "ng serve --port 4201 --watch --configuration development",
    "codegen": "graphql-codegen"
  }
}
Skript Beschreibung
build Production-Watch-Build für PHX
serve Stellt kompilierte Assets für PHX bereit (Standard-Port 3223)
plugin Führt build + serve zusammen aus — verwenden Sie yarn run plugin
client Eigenständiger Angular-Dev-Server auf Port 4201
codegen Generiert GraphQL-TypeScript-Typen neu

Dieses Repository führt zusätzlich copy-latest.mjs parallel zu build aus, um Artefakte nach latest/ für die gehostete Demo zu veröffentlichen — das ist für Ihr eigenes Plugin nicht erforderlich.


Fehlerbehebung

Symptom Zu prüfen
PHX zeigt eine alte Plugin-Version Hard-Refresh; prüfen, ob serve.json Cache-Control: no-store für JS setzt; yarn run plugin neu starten
401 / GraphQL-Auth-Fehler im Standalone-Modus apiKey in environment.development.ts setzen oder über /login anmelden
yarn add @phx-erp/shared schlägt fehl Netzwerkzugriff auf die öffentliche npm-Registry prüfen; yarn install erneut ausführen
Routing in PHX funktioniert nicht Routen-Segmente zu stripTrailingSegments in providePhoenixPluginWithPrimeNG hinzufügen
Tailwind-Klassen fehlen in einer Komponente @tailwind-Direktiven in den styles der Komponente ergänzen
yarn codegen schlägt fehl PHX muss laufen und die Schema-URL in codegen.ts erreichbar sein
Custom Element nicht gefunden Tag im Manifest muss exakt mit customElements.define / bootstrapPhoenixPluginCustomElement übereinstimmen

Support

Bei Integrationsfragen oder PHX-spezifischen APIs wenden Sie sich an Ihren PHX-Partner.