Skip to content
This repository was archived by the owner on Sep 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8,205 changes: 4,107 additions & 4,098 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package.json
Comment thread
SriramaDanturthi marked this conversation as resolved.
Resolved
Comment thread
SriramaDanturthi marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "@tylertech/forge-angular",
"version": "7.3.0",
"version": "8.0.0",
"description": "Forge components adapter library for Angular",
"repository": "tyler-technologies-oss/forge-angular",
"license": "Apache-2.0",
"scripts": {
"prepare": "husky",
"ng": "ng",
"start": "npm run build && ng serve -o",
"build": "ng build forge-angular --configuration production",
"build": "ng build forge-angular --configuration production && npm run build:schematics",
"build:schematics": "tsc -p projects/forge-angular/schematics/tsconfig.json",
"build:demo": "ng build --configuration production --stats-json",
"watch": "ng build --watch --configuration development",
"test": "ng test",
Expand All @@ -19,7 +20,6 @@
"generate-proxies": "ng generate @tylertech/forge-schematics:custom-elements --config generate-proxies.json --interactive=false"
},
"dependencies": {
"@angular/animations": "^20.3.15",
"@angular/common": "^20.3.15",
"@angular/compiler": "^20.3.15",
"@angular/core": "^20.3.15",
Expand All @@ -34,9 +34,11 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular-devkit/schematics": "^20.3.13",
"@angular/build": "^20.3.13",
"@angular/cli": "^20.3.13",
"@angular/compiler-cli": "^20.3.15",
"@schematics/angular": "^20.3.13",
"@auto-it/conventional-commits": "^11.3.6",
"@auto-it/first-time-contributor": "^11.3.6",
"@auto-it/npm": "^11.3.6",
Expand All @@ -58,5 +60,10 @@
"ng-packagr": "^20.3.2",
"typescript": "~5.8.3",
"webpack-bundle-analyzer": "^5.0.1"
},
"overrides": {
"ws": "^8.21.0",
"vite": "^7.3.5",
"piscina": "^5.2.0"
}
}
1 change: 1 addition & 0 deletions projects/forge-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Web Components (custom elements) are not fully compatible with Angular by defaul

| `@tylertech/forge-angular` | Angular | Forge |
| -------------------------- | ------------------- | --------- |
| `^8.0.0` | `>=20.0.0 < 23.0.0` | `^3.14.0` |
| `^7.0.0` | `>=20.0.0 < 22.0.0` | `^3.12.1` |
| `^6.0.0` | `>=19.0.0 < 21.0.0` | `^3.8.0` |
| `^5.0.0` | `>=18.0.0 < 20.0.0` | `^3.0.0` |
Expand Down
6 changes: 5 additions & 1 deletion projects/forge-angular/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"dest": "../../dist/forge-angular",
"lib": {
"entryFile": "src/public-api.ts"
}
},
"assets": [
"./schematics/package.json",
"./schematics/migrations/migration.json"
]
}
7 changes: 5 additions & 2 deletions projects/forge-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
"author": "Tyler Technologies, Inc.",
"license": "Apache-2.0",
"peerDependencies": {
"@angular/common": ">=20.0.0 < 22.0.0",
"@angular/core": ">=20.0.0 < 22.0.0",
"@angular/common": ">=20.0.0 < 23.0.0",
"@angular/core": ">=20.0.0 < 23.0.0",
"@tylertech/forge": "^3.14.0"
},
"dependencies": {
"tslib": "^2.8.1"
},
"ng-update": {
"migrations": "./schematics/migrations/migration.json"
}
}
10 changes: 10 additions & 0 deletions projects/forge-angular/schematics/migrations/migration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"update-8-0-0": {
"version": "8.0.0",
"description": "Flags usages of DynamicComponentService.create() that rely on APIs removed in @tylertech/forge-angular v8: the ComponentFactory overload and the moduleRef argument.",
"factory": "./update-8-0-0/index"
}
}
}
160 changes: 160 additions & 0 deletions projects/forge-angular/schematics/migrations/update-8-0-0/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { getWorkspace } from '@schematics/angular/utility/workspace';
import * as path from 'path';
import * as ts from 'typescript';

const SERVICE_NAME = 'DynamicComponentService';
const LIBRARY_MARKER = 'forge-angular';

interface Finding {
file: string;
line: number;
column: number;
message: string;
}

interface TargetLike {
options?: Record<string, unknown>;
configurations?: Record<string, Record<string, unknown> | undefined>;
}

