Angular: page templates, MX tags and the CLI
.ng.mx keeps the class and its template in one file. Two other inputs exist, and mx-angular compiles all three.
| Input | Output | Use it for |
|---|---|---|
x.component.ng.mx |
x.component.ts |
A component: class and template together |
a page template, x.component.mx |
x.component.html |
The templateUrl of a class you keep in its own .ts |
a tag, tags/badge.mx |
tags/badge.ts, a standalone component |
A reusable piece with no class of its own |
A .mx file is a tag when it sits under a tags/ directory or is listed in package.json#mx.tags; otherwise it is a page template. A mx.tags entry restricted to other hosts ("hosts": ["solid"]) is not compiled here.
Page templates
Keep x.component.ts as it is, point templateUrl at ./x.component.html, write x.component.mx beside it, and list it in mx.angular.include (for example src/**/*.mx): page templates are not found on their own, unlike .ng.mx files and tags. Compared with .ng.mx, MX cannot edit your class, so two things are yours:
- Imports. When the template calls an MX tag or needs a directive, the build warns with the exact
importandimports:entry, and repeats it in the header of the emitted.html. Without the entry Angular renders an unknown element as an empty tag, silently. - Event members. A template that binds a handler needs
__mxOnand__mxOnAton the class. ExtendMxHandlers, orMxHandlersMixin(Base), from@mxlang/angular/runtime; a missing member is TS2339 at build time understrictTemplates.
With the class in one file and the template in another, .ng.mx is the simpler form for a new component; a page template suits a component whose class you do not want to move.
MX tags
export interface Input { kind: "ok" | "warn" | "error"; label?: string }
<span.badge data-kind=input.kind>
<if=input.label>${input.label}: </if>${input.content()}
</span>
compiles to a standalone component:
@Component({
selector: "mx-badge",
standalone: true,
imports: [],
template: "<span class=\"badge\" [attr.data-kind]=\"kind\">@if (label) { {{ label }}: }<ng-content></ng-content></span>",
})
export class Badge {
@NgInput({ required: true }) kind!: "ok" | "warn" | "error";
@NgInput() label?: string;
}
The real template binds data-kind through an @let that applies the value rules in Attributes.
- Inputs come from
export interface Input: one@Input()per property,requiredunless optional, the type copied as written. The template readsinput.kind; the emitted template readskind. - Content:
${input.content()}is<ng-content>. An attribute tag,${input.header()}or<${input.header}/>, is<ng-content select="[header]">. - Selector:
mx-plus the file name (mx-badge). Change the prefix withmx.angular.tagSelectorPrefix, or one tag withexport const selector = "acme-card";. - No
@Output(): a function-typed property is an input, and the caller passes a callback. importandstaticstay in the tag’s module. An import of another.mxtag is rewritten to its generated module.
Projection places nodes; it cannot pass values or repeat. So in a tag, these are errors and not blank output:
| Written | Why |
|---|---|
${input[key]} |
The property is not known until run time |
${input.x} and ${input.x()} in one tag |
One is an input, the other a projection |
${input.header(1)}, attribute tags with params, arrays or attributes |
<ng-content> cannot take values |
| reading the same attribute tag twice | Angular fills each selector once |
<child header=input.header/> where child projects header |
Nest a <@header> block instead |
Configuration
// package.json
{
"mx": {
"host": "angular",
"angular": {
"include": ["src/**/*.mx"], // page templates to compile
"pageExtension": ".html", // defaults from here down
"tagExtension": ".ts",
"tagSelectorPrefix": "mx-",
"onError": "keep-last"
}
}
}
Tags and .ng.mx files are compiled wherever they are; include selects page templates. Emitted files are build artifacts: ignore them in git, and keep the .mx sources out of tsconfig.json’s include and angular.json’s assets.
mx-angular
mx-angular build [--project <dir>]
mx-angular watch [--project <dir>] [--once]
mx-angular map <file.html:line:col>
build compiles everything once and exits 1 on any error. Run it before ng build and before ng serve starts: Angular reads the emitted files from disk. It writes a file only when its bytes change and never overwrites a file without MX’s generated header. Warnings print as file:line:col warning: … and do not fail the build.
watch builds, then recompiles what changed: a page alone, or every page that calls a changed tag. Run it beside ng serve with concurrently -k, so stopping one stops the other. --once builds and exits.
onError decides what a compile error does to the last good output:
| Value | Effect |
|---|---|
keep-last (default) |
Keeps it and prints the error. With no earlier output, writes a visible error template |
error-template |
Always writes the error template |
delete |
Removes the output. Use it in CI, where a stale template is worse than a missing one |
map turns a position in an emitted .html (or the .ts of a .ng.mx) into the .mx position it came from, 1-based on both sides:
$ mx-angular map src/greeting.html:2:12
greeting.mx:2:8
It maps what came from your source (names, expressions, conditions) to the start of that run, and says so when a position is in markup MX generated.
Example
examples/angular-app is a stock Angular CLI 22 app with a page template (app.component.mx), a .ng.mx component and the badge tag above.