Skip to content

Commit

Permalink
Added class hierarchies and interface implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei15193 committed Oct 23, 2024
1 parent 46a02d3 commit a099cbb
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 58 deletions.
2 changes: 2 additions & 0 deletions src/forms/FormCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ReadOnlyFormCollection } from './ReadOnlyFormCollection';
*
* @template TForm The concrete type of the form.
* @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.).
*
* @see {@linkcode Form}
*/
export class FormCollection<TForm extends Form<TValidationError>, TValidationError = string> extends ReadOnlyFormCollection<TForm, TValidationError> implements IFormCollection<TForm, TValidationError> {
/**
Expand Down
109 changes: 51 additions & 58 deletions src/forms/FormField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface IFormFieldConfig<TValue, TValidationError = string> {

/**
* Represents a form field containing the minimum set of information required to describe a field in a form.
* @template TValue The value of the field.
* @template TValue The type of values the field contains.
* @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.).
*
* ----
Expand All @@ -41,80 +41,68 @@ export interface IFormFieldConfig<TValue, TValidationError = string> {
* The form fields are designed to have the absolute minimum a form would require and at the same time be easily
* extensible. It is highly encouraged that applications define their own forms and fields even if there are no
* extra features, just to make it easy to add them later on.
*
*
* The initialization follows a config-style approach where an object that contains all the values is provided to
* the field constructor. This allows for a simple syntax where properties are initialized in a similar way to
* object initializers.
*
*
* On top of this, extending the field with this approach is easy. Extend the base config interface with extra
* properties that are need, pass the same object to the base constructor and extract the newly added ones
* afterwards.
*
*
* ----
*
* @guidance
* @guidance Adding Features to a Field
*
* One of the common features that a field may use is to track whether it was touched, i.e. if the input it is
* bound to ever came into focus.
*
* This is not something provided implicity by the base implementation, however we can provide our own.
*
* First, we will extend the config interface and provide the `isTouched` flag, in case we want to initialize
* fields as already touched, the default will be `false`.
* bound to ever came into focus. This is not something provided implicity by the base implementation, however
* adding this is easy.
*
* ```ts
* interface IMyFieldConfig<TValue> extends IFormFieldConfig<TValue> {
* readonly isTouched?: boolean;
* }
* ```
*
* Next, we will define our field.
*
* ```ts
* class MyFormField<TValue> extends FormField<TValue> {
* // Define a backing field as we will be using getters and setters
* private _isTouched: boolean;
*
* public constructor(config: IMyFieldConfig<TValue>) {
* super(config);
*
* const { isTouched = false } = config;
*
* this._isTouched = isTouched;
* }
*
* class ExtendedFormField<TValue> extends FormField<TValue> {
* private _isTouched: boolean = false;
*
* public get isTouched(): boolean {
* return this._isTouched;
* }
*
*
* public set isTouched(value: boolean) {
* // If the value indeed changes, we will update and notify about this
* if (this._isTouched !== value) {
* this._isTouched = value;
* this.notifyPropertiesChanged('isTouched');
* this._isTouched = value;
* this.notifyPropertiesChanged('isTouched');
* }
* }
*
* // This gets called whenever the current instance changes and determines whether a validation
* // should occur. In our case, the `isTouched` flag does not impact validation thus we can
* // skip validation whenever this flag changes.
* // The `error`, `isValid` and `isInvalid` fields come from the base implementation.
* protected onShouldTriggerValidation(changedProperties: readonly (keyof MyFormField<TValue>)[]): boolean {
* return changedProperties.some(changedProperty => (
* changedProperty !== 'error'
* && changedProperty !== 'isValid'
* && changedProperty !== 'isInvalid'
* && changedProperty !== 'isTouched'
* ));
* }
* ```
*
* Form fields follow a config-style approach when being initialized, this allows to pass property values
* through the constructor instead of having to set each after the instance is created. Additionally,
* required and optional properties can be clearly specified.
*
* Following on the example, an `isTouched` initial value can be provided by extending the base config
* and requesting it in the constructor.
*
* ```ts
* interface IExtendedFieldConfig<TValue> extends IFormFieldConfig<TValue> {
* readonly isTouched?: boolean;
* }
*
* class ExtendedFormField<TValue> extends FormField<TValue> {
* public constructor({ isTouched = false, ...baseConfig }: IExtendedFieldConfig<TValue>) {
* super(baseConfig);
*
* this._isTouched = isTouched;
* }
*
* // ...
* }
* ```
*
* With that, we have added `isTouched` feature to our fields, we can extend this further and add more
* features, however this is application specific and only what is needed should be implemented.
*
* The library provides the basic form model that can easily be extended allowing for users to define
* the missing parts with ease while still benefiting from the full form model structure.
*
* Changes to the field may trigger validation, by default only changes to the {@linkcode value} does this,
* to change the behavior see {@linkcode onShouldTriggerValidation}.
*
* @see {@linkcode Form}
* @see {@linkcode IFormFieldConfig}
*/
export class FormField<TValue, TValidationError = string> extends Validatable<TValidationError> {
private _name: string;
Expand Down Expand Up @@ -208,18 +196,23 @@ export class FormField<TValue, TValidationError = string> extends Validatable<TV
public readonly validation: IObjectValidator<this, TValidationError>;

/**
* Resets the field. Only the validation configuration is reset,
* the field retains its current value.
* Resets the field. Only the validation configuration is reset, the field retains its current value.
*/
public reset(): void {
this.validation.reset();
}

/**
* Invoked when the current instance's properties change, this is a plugin method to help reduce validations when changes do not
* have an effect on validation.
* Invoked when the current instance's properties change, this is a plugin method to help reduce
* validations when changes do not have an effect on validation.
*
* @remarks
*
* By default, only changes to {@linkcode value} triggers validation. Changes to any other properties,
* such as {@linkcode error}, {@linkcode isValid} and {@linkcode isInvalid} as well as any other
* properties that get added to a field do not trigger validation.
*/
protected onShouldTriggerValidation(changedProperties: readonly (keyof this)[]): boolean {
return changedProperties.some(changedProperty => changedProperty !== 'error' && changedProperty !== 'isValid' && changedProperty !== 'isInvalid');
return changedProperties.some(changedProperty => changedProperty === 'value');
}
}
79 changes: 79 additions & 0 deletions webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ ${this._getPropertiesList(interfaceDeclaration)}
${this._getMethodsList(interfaceDeclaration)}
${this._getImplementations(interfaceDeclaration)}
${this._getGuidance(interfaceDeclaration)}
${this._getReferences(interfaceDeclaration)}
Expand Down Expand Up @@ -368,6 +370,8 @@ ${this._getPropertiesList(classDeclaration)}
${this._getMethodsList(classDeclaration)}
${this._getClassHierarchy(classDeclaration)}
${this._getGuidance(classDeclaration)}
${this._getReferences(classDeclaration)}
Expand Down Expand Up @@ -593,6 +597,81 @@ ${this._getReferences(functionSignature)}
}
}