/**
* Migration for @tylertech/forge-angular v8.
*
* `DynamicComponentService.create()` no longer accepts a `ComponentFactory` as its first argument,
* nor an `NgModuleRef` as its fourth argument — both rely on Angular's deprecated `ComponentFactory`/
* `ComponentFactoryResolver` APIs (removed in Angular 22). Its signature is now:
*
* create<T>(component: Type<T>, target?: ViewContainerRef | HTMLElement, injector?: Injector)
*
* This migration does not auto-rewrite call sites (the correct replacement is app-specific — usually
* a direct dynamic `import()` of the standalone component). Instead it reports every affected call
* with its file and line so it can be addressed manually.
*/
export default function (): Rule {
return async (tree: Tree, context: SchematicContext) => {
const workspace = await getWorkspace(tree);
const findings: Finding[] = [];
const visited = new Set<string>();

for (const [, project] of workspace.projects) {
for (const tsConfigPath of collectTsConfigPaths(project)) {
let program: ts.Program | undefined;
try {
program = createProgram(tsConfigPath);
} catch {
// Unparseable/missing tsconfig — skip rather than fail the whole update.
continue;
}
if (!program) {
continue;
}

const checker = program.getTypeChecker();
for (const sourceFile of program.getSourceFiles()) {
if (sourceFile.isDeclarationFile || visited.has(sourceFile.fileName)) {
continue;
}
if (sourceFile.fileName.includes('node_modules')) {
continue;
}
visited.add(sourceFile.fileName);
collectFindings(sourceFile, checker, findings);
}
}
}

if (findings.length === 0) {
context.logger.info(
`[forge-angular] No '${SERVICE_NAME}.create()' calls relying on removed APIs were found. Nothing to migrate.`
);
return;
}

context.logger.warn(
`[forge-angular] Found ${findings.length} '${SERVICE_NAME}.create()' call(s) using APIs removed in v8. ` +
`Review each and migrate to 'create(component, target?, injector?)':`
);
for (const finding of findings) {
context.logger.warn(` - ${finding.file}:${finding.line}:${finding.column} — ${finding.message}`);
}
};
}

function collectTsConfigPaths(project: { targets: Iterable<[string, TargetLike]> }): string[] {
const paths = new Set<string>();
for (const [, target] of project.targets) {
const option = target.options?.['tsConfig'];
if (typeof option === 'string') {
paths.add(option);
}
for (const configuration of Object.values(target.configurations ?? {})) {
const configOption = configuration?.['tsConfig'];
if (typeof configOption === 'string') {
paths.add(configOption);
}
}
}
return [...paths];
}

function createProgram(tsConfigPath: string): ts.Program | undefined {
const configPath = path.resolve(process.cwd(), tsConfigPath);
const host: ts.ParseConfigFileHost = {
...ts.sys,
onUnRecoverableConfigFileDiagnostic: () => undefined
};
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);
if (!parsed || parsed.fileNames.length === 0) {
return undefined;
}
return ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
}

function collectFindings(sourceFile: ts.SourceFile, checker: ts.TypeChecker, findings: Finding[]): void {
const visit = (node: ts.Node): void => {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.name.text === 'create' &&
isDynamicComponentService(checker.getTypeAtLocation(node.expression.expression))
) {
const messages: string[] = [];

const firstArg = node.arguments[0];
if (firstArg && symbolName(checker.getTypeAtLocation(firstArg)) === 'ComponentFactory') {
messages.push('first argument is a ComponentFactory (no longer supported — pass the component Type instead)');
}
if (node.arguments.length >= 4) {
messages.push('the 4th "moduleRef" argument was removed (use a module-scoped Injector via the 3rd argument instead)');
}

if (messages.length > 0) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
findings.push({
file: path.relative(process.cwd(), sourceFile.fileName).replace(/\\/g, '/'),
line: line + 1,
column: character + 1,
message: messages.join('; ')
});
}
}

ts.forEachChild(node, visit);
};

visit(sourceFile);
}

function isDynamicComponentService(type: ts.Type): boolean {
const symbol = type.getSymbol() ?? type.aliasSymbol;
if (!symbol || symbol.getName() !== SERVICE_NAME) {
return false;
}
const declarations = symbol.getDeclarations() ?? [];
return declarations.some(declaration => declaration.getSourceFile().fileName.includes(LIBRARY_MARKER));
}

function symbolName(type: ts.Type): string | undefined {
return (type.getSymbol() ?? type.aliasSymbol)?.getName();
}
3 changes: 3 additions & 0 deletions projects/forge-angular/schematics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
18 changes: 18 additions & 0 deletions projects/forge-angular/schematics/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": ["es2022"],
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"declaration": false,
"sourceMap": false,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"],
"rootDir": ".",
"outDir": "../../../dist/forge-angular/schematics"
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts"]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable, ComponentFactoryResolver, ApplicationRef, Injector, ViewContainerRef, inject } from '@angular/core';
import { Type, EmbeddedViewRef, NgModuleRef, ComponentRef, ComponentFactory } from '@angular/core';
import { Injectable, ApplicationRef, Injector, ViewContainerRef, inject, createComponent, EnvironmentInjector } from '@angular/core';
import { Type, EmbeddedViewRef, ComponentRef } from '@angular/core';

