Skip to main content
Version: Previous

Web Developer training - Day two

This day covers:

Orders screen

Let's continue the development of our web app creating an order screen. We're going to work on these files:

  • order.template.ts
  • order.ts
  • order.styles.ts

You should have created these files in the last exercise of the previous day of the training with the navigation bar pointing to them as well.

Now, let's replace the dummy content of these files with the actual implementation we want.

Requirements

The goal of our app is to list all the orders with some filters and actions to insert a new order, edit an existing order and cancel an order.

Fields

FieldTypeEditableNotes
InstrumentSelect or Search (autocomplete field)YesLoad data from ALL_INSTRUMENTS Data Server
Market dataDisplay price of the selected symbolNoLoad data from INSTRUMENT_MARKET_DATA ReqRep
QuantityIntegerYesMust be positive
PriceDoubleYesMust be positive
TotalDoubleNoDisplay Quantity * Price
DirectionDropdownYesDisplay types from ENUM DIRECTION
NotesStringYesFree text up to 50 chars

Actions

Insert, edit and cancel.

Adding the new Order modal

Let's start with the simplest way to create a form, using the foundation-form component:

order.template.ts
import {html} from '@genesislcap/web-core';
import type {Order} from './order';

export const OrderTemplate = html<Order>`
<foundation-form
class="order-entry-form"
resourceName="EVENT_ORDER_INSERT">
</foundation-form>
`;

This component is able to retrieve the meta-data from the EVENT_ORDER_INSERT backend resource (an Event Handler) and automatically builds a simple form for you. In simple scenarios, it can be good enough.

Try to run it now and you'll notice that, even though the form is displayed, nothing happens when you click on Submit. We have to bind the submit button to a function, like this:

order.template.ts
  <foundation-form
class="order-entry-form"
resourceName="EVENT_ORDER_INSERT"
@submit=${(x, c) => x.insertOrder(c.event as CustomEvent)}>
</foundation-form>
what is the @submit ?

This is related to binding as we briefly explained in the previous day. If it's still unclear, make sure to check Understanding bindings and Events

We define insertOrder function in order.ts

order.ts
  ...
import {Connect} from '@genesislcap/foundation-comms';
import {logger} from '../../utils';

...
export class Order extends GenesisElement {
@Connect connect: Connect;

constructor() {
super();
}

public async insertOrder(event) {
const formData = event.detail.payload;
const insertOrderEvent = await this.connect.commitEvent('EVENT_ORDER_INSERT', {
DETAILS: {
INSTRUMENT_ID: formData.INSTRUMENT_ID,
QUANTITY: formData.QUANTITY,
PRICE: formData.PRICE,
DIRECTION: formData.DIRECTION,
NOTES: formData.NOTES,
},
});
logger.debug('EVENT_ORDER_INSERT result -> ', insertOrderEvent);
}
}

Introducing Genesis Foundation Comms lib

As you can see in the insertOrder code, we are importing Connect from @genesislcap/foundation-comms, which is Genesis core communication system with the server.

full flexibility

You can use the foundation-comms in any modern web app, based on FAST or not. This gives you full flexibility on how to interact with the server without, necessarily, relying on the UI components provided.

Alternatively, you could use any HTTP client to access the server resources as they are exposed as HTTP endpoints as well. However, we strongly encourage the use of Foundation Comms as it handles things like the web socket connection, authentication and authorization, data subscription and so on.

One of the key objects provided by the Foundation Comms is the Connect object whose main methods are:

  • connect: connects to the server through a web socket (when WS is available or http as fallback). You must pass the server host URL. In most apps, such as the one we're building in this training, the connection is already handled by the MainApplication component on initialisation relying on the config provided by the app.

  • commitEvent: use it to call event handlers on the server. You must pass the name of the event and an object with the input data required by the event. This data must be in JSON format with key DETAILS. See the example above of the insertOrder function.

  • getMetadata: it retrieves the metadata of a resource, that can be an event handler, data server query or a request server. When we used the foundation-form component previously, for example, it used internally getMetadata passing the event handler name to get all the input fields of the event.

  • request: use it to call a request server resource. You must pass the request server resource name.

  • snapshot and stream: use them to get a snapshot of data or to stream data in real time from a resource (usually, a data server query).

