A simple(ish) module for creating accessible, reactive forms.
Complete documentation for this project is available at https://jflynn7.github.io/
Before you can start rendering elements, you'll need to start creating! SimpleFormsBuilder
exposes a function to quickly create a default option by type. For example, to create a text input element, you can do:
myFirstFormElement: FormElement = builder.createElement(Elements.Text, 'My First Form Element');
This will create a FormElement
object of type='text'
, with a label
of 'My First Form Element' and an inputId
of 'myFirstFormElement' which is calculated from the label.
The inputId
can be overriden by providing FormElementOptions
as the third parameter from createElement
, for example:
myFirstFormElement: FormElement = builder.createElement(Elements.Text, 'My First Form Element', { Properties.InputId: 'myCustomInputId' });
createElement
will create a simple element with no additional properties (unless provided in the FormElementOptions
object passed as the third parameter. However, you can add properties (for example, validation properties, or options/option groups to an element with the setProperty
function as follows:
myFirstFormElement.setProperty(Properties.Required, true);
myFirstFormElement.setProperty(Properties.MinLength, 8);
When you move on to creating a FormGroup
from your elements, these properties are used to create the relevent Validators
that Angular uses to validate your form element.
By default, form elements use Bootstrap styles, but these can be overridden using the setStyle
function on FormElement
as follows:
myFirstFormElement.setStyle(Styles.ElementWrapper, 'customElementWrapperCss');
myFirstFormElement.setStyle(Styles.ElementInput, 'customInputCss');
myFirstFormElement.setStyle(Styles.ElementLabel, 'customLabelCss');
Because of the nature of CSS specificity, you need to first set a custom wrapper for your form element, so that you can target the custom css in your styles. An example of theming in a styles scss file looks like this:
// Theming example
.form-group {
&.customElementWrapperCss {
// Custom wrapper styles
.customLabelCss {
// Custom label styles
}
.customInputCss {
// Custom input styles
}
.customGroupLabelCss {
// Custom group label styles
}
.customFieldsetCss {
// Custom fieldset styles
}
.customLegendCss {
// Custom legend styles
}
.customOptionLabelCss {
// Custom option label styles (radio button/checkbox etc)
}
}
}
NB: All elements are wrapped in a form-group
class by default. This is a Bootstrap concept, but also aids with theming your elements
The Styles
class exposes the available options that can be overriden.
export class Styles {
static ElementWrapper = 'wrapperCssClass';
static GroupLabel = 'groupLabelCssClass';
static ElementLabel = 'elementLabelCssClass';
static ElementInput = 'elementInputCssClass';
static Fieldset = 'fieldsetCssClass';
static Legend = 'legendCssClass';
static OptionLabel = 'optionLabelCssClass';
}
Accessibility options are automatically set when you create the element (using input labels for aria-labels, etc), but should you wish to override these default options, you can use the setAccessibility
function on FormElement
as follows:
myFirstFormElement.setAccessibility(Accessibility.AriaLabel, 'My First Aria Label');
The Accessibility
class exposes the available options that can be overriden.
export class Accessibility {
static AriaLabel = 'ariaLabel';
static AriaDescribedBy = 'ariaDescribedBy';
static AriaLabelledBy = 'ariaLabelledBy';
static AriaReadOnly = 'ariaReadOnly';
}
The simplest way to use a FormElement
is to render it directly in your template, and subscribe to its output. This is useful for situations such as search boxes, or mailing list signups, etc, as it doesn't require the element to be part of a parent form, and can be rendered by type as follows:
<app-text-input (changeEmitter)="myReceivingFunction($event)" [elementData]="myFirstFormElement"></app-text-input>
Note the use of (changeEmitter)
here. The change emitter emits a ComponentValue
whenever the field value changes, which you can subscribe to and decide what to do with the value after. When rendering your element, pass a function to the (changeEmitter)
property as above, then in your component you can receive the value. e.g.
myReceivingFunction(value: ComponentValue) {
// do something cool
}
The ComponentValue
object passed to your function takes the following form:
export class ComponentValue {
inputId: string;
value: any;
isValid: boolean;
constructor(data: { inputId: string, value: any, isValid: boolean }) {
this.inputId = data.inputId;
this.value = data.value;
this.isValid = data.isValid;
}
}
The inputId
is simply the input ID of the rendered element emitting the value. This is useful when you use the same function to deal with all rendered by type elements on a page (so you can determine which element is giving you the value.
The value
is just that, the value being emitted from the element.
isValid
is a boolean value, determined by validation properties added to the field. i.e true
=== value satsfies all the Validators.
Whilst rendering elements individually by type can be handy, in situations where you have a lot of elements that are part of the same form, an Inine form allows to create a full, validatable (is that a word? 🤔) form from an array of FormElements
. We can then use the FormComponent
to render an inline form from those elements (inline === elements rendered one after another, in array order).
To render a complete form, we need a FormGroup
which will track the state and validity of the form, as well as an array of FormElements
that make up the complete form. ng2-simple-forms provides a helper function to do just that;
myFirstFormElement: FormElement = builder.createElement('text', 'My First Element', { Properties.Required: true });
mySecondFormElement: FormElement = builder.createElement('text', 'My Second Element', { Properties.MinLength: 8 });
myFormDetails: FormDetails = builder.toFormDetails([this.myFirstFormElement, this.mySecondFormElement]);
Now, we have a set of elements, and a FormGroup
, you can use the FormComponent
to render the complete form from the FormDetails
object.
<app-form [form]="myFormDetails"></app-form>
This will render all of our elements in a complete form (one after another, in array order). This is handy when you just need to quickly fire out a form without any consideration of element placement. Additionally, we can set a form title and form subtitle as follows:
<app-form [form]="myFormDetails" [formTitle]="'My Form Title'" [formSubtitle]="'A simple form created with ng2-simple-forms'"></app-form>
Which will do the same thing, but with an added title and subtitle.
Inline forms are handy for rapidly developing simple, ordered forms. But what if we do care about the element placement? In that case, use a Loose form (full disclosure, I fully just made these names up, they aren't Angular conventions 😂)
For a loose form, we create the FormElement
objects and the FormDetails
object in the same way as before, the only real difference is in the way we render the elements.
ng2-simple-forms provides a .get()
function on the FormDetails
object to allow us to use the FormElementComponent
which dynamically renders the correct element component based on the type
. That is to say, with a loose form, we can render the form like so:
<app-form-element [formGroup]="myFormDetails.formGroup" [formElement]="myFormDetails.get('myFirstElement')"></app-form-element>
<app-form-element [formGroup]="myFormDetails.formGroup" [formElement]="myFormDetails.get('mySecondElement')"></app-form-element>
Using the wrapper like this, means we can put the form elements anywhere we want (on the same page), but they will still be part of the same FormGroup
, so we can get the complete forms value by:
this.myFormDetails.formGroup.getRawValue()
WIP Repo can be found at https://github.com/jflynn7/ng2-simple-forms