Skip to content

Commit

Permalink
Added documentation for fields, sections and section collections
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei15193 committed Oct 22, 2024
1 parent 2c26350 commit 3251c29
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 49 deletions.
19 changes: 15 additions & 4 deletions docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1609,7 +1609,7 @@ ${getReferences(functionSignature)}
}

function getFlagSummary(declaration: DeclarationReflection): string {
return [
let flagsSummary = [
declaration.kind !== ReflectionKind.Constructor && declaration.parent?.kind !== ReflectionKind.Interface && declaration.overwrites && '`override`',
declaration.flags.isInherited && '`inherited`',
declaration.flags.isStatic && '`static`',
Expand All @@ -1620,7 +1620,12 @@ ${getReferences(functionSignature)}
declaration.flags.isOptional && '`optional`'
]
.filter(value => !!value)
.join(' ') + ' ';
.join(' ');

if (flagsSummary.length > 0)
flagsSummary += ' ';

return flagsSummary;
}

function sortCompareDeclarations(left: DeclarationReflection, right: DeclarationReflection): number {
Expand Down Expand Up @@ -1660,8 +1665,14 @@ ${getReferences(functionSignature)}
if (references.length > 0)
return '### See also\n\n' +
references
.map(reference => '* ' + getBlock(reference.content).replace(/^[ \t]-/gm, ''))
.join('\n');
.map(
reference => getBlock(reference.content)
.split(/^[ \t]*-[ \t]*/gm)
.filter(reference => reference)
.map(reference => '* ' + reference)
.join('')
)
.join('');
else
return '';
}
Expand Down
165 changes: 120 additions & 45 deletions src/forms/Form.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IPropertiesChangedEventHandler } from '../viewModels';
import type { IReadOnlyFormCollection } from './IReadOnlyFormCollection';
import type { ReadOnlyFormCollection } from './ReadOnlyFormCollection';
import { type IReadOnlyObservableCollection, type IObservableCollection, type ICollectionChangedEventHandler, type ICollectionReorderedEventHandler, ObservableCollection, ReadOnlyObservableCollection } from '../collections';
import { type IValidatable, type IObjectValidator, Validatable, ObjectValidator } from '../validation';
import { FormField } from './FormField';
Expand Down Expand Up @@ -56,44 +57,44 @@ import { FormCollection } from './FormCollection';
* the same, but the way fields behave is different. Some fields become required
* or have different validation rules while other can become locked and are no
* longer editable.
*
*
* #### Form Structure and Change Propagation
*
*
* Forms have a hierarchical structure comprising of fields and sections which are
* forms themselves. This allows for both simple and complex form definitions
* through the same model.
*
*
* Any form contrains a collection of fields and a collection of sections, however
* propagation has an additional level of sections collections. Any form is a parent
* node while fields are leaves in the tree structure with propagation generally
* going bottom-up.
*
*
* * {@linkcode Form} - root or parent node
* * {@linkcode FormField} - leaf nodes, any changes to a field are propagated to the parent node, a {@linkcode Form}.
* * {@linkcode FormCollection} - a collection of {@linkcode Form} instances, any change to a form collection is propagated to the parent node, a {@linkcode Form}.
*
* Any changes to a {@linkcode Form} is propagated to the {@linkcode FormCollection}
* to which it was added. With this, extensions and validation can be added at any level,
* from fields, to forms and form collections themselves.
*
*
* For simple cases, defining a form is done by extending a {@linkcode Form} and
* adding fields using {@linkcode withFields}.
*
*
* In case of large forms it can be beneficial to group fields into sections,
* which are just different {@linkcode Form} composing a larger one. This can be
* done using {@linkcode withSections}.
*
*
* For more complex cases where there are collections of forms where items can
* be added and removed, and each item has its own set of editable fields, a
* {@linkcode FormCollection} must be used to allow for items to be added and
* removed. To conrol the interface for mutating the collection consider
* extending {@link ReadOnlyFormCollection} instead.
*
*
* To add your own form collections to a form use {@linkcode withSectionsCollection}
* as this will perform the same operation as {@linkcode withSections} only that
* you have control over the underlying form collection. Any changes to the
* collection are reflected on the form as well.
*
*
* All fields and sections that are added with any of the mentioned methods are
* available through the {@linkcode fields} and {@linkcode sections} properties.
*
Expand All @@ -102,34 +103,35 @@ import { FormCollection } from './FormCollection';
* Validation is one of the best examples for change propagation and is offered
* out of the box. Whenever a field becomes invalid, the entire form becomes
* invalid.
*
*
* This applies to form sections as well, whenever a section collection is
* invalid, the form (parent node) becomes invalid, and finally, when a form
* becomes invalid, the form collection it was added to also becomes invalid.
*
*
* With this, the propagation can be seen clearly as validity is determined
* completely by the status of each component of the entire form, from all levels.
* Any change in one of the nodes goes all the way up to the root node making it
* very easy to check if the entire form is valid or not, and later on checking
* which sections or fields are invalid.
*
*
* Multiple validators can be added and upon any change that is notified by the
* target invokes them until the first validator returns an error message. E.g.:
* if a field is required and has 2nd validator for checking the length of the
* content, the 2nd validator will only be invoked when the 1st one passes, when
* the field has an actual value.
*
*
* This allows for granular validation messages as well as reusing them across
* {@linkcode IValidatable} objects.
*
*
* For more complex cases when the validity of one field is dependent on the
* value of another field, such as the start date/end date pair, then validation
* triggers can be configured so that when either field changes the validators
* are invoked. This is similar in a way to how dependencies work on a ReactJS
* hook.
*
*
* All form components have a `validation` property where configuraiton can be
* made, check {@linkcode validation} for more information.
*
* ----
*
* @guidance Define a Form
Expand Down Expand Up @@ -341,6 +343,10 @@ import { FormCollection } from './FormCollection';
* .validation
* .add(field => field.value <= 0 ? 'GreaterThanZero' : null);
* ```
*
* @see {@linkcode FormField}
* @see {@linkcode ReadOnlyFormCollection}
* @see {@linkcode FormCollection}
*/
export class Form<TValidationError = string> extends Validatable<TValidationError> {
private readonly _fields: AggregateObservableCollection<FormField<unknown, TValidationError>>;
Expand Down Expand Up @@ -474,6 +480,32 @@ export class Form<TValidationError = string> extends Validatable<TValidationErro
* @param fields The fields to add to the form.
* @returns Returns a collection containing the provided fields. The form reacts to changes made in
* the returned collection always keeping in sync.
*
* @guidance Defining a Form
*
* This method adds the provided fields to the form, a necessary step to watch the form fields for
* changes as well as their validity. The {@linkcode isValid} and {@linkcode isInvalid} properties
* are dependent on the fields.
*
* ```ts
* class MyForm extends Form {
* public constructor() {
* super();
*
* this.withFields(
* this.name = new FormField({
* name: 'name',
* initialValue: ''
* })
* );
* }
*
* public readonly name: FormField<string>;
* }
* ```
*
* @see {@linkcode withSections}
* @see {@linkcode withSectionsCollection}
*/
protected withFields(...fields: readonly FormField<any, TValidationError>[]): IObservableCollection<FormField<any, TValidationError>> {
const fieldsCollection = new ObservableCollection<FormField<unknown, TValidationError>>(fields);
Expand All @@ -490,6 +522,45 @@ export class Form<TValidationError = string> extends Validatable<TValidationErro
* @param sections The sections to add to the form.
* @returns Returns a collection containing the provided sections. The form reacts to changes made in
* the returned collection always keeping in sync.
*
* @guidance Splitting a Form into Sections
*
* In some cases the form that is being edited is rather large and having all fields put together would
* make the code hard to follow. It is natural to want to group fields together in such cases as it
* provides a way to clearly group related fields together and even reuse parts of a form.
*
* Adding sections to a form is done similar to how fields are done, any form section is itself a form.
*
* ```ts
* class MyForm extends Form {
* public constructor() {
* super();
*
* this.withSections(
* this.first = new MyFirstSection(),
* this.second = new MySecondSection()
* );
* }
*
* public readonly first: MyFirstSection;
*
* public readonly second: MySecondSection;
* }
*
* class MyFirstSection extends Form {
* // ...
* }
*
* class MySecondSection extends Form {
* // ...
* }
* ```
*
* This will propagate any changes from the individual sections to the form itself making it easy
* to check the validity of the entire form.
*
* @see {@linkcode withFields}
* @see {@linkcode withSectionsCollection}
*/
protected withSections(...sections: readonly Form<TValidationError>[]): FormCollection<Form<TValidationError>, TValidationError> {
const sectionsCollection = new FormCollection<Form<TValidationError>, TValidationError>(sections);
Expand All @@ -509,69 +580,73 @@ export class Form<TValidationError = string> extends Validatable<TValidationErro
* @param sectionsCollection The sections collection to add.
* @returns Returns the provided sections collection.
*
* ----
* @guidance Collections of Editable Items
*
* One usecase for sections is to split the form up and group fields together to make it easier to understand
* and maintain.
*
* @guidance
* In this example we will define a custom sections collection for our form. We have a todo list where
* each item is editable.
* The other usecase is for more complex forms where there are lists of items that themselves are editable, but
* also the list itself, items can be added or removed. Things like a todo list, or tables with a number of fields.
*
* For full control on how items are added to the collection, extend from {@linkcode ReadOnlyFormCollection}, this
* requires that methods for adding and removing items need to be defined, all standard mutating methods are available
* as protected from the base class.
*
* ```ts
* class ToDoList extends Form {
* public constructor() {
* super();
*
* this.withFields(
* this.name = new FormField<string>({ name: 'name', initialValue: '' })
* );
* this.name = new FormField({
* name: 'name',
* initialValue: ''
* })
* )
*
* this.withSectionsCollection(
* this.items = new ToDoListItemsCollection()
* this.items = new ToDoItemsCollection()
* );
* }
*
* public readonly name: FormField<string>;
*
* public readonly items: ToDoListItemsCollection;
* public readonly items: ToDoItemsCollection
* }
*
* class ToDoListItemsCollection extends ReadOnlyFormCollection<ToDoItemForm> {
* public add(): ToDoItemForm {
* const toDoItem = new ToDoItemForm();
* this.push(toDoItem);
* class ToDoItemsCollection extends ReadOnlyFormCollection<ToDoItem> {
* public add(): ToDoItem {
* const item = new ToDoItem();
* this.push(item);
*
* return toDoItem;
* return item;
* }
*
* public remove(toDoItem: ToDoItemForm): void {
* const toDoItemIndex = this.indexOf(toDoItem);
* if (toDoItemIndex >= 0)
* this.splice(toDoItemIndex, 1);
* public remove(item: ToDoItem): void {
* const itemIndex = this.indexOf(item);
* if (itemIndex >= 0)
* this.splice(itemIndex, 1);
* }
*
* }
*
* class ToDoItemForm extends Form {
* class ToDoItem extends Form {
* public constructor() {
* super();
*
* this.withFields(
* this.description = new FormField<string>({ name: 'description', initialValue: '' })
* this.description = new FormField({
* name: 'name',
* initialValue: ''
* })
* );
* }
*
* public readonly description: FormField<string>;
* }
* ```
*
* This helps us keep our interface clean and find methods where we would expect them.
*
* ```ts
* const toDoList = new ToDoList();
*
* const toDoItem = toDoList.items.add();
* toDoItem.description.value = 'This is nice';
*
* toDoList.items.remove(toDoItem);
* ```
* @see {@linkcode withFields}
* @see {@linkcode withSections}
*/
protected withSectionsCollection(sectionsCollection: IReadOnlyFormCollection<Form<TValidationError>, TValidationError>): IReadOnlyFormCollection<Form<TValidationError>, TValidationError> {
this._sections.aggregatedCollections.push(sectionsCollection);
Expand Down

0 comments on commit 3251c29

Please sign in to comment.