diff --git a/docs.ts b/docs.ts index ebe6c62..e1750b1 100644 --- a/docs.ts +++ b/docs.ts @@ -1,6 +1,7 @@ import { Application, CommentDisplayPart, DeclarationReflection, ParameterReflection, ReferenceType, Reflection, ReflectionKind, ReflectionSymbolId, SignatureReflection, SomeType, TypeParameterReflection } from 'typedoc'; import fs, { type MakeDirectoryOptions, type WriteFileOptions } from 'fs'; import path from 'path'; +import { getNameOfDeclaration } from 'typescript'; void async function () { try { @@ -39,13 +40,13 @@ void async function () { const outputDirectory = await mkdirAsync(path.join(__dirname, 'docs'), { recursive: true }); - await writeFileAsync(path.join(outputDirectory, 'index.md'), getLandingPage()); + await writeFileAsync(path.join(outputDirectory, 'README.md'), getReadMe()); + await writeFileAsync(path.join(outputDirectory, 'Home.md'), getLandingPage()); await writeFileAsync(path.join(outputDirectory, '_Sidebar.md'), getSidebar()); + await writeFileAsync(path.join(outputDirectory, '_Footer.md'), getFooter()); - getClassDocumentation - - if (project.children) - await Promise.all(project.children.map(async declaration => { + await Promise.all(Array.from(declarationsById.values()).map(async declaration => { + try { const subDirectory1 = declaration.sources?.at(0)?.fileName.split('/').at(0); let subDirectory2: string; let documentation: string | null = null; @@ -65,6 +66,14 @@ void async function () { documentation = getClassDocumentation(declaration); break; + case ReflectionKind.Property: + case ReflectionKind.Accessor: + if (!declaration.flags.isInherited) { + subDirectory2 = 'properties'; + documentation = getPropertyDocumentation(declaration); + } + break; + case ReflectionKind.Function: subDirectory2 = 'functions'; documentation = getFunctionDocumentation(declaration); @@ -77,7 +86,11 @@ void async function () { await mkdirAsync(directoryPath, { recursive: true }); await writeFileAsync(path.join(directoryPath, `${getIdentifier(declaration)}.md`), documentation); } - })); + } + catch (error) { + throw new Error(`Could not generate documentation for '${declaration}' on '${declaration.parent}'.\n${error}.`); + } + })); function findDeclaration(target: Reflection | ReflectionSymbolId | string | undefined): DeclarationReflection { const declaration = declarationsById.get( @@ -86,7 +99,7 @@ void async function () { || Number(target) ); if (declaration === null || declaration === undefined) - throw new Error(`Cannot find declaration with id '${JSON.stringify(target)}'.`); + throw new Error(`Cannot find declaration with id (forgot to export it in root index.ts?) '${JSON.stringify(target)}'.`); return declaration; } @@ -381,23 +394,53 @@ void async function () { return documentationIndex; } + function getReadMe(): string { + return ( + `${packageInfo.description}\n` + + '\n' + + [ + '[Guides and Tutorials - Getting Started](https://github.com/Andrei15193/react-model-view-viewmodel/discussions/7)', + '[Project Discussions](https://github.com/Andrei15193/react-model-view-viewmodel/discussions)', + '[Project Wiki](https://github.com/Andrei15193/react-model-view-viewmodel/wiki)', + '[Releases](https://github.com/Andrei15193/react-model-view-viewmodel/releases)' + ].join(' | ') + '\n' + + '\n' + + '**API**\n' + + '\n' + + documentationIndex + .namespaces + .map(namespace => { + return `* **${namespace.name}**\n` + namespace + .declarations + .filter(declaration => declaration.promoted) + .map(declaration => ` * [${getSimpleName(declaration)}](${getIdentifier(declaration)})`) + .join('\n'); + }) + .join('\n') + ); + } + function getLandingPage(): string { - return '### API\n\n' + documentationIndex - .namespaces - .map(namespace => { - let listMarker = '*'; - - return `* **${namespace.name}**\n` + namespace - .declarations - .map((declaration, declarationIndex, declarations) => { - if (declarationIndex > 0 && declarations[declarationIndex - 1].promoted !== declarations[declarationIndex].promoted) - listMarker = '-'; - - return ` ${listMarker} [${getSimpleName(declaration)}](${getIdentifier(declaration)})`; - }) - .join('\n'); - }) - .join('\n'); + return ( + `${packageInfo.description}\n` + + '\n' + + '### API\n\n' + documentationIndex + .namespaces + .map(namespace => { + let listMarker = '*'; + + return `* **${namespace.name}**\n` + namespace + .declarations + .map((declaration, declarationIndex, declarations) => { + if (declarationIndex > 0 && declarations[declarationIndex - 1].promoted !== declarations[declarationIndex].promoted) + listMarker = '-'; + + return ` ${listMarker} [${getSimpleName(declaration)}](${getIdentifier(declaration)})`; + }) + .join('\n'); + }) + .join('\n') + ); } function getSidebar(): string { @@ -421,6 +464,16 @@ void async function () { ); } + function getFooter(): string { + return [ + '[Guides and Tutorials - Getting Started](https://github.com/Andrei15193/react-model-view-viewmodel/discussions/7)', + '[Motivation](https://github.com/Andrei15193/react-model-view-viewmodel/wiki#motivation)', + '[Overview](https://github.com/Andrei15193/react-model-view-viewmodel/wiki#overview)', + '[API](https://github.com/Andrei15193/react-model-view-viewmodel/wiki#api)', + '[Releases](https://github.com/Andrei15193/react-model-view-viewmodel/releases)' + ].join(' | '); + } + function getAliasDocumentation(aliasDeclaration: DeclarationReflection): string { return ` ###### [API](https://github.com/Andrei15193/react-model-view-viewmodel/wiki#api) / ${getFullName(aliasDeclaration)} alias @@ -433,7 +486,7 @@ ${getSummary(aliasDeclaration)} ${getDeclaration(aliasDeclaration)} \`\`\` -${getSourceCodeLink(aliasDeclaration)} +${getSourceReference(aliasDeclaration)} ${getGenericParameters(aliasDeclaration)} @@ -461,7 +514,7 @@ ${getInheritaceAndImplementations(interfaceDeclaration)} ${getDeclaration(interfaceDeclaration)} \`\`\` -${getSourceCodeLink(interfaceDeclaration)} +${getSourceReference(interfaceDeclaration)} ${getGenericParameters(interfaceDeclaration)} @@ -497,7 +550,7 @@ ${classDeclaration.flags.isAbstract ? 'This is an abstract class.' : ''} ${getDeclaration(classDeclaration)} \`\`\` -${getSourceCodeLink(classDeclaration)} +${getSourceReference(classDeclaration)} ${getGenericParameters(classDeclaration)} @@ -517,6 +570,36 @@ ${getReferences(classDeclaration)} `.replace(/\n{3,}/g, '\n\n').trim(); } + function getPropertyDocumentation(propertyDeclaration: DeclarationReflection): string { + return ` +###### [API](https://github.com/Andrei15193/react-model-view-viewmodel/wiki#api)/ [${getFullName(propertyDeclaration.parent as DeclarationReflection)}](${getProjectReferenceUrl(propertyDeclaration.parent as DeclarationReflection)}) / ${getFullName(propertyDeclaration)} property + +${getDeprecationNotice(propertyDeclaration)} + +${getSummary(propertyDeclaration)} + +${getOverride(propertyDeclaration)} + +${getPropertyType(propertyDeclaration)} + +${propertyDeclaration.flags.isAbstract ? 'This is an abstract property.' : ''} + +\`\`\`ts +${getDeclaration(propertyDeclaration)} +\`\`\` + +${getSourceReference(propertyDeclaration)} + +${getDescription(propertyDeclaration)} + +${getRemarks(propertyDeclaration)} + +${getGuidance(propertyDeclaration)} + +${getReferences(propertyDeclaration)} +`.replace(/\n{3,}/g, '\n\n').trim(); + } + function getFunctionDocumentation(functionDeclaration: DeclarationReflection): string { return ` ###### [API](https://github.com/Andrei15193/react-model-view-viewmodel/wiki#api) / ${getFullName(functionDeclaration)} ${functionDeclaration.name.startsWith('use') ? 'hook' : 'function'} @@ -537,7 +620,7 @@ ${getSummary(functionSignature)} ${getDeclaration(functionSignature)} \`\`\` -${getSourceCodeLink(functionSignature)} +${getSourceReference(functionSignature)} ${getGenericParameters(functionSignature)} @@ -561,7 +644,10 @@ ${getReferences(functionSignature)} case ReflectionKind.Property: case ReflectionKind.Accessor: case ReflectionKind.Method: - return declaration.parent!.name + '.' + declaration.name; + if (declaration.flags.isInherited) + return declaration.inheritedFrom!.reflection!.parent!.name + '.' + declaration.name; + else + return declaration.parent!.name + '.' + declaration.name; case ReflectionKind.Class: case ReflectionKind.Interface: @@ -627,6 +713,60 @@ ${getReferences(functionSignature)} } } + function getOverride(declaration: DeclarationReflection): string { + try { + let override = ''; + + if (declaration.parent?.kind !== ReflectionKind.Interface && declaration.overwrites) { + let declarationName: string; + switch (declaration.kind) { + case ReflectionKind.Property: + case ReflectionKind.Accessor: + declarationName = 'property'; + break; + + case ReflectionKind.Method: + declarationName = 'method'; + break; + + default: + throw new Error(`Unhandled '${declaration.kind}' overriden declaration.`); + } + + override += `This ${declarationName} overrides [${getFullName(declaration.overwrites.reflection as DeclarationReflection)}](${getProjectReferenceUrl(declaration.overwrites)}).`; + } + + return override; + } + catch (error) { + throw new Error(`Could not generate override information for ${declaration}.\n${error}`); + } + } + + function getPropertyType(declaration: DeclarationReflection): string { + try { + switch (declaration.kind) { + case ReflectionKind.Property: + if (declaration.type) + return `Property type: ${getReferenceLink(declaration.type)}.`; + else + throw new Error(`Property '${declaration.name}' on ${declaration.parent?.name} has no type.`); + + case ReflectionKind.Accessor: + if (declaration.getSignature && declaration.getSignature.type) + return `Property type: ${getReferenceLink(declaration.getSignature.type)}.`; + else + throw new Error(`Property (accessor) '${declaration.name}' on ${declaration.parent?.name} has no type.`); + + default: + throw new Error(`Unhandled '${declaration.kind}' property type.`); + } + } + catch (error) { + throw new Error(`Could not generate property type information for ${declaration}.\n${error}`); + } + } + function getDeclaration(declaration: DeclarationReflection | SignatureReflection): string { switch (declaration.kind) { case ReflectionKind.TypeAlias: @@ -694,6 +834,103 @@ ${getReferences(functionSignature)} return classDeclaration; + case ReflectionKind.Property: + let propertyDeclaration: string = [ + declaration.flags.isPrivate && 'private', + declaration.flags.isProtected && 'protected', + declaration.flags.isPublic && 'public', + + declaration.flags.isAbstract && 'abstract', + declaration.flags.isStatic && 'static', + + declaration.flags.isReadonly && 'readonly', + declaration.flags.isConst && 'const' + ] + .filter(flag => flag) + .join(' '); + + propertyDeclaration += ` ${declaration.name}`; + + if (declaration.flags.isOptional) + propertyDeclaration += '?'; + + if (declaration.type) + propertyDeclaration += ': ' + getTypeReferenceDeclaration(declaration.type); + else + throw new Error(`Property '${declaration.name}' on '${declaration.parent?.name}' has no type.`); + + return propertyDeclaration; + + case ReflectionKind.Accessor: + let signatures: string[] = []; + + if (declaration.getSignature) { + let getSignature = [ + declaration.getSignature.flags.isPrivate && 'private', + declaration.getSignature.flags.isProtected && 'protected', + declaration.getSignature.flags.isPublic && 'public', + + declaration.flags.isPrivate && 'private', + declaration.flags.isProtected && 'protected', + declaration.flags.isPublic && 'public' + ] + .filter(flag => flag) + .at(0) as string; + + getSignature += [ + declaration.flags.isAbstract && 'abstract', + declaration.flags.isStatic && 'static' + ] + .filter(flag => flag) + .join(' '); + getSignature += ' get '; + getSignature += declaration.name; + getSignature += '(): '; + if (declaration.getSignature.type) + getSignature += getTypeReferenceDeclaration(declaration.getSignature.type); + else + throw new Error(`Accessor '${declaration.name}' on '${declaration.parent?.name}' has no get type.`); + + signatures.push(getSignature); + } + + if (declaration.setSignature) { + let setSignature = [ + declaration.setSignature.flags.isPrivate && 'private', + declaration.setSignature.flags.isProtected && 'protected', + declaration.setSignature.flags.isPublic && 'public', + + declaration.flags.isPrivate && 'private', + declaration.flags.isProtected && 'protected', + declaration.flags.isPublic && 'public' + ] + .filter(flag => flag) + .at(0) as string; + + setSignature += [ + declaration.flags.isAbstract && 'abstract', + declaration.flags.isStatic && 'static' + ] + .filter(flag => flag) + .join(' '); + setSignature += ' set '; + setSignature += declaration.name; + setSignature += '('; + if (declaration.setSignature.parameters) + setSignature += declaration + .setSignature + .parameters + .map(parameter => `${parameter.name}: ${getTypeReferenceDeclaration(parameter.type!)}`); + else + throw new Error(`Accessor '${declaration.name}' on '${declaration.parent?.name}' has no set type.`); + + setSignature += ')'; + + signatures.push(setSignature); + } + + return signatures.join('\n'); + case ReflectionKind.CallSignature: let functionDeclaration = 'function ' + declaration.name; @@ -899,7 +1136,6 @@ ${getReferences(functionSignature)} throw new Error(`Could not determine URL for React reference '${typeReference.name}'.`); } - default: throw new Error(`Could not determine URL for '${typeReference}' in package '${typeReference.package}'.`); } @@ -913,7 +1149,11 @@ ${getReferences(functionSignature)} declaration = foundDeclaration; }, reference() { - declaration = findDeclaration((typeReferenceOrDeclaration as ReferenceType).reflection) + //throw new Error(`Unhandled '${typeReferenceOrDeclaration}' reference.`); + if (!(typeReferenceOrDeclaration as ReferenceType).reflection) + declaration = findDeclaration((typeReferenceOrDeclaration as ReferenceType).symbolId); + else + declaration = findDeclaration((typeReferenceOrDeclaration as ReferenceType).reflection); } }); if (declaration !== null) @@ -930,6 +1170,16 @@ ${getReferences(functionSignature)} try { switch (typeReference.type) { case 'reference': + if (!typeReference.package) { + if (typeReference.reflection) + return `[${getSimpleName(typeReference.reflection as DeclarationReflection)}](${getProjectReferenceUrl(typeReference.reflection as DeclarationReflection)})`; + + switch (typeReference.name) { + case 'ArrayLike.length': + return '[ArrayLike.length](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/length) ([ArrayLike](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Indexed_collections#working_with_array-like_objects))'; + } + } + let typeReferenceLink = typeReference.refersToTypeParameter ? `_${typeReference.name}_` : `[${typeReference.name}](${getReferenceUrl(typeReference)})`; @@ -982,12 +1232,21 @@ ${getReferences(functionSignature)} case 'intrinsic': switch (typeReference.name) { + case 'this': + return '[`this`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/this)'; + case 'undefined': return '[`undefined`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined)'; case 'string': return '[`string`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)'; + case 'boolean': + return '[`boolean`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)'; + + case 'number': + return '[`number`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)'; + case 'unknown': return '[`unknown`](https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown)'; @@ -1120,12 +1379,16 @@ ${getReferences(functionSignature)} if (genericParameterDescription.length > 0) genericParameter += ' - ' + genericParameterDescription; - genericParameter += '\n' - if (typeParameter.type) - genericParameter += ` \n Type constraints: ${getReferenceLink(typeParameter.type)}.`; + genericParameter = genericParameter.replace(/(\r?\n\r?)*$/, ''); - if (typeParameter.default) - genericParameter += ` \n Default value: ${getReferenceLink(typeParameter.default)}.`; + const genericParameterConstraints = [ + typeParameter.type && ` Type constraints: ${getReferenceLink(typeParameter.type)}.`, + typeParameter.default && ` Default value: ${getReferenceLink(typeParameter.default)}.` + ] + .filter(description => description) + .join(' \n'); + if (genericParameterConstraints.length > 0) + genericParameter += '\n\n' + genericParameterConstraints; return genericParameter; }, @@ -1192,7 +1455,7 @@ ${getReferences(functionSignature)} function getPropertiesList(declaration: DeclarationReflection): string { const properties = declaration .children - ?.filter(childDeclaration => childDeclaration.kind === ReflectionKind.Property && !childDeclaration.flags.isInherited && !childDeclaration.flags.isPrivate) + ?.filter(childDeclaration => (childDeclaration.kind === ReflectionKind.Property || childDeclaration.kind === ReflectionKind.Accessor) && !childDeclaration.flags.isPrivate) .sort(sortCompareDeclarations); if (properties !== null && properties !== undefined && properties.length > 0) { @@ -1227,6 +1490,8 @@ ${getReferences(functionSignature)} function getFlagSummary(declaration: DeclarationReflection): string { return [ + declaration.kind !== ReflectionKind.Constructor && declaration.parent?.kind !== ReflectionKind.Interface && declaration.overwrites && '`override`', + declaration.flags.isInherited && '`inherited`', declaration.flags.isStatic && '`static`', declaration.flags.isAbstract && '`abstract`', declaration.flags.isPrivate && '`private`', @@ -1240,12 +1505,20 @@ ${getReferences(functionSignature)} function sortCompareDeclarations(left: DeclarationReflection, right: DeclarationReflection): number { return ( - getStaticSortOrder(left) - getStaticSortOrder(right) + getInheritedSortOrder(left) - getInheritedSortOrder(right) + || getStaticSortOrder(left) - getStaticSortOrder(right) || getAccessModifierSortOrder(left) - getAccessModifierSortOrder(right) || left.name.localeCompare(right.name, 'en-US') ); } + function getInheritedSortOrder(declaration: DeclarationReflection): number { + if (declaration.flags.isInherited) + return 1; + + return 0; + } + function getStaticSortOrder(declaration: DeclarationReflection): number { if (declaration.flags.isStatic) return 0; @@ -1265,7 +1538,10 @@ ${getReferences(functionSignature)} function getReferences(declaration: DeclarationReflection | SignatureReflection): string { const references = declaration.comment?.blockTags.filter(blockTag => blockTag.tag === '@see') || []; if (references.length > 0) - return '### See also\n\n' + references.map(reference => getBlock(reference.content).replace(/^[ \t]-/gm, '*')).join('\n'); + return '### See also\n\n' + + references + .map(reference => '* ' + getBlock(reference.content).replace(/^[ \t]-/gm, '')) + .join('\n'); else return ''; } @@ -1340,12 +1616,19 @@ ${getReferences(functionSignature)} } } - function getSourceCodeLink(declaration: DeclarationReflection | SignatureReflection): string { - if (declaration.sources && declaration.sources.length > 0) { - const [{ fileName, line }] = declaration.sources; + function getSourceReference(declaration: DeclarationReflection | SignatureReflection): string { + if (declaration.sources && declaration.sources.length > 0) + if (declaration.sources.length === 1) { + const [{ fileName, line }] = declaration.sources; - return `Source reference: [\`src/${fileName}:${line}\`](${packageInfo.repository.url.split('+').at(-1).split('.git')[0]}/tree/${packageInfo.version}/src/${fileName}#L${line}).`; - } + return `Source reference: [\`src/${fileName}:${line}\`](${packageInfo.repository.url.split('+').at(-1).split('.git')[0]}/tree/${packageInfo.version}/src/${fileName}#L${line}).`; + } + else { + return 'Source references:\n' + declaration + .sources + .map(({ fileName, line }) => `* [\`src/${fileName}:${line}\`](${packageInfo.repository.url.split('+').at(-1).split('.git')[0]}/tree/${packageInfo.version}/src/${fileName}#L${line})`) + .join('\n') + } else return ''; } diff --git a/package.json b/package.json index 7603a19..44e6317 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-model-view-viewmodel", - "version": "3.0.0-forms.4", - "description": "A library for developing React applications using Model-View-ViewModel inspired by .NET", + "version": "3.0.0-rc.1", + "description": "A library for developing ReactJS applications using Model-View-ViewModel, inspired by .NET.", "main": "./lib/index.js", "types": "./lib/index.d.ts", "module": "./src/index.ts", diff --git a/src/collections/observableCollections/ReadOnlyObservableCollection.ts b/src/collections/observableCollections/ReadOnlyObservableCollection.ts index 91630b4..b66d145 100644 --- a/src/collections/observableCollections/ReadOnlyObservableCollection.ts +++ b/src/collections/observableCollections/ReadOnlyObservableCollection.ts @@ -61,6 +61,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR /** * Gets or sets the number of items in the collection. + * @protected * @see [Array.length](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/length) */ protected set length(value: number) { diff --git a/src/forms/Form.ts b/src/forms/Form.ts index 5440b56..40f441c 100644 --- a/src/forms/Form.ts +++ b/src/forms/Form.ts @@ -9,7 +9,7 @@ import { FormCollection } from './FormCollection'; * Represents a form for which both fields and sections can be configured. Form sections are forms themselves making this a tree structure * where fields represent leaves and sections are parent nodes. * - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). * * ---- * @@ -54,9 +54,82 @@ import { FormCollection } from './FormCollection'; * * The configuration does not change the state, the form still looks more or less * the same, but the way fields behave is different. Some fields become required - * or have different validaiton rules while other can become locked and are no + * 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. + * + * #### Validation + * + * 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 @@ -214,7 +287,7 @@ import { FormCollection } from './FormCollection'; * By default, validation errors are represented using `string`s, however this * can be changed through the {@linkcode TValidationError} generic parameter. * The snippet below illustrates using a [literal type](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) - * for validaiton results. + * for validation results. * * ```ts * type ValidationError = 'Required' | 'GreaterThanZero'; diff --git a/src/forms/FormCollection.ts b/src/forms/FormCollection.ts index 1c98c4f..c288f9c 100644 --- a/src/forms/FormCollection.ts +++ b/src/forms/FormCollection.ts @@ -7,7 +7,7 @@ import { ReadOnlyFormCollection } from './ReadOnlyFormCollection'; * form sections for cases where validation and other aspects are based on the state of an entity or the form itself. * * @template TForm The concrete type of the form. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export class FormCollection, TValidationError = string> extends ReadOnlyFormCollection implements IFormCollection { /** diff --git a/src/forms/FormField.ts b/src/forms/FormField.ts index fb900e5..4c975db 100644 --- a/src/forms/FormField.ts +++ b/src/forms/FormField.ts @@ -3,7 +3,7 @@ import { Validatable, type IValidator, type ValidatorCallback, type IObjectValid /** * Represents the configuration of a field, this can be extended for custom fields to easily add more features. * @template TValue The value of the field. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IFormFieldConfig { /** @@ -33,7 +33,7 @@ export interface IFormFieldConfig { /** * 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 TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). * * ---- * diff --git a/src/forms/IConfigurableFormCollection.ts b/src/forms/IConfigurableFormCollection.ts index d3fa031..f6c4343 100644 --- a/src/forms/IConfigurableFormCollection.ts +++ b/src/forms/IConfigurableFormCollection.ts @@ -4,16 +4,16 @@ import type { Form } from './Form'; * Represents a callback used to configure an individual form section within a collection. * * @template TSection The form section type to configure. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export type FormSetupCallback, TValidationError = string> = (section: TSection) => void; /** * Represents collection of form sections that can be configured. This is useful for cases like having a list of editable items - * and neeeding to add validaiton for each based on the state of an entity or the form itself. + * and neeeding to add validation for each based on the state of an entity or the form itself. * * @template TSection The concrete type of the form section. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IConfigurableFormCollection, TValidationError = string> { /** diff --git a/src/forms/IFormCollection.ts b/src/forms/IFormCollection.ts index 406e246..20b0054 100644 --- a/src/forms/IFormCollection.ts +++ b/src/forms/IFormCollection.ts @@ -8,7 +8,7 @@ import type { IConfigurableFormCollection } from './IConfigurableFormCollection' * form sections for cases where validation and other aspects are based on the state of an entity or the form itself. * * @template TForm The concrete type of the form section. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IFormCollection, TValidationError = string> extends IValidatable, IObservableCollection, IConfigurableFormCollection { /** diff --git a/src/forms/IReadOnlyFormCollection.ts b/src/forms/IReadOnlyFormCollection.ts index 28d9879..ee69ec0 100644 --- a/src/forms/IReadOnlyFormCollection.ts +++ b/src/forms/IReadOnlyFormCollection.ts @@ -9,7 +9,7 @@ import type { IConfigurableFormCollection } from './IConfigurableFormCollection' * form itself. * * @template TForm The concrete type of the form section. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IReadOnlyFormCollection, TValidationError = string> extends IValidatable, IReadOnlyObservableCollection, IConfigurableFormCollection { /** diff --git a/src/forms/ReadOnlyFormCollection.ts b/src/forms/ReadOnlyFormCollection.ts index ec44643..bf1dbf0 100644 --- a/src/forms/ReadOnlyFormCollection.ts +++ b/src/forms/ReadOnlyFormCollection.ts @@ -11,7 +11,7 @@ import { ReadOnlyObservableCollection } from '../collections'; * form itself. * * @template TForm The concrete type of the form section. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export class ReadOnlyFormCollection, TValidationError = string> extends ReadOnlyObservableCollection implements IReadOnlyFormCollection, IValidatable { private _error: TValidationError | null; diff --git a/src/index.ts b/src/index.ts index b95e20a..1c73b53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,9 +78,9 @@ export { type IReadOnlyValidatable, type IValidatable, Validatable, type IValidator, type ValidatorCallback, - type IReadOnlyObjectValidator, type IObjectValidator, ObjectValidator, + type IReadOnlyObjectValidator, type IObjectValidator, type IValidationTriggersSet, ObjectValidator, - type WellKnownValidationTrigger, ValidationTrigger, + type WellKnownValidationTrigger, type ValidationTriggerSelector, ValidationTrigger, type IViewModelChangedValidationTriggerConfig, ViewModelChangedValidationTrigger, type ICollectionChangedValidationTriggerConfig, CollectionChangedValidationTrigger, diff --git a/src/validation/IReadOnlyValidatable.ts b/src/validation/IReadOnlyValidatable.ts index a449e85..6bb05af 100644 --- a/src/validation/IReadOnlyValidatable.ts +++ b/src/validation/IReadOnlyValidatable.ts @@ -1,6 +1,6 @@ /** * Represents a read-only validatable object. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IReadOnlyValidatable { /** diff --git a/src/validation/IValidatable.ts b/src/validation/IValidatable.ts index 1cfdfea..33b1e92 100644 --- a/src/validation/IValidatable.ts +++ b/src/validation/IValidatable.ts @@ -2,7 +2,7 @@ import type { IReadOnlyValidatable } from './IReadOnlyValidatable'; /** * Represents a validatable object. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IValidatable extends IReadOnlyValidatable { /** diff --git a/src/validation/IValidator.ts b/src/validation/IValidator.ts index fd9e197..992a961 100644 --- a/src/validation/IValidator.ts +++ b/src/validation/IValidator.ts @@ -5,7 +5,7 @@ import type { IReadOnlyValidatable } from './IReadOnlyValidatable'; * additional actions need to be performed, such as flags, when a validator is added. * * @template TValidatable The instance type that is being validated. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IValidator, TValidationError = string> { /** diff --git a/src/validation/Validatable.ts b/src/validation/Validatable.ts index bbeccaa..fc0d081 100644 --- a/src/validation/Validatable.ts +++ b/src/validation/Validatable.ts @@ -3,7 +3,7 @@ import { ViewModel } from '../viewModels'; /** * Represents a base implementation for a validatable - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export class Validatable extends ViewModel implements IValidatable { private _error: TValidationError | null; diff --git a/src/validation/index.ts b/src/validation/index.ts index ed30ba7..abb0260 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -7,11 +7,12 @@ export type { IValidator, ValidatorCallback } from './IValidator'; export { type IReadOnlyObjectValidator, type IObjectValidator, - ObjectValidator + type IValidationTriggersSet, + ObjectValidator, } from './objectValidator'; export { - type WellKnownValidationTrigger, ValidationTrigger, + type WellKnownValidationTrigger, ValidationTriggerSelector, ValidationTrigger, type IViewModelChangedValidationTriggerConfig, ViewModelChangedValidationTrigger, type ICollectionChangedValidationTriggerConfig, CollectionChangedValidationTrigger, diff --git a/src/validation/objectValidator/IObjectValidator.ts b/src/validation/objectValidator/IObjectValidator.ts index 1eb65c5..a054b7c 100644 --- a/src/validation/objectValidator/IObjectValidator.ts +++ b/src/validation/objectValidator/IObjectValidator.ts @@ -8,7 +8,7 @@ import type { WellKnownValidationTrigger, ValidationTrigger } from '../triggers' /** * Represents an object validator. * @template TValidatable The instance type that is being validated. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IObjectValidator & INotifyPropertiesChanged, TValidationError = string> extends IReadOnlyObjectValidator { /** diff --git a/src/validation/objectValidator/IReadOnlyObjectValidator.ts b/src/validation/objectValidator/IReadOnlyObjectValidator.ts index 4bf5b44..a469a6d 100644 --- a/src/validation/objectValidator/IReadOnlyObjectValidator.ts +++ b/src/validation/objectValidator/IReadOnlyObjectValidator.ts @@ -7,7 +7,7 @@ import type { WellKnownValidationTrigger, ValidationTrigger } from '../triggers' /** * Represents a read-only object validator. * @template TValidatable The instance type that is being validated. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IReadOnlyObjectValidator & INotifyPropertiesChanged, TValidationError = string> { /** diff --git a/src/validation/objectValidator/ObjectValidator.ts b/src/validation/objectValidator/ObjectValidator.ts index 7ce5905..d6dc10b 100644 --- a/src/validation/objectValidator/ObjectValidator.ts +++ b/src/validation/objectValidator/ObjectValidator.ts @@ -10,7 +10,7 @@ import { ViewModelChangedValidationTrigger, resolveValidationTriggers } from '.. /** * Represents the object validator configuration. * @template TValidatable The instance type that is being validated. - * @template TValidationError The concrete type for representing validaiton errors (strings, enums, numbers etc.). + * @template TValidationError The concrete type for representing validation errors (strings, enums, numbers etc.). */ export interface IObjectValidatorConfig & INotifyPropertiesChanged, TValidationError = string> { readonly target: TValidatable; @@ -21,7 +21,7 @@ export interface IObjectValidatorConfig & INotifyPropertiesChanged, TValidationError = string> implements IObjectValidator { private static _defaultShouldTargetTriggerValidation(target: IValidatable, changedProperties: readonly (keyof IValidatable)[]): boolean { diff --git a/src/validation/triggers/CollectionChangedValidationTrigger.ts b/src/validation/triggers/CollectionChangedValidationTrigger.ts index bf3f8fb..b4b35e1 100644 --- a/src/validation/triggers/CollectionChangedValidationTrigger.ts +++ b/src/validation/triggers/CollectionChangedValidationTrigger.ts @@ -8,12 +8,12 @@ import { ValidationTrigger } from './ValidationTrigger'; */ export interface ICollectionChangedValidationTriggerConfig = INotifyCollectionChanged> { /** - * Gets the collection that may trigger a validaiton. + * Gets the collection that may trigger a validation. */ readonly collection: TCollection; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param collection The collection that changed. * @param collectionChange The collection change. */ diff --git a/src/validation/triggers/CollectionItemValidationTrigger.ts b/src/validation/triggers/CollectionItemValidationTrigger.ts index 63dc446..53ec84f 100644 --- a/src/validation/triggers/CollectionItemValidationTrigger.ts +++ b/src/validation/triggers/CollectionItemValidationTrigger.ts @@ -16,7 +16,7 @@ interface IItemValidationTriggers { */ export interface ICollectionItemValidationTriggerConfig { /** - * Gets the collection containing the items that may trigger validaiton. + * Gets the collection containing the items that may trigger validation. */ readonly collection: INotifyCollectionChanged & Iterable; /** @@ -25,15 +25,15 @@ export interface ICollectionItemValidationTriggerConfig { readonly validationTriggerSelector: ValidationTriggerSelector; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param item The item that changed which may trigger a validation. */ shouldTriggerValidation?(item: TItem): boolean; } /** - * Represents a collection item validation trigger. Instead of triggering a validaiton only when the collection changes, - * a validaiton may be triggered by any of the contained items when they themselves change. + * Represents a collection item validation trigger. Instead of triggering a validation only when the collection changes, + * a validation may be triggered by any of the contained items when they themselves change. * * This is useful when within the collection there is a field that needs to be unique, * such as a unique name for each item in the collection. diff --git a/src/validation/triggers/CollectionReorderedValidationTrigger.ts b/src/validation/triggers/CollectionReorderedValidationTrigger.ts index d49c599..540a8dd 100644 --- a/src/validation/triggers/CollectionReorderedValidationTrigger.ts +++ b/src/validation/triggers/CollectionReorderedValidationTrigger.ts @@ -8,12 +8,12 @@ import { ValidationTrigger } from './ValidationTrigger'; */ export interface ICollectionReorderedValidationTriggerConfig = INotifyCollectionReordered> { /** - * Gets the collection that may trigger a validaiton. + * Gets the collection that may trigger a validation. */ readonly collection: TCollection; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param collection The collection that changed. * @param collectionReorder The collection reorder information. */ diff --git a/src/validation/triggers/MapChangedValidationTrigger.ts b/src/validation/triggers/MapChangedValidationTrigger.ts index 8041d40..b5932df 100644 --- a/src/validation/triggers/MapChangedValidationTrigger.ts +++ b/src/validation/triggers/MapChangedValidationTrigger.ts @@ -9,12 +9,12 @@ import { ValidationTrigger } from './ValidationTrigger'; */ export interface IMapChangedValidationTriggerConfig = INotifyMapChanged> { /** - * Gets the map that may trigger a validaiton. + * Gets the map that may trigger a validation. */ readonly map: TMap; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param map The map that changed. * @param mapChange The map change. */ diff --git a/src/validation/triggers/MapItemValidationTrigger.ts b/src/validation/triggers/MapItemValidationTrigger.ts index ea87296..bc4935a 100644 --- a/src/validation/triggers/MapItemValidationTrigger.ts +++ b/src/validation/triggers/MapItemValidationTrigger.ts @@ -17,7 +17,7 @@ interface IItemValidationTriggers { */ export interface IMapItemValidationTriggerConfig { /** - * Gets the map containing the items that may trigger validaiton. + * Gets the map containing the items that may trigger validation. */ readonly map: INotifyMapChanged & Iterable<[TKey, TItem]>; /** @@ -26,15 +26,15 @@ export interface IMapItemValidationTriggerConfig { readonly validationTriggerSelector: ValidationTriggerSelector; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param item The item that changed which may trigger a validation. */ shouldTriggerValidation?(item: TItem): boolean; } /** - * Represents a map item validation trigger. Instead of triggering a validaiton only when the map changes, - * a validaiton may be triggered by any of the contained items when they themselves change. + * Represents a map item validation trigger. Instead of triggering a validation only when the map changes, + * a validation may be triggered by any of the contained items when they themselves change. * * This is useful when within the collection there is a field that needs to be unique, * such as a unique name for each item in the collection. diff --git a/src/validation/triggers/SetChangedValidationTrigger.ts b/src/validation/triggers/SetChangedValidationTrigger.ts index e51363e..8ab2134 100644 --- a/src/validation/triggers/SetChangedValidationTrigger.ts +++ b/src/validation/triggers/SetChangedValidationTrigger.ts @@ -8,12 +8,12 @@ import { ValidationTrigger } from './ValidationTrigger'; */ export interface ISetChangedValidationTriggerConfig = INotifySetChanged> { /** - * Gets the set that may trigger a validaiton. + * Gets the set that may trigger a validation. */ readonly set: TSet; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param set The set that changed. * @param setChange The set change. */ diff --git a/src/validation/triggers/SetItemValidationTrigger.ts b/src/validation/triggers/SetItemValidationTrigger.ts index c8303dc..eabe76d 100644 --- a/src/validation/triggers/SetItemValidationTrigger.ts +++ b/src/validation/triggers/SetItemValidationTrigger.ts @@ -15,7 +15,7 @@ interface IItemValidationTriggers { */ export interface ISetItemValidationTriggerConfig { /** - * Gets the set containing the items that may trigger validaiton. + * Gets the set containing the items that may trigger validation. */ readonly set: INotifySetChanged & Iterable; /** @@ -24,15 +24,15 @@ export interface ISetItemValidationTriggerConfig { readonly validationTriggerSelector: ValidationTriggerSelector; /** - * Optional, a guard method which controls when a validaiton should be triggered. + * Optional, a guard method which controls when a validation should be triggered. * @param item The item that changed which may trigger a validation. */ shouldTriggerValidation?(item: TItem): boolean; } /** - * Represents a set item validation trigger. Instead of triggering a validaiton only when the set changes, - * a validaiton may be triggered by any of the contained items when they themselves change. + * Represents a set item validation trigger. Instead of triggering a validation only when the set changes, + * a validation may be triggered by any of the contained items when they themselves change. * * This is useful when within the collection there is a field that needs to be unique, * such as a unique name for each item in the collection. diff --git a/src/validation/triggers/ValidationTrigger.ts b/src/validation/triggers/ValidationTrigger.ts index 09a2d50..4a68a49 100644 --- a/src/validation/triggers/ValidationTrigger.ts +++ b/src/validation/triggers/ValidationTrigger.ts @@ -3,7 +3,7 @@ import type { INotifyCollectionChanged, INotifyCollectionReordered, INotifySetCh import { type IEvent, EventDispatcher } from '../../events'; /** - * Represent a set of well-known validaiton triggers. These are used to simplify + * Represent a set of well-known validation triggers. These are used to simplify * object validator configurations as a view model or observable collection can * be directly passed as a trigger. * diff --git a/src/validation/triggers/ViewModelChangedValidationTrigger.ts b/src/validation/triggers/ViewModelChangedValidationTrigger.ts index 16f1517..884c985 100644 --- a/src/validation/triggers/ViewModelChangedValidationTrigger.ts +++ b/src/validation/triggers/ViewModelChangedValidationTrigger.ts @@ -12,7 +12,7 @@ export interface IViewModelChangedValidationTriggerConfig