Skyframe Documentation

Form

Form

What is a form component?

A form component is a component which has the main purpose of creating or editing entities. It usually contains several input fields, checkboxes, selection lists, and also other form components.

What are the benefits?

  • Retrieves the original values of the entity from a Skyframe backend API (when editing)
  • Method for submitting the entity creation / update to Skyframe backend API
  • Loading state management

Creating a Skyframe form component

Entity definition

Say we have a Skyframe entity Customer with this definition below:

In our shared domain

@demo-project/shared/src/model/Customer.ts:

@Entity()
export class Customer {
@PrimaryKey()
id: number;
@Field()
email: string;
@Field()
firstName: string;
@Field()
lastName: string;
}

The angular entity

@demo-project/shared/src/model/Customer.ts:

export class Customer extends AngularEntity(SharedModels.Customer) {}

You may want to learn how to create a Skyframe entity by reading the Domain Definition guide.

Create the Angular component

Template

@demo-project/frontend/src/app/modules/customer/components/form/form.html:

<!--
The `formGroup` field refers to the entity's form group instance
returned by `entityInstance.form`.
-->
<ng-container *ngIf="formGroup">
<form [formGroup]="formGroup">
<dl class="form-group">
<dt>Email</dt>
<dd>
<input
id="email"
formControlName="email"
placeholder="jsmith@gmail.com"
type="text"
class="form-control input-lg"
/>
</dd>
<dt>First name</dt>
<dd>
<input
id="firstName"
formControlName="firstName"
placeholder="James"
type="text"
class="form-control input-lg"
/>
</dd>
<dt>Last name</dt>
<dd>
<input
id="lastName"
formControlName="lastName"
placeholder="Smith"
type="text"
class="form-control input-lg"
/>
</dd>
</dl>
</form>
</ng-container>

Component class

First, we have to create an Angular component class for our form component:

@demo-project/frontend/src/app/modules/customer/components/form/form.ts:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'demo-project-customer-form',
templateUrl: './form.html'
})
export class CustomerFormComponent {}

In order to bring all the functionalities of Skyframe form component to our component, we should extend the FormComponent class from @skyframe/angular:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormComponent, Shell } from '@skyframe/angular';
import { Customer } from '../../customer';
@Component({
selector: 'demo-project-customer-form',
templateUrl: './form.html'
})
export class CustomerFormComponent extends FormComponent<Customer> {
constructor(shell: Shell, activeRoute: ActivatedRoute) {
super(Customer, shell, activeRoute);
}
}

What we did here?

  • We extended the FormComponent class, and passed a type parameter Customer to it, meaning that we are creating a class that represents a Skyframe form component for the Customer entity.
  • We added a constructor, which receives a two parameters:
    • shell: Shell: The orchestrator object of the Skyframe web app. It holds the context of the whole application, and it allows the form component to fetch data from the backend, know everything about a Skyframe entity, navigate to other pages, etc.
    • activatedRoute: ActivatedRoute: Angular's ActivatedRoute provider. Used for navigation and URL path lookup.
  • We added a super() call to the constructor and passed the Customer class as the first parameter. With this, we are making the shell know what entity we are working with.

Using the form component

Since we are done creating the form component, we are ready to use it in our pages:

Creation form

<h1>Create Customer</h1>
<demo-project-customer-form #form></demo-project-customer-form>
<button
class="btn btn-primary"
[ladda]="form.loading | async"
(click)="form.submit().subscribe()"
>
Create!
</button>

The FormComponent#loading attribute is very helpful when you need to determine if the form submission is in progress, so we can show a loading indicator in the UI.

The FormComponent#submit() method returns an Observable, when you subscribe() to it, it will perform a HTTP POST (for creation) / PUT (for update) request to the Skyframe server with the form values.

Edit form

Using an entity instance:

When you pass an existing entity instance to the form component through the entity input, it will be converted into an edit form:

<h1>Edit Customer</h1>
<demo-project-customer-form
#form
[entity]="customer"
></demo-project-customer-form>
<button
class="btn btn-primary"
[ladda]="form.loading | async"
(click)="form.submit().subscribe()"
>
Save!
</button>

Using router param id:

We can also make an edit form by including it in a page which has a route path with this pattern: /.../my-page/:id/.... The :id router param will be intercepted by the FormComponent, thus the component knows we are editing the entity that has the primary key that matches the :id value.

For example:

// Route definition:
const routes: Routes = [
...
{ path: ':id/customer-edit', component: CustomerEditPageComponent },
...
];
// The page template
<h1>Edit Form</h1>
<demo-project-customer-form #form></demo-project-customer-form>
<button
class="btn btn-primary"
[ladda]="form.loading | async"
(click)="form.submit().subscribe()"
>
Save!
</button>

Nested forms

Suppose we have two related entities:

@Entity()
export class Customer {
@PrimaryKey()
id: number;
@Field()
email: string;
@Field()
firstName: string;
@Field()
lastName: string;
@Owns(() => Address)
address: Address;
}
@Entity()
export class Address {
@PrimaryKey()
id: number;
@Field()
streetAddress: string;
@Field()
city: string;
@Field()
state: string;
@OwnedBy(() => Customer)
customer: Customer;
}

We can use the form of Address inside of the Customer's form component template, and attach the nested form group formGroup.address to the form group emitted by AddressFormComponent using the skfFormControlName directive:

<ng-container *ngIf="formGroup">
<form [formGroup]="formGroup">
<dl class="form-group">
<dt>Email</dt>
<dd>
<input id="email" formControlName="email" />
</dd>
<dt>First name</dt>
<dd>
<input id="firstName" formControlName="firstName" />
</dd>
<dt>Last name</dt>
<dd>
<input id="lastName" formControlName="lastName" />
</dd>
<dt>Address</dt>
<dd>
<demo-project-address-form
skfFormControlName="formGroup.address"
></demo-project-address-form>
<dd>
</dl>
</form>
</ng-container>

Passing default values

When you pass a newly created entity instance to the form component through the [entity] input:

import { Component, OnInit } from '@angular/core';
import { Shell } from '@skyframe/angular';
import { Customer } from '../../customer';
@Component({
selector: 'demo-project-example-page',
template: `
<demo-project-customer-form
[entity]="defaultEntity"
></demo-project-customer-form>
`
})
export class ExamplePage implements OnInit {
public defaultEntity: Customer;
constructor(private readonly shell: Shell) {}
public ngOnInit(): void {
// NEVER SET THE PRIMARY KEY HERE!
this.defaultEntity = this.shell.createEntity(Customer, {
firstName: 'John',
lastName: 'Smith'
});
}
}

If you are intending to make a creation form with default values, you should never set the primary key value of the entity! Otherwise, the form would act as an edit form that changes the values of the entity with that primary key.

State management

The following methods are used for managing the form component state:

// Indicates that a form submit action is in progress.
protected setLoading(loading: boolean): void;

Angular lifecycle hooks

These are the Angular lifecycle hooks defined in FormComponent:

  • ngOnInit: Initializes the component state.
  • ngAfterViewInit: Sets up preset contexts.
  • ngOnDestroy: For disposing subscriptions and destroying component state.

If you are overriding one of these methods, please make sure that you are calling the parent method, otherwise, the FormComponent would not work as expected.

import { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
export class MyFormComponent
extends FormComponent<MyEntity>
implements OnInit, AfterViewInit, OnDestroy {
public ngOnInit(): void {
super.ngOnInit();
/* Do something else... */
}
public ngAfterViewInit(): void {
super.ngAfterViewInit();
/* Do something else... */
}
public ngOnDestroy(): void {
super.ngOnDestroy();
/* Do something else... */
}
}