export interface IDynamicComponentRef<T> {
instance: T;
Expand All @@ -12,38 +12,41 @@ export interface IDynamicComponentRef<T> {
providedIn: 'root'
})
export class DynamicComponentService {
private _cfr = inject(ComponentFactoryResolver);
private _injector = inject(Injector);
private _envInjector = inject(EnvironmentInjector);
private _appRef = inject(ApplicationRef);

constructor() {}

/**
* Creates an Angular component dynamically, and optionally attaches the component instance to a given element.
* @param component The component to create.
* @param target Optional target element to attach the component element to.
* @param moduleRef Optional module to create the component factory from.
* @param target Optional target to attach the component to. When a `ViewContainerRef` is provided the component
* is created within it; when an `HTMLElement` (or omitted) is provided the component is attached to the
* `ApplicationRef` and appended to the element.
* @param injector Optional element injector to use when creating the component.
*/
public create<T, K>(component: Type<T> | ComponentFactory<T>, target?: ViewContainerRef | HTMLElement, injector?: Injector, moduleRef?: NgModuleRef<K>): IDynamicComponentRef<T> {
const cfr = moduleRef ? moduleRef.componentFactoryResolver : this._cfr;
const componentFactory = this._isComponentFactory(component) ? component : cfr.resolveComponentFactory(component);
public create<T>(component: Type<T>, target?: ViewContainerRef | HTMLElement, injector?: Injector): IDynamicComponentRef<T> {
injector = injector || this._injector;

if (target instanceof ViewContainerRef) {
const ref = target.createComponent(componentFactory, undefined, injector);
const componentRef = target.createComponent(component, { injector });
return {
instance: ref.instance,
componentRef: ref,
instance: componentRef.instance,
componentRef,
destroy: () => {
target.detach();
componentRef.destroy();
}
};
}

const componentRef = componentFactory.create(injector);
const componentRef = createComponent(component, {
environmentInjector: this._envInjector,
elementInjector: injector
});
this._appRef.attachView(componentRef.hostView);
const componentElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
const componentElement = (componentRef.hostView as EmbeddedViewRef<unknown>).rootNodes[0] as HTMLElement;

if (target) {
target.appendChild(componentElement);
Expand All @@ -59,8 +62,4 @@ export class DynamicComponentService {
}
};
}

private _isComponentFactory<T>(component: Type<T> | ComponentFactory<T>): component is ComponentFactory<T> {
return (component as ComponentFactory<T>).create !== undefined;
}
}
2 changes: 1 addition & 1 deletion src/app/components/sidenav/sidenav.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/ro
import { IconRegistry } from '@tylertech/forge';
import { tylIconHome, tylIconSettings, tylIconSettingsInputComponent } from '@tylertech/tyler-icons';
import { ForgeDialogModule, ForgeDrawerModule, ForgeListModule, ForgeListItemModule, ForgeIconModule, ForgeExpansionPanelModule, ForgeOpenIconModule } from '@tylertech/forge-angular';
import { DrawerService } from 'src/app/services/drawer.service';
import { DrawerService } from '../../services/drawer.service';

IconRegistry.define([
tylIconHome,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Compiler, Component, Injector, inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { BottomSheetService, IBottomSheetOptions, ToastService, ForgeButtonModule, ForgeCheckboxProxyModule, ForgeCheckboxModule, ForgeTextFieldModule } from '@tylertech/forge-angular';
import { take, takeUntil } from 'rxjs/operators';

Expand All @@ -14,8 +14,6 @@ import { FormsModule } from '@angular/forms';
export class BottomSheetComponent {
private _bottomSheetService = inject(BottomSheetService);
private _toastService = inject(ToastService);
private _compiler = inject(Compiler);
private _injector = inject(Injector);

public persistent = false;
public fullscreen = false;
Expand Down Expand Up @@ -66,11 +64,8 @@ export class BottomSheetComponent {
}
} else {
// lazyload
const { LazyBottomSheetModule } = await import('./lazy/lazy-bottom-sheet.module');
const moduleFactory = await this._compiler.compileModuleAsync(LazyBottomSheetModule);
const moduleRef = moduleFactory.create(this._injector);
const componentFactory = moduleRef.instance.resolveComponent();
const bottomSheetRef = this._bottomSheetService.show(componentFactory.componentType, bottomSheetOptions, bottomSheetConfig);
const { LazyBottomSheetComponent } = await import('./lazy/lazy-bottom-sheet.component');
const bottomSheetRef = this._bottomSheetService.show(LazyBottomSheetComponent, bottomSheetOptions, bottomSheetConfig);
console.log('Native Forge bottomSheet instance', bottomSheetRef.nativeElement);
console.log('[BottomSheetRef] Angular componentInstance', bottomSheetRef.componentInstance);
bottomSheetRef.afterClosed.pipe(take(1)).subscribe(result => {
Expand Down
Loading
Loading