name: jwebmp-angular description: Generates Angular 21 TypeScript projects from JWebMP annotations and serves SPAs via Vert.x with STOMP/WebSocket bridging. Provides @NgApp, @NgComponent, @NgRoutable, @NgDataService annotations, TypeScript code generation, reactive messaging, Angular control-flow components, and WebSocket group management. Use when working with JWebMP Angular integration, TypeScript generation, Angular components, STOMP/WebSocket communication, or building Angular 21 applications with JWebMP. metadata: short-description: Angular 21 integration and TypeScript generation
JWebMP Angular Plugin
Angular 21 TypeScript project generation and SPA hosting with STOMP/WebSocket bridge for JWebMP.
Core Features
- TypeScript code generation from Java annotations
- SPA hosting via Vert.x with static asset serving
- STOMP/WebSocket bridge for real-time communication
- Reactive message processing with event bus integration
- Angular control-flow components (@if, @for, @let)
- Routing module with @NgRoutable support
- Environment module for configuration
Quick Start
1. Define an Angular App
@NgApp(value = "my-app", bootComponent = AppComponent.class)
public class MyApp extends NGApplication<MyApp> { }
2. Create Boot Component
@NgComponent("app-root")
public class AppComponent extends DivSimple<AppComponent>
implements INgComponent<AppComponent> { }
3. Add Routable Pages
@NgRoutable(path = "dashboard", parent = {AppComponent.class})
@NgComponent("app-dashboard")
public class DashboardPage extends DivSimple<DashboardPage>
implements INgComponent<DashboardPage> { }
4. Enable TypeScript Generation
export JWEBMP_PROCESS_ANGULAR_TS=true
# Or
mvn verify -Djwebmp.process.angular.ts=true
5. Start Application
IGuiceContext.instance();
// → TypeScript generated to ~/.jwebmp/<appName>/
// → Vert.x serves dist at configured routes
// → STOMP/WebSocket bridge live at /eventbus
TypeScript Compiler Pipeline
TypeScriptCompiler
├── AngularAppSetup → Scaffolds angular.json, package.json, tsconfig
├── DependencyManager → Resolves @TsDependency / @TsDevDependency
├── ComponentProcessor → Processes INgComponent, INgDirective, INgDataService
├── AngularModuleProcessor → Generates boot module, routing module
├── TypeScriptCodeGenerator → Renders .ts files from annotations
├── AssetManager → Copies @NgAsset, @NgScript, @NgStyleSheet
└── TypeScriptCodeValidator → Validates generated output
Annotations
@NgApp
@NgApp(value = "my-app", bootComponent = AppComponent.class)
public class MyApp extends NGApplication<MyApp> { }
@NgComponent
@NgComponent("app-header")
public class HeaderComponent implements INgComponent<HeaderComponent> {
@Override
public String render() {
return "<header><h1>My App</h1></header>";
}
}
@NgRoutable
@NgRoutable(path = "users", parent = {AppComponent.class})
@NgComponent("app-users")
public class UsersPage implements INgComponent<UsersPage> { }
@NgDataService
@NgDataService
public class UserService implements INgDataService<UserService> {
@Override
public Object getData(AjaxCall<?> call, AjaxResponse<?> response) {
return userRepository.findAll();
}
}
@NgRestClient
Generate a fully typed, signal-based @Injectable Angular REST service from a
single Java class — the REST counterpart to @NgGraphQL. Defined in the
tsclient plugin (com.jwebmp.core.base.angular.client.annotations.angular);
see the jwebmp-tsclient skill for the full attribute reference.
@NgRestClient(
url = "/api/users",
method = NgRestClient.HttpMethod.GET,
responseType = User.class,
responseArray = true,
cachingEnabled = true,
pollingEnabled = true)
@NgRestClientHeader(name = "Accept", value = "application/json")
@NgRestClientQueryParam(name = "active", value = "true")
public class UsersClient implements INgRestClient<UsersClient> {}
Generates an @Injectable HttpClient service exposing data/loading/error/
success/polling signals, execute() / executeWithBody(), plus polling,
caching, deduplication, deep-merge, retry, and NONE/BEARER/BASIC/CUSTOM auth.
STOMP/WebSocket Communication
WebSocket Bridge Flow
Angular Client (STOMP)
└─ ws://host/eventbus
├─ SEND /toBus/incoming → Event bus consumer
│ ├─ Deserialize WebSocketMessageReceiver
│ ├─ Dispatch to IWebSocketMessageReceiver (ajax/data/dataSend)
│ ├─ Run in CallScope (WebSocket)
│ └─ Reply via event bus:
│ ├─ dataReturns → publish to per-key addresses
│ ├─ sessionStorage → publish to "SessionStorage"
│ └─ localStorage → publish to "LocalStorage"
└─ SUBSCRIBE /toStomp/* ← Server pushes via StompEventBusPublisher
Built-in Message Receivers
| Receiver | Action | Purpose |
|---|---|---|
WebSocketAjaxCallReceiver |
ajax |
Deserializes AjaxCall, fires event, returns AjaxResponse |
WebSocketDataRequestCallReceiver |
data |
Resolves INgDataService, calls getData() |
WebSocketDataSendCallReceiver |
dataSend |
Resolves INgDataService, calls receiveData() |
WSAddToGroupMessageReceiver |
AddToWebSocketGroup |
Adds session to WebSocket group |
WSRemoveFromWebsocketGroupMessageReceiver |
RemoveFromWebSocketGroup |
Removes session from group |
STOMP Configuration
| Setting | Value | Notes |
|---|---|---|
| WebSocket path | /eventbus |
STOMP over WebSocket endpoint |
| Server heartbeat | 10000 ms |
Server → client |
| Client heartbeat | 50000 ms |
Client → server (lenient for background tabs) |
| Sub-protocols | v10.stomp, v11.stomp, v12.stomp |
Advertised on HTTP upgrade |
| Idle timeout | 0 (disabled) |
Relies on STOMP heartbeats |
Angular Control-Flow Components
NgIf / NgIfElse
NgIf<MyComponent> ifBlock = new NgIf<>(this)
.setCondition("isLoggedIn")
.add(new Paragraph<>().setText("Welcome!"));
NgIfElse<MyComponent> ifElse = new NgIfElse<>(this)
.setCondition("hasData")
.add(new Div<>().setText("Data loaded"))
.setElseBlock(new NgElse<>()
.add(new Div<>().setText("No data")));
Renders:
@if (isLoggedIn) {
<p>Welcome!</p>
}
@if (hasData) {
<div>Data loaded</div>
} @else {
<div>No data</div>
}
NgFor
NgFor<MyComponent> forLoop = new NgFor<>(this)
.setIterable("users")
.setTrackBy("userId")
.add(new Div<>().setText("{{ user.name }}"));
Renders:
@for (user of users; track userId) {
<div>{{ user.name }}</div>
}
NgLet
NgLet<MyComponent> letVar = new NgLet<>(this)
.setVariable("total")
.setExpression("calculateTotal()");
Renders:
@let total = calculateTotal();
Routing
AngularRoutingModule
Scans @NgRoutable classes and generates route tree:
@NgRoutable(path = "dashboard", parent = {AppComponent.class})
public class DashboardPage { }
@NgRoutable(path = "users", parent = {DashboardPage.class})
public class UsersPage { }
@NgRoutable(path = "profile/:id", parent = {DashboardPage.class})
public class ProfilePage { }
Generates:
RouterModule.forRoot([
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: 'users', component: UsersComponent },
{ path: 'profile/:id', component: ProfileComponent }
]
}
])
RouterLink Component
RouterLink link = new RouterLink()
.setRouterLink("/dashboard")
.setText("Go to Dashboard");
RouterLink paramLink = new RouterLink()
.setRouterLink("/profile/123")
.setQueryParams(Map.of("tab", "settings"))
.setText("View Profile");
Environment Module
EnvironmentModule env = new EnvironmentModule()
.setOptions(new EnvironmentOptions()
.setProduction(false)
.setApiUrl("http://localhost:8080/api")
.addCustomProperty("featureFlags", Map.of(
"newUI", true,
"betaFeatures", false
)));
Generates:
export const environment = {
production: false,
apiUrl: 'http://localhost:8080/api',
featureFlags: {
newUI: true,
betaFeatures: false
}
};
Vert.x Router Wiring
| Route | Method | Handler | Purpose |
|---|---|---|---|
/eventbus/* |
WebSocket | STOMP server | WebSocket → STOMP bridge |
/assets/* |
GET | StaticHandler | Angular compiled assets (1-year cache) |
/{file}.{ext} |
GET | StaticHandler | Root-level static files |
/** (SPA fallback) |
GET | sendFile(index.html) |
Angular Router routes |
SPI Extension Points
| SPI | Purpose |
|---|---|
AngularScanPackages |
Add packages to Angular classpath scan |
RenderedAssets |
Provide additional build assets |
NpmrcConfigurator |
Customize .npmrc file |
WebSocketGroupAdd |
Custom logic for WebSocket group joins |
TypescriptIndexPageConfigurator |
Customize generated index.html |
IWebSocketAuthDataProvider |
Provide authentication data for WebSocket |
Configuration
| Environment Variable | Default | Purpose |
|---|---|---|
JWEBMP_PROCESS_ANGULAR_TS |
false |
Enable/disable TypeScript generation |
jwebmp.outputDirectory |
— | Override output directory |
jwebmp |
~ (user home) |
Base directory |
ENVIRONMENT |
dev |
Runtime environment hint |
PORT |
8080 |
Server port |
NPM Dependencies
The plugin manages Angular dependencies:
{
"dependencies": {
"@angular/core": "^20.0.0",
"@angular/common": "^20.0.0",
"@angular/router": "^20.0.0",
"@angular/forms": "^20.0.0",
"@stomp/ng2-stompjs": "^latest",
"rxjs": "^7.8.0"
}
}
Common Patterns
Data Service with WebSocket
@NgDataService
public class LiveDataService implements INgDataService<LiveDataService> {
@Inject
private DataRepository repository;
@Override
public Object getData(AjaxCall<?> call, AjaxResponse<?> response) {
String entityId = call.getParameters().get("id");
return repository.findById(entityId);
}
@Override
public void receiveData(AjaxCall<?> call, AjaxResponse<?> response) {
String data = call.getParameters().get("data");
repository.save(data);
// Push update to all clients in group
StompEventBusPublisher.publish("/toStomp/updates", data);
}
}
Event Handling
public class ButtonClickEvent extends OnClickAdapter {
@Override
public void onClick(AjaxCall<?> call, AjaxResponse<?> response) {
// Process server-side
String result = processData();
// Update UI
response.addComponent(new Div<>().setText("Result: " + result));
// Publish to WebSocket group
StompEventBusPublisher.publish("/toStomp/notifications", result);
}
}
WebSocket Group Management
@NgComponent("chat-room")
public class ChatRoom implements INgComponent<ChatRoom> {
@Override
public void configure(IComponentHierarchyBase<?, ?> component) {
// Component auto-joins WebSocket group
component.setAttribute("websocketgroup", "chat-room-1");
}
}
JPMS Module
module com.jwebmp.core.angular {
requires transitive com.jwebmp.core.base.angular.client;
requires transitive com.jwebmp.vertx;
requires transitive io.vertx.eventbusbridge;
requires transitive io.vertx.stomp;
provides IGuicePreStartup with AngularPreStartup;
provides IGuicePostStartup with AngularTSPostStartup;
provides IGuiceModule with AngularTSSiteBinder;
provides VertxRouterConfigurator with AngularTSSiteBinder;
provides IWebSocketMessageReceiver with
WebSocketAjaxCallReceiver,
WebSocketDataRequestCallReceiver,
WebSocketDataSendCallReceiver,
WSAddToGroupMessageReceiver,
WSRemoveFromWebsocketGroupMessageReceiver;
}
Key Classes
AngularTSSiteBinder— Core module + router configurationTypeScriptCompiler— Orchestrates TS generationNGApplication— Base class for @NgAppAngularRoutingModule— Generates RouterModule.forRoot()EnvironmentModule— Generates environment configStompEventBusPublisher— Helper for STOMP publishing
Installation
<dependency>
<groupId>com.jwebmp.plugins</groupId>
<artifactId>angular</artifactId>
</dependency>
References
- Module:
com.jwebmp.core.angular - Java: 25+
- Angular: 20
- Dependencies: JWebMP Core, Vert.x, STOMP
- License: Apache 2.0
Build Notes
Plugin generates TypeScript but does not run ng build. Build separately:
cd ~/.jwebmp/my-app
npm install
ng build
Dist output served automatically by Vert.x.