BOX Template Development Guide
This guide shows how to create BOX templates for the Ampersand prototype framework. We use PROPBUTTON as the complete example, progressing from basic concepts to advanced implementation.
Start with the template structure, build the component functionality, and test thoroughly. Use the established patterns for backend communication and error handling to create robust, reusable templates.
Prerequisites
Creating BOX templates requires understanding three key components: Mustache templates, Angular components, and module registration. You need basic Angular knowledge and understanding of Ampersand concepts. You must know how BOX templates work and how interfaces generate frontend code. You require access to the prototype framework codebase.
Overview: The PROPBUTTON Example
PROPBUTTON creates interactive buttons that modify boolean properties. Users click buttons to toggle, set, or clear boolean values. This example demonstrates all key concepts you need to create BOX templates.
What PROPBUTTON Does
- Creates buttons with custom labels
- Supports three actions: toggle, set, clear
- Modifies boolean properties in the backend
- Updates the interface immediately after changes
Part 1: Understanding the Architecture
BOX templates consist of three components working together:
1. Mustache Template
Location: frontend/src/app/generated/.templates/Box-PROPBUTTON.html
The "mustache template" is the .html file with placeholders that the Ampersand compiler processes to create your actual Angular components. It's called "Mustache" because the placeholder syntax {{ }} looks like a sideways mustache, though Ampersand uses instead.
<!-- Box-PROPBUTTON.html -->
<div $if(isRoot)$*ngIf="resource?.data" $endif$>
<app-box-prop-button
crud="$crud$"
$if(action)$action="$action$"
$endif$[interfaceComponent]="this"
[resource]="resource"
propertyName="$name$"
[data]="$if(exprIsUni)$[resource.$name$]$else$resource.$name$$endif$"
tgtResourceType="$target$"
$if(exprIsUni)$isUni$endif$
$if(exprIsTot)$isTot$endif$
>
</app-box-prop-button>
</div>
2. Angular Component
Location: frontend/src/app/shared/box-components/box-prop-button/
The component has three files:
box-prop-button.component.ts- TypeScript logicbox-prop-button.component.html- HTML templatebox-prop-button.component.scss- Styling
3. Module Registration
Location: frontend/src/app/shared/shared.module.ts
The component is imported and declared in the shared module.
Part 2: Quick Start - Template Creation
Step 1: Create the Mustache Template
Start with the template file. The Ampersand compiler uses this to generate Angular component calls.
Create: frontend/src/app/generated/.templates/Box-YOURTEMPLATE.html
Template Pattern:
<!-- Box-YOURTEMPLATE.html -->
<div $if(isRoot)$*ngIf="resource?.data" $endif$>
<app-box-your-template
crud="$crud$"
[interfaceComponent]="this"
[resource]="resource"
propertyName="$name$"
[data]="$if(exprIsUni)$[resource.$name$]$else$resource.$name$$endif$"
tgtResourceType="$target$"
$if(exprIsUni)$isUni$endif$
$if(exprIsTot)$isTot$endif$
>
</app-box-your-template>
</div>
Understanding Template Variables
These variables get substituted during compilation:
$crud$- Permission string (cRud, CRud, etc.)$name$- Property name from the Ampersand interface$target$- Target concept type$if(exprIsUni)$- Conditional for univalent relations$if(exprIsTot)$- Conditional for total relations
PROPBUTTON Addition: The action parameter
$if(action)$action="$action$"$endif$
Step 2: Test with Ampersand Script
Create a test script to see your template work:
Location: test/projects/yourtemplate-test/model/test.adl
CONTEXT YourTemplateTest IN ENGLISH
CONCEPT Task "A task for testing"
RELATION taskName[Task*TaskName] [UNI,TOT]
RELATION isCompleted[Task] [PROP]
REPRESENT TaskName TYPE ALPHANUMERIC
POPULATION taskName CONTAINS
[ ("task1", "Test task") ]
INTERFACE TestInterface : "_SESSION" ; V[SESSION*Task] cRud BOX<FORM>
[ "Task Name" : taskName cRud
, "Complete Task" : I cRud BOX<YOURTEMPLATE>
[ "label" : TXT "Mark Complete"
, "property" : isCompleted cRUd
]
]
Step 3: Compile and Test
# In your test project directory
docker build -t yourtemplate-test .
docker run -d -p 8080:80 yourtemplate-test
Access http://localhost:8080 to see your interface.
Part 3: Component Implementation
Now we create the Angular component that provides the actual functionality.
Step 1: Component Structure
Create the component directory:
frontend/src/app/shared/box-components/box-prop-button/
├── box-prop-button.component.ts
├── box-prop-button.component.html
└── box-prop-button.component.scss
Step 2: TypeScript Implementation
File: box-prop-button.component.ts
import { Component, Input } from '@angular/core';
import { BaseBoxComponent } from '../BaseBoxComponent.class';
import { ObjectBase } from '../../objectBase.interface';
type PropButtonItem = ObjectBase & {
label: string;
property: boolean;
};
@Component({
selector: 'app-box-prop-button',
templateUrl: './box-prop-button.component.html',
styleUrls: ['./box-prop-button.component.scss'],
})
export class BoxPropButtonComponent<
TItem extends PropButtonItem,
I extends ObjectBase | ObjectBase[],
> extends BaseBoxComponent<TItem, I> {
@Input() action?: 'toggle' | 'set' | 'clear' = 'toggle';
handleClick(item: TItem) {
let value: boolean;
switch (this.action) {
case 'set':
value = true;
break;
case 'clear':
value = false;
break;
case 'toggle':
default:
value = !item.property;
break;
}
this.interfaceComponent
.patch(item._path_, [{ op: 'replace', path: 'property', value: value }])
.subscribe((x) => {
if (x.isCommitted) {
this.data = [x.content as any as TItem];
}
});
}
}
Key Implementation Points
- Extend BaseBoxComponent - Provides standard BOX functionality
- Define Type Interface - Specifies expected data structure
- Use Generic Types - Enables proper TypeScript checking
- Handle User Actions - Implements click handlers
- Update Backend - Uses patch() method to modify data
- Refresh State - Updates local data after backend confirmation
Step 3: HTML Template
File: box-prop-button.component.html
<div *ngFor="let item of data; let i = index">
<button pButton (click)="handleClick(item)" label="{{ item.label }}" type="button"></button>
</div>
Step 4: Component Styling
File: box-prop-button.component.scss
// Component-specific styles
button {
margin: 0.25rem;
}
Step 5: Register Component
Add to frontend/src/app/shared/shared.module.ts:
// Import
import { BoxPropButtonComponent } from './box-components/box-prop-button/box-prop-button.component';
// Add to declarations array
declarations: [
// ... other components
BoxPropButtonComponent,
],
// Add to exports array
exports: [
// ... other components
BoxPropButtonComponent,
],
Part 4: Advanced Features
Custom Actions
PROPBUTTON supports different action types through the action parameter:
-- Toggle action (default)
BOX<PROPBUTTON>
[ "label" : TXT "Toggle Status"
, "property" : isActive cRUd
]
-- Set action (always true)
BOX<PROPBUTTON action="set">
[ "label" : TXT "Activate"
, "property" : isActive cRUd
]
-- Clear action (always false)
BOX<PROPBUTTON action="clear">
[ "label" : TXT "Deactivate"
, "property" : isActive cRUd
]
Template Variable Handling
The template handles the action parameter conditionally:
$if(action)$action="$action$"$endif$
This generates:
- No action attribute for default toggle behavior
action="set"for set behavioraction="clear"for clear behavior
Data Binding Patterns
The template uses conditional data binding:
[data]="$if(exprIsUni)$[resource.$name$]$else$resource.$name$$endif$"
This handles:
- Univalent relations: Single item wrapped in array
- Non-univalent relations: Direct array access
Error Handling
Add robust error handling to your component:
this.interfaceComponent
.patch(item._path_, [{ op: 'replace', path: 'property', value: value }])
.subscribe({
next: (result) => {
if (result.isCommitted) {
this.data = [result.content as any as TItem];
}
},
error: (error) => {
console.error('Update failed:', error);
// Handle error appropriately
}
});
Since the Ampersand compiler knows nothing about the frontend, it cannot enforce typing constraints on your templates. Therefore, you must generate your own informative error messages, which users will get on runtime.
Part 5: Testing Your Implementation
Complete Test Script
File: test/projects/propbutton-unit-test/model/PropButtonTest.adl
CONTEXT PropButtonTest LABEL "PROPBUTTON Unit Test" IN ENGLISH
CONCEPT Task "A simple task for testing PROPBUTTON functionality"
RELATION taskName[Task*TaskName] [UNI,TOT]
RELATION isCompleted[Task] [PROP]
RELATION isActive[Task] [PROP]
REPRESENT TaskName TYPE ALPHANUMERIC
POPULATION taskName CONTAINS
[ ("task1", "Test PROPBUTTON toggle functionality")
, ("task2", "Test PROPBUTTON set/clear actions")
]
INTERFACE PropButtonDemo LABEL "PROPBUTTON Test Interface" : "_SESSION" ; V[SESSION*Task] cRud BOX<FORM>
[ "Task Name" : taskName cRud
, "Status Info" : I cRud BOX<FORM>
[ "Completed" : isCompleted cRud
, "Active" : isActive cRud
]
, "PROPBUTTON Actions" : I cRud BOX<FORM>
[ "Toggle Complete" : I cRud BOX<PROPBUTTON>
[ "label" : TXT "Mark Complete"
, "property" : isCompleted cRUd
]
, "Activate Task" : I cRud BOX<PROPBUTTON action="set">
[ "label" : TXT "Activate"
, "property" : isActive cRUd
]
, "Deactivate Task" : I cRud BOX<PROPBUTTON action="clear">
[ "label" : TXT "Deactivate"
, "property" : isActive cRUd
]
]
]
INTERFACE TaskOverview LABEL "Task Status Overview" : V[SESSION*Task] BOX<TABLE>
[ "Task" : taskName
, "Completed" : isCompleted
, "Active" : isActive
]
Testing Scenarios
Toggle Action Test
- Click "Mark Complete" button
- Property should alternate between true/false
- Verify in Task Status Overview
Set Action Test
- Click "Activate" button multiple times
- Property should always be true
- Verify persistence across clicks
Clear Action Test
- Click "Deactivate" button multiple times
- Property should always be false
- Verify persistence across clicks
Compilation Process
Your template goes through this process:
- Ampersand Compilation - Processes
.adlfiles, generates TypeScript - Template Processing - Substitutes variables in Mustache templates
- Angular Compilation - Compiles TypeScript to JavaScript
Part 6: Common Patterns and Best Practices
Component Design Patterns
1. Type Safety
type YourComponentItem = ObjectBase & {
requiredField: string;
optionalField?: boolean;
};
2. Input Validation
@Input() customParameter?: string;
ngOnInit() {
if (this.customParameter && !this.isValidParameter(this.customParameter)) {
console.warn('Invalid parameter:', this.customParameter);
}
}
3. Naming Conventions
- Component selector:
app-box-your-template - Template file:
Box-YOURTEMPLATE.html - Component class:
BoxYourTemplateComponent
Backend Communication
PROPBUTTON Pattern:
this.interfaceComponent
.patch(item._path_, [{ op: 'replace', path: 'property', value: newValue }])
.subscribe((result) => {
if (result.isCommitted) {
this.data = [result.content as any as TItem];
}
});
Key Points:
- Use
patch()for data modifications - Check
isCommittedbefore updating local state - Update
this.datato refresh the interface
Template Variable Best Practices
Standard Variables (always include):
crud="$crud$"
[interfaceComponent]="this"
[resource]="resource"
propertyName="$name$"
tgtResourceType="$target$"
Multiplicity Handling:
[data]="$if(exprIsUni)$[resource.$name$]$else$resource.$name$$endif$"
$if(exprIsUni)$isUni$endif$
$if(exprIsTot)$isTot$endif$
Custom Parameters:
$if(yourParameter)$yourParameter="$yourParameter$"$endif$
Part 7: Troubleshooting
Common Issues
Template Not Found
- Check file naming:
Box-PROPBUTTON.html(exact case) - Verify location:
frontend/src/app/generated/.templates/ - Confirm compilation completed successfully
Variables Not Substituted
- Check syntax:
$variableName$(with dollar signs) - Verify conditional syntax:
$if(condition)$...$endif$ - Ensure variables exist in compilation context
Component Not Working
- Verify registration in
shared.module.ts - Check component selector matches template
- Confirm component extends
BaseBoxComponent - Check property names and data binding
Backend Communication Fails
- Verify CRUD permissions: property needs
cRUd(capital U) - Check property path in patch operation
- Confirm backend API endpoints work
- Use browser developer tools to inspect network requests
Debugging Steps
Check Template Generation
# Look for generated files
ls frontend/src/app/generated/Verify Component Registration
// In shared.module.ts, confirm:
import { BoxPropButtonComponent } from './box-components/box-prop-button/box-prop-button.component';
// ... in declarations and exports arraysTest Backend API
// In browser console
fetch('/api/v1/resource/Task/task1', {method: 'GET'})
.then(r => r.json())
.then(console.log)Inspect Generated Code Look at
frontend/src/app/generated/project.module.tsfor your interface components.