private _getImplementations(declaration: DeclarationReflection): string {
try {
const implementations: ReferenceType[] = [];
const toVisit = [declaration];
do {
const current = toVisit.shift()!;
if (current.implementedBy && current.implementedBy.length > 0)
implementations.push(...current.implementedBy);

if (current.extendedBy && current.extendedBy.length > 0)
toVisit.unshift(...current.extendedBy.map(extension => this._findDeclaration(extension.reflection)));
} while (toVisit.length > 0);

if (implementations.length > 0)
return '### Implementations\n\n' +
implementations
.sort((left, right) => left.name.localeCompare(right.name, 'en-US'))
.map(implementation => `* ${this._getReferenceLink(implementation)}`)
.join('\n');
else
return '';
}
catch (error) {
throw new Error(`Could not generate class hierarchy information for ${declaration}.\n${error}`);
}
}

private _getClassHierarchy(declaration: DeclarationReflection): string {
try {
let root = declaration;
while (root.extendedTypes && root.extendedTypes.length > 0)
root = this._findDeclaration((root.extendedTypes[0]! as ReferenceType).reflection);

let hirerachy = '### Inheritance Hierarchy\n\n';
let level = 0;

const toVisit: (DeclarationReflection | 'increment' | 'decrement')[] = [root];
do {
const current = toVisit.shift()!;

switch (current) {
case 'increment':
level++;
break;

case 'decrement':
level--;
break;

default:
const prefix = ' '.repeat(level);
if (current === declaration)
hirerachy += `${prefix}* **${this._getSimpleName(current)}**\n`;
else
hirerachy += `${prefix}* [${this._getSimpleName(current)}](${this._getProjectReferenceUrl(current)})\n`;
if (current.extendedBy && current.extendedBy.length > 0) {
toVisit.unshift(
'increment',
...current
.extendedBy
.map(derivative => this._findDeclaration(derivative.reflection)),
'decrement'
);
}
break;
}
} while (toVisit.length > 0)

return hirerachy;
}
catch (error) {
throw new Error(`Could not generate class hierarchy information for ${declaration}.\n${error}`);
}
}

private _getOverride(declaration: DeclarationReflection): string {
try {
let override = '';
Expand Down

0 comments on commit a099cbb

Please sign in to comment.