Those are the most common features from Foundation Comms you will use. We're going to use most of them and give more practical examples throughout the training. However, please note that there are more components provided by Foundation Comms such as Auth, Session, User, Analytics. Feel free to import these components and explore their methods to get a sense of what's provided.

Creating a custom form

Using foundation-form is good for simple forms or prototyping, but it is probably not enough for our use case - we need much more customisation.

To achieve this, you must create each form element manually, and take care of storing user data that the user inputs.

Start by adding elements to the template:

order.template.ts
export const OrderTemplate = html<Order>`
<div class="row-split-layout">
<div class="column-split-layout">
<span>Instrument</span>
<zero-select>Instrument</zero-select>
<label>Last price</label>
<zero-text-field type="number">Quantity</zero-text-field>
<zero-text-field type="number">Price</zero-text-field>
<label>Total</label>
<zero-select>Direction</zero-select>
<zero-text-area>Notes</zero-text-area>
</div>
</div>
`;

Add to your order.styles.ts the following, so you get a nice look on your forms

order.styles.ts
import {css} from "@genesislcap/web-core";
import { mixinScreen } from '../../styles';

export const OrderStyles = css`
:host {
${mixinScreen('flex')}
align-items: center;
justify-content: center;
flex-direction: column;
}
.column-split-layout {
text-align: center;
flex-direction: column;
flex: 1;
width: 100%;
}

.row-split-layout {
justify-content: center;
display: block;
flex-direction: row;
flex: 1;
width: 100%;
height: 50%;
}

zero-select, zero-text-area, span{
display: block;
}
`
form style

We're just showing the relevant code for the functionality we're building, with an example of customisation. Feel free to surround the elements with div, or to use any other resource to make your form look better. For that, you will only need some css styling knowledge.

Then, define the variables that will hold the values that are entered.

In the file order.ts, add the following properties to the class: Order:

order.ts
import {customElement, GenesisElement, observable} from '@genesislcap/web-core';
...

@observable public instrument: string;
@observable public lastPrice: number;
@observable public quantity: number;
@observable public price: number;
@observable public direction: string;
@observable public notes: string;

...

Now we need to add event handlers that respond to user changes and store the data that the user has input.

We can do it in the traditional way by adding @change event handler or we can use the sync directive from Genesis Foundation Utls that would do that for us.

Let's add it to each form element:

order.template.ts
import {html} from '@genesislcap/web-core';
import type {Order} from './order';
import { sync } from '@genesislcap/foundation-utils';

export const OrderTemplate = html<Order>`
<span>Instrument</span>
<zero-select :value=${sync((x)=> x.instrument)}></zero-select>

<span>Last price: ${(x) => x.lastPrice}</span>
<zero-text-field :value=${sync((x)=> x.quantity)}>Quantity</zero-text-field>
<zero-text-field :value=${sync((x)=> x.price)}>Price</zero-text-field>
<span>Total: ${(x) => x.quantity * x.price}</span>
<span>Direction</span>
<zero-select :value=${sync((x)=> x.direction)}>Direction</zero-select>
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;

Note that we have also added the calculation of the total field that doesn't require a property in the Order class. It's just an information on the screen that should not be sent to the server.

You probably realized we don't have any options in our select components, so let's fix that now.

Loading data from the server into the select fields

Let's start with instrument field. We want to load the data once Order the component is initialized so, then, the select field can just iterate through the list of instruments loaded from the server.

Order is a Web Component and, as such, it supports a series of lifecycle events that you can tap into to execute custom code at specific points in time. To make the Order component load data on initialisation, we can override one of the lifecycle events called connectedCallback that runs when the element is inserted into the DOM.

order.ts
...
export class Order extends GenesisElement {
@Connect connect: Connect;
...
@observable public allInstruments: Array<{value: string, label: string}>; //add this property

constructor() {
super();
}

public async connectedCallback() { //add this method to Order class
super.connectedCallback(); //GenesisElement implementation

const msg = await this.connect.snapshot('ALL_INSTRUMENTS'); //get a snapshot of data from ALL_INSTRUMENTS data server
console.log(msg); //add this to look into the data returned and understand its structure
this.allInstruments = msg.ROW?.map(instrument => ({
value: instrument.INSTRUMENT_ID, label: instrument.INSTRUMENT_NAME}));
}
...
}
async and await

If you're not entirely familiar with async function, it is a modern JavaScript function to enable asynchronous behaviour and the await keyword is permitted within it. They enable asynchronous, promise-based behaviour to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

Also, check this practical resource on Async Await.

As you can see, we used connect.snapshot to retrieve the data from a data server resource called ALL_INSTRUMENTS. If you wanted to stream data in real time, you could use the connect.stream method instead. Remember to always use these methods to get data from data server resources.

Once we have the list of instruments from the server we can make use of it in the template file.

To dynamically include list of options we use repeat directive and iterate through the items.

order.template.ts
import {html, repeat} from '@genesislcap/web-core';
...
export const OrderTemplate = html<Order>`
<span>Instrument</span>
<zero-select :value=${sync((x)=> x.instrument)}>
${repeat((x) => x.allInstruments, html`
<zero-option value=${(x) => x.value}>${(x) => x.label}</zero-option>
`)}
</zero-select>
...
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;

