Dear community, I have a confession to make. I have not gotten over Zen yet. Alas, all good things must come to an EOF, so I am currently learning about Angular. I am working on proving to myself that with the right back end and Angular components, I can deliver to myself and my team a very Zen-like experience in this environment. Since this is my first attempt, here is a fair warning: I will be providing some rather large code samples before discussing them. Please warm up your mouse and hand for extensive upcoming scrolling! Also, a note on the code snippets: many of them are labeled "Javascript" when they should actually be "Typescript"; Typescript simply is not an option when inserting a code snippet.
The Back End
As with most front ends, I will need a REST API to call. If you are familiar with how the %CSP.REST class extends, you should be very comfortable with the class below.
Class DH.REST Extends %CSP.REST
{
Parameter HandleCorsRequest = 1
XData UrlMap [ XMLNamespace = "http://www.intersystems.com" ]
{
<Routes>
<Route Url="/select/:class/:id" Method="GET" Call="Select" />
<Route Url="/update/:class/:id" Method="PUT" Call="Update" />
<Route Url="/delete/:class/:id" Method="DELETE" Call="Delete" />
<Route Url="/insert/:class" Method="POST" Call="Update" />
</Routes>
}
ClassMethod Select(class, id) As %Status
{
try{
set myobj = $CLASSMETHOD(class,"%OpenId",id,,.sc)
$$$ThrowOnError(sc)
do myobj.%JSONExport()
return $$$OK
}
catch ex{
return ex.AsStatus()
}
}
ClassMethod Delete(class, id) As %Status
{
try{
return $CLASSMETHOD(class,"%DeleteId",id)
}
catch ex{
return ex.AsStatus()
}
}
ClassMethod Update(class, id = 0) As %Status
{
try{
set myobj = ##class(%Library.DynamicObject).%FromJSON(%request.Content)
if %request.Method = "POST"{
set record = $CLASSMETHOD(class,"%New")
}
else{
set record = $CLASSMETHOD(class,"%OpenId",id,,.sc)
$$$ThrowOnError(sc)
}
$$$ThrowOnError(record.%JSONImport(%request.Content))
$$$ThrowOnError(record.%Save())
do record.%JSONExport()
return $$$OK
}
catch ex{
return ex.AsStatus()
}
}
}
Most of this is basic IRIS stuff. I have a route map with routes defined to select, update, and delete records. I have the methods defined to follow that. Those methods are generally usable for any persistent class that extends the %JSON.Adaptor through the use of $CLASSMETHOD. In the update method, I utilize the %JSON.Adaptor’s %JSONImport method. Also, note that I have written this method in a way that allows it to handle both creating new records and updating existing ones, eliminating redundant code. To accomplish it, I have defined two different routes and checked the %request.Method to determine whether to open an existing record or create a new one.
I have this class set as a dispatch class for a web application called /zenular. Since I am only testing the concept on my local PC and not planning to go live, I am allowing unauthenticated access for simplicity’s sake. Of course, in a production environment, you would prefer to secure the API and limit the tables it can work on. However, it is a discussion for another article.
For my proof-of-concept purposes today, I will be using a very simple persistent class called DH.Person. It will contain just three properties: a first name, a last name, and a calculated MyId field, which will represent the object's ID. We will use it (MyId) when creating new objects. Without it, when we ask the API to save a new object, the JSON we return would not include the newly assigned ID, which we will need for our form. It will extend the %JSON.Adaptor class, as my API utilizes it.
Class DH.Person Extends (%Persistent, %JSON.Adaptor)
{
Property FirstName As %String
Property LastName As %String
Property MyId As %Integer [ Calculated, SqlComputeCode = { set {*}={%%ID}}, SqlComputed ]
}
The Project
In my Angular project directory, I have created a new project called “zenular” using the command below:
ng new zenular
It generates numerous default files and folders for the project. Within the app subfolder, I have made another subfolder, also named "zenular," to house all my Angular bits and pieces. Currently, I will only be modifying one of those files (app.config.ts):
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes),provideHttpClient()]
};
Note that I have added an import for provideHttpClient as well as provideHttpClient() to the providers here. I did it to make the HttpClient class available and ready to go throughout our other components. Since we are going to call a REST API, we will need to use it.
From the command line, within the /app/zenular project folder, I also ran the following commands to generate the pieces I will require:
ng generate service zervice
ng generate component ztext
ng generate component zformbutton
ng generate component zform
For each generated component, I get a folder with four files: a .component.css file, a .component.html file, a .component.ts file, and a .component.ts.spec file. However, we have just one file (zervice.ts) for our purposes. Therefore, we will perform most of our work in the .ts files.
zservice
import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
@Injectable({providedIn: 'root',})
export class DataControl{
private updatecount = new BehaviorSubject<number>(0);
zform: string = "";
zid: string = "";
zop: string = "";
currentcount = this.updatecount.asObservable();
updateForm(zformid: string, zid: string, zop: string){
this.zform = zformid;
this.zid = zid;
this.zop = zop;
this.updatecount.next(this.updatecount.value + 1);
}
}
This is the shortest and simplest part of the project. An Angular service provides a place for various Angular components to share data, meaning that when one of our components updates the zservice, those changes become visible to the other components. To enable components to watch for alterations, you require something observable. In this case, I have created a BehaviorSubject that is simply a number. My plan is to allow various components to utilize the updateForm function to notify forms on the page about the following: which form they are requesting a change on, a record ID if needed, and an operation to perform. After setting that, we increment our BehaviorSubject. The forms will detect it, check whether they are the intended target, and execute the requested operation accordingly.
ztext
Iplan to attempt using text fields for my proof of concept. Naturally, I could go for other inputs if I wanted, but this is a good place to start. I will be working within the .ts file.
import { Component, Attribute } from '@angular/core';
import { DataControl } from '../zservice'
@Component({
selector: 'ztext',
imports: [],
template: '<input type="text" value="{{value}}" (change)="onChange($event)" />',
styleUrl: './ztext.component.css'
})
export class ZtextComponent {
value: string = ""
fieldname: string = ""
dcform: string = ""
constructor(@Attribute('fieldname') myfieldname:string, @Attribute('dataControllerForm') mydcform:string, private dc:DataControl){
this.fieldname = myfieldname
this.dcform = mydcform
}
onChange(e: Event){
const target = e.target as HTMLInputElement
this.value = target.value
if (this.dcform !== null){
this.dc.updateForm(this.dcform,target.value,'select')
}
}
}
You might notice that I am importing the Attribute class from the @angular.core package. It will allow us to read the attributes of the HTML element that we will employ to put this component on a template later on. I have set the selector to ztext, meaning that I will utilize a <ztext /> HTML-style tag to operate this component. I have replaced the default templateUrl with a simple template here. I used (change) to ensure this component utilizes the onChange method, defined in this class, whenever the text box's value changes.
By default, there is an exported class ZtextComponent in the file. However, I have modified it by adding a few string values. One of them is for the value of the component. Another one is called fieldname, and I will use it to indicate which property of a persistent class in IRIS each ztext component corresponds to. I also want to allow a text box to control a form, so I have one more property called dcform that defines which form it will control. I intend to allow users to type a record ID into the text input and utilize the REST API to populate the remaining form with the relevant data.
The constructor is also crucial here. I have used the @Attribute decorator in the constructor definition to specify which attributes of the component’s tag will be used in the constructor to set the component’s properties. In this case, I have configured it to use the fieldname attribute to set the component's fieldname property and the dataControllerForm attribute to set the dcform for the component. I have also added a reference to the DataControl class from the zervice to give the class access to that service.
In the onChange event, we retrieve the value from the HTML text input. If this component is configured to be a data controller for a form, we then pass the form name, the value, and the string "select" to the zservice. When we decide to create the form itself later, we will ensure it watches for those requests and handles them accordingly.
zformbutton
Being able to load data is important, but it is no fun if we cannot also save or delete it! We need buttons.
import { Component, Attribute } from '@angular/core';
import { DataControl } from '../zservice';
@Component({
selector: 'zformbutton',
imports: [],
template: '<button (click)="onClick()"><ng-content></ng-content></button>',
styleUrl: './zformbutton.component.css'
})
export class ZformbuttonComponent {
dcform: string = "";
dcop: string = "";
constructor(@Attribute('dataControllerForm') mydcform:string, @Attribute('dataControllerOp') mydcop:string, private dc:DataControl){
this.dcform = mydcform;
this.dcop = mydcop;
}
onClick(){
this.dc.updateForm(this.dcform,'',this.dcop);
}
}
There are various similarities between this and the ztext. We use attributes to set the component’s dcform and dcop, which I will employ to define whether this button is supposed to update, delete, or create a new record. I have once more provided it with the DataControl class from zservice. I have also replaced the templateUrl with a simple template again. You should notice a matched set of ng-content tags in the middle of it. This is how you tell Angular where any content placed between the opening and closing selector tags for a component should go. We similarly have an event that utilizes the zservice’s updateForm method to request a data operation. This time, it is the click event instead of the change, and we pass the form name (not an ID) since the form will operate on its current record and component’s dcop.
The App Component
When I created my project, I got a default app component. I have modified the app.component.ts file as shown below:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ZtextComponent } from './zenular/ztext/ztext.component';
import { ZformComponent } from './zenular/zform/zform.component';
import { ZformbuttonComponent } from './zenular/zformbutton/zformbutton.component';
@Component({
selector: 'app-root',
imports: [RouterOutlet, ZtextComponent, ZformComponent, ZformbuttonComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
title = 'zenular';
apiroot = 'http://localhost:52773'
}
As of this moment, we have not yet discussed the zform. However, I am bringing up this app component now because I have included a property called apiroot within it. I have done this exclusively to make the project more portable. I am not entirely sure whether it will be necessary in the long run. Yet, it is important to keep that in mind as we move into the zform. Also, note the imports in this file, both at the top and in the @Component. It will be essential for using those components in the app.component.html file later on.
Zform
This is the component that will do the heavy lifting and interact with the API.
import { Component, OnDestroy, Attribute, ContentChildren, QueryList } from '@angular/core';
import { DataControl } from '../zservice';
import { Subscription } from 'rxjs';
import { ZtextComponent } from '../ztext/ztext.component';
import {HttpClient} from '@angular/common/http';
import { AppComponent } from '../../app.component';
@Component({
selector: 'zform',
imports: [],
template: '<ng-content></ng-content>',
styleUrl: './zform.component.css'
})
export class ZformComponent implements OnDestroy {
subscription:Subscription;
formname: string = "";
currentId: string = "";
table: string = "";
@ContentChildren(ZtextComponent) contentChildren!: QueryList<any>;
constructor(@Attribute('name') myname:string, @Attribute('tablename') mytable: string, private dc:DataControl, private http: HttpClient, private appcon: AppComponent){
this.subscription = this.dc.currentcount.subscribe(count => (this.updateData()));
this.table = mytable
this.formname = myname
}
updateData(){
if (this.formname === this.dc.zform){
if(this.dc.zop === 'select'){
this.currentId = this.dc.zid
this.http.get(this.appcon.apiroot+'/zenular/select/'+this.table+'/'+this.dc.zid, {responseType: 'text'}).subscribe((json: string) => {this.reloadForm(json)})
}
}
if(this.dc.zop === 'update'){
var jsonData : { [ key: string ]: any} = {};
this.contentChildren.forEach(child => {
jsonData[child.fieldname] = child.value;
})
if(this.currentId == ""){
var reqid = "0"
this.http.post(this.appcon.apiroot+'/zenular/insert/'+this.table, jsonData, {responseType: 'text'}).subscribe((json: string) => {this.reloadForm(json)})
}
else{
var reqid = this.currentId
this.http.put(this.appcon.apiroot+'/zenular/update/'+this.table+'/'+reqid, jsonData, {responseType: 'text'}).subscribe((json: string) => {this.reloadForm(json)})
}
}
if(this.dc.zop === 'delete'){
this.http.delete(this.appcon.apiroot+'/zenular/delete/'+this.table+'/'+this.currentId, {responseType: 'text'}).subscribe((json: string) => console.log(json))
this.contentChildren.forEach(child => {
child.value="";
})
this.currentId="";
}
if(this.dc.zop === 'new'){
this.currentId = "";
}
}
reloadForm(json: string){
var jsonData = JSON.parse(json);
this.contentChildren.forEach(child => {
if(child.dcform !== this.formname){
child.value=jsonData[child.fieldname];
}
else{
child.value=jsonData['MyId'];
}
})
this.currentId=jsonData['MyId'];
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
My import list is significantly longer this time, but I still need the component and attribute. From the same package, I also require OnDestroy for cleanup when the component is destroyed, ContentChildren to retrieve a list of components within this component's ng-content, and QueryList to iterate over those components. Ialso still need the DataControl and Subscription to subscribe to the Observable in the zservice to trigger updates. Since I will be programmatically working with the ztext components, I must interact with their class. As this is where I will make the API requests, HttpClient will be required. I am also importing the previously mentioned AppComponent solely to access the API root. I have kept the template very simple, containing only the ng-content tags.
The class includes a property called contentChildren, which will provide a list of all the components within the ng-content tags that are the ztext components. The class also has a subscription, which is where the @Observable in the zservice comes into play. In the constructor, I set up that subscription to subscribe to that Observable. When it gets triggered, the zform calls its private updateData method. The subscription is also the reason for implementing the OnDestroy. I have defined an ngOnDestroy method that unsubscribes from the subscription. This is simply the best practice cleanup step.
The updateData form is where the interaction between the server and the application occurs. As you can see, the form first checks the zservice to determine whether it is the form whose update has been requested. If the operation is "new," we simply clear the form's currentId. Depending on your usage, you might also want to clear the entire form. For my particular implementation, we do not typically empty the form to create a new record. Since we are an ERP system, it is common to make multiple records for such things as item numbers, where the majority of fields are identical, with only three or four different ones. This is why we leave them populated to facilitate the quick production of similar records. However, your use case may demand something else.
If the operation is "delete", we send a request to the delete API to remove the current record. We also clear all form inputs and the form's currentId.
If the request is "select", we check the zservice for the ID we are looking for, then send that request to the API. Upon receiving the response, we iterate over the inputs and pull values for them out of the JSON according to their fieldname properties.
If it is an "update", I package the values of all text inputs into JSON and apply a "put" request to the API with the help of the currentId. Alternatively, I utilize a "post" request without the ID if the currentId is not set, since this is how my API knows it should make a new record. The API responds with the JSON of the new object, which I then reload into the form. This is where the MyId field comes into play. When we create a new record, we need to pay attention to that and set the form’s currentId and data controller field. It will ensure that further changes to the object will result in an update rather than the creation of a new form.
Back to the App Component
When I previously discussed the app component, I left the templateUrl in the .ts file intact. This means that to add components to it, we should go to app.component.html. Now it is time to see how all that groundwork will pay off! We will replace the entire default contents of that file with the following:
<main class="main">
<zform tablename="DH.Person" name="PersonForm">
<ztext dataControllerForm="PersonForm" fieldname="ID" table="DH.Person"/>
<ztext fieldname="FirstName" />
<ztext fieldname="LastName" />
<zformbutton dataControllerForm="PersonForm" dataControllerOp="update">Save</zformbutton>
<zformbutton dataControllerForm="PersonForm" dataControllerOp="delete">Delete</zformbutton>
<zformbutton dataControllerForm="PersonForm" dataControllerOp="new">New</zformbutton>
</zform>
</main>
With just those steps, we have successfully created a basic form featuring fields that synchronize with IRIS via our API, complete with buttons to save and delete records.

While this is still a bit rough, it has provided strong reassurance that this is a promising direction for us! If you would like to witness how this project continues to develop, let me know and suggest what you would like to see refined next. Also, since I have only been using Angular for a couple of weeks, please let me know what I could have done better.