DORF (Domain Object Reactive Forms) is a library for Angular, which speeds up the creation of Dynamic Forms.
First part of DORF QuickStart tutorial covers the following topics:
It may be useful to read the following tutorials on Angular:
The latter from the list was a direct inspiration for DORF.
Library has to have a catchy name and DORF sounds better than ORF (only Germans are allowed to disagree :)). The term is taken from the Domain Driven Design approach (DDD), where system is divided into separate parts (domains). It’s not like every object in the system should have its own form. It is needed for the selected, main ones. And those can be called Domain Objects even if the architecture is not DDD.
We are going to create a simple form, getting to know DORF better and better with each step.
In order to start we should generate/download an app according to Angular QuickStart.
Then it is needed to install DORF, e.g. by using npm install dorf --save
command.
DORF is very configurable. Especially when it comes to the CSS classes. From the beginning, the main idea was to leave a choose of CSS framework to the end library user.
For the tutorial let’s choose Bootstrap, while the library’s GitHub examples
use rather Pure. It is enough to include just CSS part from the library, so the changed
index.html
can look like this:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>DORF App</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://unpkg.com/bootstrap@4.0.0-alpha.6/dist/css/bootstrap.min.css">
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
DORF can be imported in a way, which matches our needs. Let’s keep things simple and define the following requirements:
To understand how to configure CSS in DORF, let’s take a look at the skeleton of the form generated by the library:
<dorf-form-component>
<form class="form">
<fieldset class="fieldset"><!-- optional -->
<section class="section">
<dorf-field-wrapper class="wrapper"><!--...--></dorf-field-wrapper>
<dorf-field-wrapper class="wrapper"><!--...--></dorf-field-wrapper>
<!-- ... -->
</section>
<dorf-group-wrapper><!--...--></dorf-group-wrapper>
<dorf-group-wrapper><!--...--></dorf-group-wrapper>
<section class="section">
<!--...-->
</section>
<!-- ... -->
</fieldset>
<dorf-buttons><!--...--></dorf-buttons>
</form>
</dorf-form-component>
In general:
dorf-form-component
- Angular component created by the library consumer to manage the formform
- standard HTML element; first place where classes can be set during importing DORF module (form
property)fieldset
- optional parameter. Visible when renderFieldsetAroundFields
set to true
inside @DorfForm
annotation. CSS classes for this can be set when importing DORF module (fieldset
property). This main fieldset
doesn’t contain any legend
(unlike the fieldset
from dorf-group-wrapper
)section
- HTML element; it is always around dorf-field-wrapper
elements. When importing DORF
module, there is a columnsNumber
property which defines how many dorf-field-wrapper
elements
should be inside each section
. Section CSS classes can by set when importing DORF module (section
property)dorf-field-wrapper
- DORF component which “wraps” the field context. It stores label, field and the error
message. It is described in detail later. CSS classes for this component can be assigned at many levels, but always
with a wrapper
propertydorf-group-wrapper
- another DORF component, used when nesting DORF Objects. We are not going to use this
in this tutorial and CSS classes cannot be assigned directly at its level anywaydorf-buttons
- DORF component for storing form buttons. When importing DORF module, there is renderWithoutButtons
property and when it is set to true
, dorf-buttons
won’t be presented. CSS classes cannot
be set directly on the component, but later, within its bodydorf-field-wrapper
The content of dorf-field-wrapper
looks like this:
<dorf-field-wrapper class="wrapper">
<label class="label">...</label>
<dorf-field class="fieldGeneralization">
<dorf-input class="dorfField"><!--...--></dorf-input>
<dorf-radio class="dorfField"><!--...--></dorf-radio>
<dorf-select class="dorfField"><!--...--></dorf-select>
<dorf-checkbox class="dorfField"><!--...--></dorf-checkbox>
<!--...-->
</dorf-field>
<div class="error">...</div>
</dorf-field-wrapper>
In short words:
label
and error are within standard HTML elements. CSS classes for them can be set thanks to label
and error
propertiesdorf-field
is a DORF component which allows operating on fields without going into detail. It stores both
out of the box fields and the custom ones, added with dorfFields
property when importing DORF module.
At the end only one of the fields listed within dorf-field
body is presented. Therefore the good way of
thinking about this component is “field generalization”. Therefore CSS classes can be assigned to this, with a fieldGeneralization
propertydorf-input
, dorf-radio
, dorf-select
, dorf-checkbox
- out of the box
DORF components. Each one represents a different HTML field. As mentioned above, only one of them would be presented
under the dorf-field
under the concrete conditions. It is possible to assign CSS classes at this level
with dorfField
propertyDORF is written in a very modular way, that’s why each field is defined by its own component.
We can divide out of the box fields into 2 groups: those which support additional labeling and those which don’t. Knowing
HTML, you can guess that dorf-checkbox
and dorf-radio
are supporting additional labeling (inner
label).
Inner label means here that we have a label around the field. Let’s take a look at the simplified content of dorf-radio
:
<label *ngFor class="innerLabel">
<input type="radio" class="htmlField"> ...
</label>
Each option is wrapped with the label. Label can have CSS classes, defined by an innerLabel
property. Options
are standard HTML input
elements which can have CSS classes assigned with htmlField
property.
Worth mentioning that innerLabel
is independent from label
underneath dorf-field-wrapper
,
so it is possible to have 2 labels, to have just a chosen one or to not have any at all.
On the other hand, dorf-select
and dorf-input
don’t support inner labels. Simplified template
of dorf-input
looks like this:
<input class="htmlField" />
Nothing fancy :) once again, htmlField
property is strictly connected with the HTML representation of the
form field.
dorf-buttons
The last DORF component is pretty simple when it comes to its body:
<section class="group">
<button class="save">Save</button>
<button class="reset">Reset</button>
</section>
There are 2 predefined buttons, grouped within the section
HTML element. CSS classes can be assigned to them
thanks to group
, save
and reset
properties.
From the requirements we can figure out that just input
and checkbox
fields should be used. When
configuring CSS classes, it is good to have as much as possible at the general level and override just a couple of styles
at the field level. Then, in rare cases, everything can be overriden at the definition level. DORF approach to CSS is
similar to the well-known browser one - the closer the element, the more likely to be assigned.
At the end app.module
can look like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { DorfModule, DorfField } from 'dorf';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
DorfModule.forRoot({
css: {
section: 'row',
wrapper: 'form-group col-12 row',
label: 'col-2 col-form-label',
fieldGeneralization: 'col-10',
htmlField: 'form-control'
},
dorfFields: [{
tag: DorfField.CHECKBOX,
css: {
wrapper: 'checkbox col-12 row',
htmlField: 'checkbox'
}
}]
})
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Importing DorfModule
at the beginning is needed in order to use it under the imports
module property.
Then, the configuration is done by executing forRoot
static method on the module level and passing IDorfService
object as a parameter. Many things are defined at the general, css
level. CSS classes are taken from the
Bootstrap examples and mapped according to the knowledge about the rendering:
section
should be a separate block element ('row'
),wrapper
) should be both 'form-group'
and 'row'
;
it is already under the 'row'
, so we should add 'col-12'
at this level as well,label
and field in the same line, 'col-'
classes should be assigned (to label
and to fieldGeneralization
),htmlField
got 'form-control'
classThe only exception from those patterns is dorf-checkbox
. In order to assign CSS classes just to this kind
of field (and override the previous classes if exist), dorfFields
array is used. It can change existing
fields and/or add new ones. tag
property is the required key for elements in this array (in our case key
was taken from imported DorfField
class).
For the simple requirements we have here, there is a simple model to be created. It’s a good idea to start with a “contract”.
Let’s create a file src/app/user/model.ts
with a following interface inside:
export interface IUser {
_login: string;
_password: string;
_acceptance: boolean;
}
Interface defines what will be returned from our form. Interface properties have to match the future annotated Domain Object class properties. Let’s create a class now and enrich it with a constructor, consuming the interface. Let’s act as guys
who care about the security (the usage of btoa
and stuff):
export class User {
private _login: string;
private _password: string;
private _acceptance: boolean;
constructor(options?: IUser) {
if (options) {
this._login = options._login;
this._password = options._password;
this._acceptance = options._acceptance;
}
}
update(options?: IUser) {
if (options) {
this._login = options._login;
this._password = options._password;
this._acceptance = options._acceptance;
}
}
get login() { return this._login; }
get password() { return btoa(this._password); }
get acceptance() { return this._acceptance; }
get basicAuth() {
if (this._login && this._password) {
return btoa(`${this._login}:${this._password}`);
}
}
}
Model is almost ready. The last part is to make it DORF! The final shape of model.ts
can look like this:
import { Validators } from '@angular/forms';
import { DorfObject, InputType, DorfInput, DorfCheckbox } from 'dorf';
export interface IUser {
_login: string;
_password: string;
_acceptance: boolean;
}
@DorfObject()
export class User {
@DorfInput({
label: 'Username',
type: 'input' as InputType,
validator: Validators.required
})
private _login: string;
@DorfInput({
label: 'Password',
type: 'password' as InputType,
validator: Validators.required
})
private _password: string;
@DorfCheckbox({
innerLabel: 'I accept the terms and conditions',
validator: Validators.requiredTrue
})
private _acceptance: boolean;
constructor(options?: IUser) {
if (options) {
this._login = options._login;
this._password = options._password;
this._acceptance = options._acceptance;
}
}
update(options?: IUser) {
if (options) {
this._login = options._login;
this._password = options._password;
this._acceptance = options._acceptance;
}
}
get login() { return this._login; }
get password() { return btoa(this._password); }
get acceptance() { return this._acceptance; }
get basicAuth() {
if (this._login && this._password) {
return btoa(`${this._login}:${this._password}`);
}
}
}
It is OK to put DORF annotations on the private fields. It is a property name which matters here, not an access modifier. And the above piece of code, should prove that DORF is about Model-driven forms within the model.
To finalize the app, we need just one more piece from the Angular library - component which consumes DorfObject
.
Here is the example code of user-form.component.ts
from src/app/user/
directory:
import { Component, Output, EventEmitter } from '@angular/core';
import { IDorfForm, DorfForm, DorfObjectInput, DorfConfigService } from 'dorf';
import { IUser, User } from './model';
@DorfForm()
@Component({
selector: 'app-user-form'
})
export class UserFormComponent implements IDorfForm {
@DorfObjectInput() user: User;
constructor(public config: DorfConfigService) { }
onDorfSubmit() {
this.user.update(this['form'].value as IUser);
}
}
A couple of things worth mentioning:
DorfForm
is a special annotation, which should be placed over Component
annotation; if Component
has no template
nor templateUrl
, then DorfForm
generates the template for us!
DorfForm
can consume an interface with 3 options: additionalTags
, renderFieldsetAroundFields
and renderWithoutButtons
IDorfForm
is a helper interface, something like e.g. OnChange
from Angular which forces us
to have DorfConfigService
somewhere inside the componentDorfObjectInput
works like Angular Input
, but should be used once within the component in order
to point out an object marked as DorfObject
previouslyDorfConfigService
is needed within the component; it should be injected and used in the constructor e.g.
to disable all the fieldsonDorfSubmit
is a special method, connected with DORF save button from dorf-buttons
; this['form'].value
is the way of getting an object with actual form values (which can be casted to IUser
in our case)UserFormComponent
has to be presented inside the main module declarations
array.
At the end of the first iteration, we should update AppComponent
:
import { Component } from '@angular/core';
import { User } from './user/model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'My DORF App';
// object to be passed to the form
user: User = new User();
}
And its HTML:
<main class="container">
<h1>
{{title}}
</h1>
<app-user-form [user]="user"></app-user-form>
<!-- the evidence that the user has changed -->
<hr> Basic {{user.basicAuth}}
</main>
First version is ready. It can be run with npm start
command and verified on localhost:4200. Things
to be improved in the second part of the tutorial:
'Save'
instead of 'Submit'
update
function is not the perfect way of acting with DORF ObjectDORF is still under the development, but its code already allows for handling plenty of use cases and scenarios, which are not yet presented in tutorials.