You should see the instrument field populated now with the instruments from the server.

Now let's get the direction field sorted. We could just add two static options BUY and SELL like this:

order.template.ts
...
export const OrderTemplate = html<Order>`
...
<span>Direction</span>
<zero-select :value=${sync((x)=> x.direction)}>
<zero-option>BUY</zero-option>
<zero-option>SELL</zero-option>
</zero-select>
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;

However, any changes on the backend would require a change in the options. Wouldn't it be much better if we could just retrieve all direction options from the server? We already know how to get data from a data server resource, now let's use the getMetadata method from Connect to get some metadata of a field, direction field in our case.

order.ts
...
@observable public allInstruments: Array<{value: string, label: string}>; //add this property
@observable public directionOptions: Array<{value: string, label: string}>; //add this property
...
public async connectedCallback() {
super.connectedCallback(); //GenesisElement implementation

const msg = await this.connect.snapshot('ALL_INSTRUMENTS');
console.log(msg);
this.allInstruments = msg.ROW?.map(instrument => ({
value: instrument.INSTRUMENT_ID, label: instrument.NAME}));
console.log(this.allInstruments);

const metadata = await this.connect.getMetadata('ALL_ORDERS');
console.log(metadata);
const directionField = metadata.FIELD?.find(field => field.NAME == 'DIRECTION');
this.directionOptions = Array.from(directionField.VALID_VALUES).map(v => ({value: v, label: v}));
}
...

Next, let's just use the repeat directive again to iterate through the directionOptions:

order.template.ts
...
export const OrderTemplate = html<Order>`
...
<span>Direction</span>
<zero-select :value=${sync((x)=> x.direction)}>
${repeat((x) => x.directionOptions, html`
<zero-option value=${(x) => x.value}>${(x) => x.label}</zero-option>
`)}
</zero-select>
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
`;

Reload your screen and should see the select fields being populated now!

ERROR HANDLING

For learning purposes, we are not doing proper error handling in our code.

Things like checking null or empty data from the server, arrays out of bounds etc.

When working on production code, make sure to add those validations.

Loading Market Data

We're still missing the lastPrice field that, based on the instrument selected, must display the corresponding lastPrice.

We have a request server resource (a.k.a reqRep) available on the server called INSTRUMENT_MARKET_DATA. It takes the INSTRUMENT_ID as input and returns the last price of the given instrument.

We already know how to get data from data servers, now let's see how to get data from a reqRep.

Add this method to the Order class:

order.ts
...
export class Order extends GenesisElement {
...
public async getMarketData() {
const msg = await this.connect.request('INSTRUMENT_MARKET_DATA', {
REQUEST: {
INSTRUMENT_ID: this.instrument,
}});
console.log(msg);

this.lastPrice = msg.REPLY[0].LAST_PRICE;
}
}

And change the template to make the instrument field like this:

order.template.ts
...
export const OrderTemplate = html<Order>`
<span>Instrument</span>
<zero-select :value=${sync((x)=> x.instrument)} @change=${(x) => x.getMarketData()}>
<zero-option :selected=${sync((x) => x.instrument==undefined)}>-- Select --</zero-option>
${repeat((x) => x.allInstruments, html`
<zero-option value=${(x) => x.value}>${(x) => x.label}</zero-option>
`)}
</zero-select>
...
`;

Note that we used the @change binding to call getMarketData() when the value selected changed.

tip

We've used console.log to display the data returned from the server so we can get a better understanding of the data structure returned by each kind of resource (data servers, request replies, metadata etc).

Remember that you can also use POSTMAN or any HTTP client to retrieve and analyse the data as we saw in the Developer Training.

Exercise 2.1: using Foundation Comms

estimated time

30min

Let's revisit our Marketdata component. Make it retrieve all instruments from the server and display all instrument names and their corresponding last prices.

Server resources to be used: ALL_INSTRUMENTS and INSTRUMENT_MARKET_DATA.

Sending the data

Now when we gathered all the data we're ready to send it over the wire:

Let's add a simple button with click event handler:

order.template.ts
...
export const OrderTemplate = html<Order>`
...
...
<zero-text-area :value=${sync((x)=> x.notes)}>Notes</zero-text-area>
<zero-button @click=${(x)=> x.insertOrder()}>Add Order</zero-button>
</div>
`;

Then let's amend our insertOrder function to work with the custom form now:

order.ts
...
export class Order extends GenesisElement {
...
public async insertOrder() {
const insertOrderEvent = await this.connect.commitEvent('EVENT_ORDER_INSERT', {
DETAILS: {
INSTRUMENT_ID: this.instrument,
QUANTITY: this.quantity,
PRICE: this.price,
DIRECTION: this.direction,
NOTES: this.notes,
},
});
console.log(insertOrderEvent);
}
...
}

Reload your screen and try to insert a new order. For now, just check your browser console and see if you find the result of the insertOrder() call.

Let's improve our screen a little bit and add a simple success or error message based on the result from the EVENT_ORDER_INSERT event to showcase how to handle the response from the server.

order.ts
...
export class Order extends GenesisElement {
...
public async insertOrder() {
const insertOrderEvent = await this.connect.commitEvent('EVENT_ORDER_INSERT', {
DETAILS: {
INSTRUMENT_ID: this.instrument,
QUANTITY: this.quantity,
PRICE: this.price,
DIRECTION: this.direction,
NOTES: this.notes,
},
});
console.log(insertOrderEvent);

if (insertOrderEvent.MESSAGE_TYPE == 'EVENT_NACK') {
const errorMsg = insertOrderEvent.ERROR[0].TEXT;
alert(errorMsg);
} else {
alert("Order inserted successfully.")
}
}
...
}

Adding a simple Orders data grid

In the template file, let's add the Genesis data source pointing to the ALL_ORDERS resource and wrap it in grid-pro.

Add this code to the end of html template code:

order.template.ts
...
export const OrderTemplate = html<Order>`
...
<div class="row-split-layout">
<zero-grid-pro>
<grid-pro-genesis-datasource
resource-name="ALL_ORDERS"
order-by="ORDER_ID">
</grid-pro-genesis-datasource>
</zero-grid-pro>
</div>
...
`;

This will result in a grid displaying all the columns available in the for the ALL_ORDERS resource.

Take a moment to play around, insert new orders and see the orders in the grid.

Exercise 2.2: customizing order entry further

estimated time

30min

Implement these changes in the order entry form:

  • There's a field ORDER_ID in the ORDER table which is generated automatically by the server. However, if a value is given, it will use the given value instead. Generate a random value on the frontend and pass the value to the EVENT_ORDER_INSERT event.
  • Fields instrument, quantity and price are mandatory on the server. Whenever a null or empty value is passed, make sure to capture the error response from the server and paint the missing field label in red.
tip

To generate the ORDER_ID value you can use Date.now()

Exercise 2.3: revamp the Trade screen

estimated time

60min

Remember the Trade screen from the Developer Training? Rebuilt it now using a custom form like we did with the Order screen instead of using the entity-management micro frontend. Make sure to populate the dropdown fields and handle the server response as well.