diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d9e8ae3ce..16d20e4bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,58 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ +## v4.0.0-beta.?? (2024-??-??) + +#### :boom: Breaking Change + +* The `getPassedProps` method now returns a dictionary instead of a set `core/component` + +* `iBlock`: + * The API for `InferComponentEvents` has been changed so that you no longer need to pass `this` as the first argument + * Component events without an `error` or `warning` status are logged only if the `verbose` prop is set + * The `strictEmit` method no longer performs normalization of the event name + +* To automatically implement traits or characteristics for components, + the `derive` decorator from the `components/traits` module should now be used. + +* Removed the constants `COMPONENTS` and `BLOCK_NAMES` `build/globals.webpack` +* Removed the `components` property `config` + +#### :bug: Bug Fix + +* Fixed an error in normalizing attribute and prop values in Snakeskin `build` + +#### :rocket: New Feature + +* `core/component/decorators`: + * Added new decorators, defaultValue and method, for the class-based DSL. + These decorators are used during code generation by the TS transformer DSL. + + * The prop, field, and system decorators can now accept a default value for the field as a second argument. + This argument is used during code generation by the TS transformer DSL. + +#### :house: Internal + +* Various micro-optimizations + +* `build`: + * Added a new TypeScript transformer to automatically apply decorators to parts of a component: + methods, accessors, field overrides, etc. + * Now, only the value from the decorator is used to get the default field value. + Default values specified in the class property will automatically be passed to the decorator by the transformer. + +* The observation of accessor dependencies is now initialized only if the accessor has been used at least once `core/component/accessor` + +* The decorators from `core/component/decorators` no longer use a single factory module. + Now, each decorator is implemented independently. + +* `core/component/meta`: + * When inheriting metaobjects, prototype chains are now used instead of full copying. + This optimizes the process of creating metaobjects. + * Methods and accessors are now added to the metaobject via the `method` decorator instead of runtime reflection. + This decorator is automatically added during the build process. + * Optimized creation of metaobjects. + ## v4.0.0-beta.153 (2024-11-15) #### :house: Internal @@ -30,7 +82,7 @@ Reloading now occurs for unloaded components or when explicitly specified with ` * Fix error "ctx.$vueWatch is not a function" caused by the incorrect fix in the v4.0.0-beta.146 `core/component/watch` * Fixed endless attempts to load a component template that is not in use. Added a 10-second limit for attempts to load the template. `core/component/decorators/component` -* Default `forceUpdate` param of a property no longer overrides its value inherited from parent component `core/component/decorators/prop` +* Default `forceUpdate` param of a property no longer overrides its value inherited from the parent component `core/component/decorators/prop` * Fixed typo: `"prop"` -> `"props"` when inheriting parent properties `core/component/decorators/factory` ## v4.0.0-beta.152 (2024-11-11) diff --git a/build/CHANGELOG.md b/build/CHANGELOG.md index ebfd6c1295..8a47fe7bfa 100644 --- a/build/CHANGELOG.md +++ b/build/CHANGELOG.md @@ -9,6 +9,23 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :boom: Breaking Change + +* Removed the constants `COMPONENTS` and `BLOCK_NAMES` `globals.webpack` + +#### :bug: Bug Fix + +* Fixed an error in normalizing attribute and prop values in Snakeskin + +#### :house: Internal + +* Added a new TypeScript transformer to automatically apply decorators to parts of a component: + methods, accessors, field overrides, etc. +* Now, only the value from the decorator is used to get the default field value. + Default values specified in the class property will automatically be passed to the decorator by the transformer. + ## v4.0.0-beta.138.dsl-speedup (2024-10-01) #### :house: Internal diff --git a/build/globals.webpack.js b/build/globals.webpack.js index 902e0a1d58..d44225b0a1 100644 --- a/build/globals.webpack.js +++ b/build/globals.webpack.js @@ -8,9 +8,7 @@ 'use strict'; -const - $C = require('collection.js'), - config = require('@config/config'); +const config = require('@config/config'); const {csp, build, webpack, i18n} = config, @@ -18,9 +16,7 @@ const {collectI18NKeysets} = include('build/helpers'), {getDSComponentMods, getThemes, getDS} = include('build/ds'); -const - projectGraph = include('build/graph'), - s = JSON.stringify; +const s = JSON.stringify; const locales = i18n.supportedLocales(), @@ -53,31 +49,6 @@ module.exports = { LANG_KEYSETS: s(collectI18NKeysets(locales)), LANG_PACKS: s(config.i18n.langPacksStore), - COMPONENTS: projectGraph.then(({components}) => { - if (Object.isMap(components)) { - return $C(components).to({}).reduce((res, el, key) => { - res[key] = { - parent: JSON.stringify(el.parent), - dependencies: JSON.stringify(el.dependencies) - }; - - return res; - }); - } - - return {}; - }), - - BLOCK_NAMES: runtime.blockNames ? - projectGraph.then(({components}) => { - if (Object.isMap(components)) { - const blockNames = Array.from(components.keys()).filter((el) => /^b-/.test(el)); - return s(blockNames); - } - }) : - - null, - THEME: s(config.theme.default()), THEME_ATTRIBUTE: s(config.theme.attribute), AVAILABLE_THEMES: pzlr.designSystem ? diff --git a/build/graph/component-params.js b/build/graph/component-params.js index 2921194bfc..49ed084582 100644 --- a/build/graph/component-params.js +++ b/build/graph/component-params.js @@ -12,8 +12,7 @@ const $C = require('collection.js'), escaper = require('escaper'); -const - fs = require('node:fs'); +const fs = require('node:fs'); const { componentRgxp, @@ -67,8 +66,7 @@ Object.assign(componentParams, { * Load component runtime parameters to a map */ componentFiles.forEach((el) => { - const - escapedFragments = []; + const escapedFragments = []; const file = escaper.replace(fs.readFileSync(el).toString(), escapedFragments), @@ -94,8 +92,7 @@ componentFiles.forEach((el) => { parent }; - const - obj = componentParams[component]; + const obj = componentParams[component]; obj.deprecatedProps = p.deprecatedProps ?? {}; diff --git a/build/graph/graph.js b/build/graph/graph.js index 805a27b2af..4b2b3d06ff 100644 --- a/build/graph/graph.js +++ b/build/graph/graph.js @@ -71,6 +71,7 @@ async function buildProjectGraph() { if (build.buildGraphFromCache && fs.existsSync(graphCacheFile)) { const cache = loadFromCache(); buildFinished(); + module.exports.graph = await cache; return cache; } @@ -168,6 +169,7 @@ async function buildProjectGraph() { console.log('The project graph is initialized'); buildFinished(); + module.exports.graph = res; return res; /** diff --git a/build/snakeskin/default-filters.js b/build/snakeskin/default-filters.js index e61d79d180..690670a670 100644 --- a/build/snakeskin/default-filters.js +++ b/build/snakeskin/default-filters.js @@ -59,28 +59,20 @@ Snakeskin.importFilters({ }); function tagFilter({name: tag, attrs = {}}, _, rootTag, forceRenderAsVNode, tplName, cursor) { - Object.entries(attrs).forEach(([key, attr]) => { - if (isStaticV4Prop.test(key)) { - // Since HTML is not case-sensitive, the name can be written differently. - // We will explicitly normalize the name to the most popular format for HTML notation. - const tmp = key.dasherize(key.startsWith(':')); - - if (tmp !== key) { - delete attrs[key]; - attrs[tmp] = attr; - } - } - }); - let componentName; + if (attrs[':instance-of']) { + attrs[':instanceOf'] = attrs[':instance-of']; + delete attrs[':instance-of']; + } + if (attrs[TYPE_OF]) { componentName = attrs[TYPE_OF]; } else if (tag === 'component') { - if (attrs[':instance-of']) { - componentName = attrs[':instance-of'][0].camelize(false); - delete attrs[':instance-of']; + if (attrs[':instanceOf']) { + componentName = attrs[':instanceOf'][0].camelize(false); + delete attrs[':instanceOf']; } else { componentName = 'iBlock'; @@ -92,6 +84,41 @@ function tagFilter({name: tag, attrs = {}}, _, rootTag, forceRenderAsVNode, tplN const component = componentParams[componentName]; + Object.entries(attrs).forEach(([key, attr]) => { + if (isStaticV4Prop.test(key)) { + // Since HTML is not case-sensitive, the name can be written differently. + // We will explicitly normalize the name to the most popular format for HTML notation. + // For Vue component attributes such as `:` and `@`, we convert the prop to camelCase format. + let normalizedKey; + + if (component) { + if (key.startsWith('@')) { + normalizedKey = key.camelize(false); + + } else if (key.startsWith('v-on:')) { + normalizedKey = key.replace(/^v-on:([^.[]+)(.*)/, (_, event, rest) => + `v-on:${event.camelize(false)}${rest}`); + + } else if (key.startsWith(':') && !key.startsWith(':-') && !key.startsWith(':v-')) { + const camelizedKey = key.camelize(false); + + if (component.props[camelizedKey.slice(1)]) { + normalizedKey = camelizedKey; + } + } + } + + if (!normalizedKey) { + normalizedKey = key.dasherize(); + } + + if (normalizedKey !== key) { + delete attrs[key]; + attrs[normalizedKey] = attr; + } + } + }); + if (isSmartComponent(component)) { attrs[SMART_PROPS] = component.functional; } @@ -177,8 +204,8 @@ function tagFilter({name: tag, attrs = {}}, _, rootTag, forceRenderAsVNode, tplN attrs[':componentIdProp'] = [`componentId + ${JSON.stringify(id)}`]; } - if (component.inheritMods !== false && !attrs[':mods'] && !attrs[':modsProp']) { - attrs[':mods'] = ['provide.mods()']; + if (component.inheritMods !== false) { + attrs[':inheritMods'] = ['sharedMods != null']; } Object.entries(attrs).forEach(([name, val]) => { diff --git a/build/ts-transformers/CHANGELOG.md b/build/ts-transformers/CHANGELOG.md index 3a4087b7be..d596fbde96 100644 --- a/build/ts-transformers/CHANGELOG.md +++ b/build/ts-transformers/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :rocket: New Feature + +* Added a new transformer `register-component-default-values` + ## v3.23.5 (2022-07-12) #### :rocket: New Feature diff --git a/build/ts-transformers/README.md b/build/ts-transformers/README.md index 5d68f868f4..8a95e4cd67 100644 --- a/build/ts-transformers/README.md +++ b/build/ts-transformers/README.md @@ -1,3 +1,10 @@ # build/ts-transformers -This module provides a bunch of custom transformers for TypeScript. \ No newline at end of file +This module provides a bunch of custom transformers for TypeScript/TSC. + +## Default Transformers + +* `set-component-layer` - this module provides a transformer that adds information to each component declaration about + the application layer in which the component is declared. + +* `register-component-parts` - this module provides a transformer for registering parts of a class as parts of the associated component. diff --git a/build/ts-transformers/index.js b/build/ts-transformers/index.js index bb25e93d79..5eef9935c1 100644 --- a/build/ts-transformers/index.js +++ b/build/ts-transformers/index.js @@ -9,16 +9,15 @@ 'use strict'; const - setComponentLayer = include('build/ts-transformers/set-component-layer'); + setComponentLayer = include('build/ts-transformers/set-component-layer'), + resisterComponentParts = include('build/ts-transformers/register-component-parts'); /** - * Returns a settings object for setting up TypeScript transformers - * - * @param {import('typescript').Program} program + * Returns a settings object for configuring TypeScript transformers * @returns {object} */ -module.exports = (program) => ({ - before: [setComponentLayer(program)], +module.exports = () => ({ + before: [setComponentLayer, resisterComponentParts], after: {}, afterDeclarations: {} }); diff --git a/build/ts-transformers/register-component-parts/CHANGELOG.md b/build/ts-transformers/register-component-parts/CHANGELOG.md new file mode 100644 index 0000000000..6fe986f267 --- /dev/null +++ b/build/ts-transformers/register-component-parts/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v4.0.0-beta.?? (2024-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/build/ts-transformers/register-component-parts/README.md b/build/ts-transformers/register-component-parts/README.md new file mode 100644 index 0000000000..bb2d7a0b03 --- /dev/null +++ b/build/ts-transformers/register-component-parts/README.md @@ -0,0 +1,67 @@ +# build/ts-transformers/register-component-parts + +This module provides a transformer for registering parts of a class as parts of the associated component. + +## Example + +```typescript +import iBlock, { component, prop } from 'components/super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop(Array) + prop: string[] = []; + + get answer() { + return 42; + } + + just() { + return 'do it'; + } +} +``` + +Will transform to + +```typescript +import { method } from 'core/component/decorators/method'; +import { defaultValue } from 'core/component/decorators/default-value'; +import { registeredComponent } from 'core/component/decorators/const'; + +import iBlock, { component, prop } from 'components/super/i-block/i-block'; + +registeredComponent.name = 'bExample'; +registeredComponent.layer = '@v4fire/client'; +registeredComponent.event = 'constructor.b-example.@v4fire/client'; + +@component() +class bExample extends iBlock { + @prop(Array, () => { return []; }) + prop: string[] = []; + + @method('accessor') + get answer() { + return 42; + } + + @method('method') + just() { + return 'do it'; + } +} +``` + +## How to Attach the Transformer? + +To attach the transformer, you need to add its import to `build/ts-transformers`. + +```js +const registerComponentParts = include('build/ts-transformers/register-component-parts'); + +module.exports = () => ({ + before: [registerComponentParts], + after: {}, + afterDeclarations: {} +}); +``` diff --git a/build/ts-transformers/register-component-parts/helpers.js b/build/ts-transformers/register-component-parts/helpers.js new file mode 100644 index 0000000000..c1cc051778 --- /dev/null +++ b/build/ts-transformers/register-component-parts/helpers.js @@ -0,0 +1,159 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +'use strict'; + +const ts = require('typescript'); + +/** + * @typedef {import('typescript').TransformationContext} TransformationContext + * @typedef {import('typescript').Node} Node + * @typedef {import('typescript').Decorator} Decorator + */ + +exports.addNamedImport = addNamedImport; + +/** + * Adds an import statement with the specified name to the specified file + * + * @param {string} name - the name of the decorator to be imported and applied (e.g., `defaultValue`) + * @param {string} path - the path from which the decorator should be imported (e.g., `core/component/decorators`) + * @param {TransformationContext} context - the transformation context + * @param {Node} node - the source file node in the AST + * @returns {Node} + */ +function addNamedImport(name, path, context, node) { + const {factory} = context; + + const decoratorSrc = factory.createStringLiteral(path); + + const importSpecifier = factory.createImportSpecifier( + undefined, + undefined, + factory.createIdentifier(name) + ); + + const importClause = factory.createImportClause( + undefined, + undefined, + factory.createNamedImports([importSpecifier]) + ); + + const importDeclaration = factory.createImportDeclaration( + undefined, + undefined, + importClause, + decoratorSrc + ); + + const updatedStatements = factory.createNodeArray([ + importDeclaration, + ...node.statements + ]); + + return factory.updateSourceFile(node, updatedStatements); +} + +exports.isComponentClass = isComponentClass; + +/** + * Returns true if the specified class is a component + * + * @param {Node} node - the class node in the AST + * @returns {boolean} + */ +function isComponentClass(node) { + const {decorators} = node; + + if (!ts.isClassDeclaration(node)) { + return false; + } + + if (decorators != null && decorators.length > 0) { + return decorators.some((node) => isDecorator(node, 'component')); + } + + return false; +} + +exports.getPartialName = getPartialName; + +/** + * Returns the value of the `partial` parameter from the parameters of the @component decorator + * + * @param {Node} node - the class node in the AST + * @returns {boolean} + */ +function getPartialName(node) { + const {decorators} = node; + + if (!ts.isClassDeclaration(node)) { + return false; + } + + if (decorators != null && decorators.length > 0) { + for (const decorator of node.decorators) { + if (isDecorator(decorator, 'component')) { + const args = decorator.expression.arguments; + + if (args.length > 0) { + const params = args[0]; + + if (ts.isObjectLiteralExpression(params)) { + for (const property of params.properties) { + if ( + ts.isPropertyAssignment(property) && + ts.isIdentifier(property.name) && + property.name.text === 'partial' && + ts.isStringLiteral(property.initializer) + ) { + return property.initializer.text; + } + } + } + } + + break; + } + } + } + + return undefined; +} + +const pathToRootRgxp = /(?.+)[/\\]src[/\\]/; + +exports.getLayerName = getLayerName; + +/** + * Takes a file path and returns the package name from the package.json file of the package the provided file belongs to + * + * @param {string} path + * @returns {string} + */ +function getLayerName(path) { + const pathToRootDir = path.match(pathToRootRgxp).groups.path; + return require(`${pathToRootDir}/package.json`).name; +} + +exports.isDecorator = isDecorator; + +/** + * Returns true if the given decorator has the specified name + * + * @param {Decorator} decorator + * @param {string|string[]} name - a name or a list of possible names to match against the decorator. + * @returns {boolean} + */ +function isDecorator(decorator, name) { + return ( + ts.isCallExpression(decorator.expression) && + ts.isIdentifier(decorator.expression.expression) && + Array.toArray(name).includes(decorator.expression.expression.text) + ); +} diff --git a/build/ts-transformers/register-component-parts/index.js b/build/ts-transformers/register-component-parts/index.js new file mode 100644 index 0000000000..6361767dbd --- /dev/null +++ b/build/ts-transformers/register-component-parts/index.js @@ -0,0 +1,444 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +'use strict'; + +const ts = require('typescript'); + +const build = include('build/graph'); + +const { + addNamedImport, + + isDecorator, + isComponentClass, + + getPartialName, + getLayerName +} = include('build/ts-transformers/register-component-parts/helpers'); + +/** + * @typedef {import('typescript').Transformer} Transformer + * @typedef {import('typescript').TransformationContext} TransformationContext + * @typedef {import('typescript').Node} Node + * @typedef {import('typescript').ClassDeclaration} ClassDeclaration + */ + +module.exports = registerComponentParts; + +/** + * Registers parts of a class as parts of the associated component. + * For example, all methods and accessors of the class are registered as methods and accessors of the component. + * + * @param {TransformationContext} context + * @returns {Transformer} + * + * @example + * + * ```typescript + * import iBlock, { component, prop } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @prop(Array) + * prop = []; + * + * get answer() { + * return 42; + * } + * + * just() { + * return 'do it'; + * } + * } + * ``` + * + * Will transform to + * + * ```typescript + * import { method } from 'core/component/decorators/method'; + * import { defaultValue } from 'core/component/decorators/default-value'; + * import { registeredComponent } from 'core/component/decorators/const'; + * + * import iBlock, { component, prop } from 'components/super/i-block/i-block'; + * + * registeredComponent.name = 'bExample'; + * registeredComponent.layer = '@v4fire/client'; + * registeredComponent.event = 'constructor.b-example.@v4fire/client'; + * + * @component() + * class bExample extends iBlock { + * @prop(Array, () => { return []; }) + * prop: string[] = []; + * + * @method('accessor') + * get answer() { + * return 42; + * } + * + * @method('method') + * just() { + * return 'do it'; + * } + * } + * ``` + */ +function registerComponentParts(context) { + const {factory} = context; + + let + componentName, + originalComponentName; + + let + needImportMethodDecorator = false, + needImportDefaultValueDecorator = false; + + return (node) => { + node = ts.visitNode(node, visitor); + + if (componentName) { + node = registeredComponentParams( + context, + node, + componentName, + originalComponentName, + getLayerName(node.path) + ); + + node = addNamedImport('registeredComponent', 'core/component/decorators/const', context, node); + } + + if (needImportMethodDecorator) { + node = addNamedImport('method', 'core/component/decorators/method', context, node); + } + + if (needImportDefaultValueDecorator) { + node = addNamedImport('defaultValue', 'core/component/decorators/default-value', context, node); + } + + return node; + }; + + /** + * A visitor for the AST node + * + * @param {Node} node + * @returns {Node|ClassDeclaration} + */ + function visitor(node) { + if (isComponentClass(node, 'component')) { + originalComponentName = node.name.text; + componentName = getPartialName(node) ?? originalComponentName; + + const + normalizedComponentName = originalComponentName.dasherize(), + componentInfo = build.graph.components.get(componentName.dasherize()); + + if (normalizedComponentName !== 'i-block' && componentInfo != null) { + const hasRemoteProviders = componentInfo.dependencies.find((dep) => dep.includes('remote-provider')); + + if (hasRemoteProviders) { + node = addDontWaitRemoteProvidersHint(context, node); + } + } + + if (node.members != null) { + const newMembers = node.members.map((node) => { + if ( + ts.isPropertyDeclaration(node) && + ts.hasInitializer(node) && + !node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ) { + needImportDefaultValueDecorator = true; + return addDefaultValueDecorator(context, node); + } + + if ( + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) || + ts.isMethodDeclaration(node) + ) { + needImportMethodDecorator = true; + node = addMethodDecorator(context, node); + } + + return node; + }); + + return factory.updateClassDeclaration( + node, + node.decorators, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + newMembers + ); + } + } + + return ts.visitEachChild(node, visitor, context); + } +} + +/** + * Registers the component parameters for initializing the DSL + * + * @param {TransformationContext} context - the transformation context + * @param {Node} node - the node representing the component class + * @param {string} componentName - the name of the component being targeted for registration + * @param {string} originalComponentName - the original name of the component for registration + * @param {string} layerName - the name of the layer in which the component is registered + * @returns {Node} + */ +function registeredComponentParams( + context, + node, + componentName, + originalComponentName, + layerName +) { + const statements = []; + + const {factory} = ts; + + node.statements.forEach((node) => { + if (isComponentClass(node, 'component') && node.name.text === originalComponentName) { + statements.push( + register('name', componentName), + register('name', componentName), + register('layer', layerName), + register('event', `constructor.${componentName.dasherize()}.${layerName}`), + node + ); + + } else { + statements.push(node); + } + }); + + return factory.updateSourceFile(node, factory.createNodeArray(statements)); + + function register(name, value) { + const exprValue = Object.isString(value) ? + factory.createStringLiteral(value) : + factory.createArrayLiteralExpression([...value].map((value) => factory.createStringLiteral(value))); + + return factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('registeredComponent'), + factory.createIdentifier(name) + ), + + factory.createToken(ts.SyntaxKind.EqualsToken), + exprValue + ) + ); + } +} + +/** + * Adds the @defaultValue decorator for the specified class property + * + * @param {TransformationContext} context - the transformation context + * @param {Node} node - the property node in the AST + * @returns {Node} + */ +function addDefaultValueDecorator(context, node) { + const {factory} = context; + + const defaultValue = ts.getEffectiveInitializer(node); + + let getter; + + if ( + ts.isNumericLiteral(defaultValue) || + ts.isBigIntLiteral(defaultValue) || + ts.isStringLiteral(defaultValue) || + defaultValue.kind === ts.SyntaxKind.UndefinedKeyword || + defaultValue.kind === ts.SyntaxKind.NullKeyword || + defaultValue.kind === ts.SyntaxKind.TrueKeyword || + defaultValue.kind === ts.SyntaxKind.FalseKeyword + ) { + getter = defaultValue; + + } else { + const getterValue = factory.createBlock( + [factory.createReturnStatement(defaultValue)], + true + ); + + getter = factory.createFunctionExpression( + undefined, + undefined, + 'getter', + undefined, + [], + undefined, + getterValue + ); + } + + let hasOwnDecorator = false; + + if (node.decorators != null) { + node.decorators = node.decorators.map((node) => { + if (isDecorator(node, ['prop', 'field', 'system'])) { + hasOwnDecorator = true; + + const expr = node.expression; + + const decoratorExpr = factory.createCallExpression( + expr.expression, + undefined, + [expr.arguments[0] ?? factory.createIdentifier('undefined'), getter] + ); + + return factory.updateDecorator(node, decoratorExpr); + } + + return node; + }); + } + + if (!hasOwnDecorator) { + const decoratorExpr = factory.createCallExpression( + factory.createIdentifier('defaultValue'), + undefined, + [getter] + ); + + const decorator = factory.createDecorator(decoratorExpr); + + const decorators = factory.createNodeArray([decorator, ...(node.decorators || [])]); + + return factory.updatePropertyDeclaration( + node, + decorators, + node.modifiers, + node.name, + ts.SyntaxKind.ExclamationToken, + node.type, + undefined + ); + } + + return factory.updatePropertyDeclaration( + node, + factory.createNodeArray(node.decorators), + node.modifiers, + node.name, + ts.SyntaxKind.ExclamationToken, + node.type, + undefined + ); +} + +/** + * Adds the `@method` decorator for the specified class method or accessor + * + * @param {TransformationContext} context - the transformation context + * @param {Node} node - the method/accessor node in the AST + * @returns {Node} + */ +function addMethodDecorator(context, node) { + const {factory} = context; + + let type; + + if (ts.isMethodDeclaration(node)) { + type = 'method'; + + } else { + type = 'accessor'; + } + + const decoratorExpr = factory.createCallExpression( + factory.createIdentifier('method'), + undefined, + [factory.createStringLiteral(type)] + ); + + const decorator = factory.createDecorator(decoratorExpr); + + const decorators = factory.createNodeArray([decorator, ...(node.decorators || [])]); + + if (ts.isMethodDeclaration(node)) { + return factory.updateMethodDeclaration( + node, + decorators, + node.modifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + node.body + ); + } + + if (ts.isGetAccessorDeclaration(node)) { + return factory.updateGetAccessorDeclaration( + node, + decorators, + node.modifiers, + node.name, + node.parameters, + node.type, + node.body + ); + } + + return factory.updateSetAccessorDeclaration( + node, + decorators, + node.modifiers, + node.name, + node.parameters, + node.body + ); +} + +/** + * Adds the `dontWaitRemoteProvidersHint` method to the specified component class + * + * @param {TransformationContext} context - the transformation context + * @param {Node} node - the class node in the AST + * @returns {Node} + */ +function addDontWaitRemoteProvidersHint(context, node) { + const {factory} = context; + + const method = factory.createMethodDeclaration( + undefined, + [ + ts.factory.createModifier(ts.SyntaxKind.ProtectedKeyword), + ts.factory.createModifier(ts.SyntaxKind.OverrideKeyword) + ], + undefined, + 'dontWaitRemoteProvidersHint', + undefined, + undefined, + [], + factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + factory.createBlock([factory.createReturnStatement(factory.createFalse())]) + ); + + return factory.updateClassDeclaration( + node, + node.decorators, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + factory.createNodeArray([...node.members, method]) + ); +} diff --git a/build/ts-transformers/set-component-layer.js b/build/ts-transformers/set-component-layer.js deleted file mode 100644 index 31097ca027..0000000000 --- a/build/ts-transformers/set-component-layer.js +++ /dev/null @@ -1,149 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/* eslint-disable capitalized-comments */ - -'use strict'; - -const ts = require('typescript'); -const {validators} = require('@pzlr/build-core'); - -/** - * @typedef {import('typescript').TransformationContext} Context - * @typedef {import('typescript').Node} Node - * @typedef {import('typescript').VisitResult} VisitResult - * @typedef {import('typescript').Transformer} Transformer - */ - -const - pathToRootRgxp = /(?.+)[/\\]src[/\\]/, - isComponentPath = new RegExp(`\\/(${validators.blockTypeList.join('|')})-.+?\\/?`); - -/** - * The transformer that adds the "layer" property to component-meta objects - * to indicate the name of the package in which it is defined - * - * @param {Context} context - * @returns {Transformer} - * @example - * ```typescript - * @component() - * class bExample {} - * - * // Becomes - * @component({ layer: '@v4fire/client' }) - * class bExample {} - * ``` - * - * ``` - * @component({functional: true}) - * class bExample {} - * - * // Becomes - * @component({ - * functional: true, - * layer: '@v4fire/client' - * }) - * class bExample {} - * ``` - */ -const setComponentLayerTransformer = (context) => (sourceFile) => { - if (!isInsideComponent(sourceFile.path)) { - return sourceFile; - } - - const layer = getLayerName(sourceFile.path); - const {factory} = context; - - /** - * A visitor for the AST node - * - * @param {Node} node - * @returns {Node} - */ - const visitor = (node) => { - if (ts.isDecorator(node) && isComponentCallExpression(node)) { - const - expr = node.expression; - - if (!ts.isCallExpression(expr)) { - return node; - } - - // noinspection JSAnnotator - const properties = expr.arguments?.[0]?.properties ?? []; - - const updatedCallExpression = factory.updateCallExpression( - expr, - expr.expression, - expr.typeArguments, - - [ - factory.createObjectLiteralExpression( - [ - ...properties, - factory.createPropertyAssignment( - factory.createIdentifier('layer'), - factory.createStringLiteral(layer) - ) - ], - - false - ) - ] - ); - - return factory.updateDecorator(node, updatedCallExpression); - } - - return ts.visitEachChild(node, visitor, context); - }; - - return ts.visitNode(sourceFile, visitor); -}; - -// eslint-disable-next-line @v4fire/require-jsdoc -module.exports = () => setComponentLayerTransformer; - -/** - * The function determines the package in which the module is defined and - * returns the name of this package from the `package.json` file - * - * @param {string} filePath - * @returns {string} - */ -function getLayerName(filePath) { - const pathToRootDir = filePath.match(pathToRootRgxp).groups.path; - return require(`${pathToRootDir}/package.json`).name; -} - -/** - * Returns true if the specified path is within the context of the component - * - * @param {string} filePath - * @returns {boolean} - */ -function isInsideComponent(filePath) { - return isComponentPath.test(filePath); -} - -/** - * Returns true if the specified call expression is `component()` - * - * @param {Node} node - * @returns {boolean} - */ -function isComponentCallExpression(node) { - const expr = node.expression; - - if (Boolean(expr) && !ts.isCallExpression(expr) || !ts.isIdentifier(expr?.expression)) { - return false; - } - - return expr.expression.escapedText === 'component'; -} diff --git a/build/ts-transformers/set-component-layer/README.md b/build/ts-transformers/set-component-layer/README.md new file mode 100644 index 0000000000..0695e2a2dd --- /dev/null +++ b/build/ts-transformers/set-component-layer/README.md @@ -0,0 +1,37 @@ +# build/ts-transformers/set-component-layer + +This module provides a transformer that adds information to each component declaration about the application layer +in which the component is declared. +This is necessary for the correct functioning of component overrides in child layers. + +## Example + +```typescript +import iBlock, { component } from 'components/super/i-block/i-block'; + +@component() +class bExample extends iBlock {} +``` + +Will transform to + +```typescript +import iBlock, { component, prop } from 'components/super/i-block/i-block'; + +@component({layer: '@v4fire/client'}) +class bExample extends iBlock {} +``` + +## How to Attach the Transformer? + +To attach the transformer, you need to add its import to `build/ts-transformers`. + +```js +const setComponentLayer = include('build/ts-transformers/set-component-layer'); + +module.exports = () => ({ + before: [setComponentLayer], + after: {}, + afterDeclarations: {} +}); +``` diff --git a/build/ts-transformers/set-component-layer/index.js b/build/ts-transformers/set-component-layer/index.js new file mode 100644 index 0000000000..b2c6abc6f6 --- /dev/null +++ b/build/ts-transformers/set-component-layer/index.js @@ -0,0 +1,145 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/* eslint-disable capitalized-comments */ + +'use strict'; + +const ts = require('typescript'); +const {validators} = require('@pzlr/build-core'); + +/** + * @typedef {import('typescript').TransformationContext} Context + * @typedef {import('typescript').Node} Node + * @typedef {import('typescript').VisitResult} VisitResult + * @typedef {import('typescript').Transformer} Transformer + */ + +const + pathToRootRgxp = /(?.+)[/\\]src[/\\]/, + isComponentPath = new RegExp(`\\/(${validators.blockTypeList.join('|')})-.+?\\/?`); + +module.exports = setComponentLayer; + +/** + * Adds the "layer" property to the component declaration parameters + * to indicate the name of the package in which it is defined + * + * @param {Context} context + * @returns {Transformer} + * + * @example + * ```typescript + * import iBlock, { component } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock {} + * ``` + * + * Will transform to + * + * ```typescript + * import iBlock, { component } from 'components/super/i-block/i-block'; + * + * @component({layer: '@v4fire/client'}) + * class bExample extends iBlock {} + * ``` + */ +function setComponentLayer(context) { + return (sourceFile) => { + if (!isInsideComponent(sourceFile.path)) { + return sourceFile; + } + + const layer = getLayerName(sourceFile.path); + const {factory} = context; + + return ts.visitNode(sourceFile, visitor); + + /** + * A visitor for the AST node + * + * @param {Node} node + * @returns {Node} + */ + function visitor(node) { + if (ts.isDecorator(node) && isComponentCallExpression(node)) { + const expr = node.expression; + + if (!ts.isCallExpression(expr)) { + return node; + } + + // noinspection JSAnnotator + const properties = expr.arguments?.[0]?.properties ?? []; + + const updatedCallExpression = factory.updateCallExpression( + expr, + expr.expression, + expr.typeArguments, + + [ + factory.createObjectLiteralExpression( + [ + ...properties, + factory.createPropertyAssignment( + factory.createIdentifier('layer'), + factory.createStringLiteral(layer) + ) + ], + + false + ) + ] + ); + + return factory.updateDecorator(node, updatedCallExpression); + } + + return ts.visitEachChild(node, visitor, context); + } + }; +} + +/** + * The function determines the package in which the module is defined and + * returns the name of that package from the `package.json` file + * + * @param {string} filePath + * @returns {string} + */ +function getLayerName(filePath) { + const pathToRootDir = filePath.match(pathToRootRgxp).groups.path; + return require(`${pathToRootDir}/package.json`).name; +} + +/** + * Returns true if the specified path is within the component's context + * + * @param {string} filePath + * @returns {boolean} + */ +function isInsideComponent(filePath) { + return isComponentPath.test(filePath); +} + +/** + * Returns true if the specified call expression is `component()` + * + * @param {Node} node + * @returns {boolean} + */ +function isComponentCallExpression(node) { + const expr = node.expression; + + if (Boolean(expr) && !ts.isCallExpression(expr) || !ts.isIdentifier(expr?.expression)) { + return false; + } + + return expr.expression.escapedText === 'component'; +} diff --git a/config/default.js b/config/default.js index ff24a7962d..fcd1db153b 100644 --- a/config/default.js +++ b/config/default.js @@ -11,15 +11,13 @@ /* eslint-disable max-lines */ const - config = require('@v4fire/core/config/default'); + config = require('@v4fire/core/config/default'), + o = require('@v4fire/config/options').option; const fs = require('node:fs'), path = require('upath'); -const - o = require('@v4fire/config/options').option; - const browserslist = require('browserslist'), {nanoid} = require('nanoid'); @@ -45,8 +43,10 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }, /** - * Returns browserslist env - * @param {string} env - custom environment + * Returns the environment for Browserslist + * + * @param {string} [env] - custom environment + * @returns {string} */ browserslistEnv(env) { if (env == null) { @@ -68,7 +68,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { src: { /** - * Returns a path to the application dist directory for client scripts + * Returns the path to the application distribution directory for client-side scripts of the application * * @cli client-output * @env CLIENT_OUTPUT @@ -96,7 +96,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }, /** - * Test server port + * The port number for the test server * @env TEST_PORT */ testPort: o('test-port', { @@ -105,7 +105,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }), /** - * Option for configuring target environment for build, for example list of polyfills + * Option for specifying the target environment in the build process * * @cli build-edition * @env BUILD_EDITION @@ -181,7 +181,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { env: true, short: 'p', type: 'number', - default: require('os').cpus().length - 1 + default: require('node:os').cpus().length - 1 }), /** @@ -208,7 +208,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * This information is used by WebPack for code block deduplication and build optimization, * but the graph calculation process may take some time. * - * This option allows to take the project graph from a previous build if it exists. + * This option allows taking the project graph from a previous build if it exists. * Keep in mind, an incorrect graph can break the application build. * * @cli build-graph-from-cache @@ -259,9 +259,11 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }, /** - * Returns `true` if the application build times should be traced. - * Trace file will be created in the project's root. - * It's highly recommended to use this option with `module-parallelism=1`. + * Determines whether the application build times should be traced. + * If enabled, a trace file will be created in the project's root directory. + * + * Note: It is highly recommended to use this option with `module-parallelism=1` + * to ensure more accurate tracing results. * * @cli trace-build-times * @env TRACE_BUILD_TIMES @@ -332,7 +334,6 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * @cli target * @env TARGET * - * @param {string} [def] - default value * @returns {?string} */ target() { @@ -579,7 +580,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { const aliases = { dompurify: this.config.es().toLowerCase() === 'es5' ? 'dompurify-v2' : 'dompurify-v3', 'vue/server-renderer': 'assets/lib/server-renderer.js', - // Disable setImmedate polyfill from core-js in favor of our realisation `core/shims/set-immediate` + // Disable setImmedate polyfill from core-js in favor of our realization `core/shims/set-immediate` 'core-js/modules/web.immediate.js': false }; @@ -723,8 +724,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * ``` */ output(vars) { - const - res = this.mode() !== 'production' || this.fatHTML() ? '[name]' : '[hash]_[name]'; + const res = this.mode() !== 'production' || this.fatHTML() ? '[name]' : '[hash]_[name]'; if (vars) { return res.replace(/_?\[(.*?)]/g, (str, key) => { @@ -770,8 +770,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * ``` */ assetsOutput(params) { - const - root = 'assets'; + const root = 'assets'; if (this.mode() !== 'production' || this.fatHTML()) { return this.output({ @@ -874,7 +873,10 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }, /** - * SWC webpack loader configuration + * Returns parameters for `swc-loader` + * + * @param {string} env - custom environment + * @returns {{ts: object, js: object, ss: object}} */ swc(env) { const @@ -886,6 +888,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { jsc: { externalHelpers: true }, + env: { mode: 'entry', targets, @@ -902,13 +905,13 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { } }); - const - js = this.config.extend({}, base), - ss = this.config.extend({}, base, { - jsc: { - externalHelpers: false - } - }); + const js = this.config.extend({}, base); + + const ss = this.config.extend({}, base, { + jsc: { + externalHelpers: false + } + }); return {js, ts, ss}; } @@ -954,7 +957,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }, /** - * Returns parameters for a TypeScript compiler: + * Returns parameters for the TypeScript compiler: * * 1. server - options for compiling the application as a node.js library; * 2. client - options for compiling the application as a client app. @@ -967,12 +970,9 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { 'client.tsconfig.json' : 'tsconfig.json'; - const - server = super.typescript(); + const server = super.typescript(); - const { - compilerOptions: {module} - } = require(path.join(this.src.cwd(), configFile)); + const {compilerOptions: {module}} = require(path.join(this.src.cwd(), configFile)); const client = this.extend({}, server, { configFile, @@ -990,10 +990,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { } }); - return { - client, - server - }; + return {client, server}; }, /** @@ -1005,7 +1002,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { }, /** - * Returns a component dependency map. + * Returns the component dependency map. * This map can be used to provide dynamic component dependencies in `index.js` files. * * @returns {object} @@ -1219,7 +1216,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * @returns {object} */ compilerSFC() { - const {ssr} = this.config.webpack; + const {webpack} = this.config; const NOT_CONSTANT = 0, @@ -1232,7 +1229,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { (node) => { const {props} = node; - if (!ssr || props == null) { + if (!webpack.ssr || props == null) { return; } @@ -1317,9 +1314,10 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { ]; return { - ssr: this.config.webpack.ssr, - ssrCssVars: {}, - compilerOptions: {nodeTransforms} + isProd: webpack.mode() === 'production', + compilerOptions: {nodeTransforms}, + ssr: webpack.ssr, + ssrCssVars: {} }; } }, diff --git a/index.d.ts b/index.d.ts index e4a72652f9..760bf09845 100644 --- a/index.d.ts +++ b/index.d.ts @@ -34,9 +34,7 @@ declare const MODULE: string; declare const PATH: Dictionary>; declare const PUBLIC_PATH: CanUndef; -declare const COMPONENTS: Dictionary<{parent: string; dependencies: string[]}>; declare const TPLS: Dictionary>; -declare const BLOCK_NAMES: CanUndef; declare const THEME: CanUndef; declare const THEME_ATTRIBUTE: CanUndef; diff --git a/src/components/base/b-bottom-slide/b-bottom-slide.ts b/src/components/base/b-bottom-slide/b-bottom-slide.ts index 16cc88c289..609b1afa55 100644 --- a/src/components/base/b-bottom-slide/b-bottom-slide.ts +++ b/src/components/base/b-bottom-slide/b-bottom-slide.ts @@ -14,7 +14,7 @@ import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import History from 'components/traits/i-history/history'; import type iHistory from 'components/traits/i-history/i-history'; diff --git a/src/components/base/b-dynamic-page/b-dynamic-page.ss b/src/components/base/b-dynamic-page/b-dynamic-page.ss index 3321b46ea7..0754bd6521 100644 --- a/src/components/base/b-dynamic-page/b-dynamic-page.ss +++ b/src/components/base/b-dynamic-page/b-dynamic-page.ss @@ -17,13 +17,6 @@ ? rootAttrs['v-memo'] = '[]' - block body - : graph = include('build/graph/component-params') - - ? Object.assign(attrs, graph.getComponentPropAttrs(self.name(PARENT_TPL_NAME))) - ? delete attrs[':is'] - ? delete attrs[':keepAlive'] - ? delete attrs[':dispatching'] - < template v-for = el in asyncRender.iterate(renderIterator, {filter: renderFilter, group: registerRenderGroup}) < component.&__component & v-if = !pageTakenFromCache && page != null | @@ -31,9 +24,7 @@ :is = page | :instanceOf = iDynamicPage | - :dispatching = true | :canFunctional = false | - v-attrs = {'@hook:destroyed': createPageDestructor()} | - ${attrs} + v-attrs = getPageProps() . diff --git a/src/components/base/b-dynamic-page/b-dynamic-page.ts b/src/components/base/b-dynamic-page/b-dynamic-page.ts index 63641ae603..b442efee28 100644 --- a/src/components/base/b-dynamic-page/b-dynamic-page.ts +++ b/src/components/base/b-dynamic-page/b-dynamic-page.ts @@ -64,18 +64,10 @@ export * from 'components/base/b-dynamic-page/interface'; Block.addToPrototype({element}); AsyncRender.addToPrototype({iterate}); -const - $$ = symbolGenerator(); - -@component({ - inheritMods: false, - defaultProps: false -}) +const $$ = symbolGenerator(); +@component({inheritMods: false}) export default class bDynamicPage extends iDynamicPage { - @prop({forceDefault: true}) - override readonly selfDispatching: boolean = true; - /** * The initial name of the page to load */ @@ -103,23 +95,23 @@ export default class bDynamicPage extends iDynamicPage { page?: string; /** - * Active page unique key. - * It is used to determine whether to reuse current page component or create a new one when switching between routes - * with the same page component. + * The active page unique key. + * It is used to determine whether to reuse the current page component + * or create a new one when switching between routes with the same page component. */ @system() pageKey?: CanUndef; /** - * A function that takes a route object and returns the name of the page component to load. - * Also, this function can return a tuple consisting of component name and unique key for the passed route. The key - * will be used to determine whether to reuse current page component or create a new one - * when switching between routes with the same page component. + * A function that takes a route object and returns the name of the page component to be loaded. + * Additionally, this function can return a tuple consisting of the component name and a unique key + * for the given route. + * The key will be used to determine whether to reuse the current page component + * or create a new one when switching between routes that use the same page component. */ @prop({ type: Function, - default: (route: bDynamicPage['route']) => route != null ? (route.meta.component ?? route.name) : undefined, - forceDefault: true + default: (route: bDynamicPage['route']) => route != null ? (route.meta.component ?? route.name) : undefined }) readonly pageGetter!: PageGetter; @@ -198,12 +190,7 @@ export default class bDynamicPage extends iDynamicPage { /** * The page switching event name */ - @prop({ - type: String, - required: false, - forceDefault: true - }) - + @prop({type: String, required: false}) readonly event?: string = 'setRoute'; /** @@ -326,8 +313,39 @@ export default class bDynamicPage extends iDynamicPage { return component.reload(params); } - override canSelfDispatchEvent(event: string): boolean { - return !/^hook(?::\w+(-\w+)*|-change)$/.test(event.dasherize()); + /** + * Returns a dictionary of props for the page being created. + * The component interprets most of its input props as parameters for the page being created. + */ + protected getPageProps(): Dictionary { + const + props = {'@hook:destroyed': this.createPageDestructor()}, + passedProps = this.getPassedProps?.(); + + if (passedProps != null) { + const rejectedProps = { + is: true, + dispatching: true, + componentIdProp: true, + getRoot: true, + getParent: true, + getPassedProps: true + }; + + Object.entries(passedProps).forEach(([propName, prop]) => { + if (rejectedProps.hasOwnProperty(propName) || this.meta.props.hasOwnProperty(propName)) { + return; + } + + if (propName.startsWith('on')) { + propName = `@${propName[2].toLowerCase()}${propName.slice(3)}`; + } + + props[propName] = prop; + }); + } + + return props; } /** @@ -424,8 +442,7 @@ export default class bDynamicPage extends iDynamicPage { }); } else { - const - pageComponentFromCache = pageElFromCache.component; + const pageComponentFromCache = pageElFromCache.component; if (pageComponentFromCache != null) { pageComponentFromCache.activate(); @@ -477,8 +494,7 @@ export default class bDynamicPage extends iDynamicPage { return loopbackStrategy; } - const - {exclude, include} = this; + const {exclude, include} = this; if (exclude != null) { if (Object.isFunction(exclude)) { @@ -491,11 +507,9 @@ export default class bDynamicPage extends iDynamicPage { } } - let - cacheKey = page; + let cacheKey = page; - const - globalCache = this.keepAliveCache.global!; + const globalCache = this.keepAliveCache.global!; const globalStrategy: KeepAliveStrategy = { isLoopback: false, @@ -507,8 +521,7 @@ export default class bDynamicPage extends iDynamicPage { if (include != null) { if (Object.isFunction(include)) { - const - res = include(page, route, this); + const res = include(page, route, this); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (res == null || res === false) { @@ -540,11 +553,6 @@ export default class bDynamicPage extends iDynamicPage { return globalStrategy; } - protected override initBaseAPI(): void { - super.initBaseAPI(); - this.addClearListenersToCache = this.instance.addClearListenersToCache.bind(this); - } - /** * Wraps the specified cache object and returns a wrapper. * The method adds listeners to destroy unused pages from the cache. @@ -552,11 +560,9 @@ export default class bDynamicPage extends iDynamicPage { * @param cache */ protected addClearListenersToCache>(cache: T): T { - const - wrappedCache = addEmitter>(cache); + const wrappedCache = addEmitter>(cache); - let - instanceCache: WeakMap = new WeakMap(); + let instanceCache: WeakMap = new WeakMap(); wrappedCache.subscribe('set', cache, changeCountInMap(0, 1)); wrappedCache.subscribe('remove', cache, changeCountInMap(1, -1)); @@ -668,6 +674,11 @@ export default class bDynamicPage extends iDynamicPage { } } + protected override initBaseAPI(): void { + super.initBaseAPI(); + this.addClearListenersToCache = this.instance.addClearListenersToCache.bind(this); + } + protected override initModEvents(): void { super.initModEvents(); diff --git a/src/components/base/b-dynamic-page/test/unit/main.ts b/src/components/base/b-dynamic-page/test/unit/main.ts index b51ca3d184..93234aa3fd 100644 --- a/src/components/base/b-dynamic-page/test/unit/main.ts +++ b/src/components/base/b-dynamic-page/test/unit/main.ts @@ -95,7 +95,7 @@ test.describe('', () => { await test.expect( target.evaluate((ctx) => { const {meta} = ctx.unsafe; - return 'component' in meta.accessors && !('component' in meta.computedFields); + return meta.accessors.component != null && meta.computedFields.component == null; }) ).toBeResolvedTo(true); }); diff --git a/src/components/base/b-list/b-list.ts b/src/components/base/b-list/b-list.ts index b7cacc090b..910c3463a3 100644 --- a/src/components/base/b-list/b-list.ts +++ b/src/components/base/b-list/b-list.ts @@ -12,7 +12,7 @@ */ import SyncPromise from 'core/promise/sync'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import DOM, { delegateElement } from 'components/friends/dom'; import Block, { element, elements, setElementMod } from 'components/friends/block'; @@ -302,8 +302,7 @@ class bList extends iListProps implements iVisible, iWidth, iActiveItems { protected override initBaseAPI(): void { super.initBaseAPI(); - const - i = this.instance; + const i = this.instance; this.isActive = i.isActive.bind(this); this.setActive = i.setActive.bind(this); diff --git a/src/components/base/b-prevent-ssr/b-prevent-ssr.ts b/src/components/base/b-prevent-ssr/b-prevent-ssr.ts index 2d0abfe9e3..9977fda50b 100644 --- a/src/components/base/b-prevent-ssr/b-prevent-ssr.ts +++ b/src/components/base/b-prevent-ssr/b-prevent-ssr.ts @@ -11,12 +11,11 @@ * @packageDocumentation */ -import iBlock, { component, prop } from 'components/super/i-block/i-block'; +import iBlock, { component } from 'components/super/i-block/i-block'; export * from 'components/super/i-block/i-block'; @component() export default class bPreventSSR extends iBlock { - @prop({forceDefault: true}) override readonly ssrRenderingProp: boolean = false; } diff --git a/src/components/base/b-remote-provider/b-remote-provider.ts b/src/components/base/b-remote-provider/b-remote-provider.ts index 60fcabb97b..c7f1842f3b 100644 --- a/src/components/base/b-remote-provider/b-remote-provider.ts +++ b/src/components/base/b-remote-provider/b-remote-provider.ts @@ -16,8 +16,7 @@ import iData, { component, prop, RequestError, RetryRequestFn } from 'components export * from 'components/super/i-data/i-data'; -const - $$ = symbolGenerator(); +const $$ = symbolGenerator(); @component() export default class bRemoteProvider extends iData { @@ -50,23 +49,20 @@ export default class bRemoteProvider extends iData { * @emits `change(db: CanUndef)` */ protected syncDBWatcher(value: CanUndef): void { - const - parent = this.$parent; + const parent = this.$parent; if (parent == null) { return; } - const - fieldToUpdate = this.fieldProp; + const fieldToUpdate = this.fieldProp; let needUpdate = fieldToUpdate == null, action: Function; if (fieldToUpdate != null) { - const - field = parent.field.get(fieldToUpdate); + const field = parent.field.get(fieldToUpdate); if (Object.isFunction(field)) { action = () => field.call(parent, value); @@ -100,8 +96,7 @@ export default class bRemoteProvider extends iData { * @emits `error(err:Error |` [[RequestError]]`, retry:` [[RetryRequestFn]]`)` */ protected override onRequestError(err: Error | RequestError, retry: RetryRequestFn): void { - const - a = this.$attrs; + const a = this.$attrs; if (a.onError == null && a.onOnError == null) { super.onRequestError(err, retry); @@ -117,8 +112,7 @@ export default class bRemoteProvider extends iData { * @emits `addData(data: unknown)` */ protected override onAddData(data: unknown): void { - const - a = this.$attrs; + const a = this.$attrs; if (a.onAddData == null && a.onOnAddData == null) { return super.onAddData(data); @@ -134,8 +128,7 @@ export default class bRemoteProvider extends iData { * @emits `updateData(data: unknown)` */ protected override onUpdateData(data: unknown): void { - const - a = this.$attrs; + const a = this.$attrs; if (a.onUpdateData == null && a.onOnUpdateData == null) { return super.onUpdateData(data); @@ -151,8 +144,7 @@ export default class bRemoteProvider extends iData { * @emits `deleteData(data: unknown)` */ protected override onDeleteData(data: unknown): void { - const - a = this.$attrs; + const a = this.$attrs; if (a.onDeleteData == null && a.onOnDeleteData == null) { return super.onDeleteData(data); diff --git a/src/components/base/b-router/b-router.ts b/src/components/base/b-router/b-router.ts index 570f4c21bf..a77203c3f0 100644 --- a/src/components/base/b-router/b-router.ts +++ b/src/components/base/b-router/b-router.ts @@ -424,8 +424,7 @@ export default class bRouter extends iRouterProps { protected override initBaseAPI(): void { super.initBaseAPI(); - const - i = this.instance; + const i = this.instance; this.compileStaticRoutes = i.compileStaticRoutes.bind(this); this.emitTransition = i.emitTransition.bind(this); diff --git a/src/components/base/b-sidebar/b-sidebar.ts b/src/components/base/b-sidebar/b-sidebar.ts index 7ef848de07..e63f5df369 100644 --- a/src/components/base/b-sidebar/b-sidebar.ts +++ b/src/components/base/b-sidebar/b-sidebar.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import Block, { getElementSelector } from 'components/friends/block'; diff --git a/src/components/base/b-slider/b-slider.ts b/src/components/base/b-slider/b-slider.ts index ddf7d8cf45..86509a00f0 100644 --- a/src/components/base/b-slider/b-slider.ts +++ b/src/components/base/b-slider/b-slider.ts @@ -13,7 +13,7 @@ import symbolGenerator from 'core/symbol'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import iObserveDOM from 'components/traits/i-observe-dom/i-observe-dom'; import iItems, { IterationKey } from 'components/traits/i-items/i-items'; diff --git a/src/components/base/b-tree/b-tree.ts b/src/components/base/b-tree/b-tree.ts index 806ae48c84..f051ae2775 100644 --- a/src/components/base/b-tree/b-tree.ts +++ b/src/components/base/b-tree/b-tree.ts @@ -14,7 +14,7 @@ import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import AsyncRender, { iterate, TaskOptions } from 'components/friends/async-render'; import Block, { getElementMod, setElementMod, getElementSelector, getFullElementName } from 'components/friends/block'; @@ -136,7 +136,7 @@ class bTree extends iTreeProps implements iActiveItems, iFoldable { return normalizeItems.call(o, val); })) - protected itemsStore: this['Items'] = []; + protected itemsStore!: this['Items']; /** @inheritDoc */ declare protected readonly $refs: iData['$refs'] & { @@ -185,11 +185,10 @@ class bTree extends iTreeProps implements iActiveItems, iFoldable { renderFilter }; - const - a = this.$attrs; + const a = this.$attrs; if (a.onFold != null) { - opts['@fold'] = a.onFold; + opts['@fold'] = this.$attrs.onFold; } return opts; diff --git a/src/components/base/b-tree/test/unit/active-items.ts b/src/components/base/b-tree/test/unit/active-items.ts index 284be3aa4f..121a191021 100644 --- a/src/components/base/b-tree/test/unit/active-items.ts +++ b/src/components/base/b-tree/test/unit/active-items.ts @@ -352,7 +352,6 @@ test.describe(' active items API', () => { await expectActive(true, activeNodes); await expectActive(false, inactiveNodes); - }); test('should unset the previous active items with `unsetPrevious = true`', async ({page}) => { diff --git a/src/components/base/b-virtual-scroll-new/README.md b/src/components/base/b-virtual-scroll-new/README.md index cd6210d7ab..5e3860a0ad 100644 --- a/src/components/base/b-virtual-scroll-new/README.md +++ b/src/components/base/b-virtual-scroll-new/README.md @@ -591,7 +591,7 @@ Let's also look at another common scenario: . ``` - 2. Implement a global `itemsProcessor` that will add advertisements based on the meta-information. + 2. Implement a global `itemsProcessor` that will add advertisements based on the metainformation. ```typescript import { itemsProcessors } from '@v4fire/client/components/base/b-virtual-scroll-new/const.ts' diff --git a/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts b/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts index cd3dca7998..6a3585fbfe 100644 --- a/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts +++ b/src/components/base/b-virtual-scroll-new/b-virtual-scroll-new.ts @@ -12,7 +12,7 @@ */ import symbolGenerator from 'core/symbol'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import type { AsyncOptions } from 'core/async'; import SyncPromise from 'core/promise/sync'; diff --git a/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts index 15cb727061..1be4a198d9 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/emitter/payload.ts @@ -41,14 +41,14 @@ test.describe('', () => { .withProps({ chunkSize, shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit') }) .build(); await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ @@ -86,7 +86,7 @@ test.describe('', () => { .withProps({ chunkSize, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit') }) .build(); @@ -95,7 +95,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ @@ -139,7 +139,7 @@ test.describe('', () => { .withProps({ chunkSize, shouldStopRequestingData: ({lastLoadedData}) => lastLoadedData.length === 0, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit') }) .build(); @@ -147,7 +147,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ @@ -181,7 +181,7 @@ test.describe('', () => { .withProps({ chunkSize, shouldStopRequestingData: () => true, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit') }) .build(); @@ -195,7 +195,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), calls = filterEmitterCalls(await spy.calls); test.expect(calls).toEqual([ diff --git a/src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts index 42e6ae3fd0..33e2fa3103 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/props/props.ts @@ -287,9 +287,9 @@ test.describe('', () => { chunkSize, request: {get: {test: 1}}, '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; + const original = ctx.strictEmit; - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -305,7 +305,7 @@ test.describe('', () => { const virtualScrolLState = await component.getVirtualScrollState(), - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), loadSuccessCalls = (await spy.results).filter(({value: [event]}) => event === 'dataLoadSuccess'); test.expect(loadSuccessCalls).toHaveLength(1); diff --git a/src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts b/src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts index 788b33ccae..cb0b1d486a 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/functional/state/emitter.ts @@ -79,9 +79,9 @@ test.describe('', () => { chunkSize, shouldStopRequestingData: () => true, '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; + const original = ctx.strictEmit; - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -92,7 +92,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); test.expect(results).toEqual([ @@ -163,9 +163,9 @@ test.describe('', () => { .withProps({ chunkSize, '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; + const original = ctx.strictEmit; - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -184,7 +184,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); test.expect(results).toEqual([ @@ -295,9 +295,9 @@ test.describe('', () => { Object.get(state, 'lastLoadedRawData.total') === state.data.length, '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; + const original = ctx.strictEmit; - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -312,7 +312,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); test.expect(results).toEqual([ @@ -382,9 +382,9 @@ test.describe('', () => { items: state.data.getDataChunk(0), '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; + const original = ctx.strictEmit; - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -399,7 +399,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); test.expect(results).toEqual([ @@ -452,9 +452,9 @@ test.describe('', () => { state.reset(); await component.evaluate((ctx) => { - const original = Object.cast(ctx.emit); + const original = Object.cast(ctx.strictEmit); - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -503,7 +503,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); test.expect(results).toEqual([ diff --git a/src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts b/src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts index 0267962350..32911485fc 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/scenario/last-render.ts @@ -101,9 +101,9 @@ test.describe('', () => { shouldStopRequestingData: (state: VirtualScrollState): boolean => state.lastLoadedData.length === 0, '@hook:beforeDataCreate': (ctx) => { - const original = ctx.emit; + const original = ctx.strictEmit; - ctx.emit = jestMock.mock((...args) => { + ctx.strictEmit = jestMock.mock((...args) => { original(...args); return [args[0], Object.fastClone(ctx.getVirtualScrollState())]; }); @@ -116,7 +116,7 @@ test.describe('', () => { await component.waitForLifecycleDone(); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), results = filterEmitterResults(await spy.results, true, ['initLoadStart', 'initLoad']); test.expect(results).toEqual([ diff --git a/src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts b/src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts index 89ee723ba7..574393d586 100644 --- a/src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts +++ b/src/components/base/b-virtual-scroll-new/test/unit/scenario/reload.ts @@ -42,7 +42,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize: chunkSize[0]}) .withProps({ chunkSize: chunkSize[0], - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit') }) .build({useDummy: true}); @@ -62,7 +62,7 @@ test.describe('', () => { await component.waitForDataIndexChild(chunkSize[1] - 1); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), calls = filterEmitterCalls(await spy.calls, true, ['initLoadStart', 'initLoad']).map(([event]) => event); test.expect(calls).toEqual([ @@ -110,7 +110,7 @@ test.describe('', () => { .withDefaultPaginationProviderProps({chunkSize}) .withProps({ chunkSize, - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit') + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit') }) .build(); @@ -119,7 +119,7 @@ test.describe('', () => { await component.waitForDataIndexChild(chunkSize * 2 - 1); const - spy = await component.getSpy((ctx) => ctx.emit), + spy = await component.getSpy((ctx) => ctx.strictEmit), calls = filterEmitterCalls(await spy.calls, true, ['initLoadStart', 'initLoad']).map(([event]) => event); test.expect(calls).toEqual([ diff --git a/src/components/base/b-window/b-window.ts b/src/components/base/b-window/b-window.ts index 6c156f15e7..b9088fae54 100644 --- a/src/components/base/b-window/b-window.ts +++ b/src/components/base/b-window/b-window.ts @@ -12,7 +12,7 @@ */ import symbolGenerator from 'core/symbol'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import Block, { getElementSelector } from 'components/friends/block'; diff --git a/src/components/directives/bind-with/README.md b/src/components/directives/bind-with/README.md index 0f2ec16f50..a17d86c000 100644 --- a/src/components/directives/bind-with/README.md +++ b/src/components/directives/bind-with/README.md @@ -17,7 +17,7 @@ which receives a reference to the element the directive is applied to as its fir } . ``` -## Why is This Directive Needed? +## Why is This Directive Necessary? When using regular components, we don't have to think about how the data used in the template is tied to the component's properties. diff --git a/src/components/directives/image/README.md b/src/components/directives/image/README.md index 07b5f6ea29..0be947654c 100644 --- a/src/components/directives/image/README.md +++ b/src/components/directives/image/README.md @@ -11,7 +11,7 @@ The directive cannot be applied to `img`, `picture`, or `object` tags. } . ``` -## Why is This Directive Needed? +## Why is This Directive Necessary? When working with images, it is common to display a placeholder or an error message during the loading process. However, there is no native API to implement this feature. diff --git a/src/components/form/b-button/b-button.ts b/src/components/form/b-button/b-button.ts index 3773f437af..576a01b475 100644 --- a/src/components/form/b-button/b-button.ts +++ b/src/components/form/b-button/b-button.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import DataProvider, { getDefaultRequestParams, base, get } from 'components/friends/data-provider'; import type bForm from 'components/form/b-form/b-form'; @@ -65,8 +65,7 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi */ @computed({dependencies: ['type', 'form', 'href', 'hasDropdown']}) get attrs(): Dictionary { - const - attrs = {...this.attrsProp}; + const attrs = {...this.attrsProp}; if (this.type === 'link') { attrs.href = this.href; @@ -87,8 +86,7 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi /** {@link iAccess.prototype.isFocused} */ @computed({dependencies: ['mods.focused']}) get isFocused(): boolean { - const - {button} = this.$refs; + const {button} = this.$refs; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (button != null) { @@ -141,8 +139,7 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi */ @wait('ready') reset(): CanPromise { - const - {file} = this.$refs; + const {file} = this.$refs; if (file != null) { file.value = ''; @@ -155,16 +152,28 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi iOpenToggle.initCloseHelpers(this, events); } + protected override syncDataProviderWatcher(initLoad: boolean = true): void { + if ( + this.href != null || + this.request != null || + this.dataProviderProp !== 'Provider' || + this.dataProviderOptions != null + ) { + super.syncDataProviderWatcher(initLoad); + } + } + protected override initModEvents(): void { - const - {localEmitter: $e} = this; + const {localEmitter: $e} = this; super.initModEvents(); iProgress.initModEvents(this); iProgress.initDisableBehavior(this); + iAccess.initModEvents(this); iOpenToggle.initModEvents(this); + iVisible.initModEvents(this); $e.on('block.mod.*.opened.*', (e: ModEvent) => this.waitComponentStatus('ready', () => { @@ -173,10 +182,7 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi })); $e.on('block.mod.*.disabled.*', (e: ModEvent) => this.waitComponentStatus('ready', () => { - const { - button, - file - } = this.$refs; + const {button, file} = this.$refs; const disabled = e.value !== 'false' && e.type !== 'remove'; button.disabled = disabled; @@ -187,8 +193,7 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi })); $e.on('block.mod.*.focused.*', (e: ModEvent) => this.waitComponentStatus('ready', () => { - const - {button} = this.$refs; + const {button} = this.$refs; if (e.value !== 'false' && e.type !== 'remove') { button.focus(); @@ -217,8 +222,7 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi default: { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.dataProviderProp != null && (this.dataProviderProp !== 'Provider' || this.href != null)) { - let - {dataProvider} = this; + let {dataProvider} = this; if (dataProvider == null) { throw new ReferenceError('Missing data provider to send data'); @@ -237,8 +241,9 @@ class bButton extends iButtonProps implements iOpenToggle, iVisible, iWidth, iSi // Form attribute fix for MS Edge && IE } else if (this.form != null && this.type === 'submit') { e.preventDefault(); + const form = this.dom.getComponent(`#${this.form}`); - form && await form.submit(); + await form?.submit(); } await this.toggle(); diff --git a/src/components/form/b-checkbox/b-checkbox.ts b/src/components/form/b-checkbox/b-checkbox.ts index 3759106403..7220f58c16 100644 --- a/src/components/form/b-checkbox/b-checkbox.ts +++ b/src/components/form/b-checkbox/b-checkbox.ts @@ -241,8 +241,7 @@ export default class bCheckbox extends iInput implements iSize { protected override initBaseAPI(): void { super.initBaseAPI(); - const - i = this.instance; + const i = this.instance; this.convertValueToChecked = i.convertValueToChecked.bind(this); this.onCheckedChange = i.onCheckedChange.bind(this); @@ -286,8 +285,7 @@ export default class bCheckbox extends iInput implements iSize { } protected override resolveValue(value?: this['Value']): this['Value'] { - const - i = this.instance; + const i = this.instance; const canApplyDefault = value === undefined && diff --git a/src/components/form/b-select/b-select.ts b/src/components/form/b-select/b-select.ts index 85399a1f8b..b7095d3e34 100644 --- a/src/components/form/b-select/b-select.ts +++ b/src/components/form/b-select/b-select.ts @@ -13,7 +13,7 @@ import SyncPromise from 'core/promise/sync'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import Block, { setElementMod, removeElementMod, getElementSelector, element, elements } from 'components/friends/block'; import DOM, { delegateElement } from 'components/friends/dom'; @@ -441,11 +441,7 @@ class bSelect extends iSelectProps implements iOpenToggle, iActiveItems { protected override initBaseAPI(): void { super.initBaseAPI(); - - const - i = this.instance; - - this.normalizeItems = i.normalizeItems.bind(this); + this.normalizeItems = this.instance.normalizeItems.bind(this); } /** {@link iOpenToggle.initCloseHelpers} */ diff --git a/src/components/friends/block/README.md b/src/components/friends/block/README.md index 44266fedb4..55f0470427 100644 --- a/src/components/friends/block/README.md +++ b/src/components/friends/block/README.md @@ -233,7 +233,7 @@ To disable modifier inheritance, pass the `inheridMods: false` option when creat ```typescript import iBlock, { component } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock {} ``` @@ -246,7 +246,7 @@ Please note that the key must be in a dash style, i.e., normalized. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -266,7 +266,7 @@ This property can be observed using the watch API. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -310,7 +310,7 @@ To set a new modifier value or remove an old one, you must use the `setMod` and ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -343,7 +343,7 @@ inside and outside the component. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -380,7 +380,7 @@ this can be more convenient than handling each event individually. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -427,7 +427,7 @@ __b-example.ts__ ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { mounted() { const diff --git a/src/components/friends/block/block.ts b/src/components/friends/block/block.ts index 8d66531e5f..f95773d350 100644 --- a/src/components/friends/block/block.ts +++ b/src/components/friends/block/block.ts @@ -171,8 +171,8 @@ export function setMod(this: Block, name: string, value: unknown, reason: ModEve this.localEmitter.emit(`block.mod.set.${name}.${normalizedValue}`, event); if (!isInit) { - ctx.emit(`mod:set:${name}`, event); - ctx.emit(`mod:set:${name}:${normalizedValue}`, event); + ctx.strictEmit(`mod:set:${name}`, event); + ctx.strictEmit(`mod:set:${name}:${normalizedValue}`, event); } return true; @@ -251,7 +251,7 @@ export function removeMod(this: Block, name: string, value?: unknown, reason: Mo this.localEmitter.emit(`block.mod.remove.${name}.${currentValue}`, event); if (needNotify) { - ctx.emit(`mod:remove:${name}`, event); + ctx.strictEmit(`mod:remove:${name}`, event); } } diff --git a/src/components/friends/daemons/class.ts b/src/components/friends/daemons/class.ts index 456db1a296..d3371c5989 100644 --- a/src/components/friends/daemons/class.ts +++ b/src/components/friends/daemons/class.ts @@ -25,7 +25,7 @@ class Daemons extends Friend { * A dictionary with the declared component daemons */ protected get daemons(): WrappedDaemonsDict { - return Object.cast((this.ctx.instance.constructor).daemons); + return Object.cast((this.ctx.constructor).daemons); } init(): void { diff --git a/src/components/friends/field/delete.ts b/src/components/friends/field/delete.ts index 5eff50ffe1..008cd8380c 100644 --- a/src/components/friends/field/delete.ts +++ b/src/components/friends/field/delete.ts @@ -164,26 +164,26 @@ export function deleteField( prop = keyGetter ? keyGetter(chunks[0], ref) : chunks[0]; if (chunks.length > 1) { - chunks.some((chunk, i) => { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + prop = keyGetter ? keyGetter(chunk, ref) : chunk; if (i + 1 === chunks.length) { - return true; + break; } const newRef = Object.isMap(ref) ? ref.get(prop) : ref[prop]; if (newRef == null || typeof newRef !== 'object') { needDelete = false; - return true; + break; } ref = newRef; - return false; - }); + } } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (needDelete) { if (needDeleteToWatch) { ctx.$delete(ref, prop); diff --git a/src/components/friends/field/get.ts b/src/components/friends/field/get.ts index aabde4de90..c93158c02a 100644 --- a/src/components/friends/field/get.ts +++ b/src/components/friends/field/get.ts @@ -149,9 +149,11 @@ export function getField( } } else { - const hasNoProperty = chunks.some((chunk) => { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + if (res == null) { - return true; + return undefined; } if (Object.isPromiseLike(res) && !(chunk in res)) { @@ -186,12 +188,6 @@ export function getField( res = typeof obj !== 'object' || chunk in obj ? obj[chunk] : undefined; } } - - return false; - }); - - if (hasNoProperty) { - return undefined; } } diff --git a/src/components/friends/field/set.ts b/src/components/friends/field/set.ts index 7ed6fe5770..df8c05dbb3 100644 --- a/src/components/friends/field/set.ts +++ b/src/components/friends/field/set.ts @@ -165,11 +165,13 @@ export function setField( let prop = keyGetter ? keyGetter(chunks[0], ref) : chunks[0]; if (chunks.length > 1) { - chunks.some((chunk, i) => { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + prop = keyGetter ? keyGetter(chunk, ref) : chunk; if (i + 1 === chunks.length) { - return true; + break; } type AnyMap = Map; @@ -198,9 +200,7 @@ export function setField( } else { ref = ref[prop]; } - - return false; - }); + } } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/src/components/friends/provide/classes.ts b/src/components/friends/provide/classes.ts index 360527803d..117d25ced3 100644 --- a/src/components/friends/provide/classes.ts +++ b/src/components/friends/provide/classes.ts @@ -84,9 +84,15 @@ export function classes( classes ??= {}; - const map = {}; + const + classNames = Object.keys(classes), + classesMap = {}; + + for (let i = 0; i < classNames.length; i++) { + const innerEl = classNames[i]; + + let outerEl = classes[innerEl]; - Object.entries(classes).forEach(([innerEl, outerEl]) => { if (outerEl === true) { outerEl = innerEl; @@ -98,18 +104,12 @@ export function classes( outerEl[i] = innerEl; } } - - outerEl.forEach((el, i) => { - if (el === true) { - outerEl![i] = innerEl; - } - }); } - map[innerEl.dasherize()] = fullElementName.apply(this, Array.toArray(componentName, outerEl)); - }); + classesMap[innerEl.dasherize()] = fullElementName.apply(this, Array.toArray(componentName, outerEl)); + } - return map; + return classesMap; } /** @@ -174,13 +174,19 @@ export function componentClasses( mods ??= {}; - const classes = [(fullComponentName).call(this, componentName)]; + const + modNames = Object.keys(mods), + classes = [(fullComponentName).call(this, componentName)]; + + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modVal = mods[modName]; - Object.entries(mods).forEach(([key, val]) => { - if (val !== undefined) { - classes.push(fullComponentName.call(this, componentName, key, val)); + if (modVal !== undefined) { + classes.push(fullComponentName.call(this, componentName, modName, modVal)); } - }); + } return classes; } @@ -257,23 +263,35 @@ export function elementClasses( return []; } - const classes = componentId != null ? [componentId] : []; + const + elNames = Object.keys(els), + classes = componentId != null ? [componentId] : []; + + for (let i = 0; i < elNames.length; i++) { + const + elName = elNames[i], + elMods = els[elName]; - Object.entries(els).forEach(([el, mods]) => { classes.push( - (fullElementName).call(this, componentName, el) + (fullElementName).call(this, componentName, elName) ); - if (!Object.isDictionary(mods)) { - return; + if (!Object.isDictionary(elMods)) { + continue; } - Object.entries(mods).forEach(([key, val]) => { - if (val !== undefined) { - classes.push(fullElementName.call(this, componentName, el, key, val)); + const modNames = Object.keys(elMods); + + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modVal = elMods[modName]; + + if (modVal !== undefined) { + classes.push(fullElementName.call(this, componentName, elName, modName, modVal)); } - }); - }); + } + } return classes; } diff --git a/src/components/friends/provide/mods.ts b/src/components/friends/provide/mods.ts index 362856c6c7..2e280f264b 100644 --- a/src/components/friends/provide/mods.ts +++ b/src/components/friends/provide/mods.ts @@ -6,11 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/** - * [[include:components/super/i-block/modules/provide/README.md]] - * @packageDocumentation - */ - import type Friend from 'components/friends/friend'; import type iBlock from 'components/super/i-block/i-block'; @@ -38,18 +33,20 @@ import type { Mods } from 'components/friends/provide/interface'; * ``` */ export function mods(this: Friend, mods?: Mods): CanNull { - const {sharedMods} = this.ctx; - - if (sharedMods == null && mods == null) { + if (mods == null) { return null; } - const resolvedMods = {...sharedMods}; + const + resolvedMods = {}, + modNames = Object.keys(mods); + + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modVal = mods[modName]; - if (mods != null) { - Object.entries(mods).forEach(([key, val]) => { - resolvedMods[key.dasherize()] = val != null ? String(val) : undefined; - }); + resolvedMods[modName.dasherize()] = modVal != null ? String(modVal) : undefined; } return resolvedMods; diff --git a/src/components/friends/provide/names.ts b/src/components/friends/provide/names.ts index f18c59c831..fab959e836 100644 --- a/src/components/friends/provide/names.ts +++ b/src/components/friends/provide/names.ts @@ -202,8 +202,7 @@ export function fullElementName( modNameOrModValue?: string | unknown, modValue?: unknown ): string { - const - l = arguments.length; + const l = arguments.length; let componentName: string, diff --git a/src/components/friends/provide/test/unit/main.ts b/src/components/friends/provide/test/unit/main.ts index 1becb0835d..a80d6541f9 100644 --- a/src/components/friends/provide/test/unit/main.ts +++ b/src/components/friends/provide/test/unit/main.ts @@ -89,14 +89,9 @@ test.describe('friends/provide', () => { }); test.describe('`mods`', () => { - test('should return a dictionary of active modifiers and their values', async () => { - await test.expect(target.evaluate(({provide}) => provide.mods())) - .resolves.toEqual({foo: 'bar'}); - }); - - test('should return a dictionary of active and provided modifiers and their values', async () => { + test('should return a dictionary of provided modifiers and their values', async () => { await test.expect(target.evaluate(({provide}) => provide.mods({baz: 'bla'}))) - .resolves.toEqual({foo: 'bar', baz: 'bla'}); + .resolves.toEqual({baz: 'bla'}); }); }); diff --git a/src/components/friends/state/class.ts b/src/components/friends/state/class.ts index 33af4723e8..e7e47e7442 100644 --- a/src/components/friends/state/class.ts +++ b/src/components/friends/state/class.ts @@ -47,12 +47,7 @@ class State extends Friend { get needRouterSync(): boolean { // @ts-ignore (access) baseSyncRouterState ??= iBlock.prototype.syncRouterState; - return baseSyncRouterState !== Object.cast(this.instance).syncRouterState; - } - - /** {@link iBlock.instance} */ - protected get instance(): this['CTX']['instance'] { - return this.ctx.instance; + return baseSyncRouterState !== this.ctx.constructor.prototype.syncRouterState; } } diff --git a/src/components/friends/state/router.ts b/src/components/friends/state/router.ts index e1ebc8a28f..df3a69f96e 100644 --- a/src/components/friends/state/router.ts +++ b/src/components/friends/state/router.ts @@ -25,10 +25,7 @@ export function initFromRouter(this: State): boolean { return false; } - const { - ctx, - async: $a - } = this; + const {ctx, async: $a} = this; const routerWatchers = {group: 'routerWatchers'}; $a.clearAll(routerWatchers); @@ -37,11 +34,9 @@ export function initFromRouter(this: State): boolean { return true; async function loadFromRouter() { - const - {r} = ctx; + const {r} = ctx; - let - {router} = r; + let {router} = r; if (router == null) { await ($a.promisifyOnce(r, 'initRouter', { @@ -97,8 +92,7 @@ export function initFromRouter(this: State): boolean { if (Object.isDictionary(stateFields)) { Object.keys(stateFields).forEach((key) => { - const - p = key.split('.'); + const p = key.split('.'); if (p[0] === 'mods') { $a.on(ctx.localEmitter, `block.mod.*.${p[1]}.*`, sync, routerWatchers); diff --git a/src/components/friends/state/storage.ts b/src/components/friends/state/storage.ts index 624f58ab3b..d769bd4e33 100644 --- a/src/components/friends/state/storage.ts +++ b/src/components/friends/state/storage.ts @@ -25,17 +25,13 @@ export function initFromStorage(this: Friend): CanPromise { return false; } - const - key = $$.pendingLocalStore; + const key = $$.pendingLocalStore; if (this[key] != null) { return this[key]; } - const { - ctx, - async: $a - } = this; + const {ctx, async: $a} = this; const storeWatchers = {group: 'storeWatchers'}; $a.clearAll(storeWatchers); diff --git a/src/components/friends/sync/class.ts b/src/components/friends/sync/class.ts index ec7e15d9fd..16272b55c8 100644 --- a/src/components/friends/sync/class.ts +++ b/src/components/friends/sync/class.ts @@ -61,17 +61,17 @@ interface Sync { @fakeMethods('object') class Sync extends Friend { /** - * Cache of functions to synchronize modifiers + * A cache of functions for synchronizing modifiers */ readonly syncModCache!: Dictionary; /** - * Cache for links + * A cache for storing links */ protected readonly linksCache!: Dictionary; /** - * The index of the last added link + * An index of the last added link */ protected lastSyncIndex: number = 0; diff --git a/src/components/friends/sync/mod.ts b/src/components/friends/sync/mod.ts index 7b6a17bee3..eee035d46c 100644 --- a/src/components/friends/sync/mod.ts +++ b/src/components/friends/sync/mod.ts @@ -128,19 +128,19 @@ export function mod( const {path} = info; - let val: unknown; + let rawVal: unknown; if (path.includes('.')) { - val = that.field.get(info.originalPath); + rawVal = that.field.get(info.originalPath); } else { - val = info.type === 'field' ? that.field.getFieldsStore(info.ctx)[path] : info.ctx[path]; + rawVal = info.type === 'field' ? that.field.getFieldsStore(info.ctx)[path] : info.ctx[path]; } - val = (converter).call(that.component, val); + const modVal = (converter).call(that.component, rawVal); - if (val !== undefined) { - ctx.mods[modName] = String(val); + if (modVal !== undefined) { + ctx.mods[modName] = String(modVal); } } diff --git a/src/components/super/i-block/CHANGELOG.md b/src/components/super/i-block/CHANGELOG.md index 862f8e0288..6526f3820e 100644 --- a/src/components/super/i-block/CHANGELOG.md +++ b/src/components/super/i-block/CHANGELOG.md @@ -9,6 +9,14 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :boom: Breaking Change + +* The API for `InferComponentEvents` has been changed so that you no longer need to pass this as the first argument +* Component events without an `error` or `warning` status are logged only if the `verbose` prop is set +* The `strictEmit` method no longer performs normalization of the event name + ## v4.0.0-beta.139.dsl-speedup-2 (2024-10-03) #### :house: Internal diff --git a/src/components/super/i-block/base/index.ts b/src/components/super/i-block/base/index.ts index 80d9b297be..2803c538a7 100644 --- a/src/components/super/i-block/base/index.ts +++ b/src/components/super/i-block/base/index.ts @@ -28,20 +28,17 @@ import { } from 'core/async'; -import config from 'config'; - import { component, - getComponentName, getPropertyInfo, canSkipWatching, bindRemoteWatchers, isCustomWatcher, - RawWatchHandler, WatchPath, + RawWatchHandler, SetupContext @@ -128,24 +125,20 @@ export default abstract class iBlockBase extends iBlockFriends { }); } - if (!o.isFunctional) { - o.watch('activatedProp', (val: CanUndef) => { - val = val !== false; + return o.sync.link('activatedProp', (isActivated: CanUndef) => { + isActivated = isActivated !== false; - if (o.hook !== 'beforeDataCreate') { - if (val) { - o.activate(); + if (o.hook !== 'beforeDataCreate') { + if (isActivated) { + o.activate(); - } else { - o.deactivate(); - } + } else { + o.deactivate(); } + } - o.isActivated = val; - }); - } - - return o.activatedProp; + return isActivated; + }); }) isActivated!: boolean; @@ -243,19 +236,19 @@ export default abstract class iBlockBase extends iBlockFriends { */ @computed({cache: 'forever'}) protected get componentI18nKeysets(): string[] { - const {constructor} = this.meta; + const {meta} = this; - let keysets: CanUndef = i18nKeysets.get(constructor); + let keysets: CanUndef = i18nKeysets.get(meta.constructor); if (keysets == null) { - keysets = []; - i18nKeysets.set(constructor, keysets); + keysets = [meta.componentName]; + i18nKeysets.set(meta.constructor, keysets); - let keyset: CanUndef = getComponentName(constructor); + let {parentMeta} = meta; - while (keyset != null) { - keysets.push(keyset); - keyset = config.components[keyset]?.parent; + while (parentMeta != null) { + keysets.push(parentMeta.componentName); + parentMeta = parentMeta.parentMeta; } } @@ -530,17 +523,12 @@ export default abstract class iBlockBase extends iBlockFriends { wrappedHandler['originalLength'] = handler['originalLength'] ?? handler.length; handler = wrappedHandler; - $a.worker(() => { - if (link != null) { - $a.off(link); - } - }, opts); - + $a.worker(() => link != null && $a.off(link), opts); return () => unwatch?.(); }; link = $a.on(emitter, 'mutation', handler, wrapWithSuspending(opts, 'watchers')); - unwatch = that.$watch(Object.cast(path), opts, handler); + unwatch = that.$watch(info ?? Object.cast(path), opts, handler); } } diff --git a/src/components/super/i-block/decorators/README.md b/src/components/super/i-block/decorators/README.md index e4444da277..8fe706493a 100644 --- a/src/components/super/i-block/decorators/README.md +++ b/src/components/super/i-block/decorators/README.md @@ -8,7 +8,7 @@ This module re-exports the base decorators from `core/component/decorators` and * `@prop` to declare a component input property (aka "prop"); * `@field` to declare a component field; * `@system` to declare a component system field (system field mutations never cause components to re-render); -* `@computed` to attach meta-information to a component computed field or accessor; +* `@computed` to attach metainformation to a component computed field or accessor; * `@hook` to attach a hook listener; * `@watch` to attach a watcher. diff --git a/src/components/super/i-block/event/README.md b/src/components/super/i-block/event/README.md index aa53a363b5..bf033aefca 100644 --- a/src/components/super/i-block/event/README.md +++ b/src/components/super/i-block/event/README.md @@ -252,7 +252,7 @@ import iBlock, { component, InferComponentEvents } from 'components/super/i-bloc @component() export default class bExample extends iBlock { - declare readonly SelfEmitter: InferComponentEvents; @@ -279,7 +279,7 @@ import iBlock, { component, InferComponentEvents } from 'components/super/i-bloc @component() export default class bExample extends iBlock { - declare readonly SelfEmitter: InferComponentEvents; diff --git a/src/components/super/i-block/event/index.ts b/src/components/super/i-block/event/index.ts index 1b57990cd7..a434b1dd4e 100644 --- a/src/components/super/i-block/event/index.ts +++ b/src/components/super/i-block/event/index.ts @@ -21,6 +21,8 @@ import type { AsyncOptions, EventEmitterWrapper, ReadonlyEventEmitterWrapper, Ev import { component, globalEmitter, ComponentEmitterOptions } from 'core/component'; +import type { SetModEvent, ModEvent } from 'components/friends/block'; + import { computed, hook, watch } from 'components/super/i-block/decorators'; import { initGlobalListeners } from 'components/super/i-block/modules/listeners'; @@ -42,8 +44,11 @@ export default abstract class iBlockEvent extends iBlockBase { * An associative type for typing events emitted by the component. * Events are described using tuples, where the first element is the event name, and the rest are arguments. */ - readonly SelfEmitter!: InferComponentEvents; /** @@ -103,7 +108,7 @@ export default abstract class iBlockEvent extends iBlockBase { }, emit: this.emit.bind(this), - strictEmit: this.emit.bind(this) + strictEmit: this.strictEmit.bind(this) }); return Object.cast(emitter); @@ -333,8 +338,47 @@ export default abstract class iBlockEvent extends iBlockBase { */ emit: typeof this['selfEmitter']['emit'] = function emit(this: iBlockEvent, event: string | ComponentEvent, ...args: unknown[]): void { + // @ts-ignore (cast) + this.strictEmit(normalizeEvent(event), ...args); + }; + + /** + * Emits a component event. + * + * All events fired by this method can be listened to "outside" using the `v-on` directive. + * Also, if the component is in `dispatching` mode, then this event will start bubbling up to the parent component. + * + * In addition, all emitted events are automatically logged using the `log` method. + * The default logging level is `info` (logging requires the `verbose` prop to be set to true), + * but you can set the logging level explicitly. + * + * Note that this method always fires three events: + * + * 1. `${event}`(self, ...args) - the first argument is passed as a link to the component that emitted the event. + * 2. `${event}:component`(self, ...args) - the event to avoid collisions between component events and + * native DOM events. + * + * 3. `on-${event}`(...args) + * + * @param event - the event name to dispatch + * @param args - the event arguments + * + * @example + * ```js + * this.on('someEvent', console.log); // [this, 42] + * this.on('onSomeEvent', console.log); // [42] + * + * this.emit('someEvent', 42); + * + * // Enable logging + * setEnv('log', {patterns: ['event:']}); + * this.emit({event: 'someEvent', logLevel: 'warn'}, 42); + * ``` + */ + strictEmit: typeof this['selfEmitter']['strictEmit'] = + function strictEmit(this: iBlockEvent, event: string | ComponentEvent, ...args: any[]): void { const - eventDecl = normalizeEvent(event), + eventDecl = normalizeStrictEvent(event), eventName = eventDecl.event; this.$emit(eventName, this, ...args); @@ -342,32 +386,22 @@ export default abstract class iBlockEvent extends iBlockBase { this.$emit(getWrappedEventName(eventName), ...args); if (this.dispatching) { - this.dispatch(eventDecl, ...args); + this.dispatch(event, ...args); } - const logArgs = args.slice(); + if (this.verbose || eventDecl.logLevel !== 'info') { + const logArgs = args.slice(); - if (eventDecl.logLevel === 'error') { - logArgs.forEach((el, i) => { - if (Object.isFunction(el)) { - logArgs[i] = () => el; - } - }); - } - - this.log({context: `event:${eventName}`, logLevel: eventDecl.logLevel}, this, ...logArgs); - }; + if (eventDecl.logLevel === 'error') { + logArgs.forEach((el, i) => { + if (Object.isFunction(el)) { + logArgs[i] = () => el; + } + }); + } - /** - * An alias for the `emit` method, but with stricter type checking - * - * @alias - * @param event - * @param args - */ - strictEmit: typeof this['selfEmitter']['strictEmit'] = - function strictEmit(this: iBlockEvent, event: string | ComponentEvent, ...args: any[]): void { - this.emit(event, ...args); + this.log({context: `event:${eventName}`, logLevel: eventDecl.logLevel}, this, ...logArgs); + } }; /** @@ -426,21 +460,19 @@ export default abstract class iBlockEvent extends iBlockBase { function dispatch(this: iBlockEvent, event: string | ComponentEvent, ...args: unknown[]): void { const that = this; - const eventDecl = normalizeEvent(event); + const eventDecl = normalizeStrictEvent(event); const eventName = eventDecl.event, wrappedEventName = getWrappedEventName(eventName); - let { - globalName, - componentName, - $parent: parent - } = this; + let {globalName, componentName, $parent: parent} = this; - const logArgs = args.slice(); + const + log = this.verbose || eventDecl.logLevel !== 'info', + logArgs = log ? args.slice() : args; - if (eventDecl.logLevel === 'error') { + if (log && eventDecl.logLevel === 'error') { logArgs.forEach((el, i) => { if (Object.isFunction(el)) { logArgs[i] = () => el; @@ -453,16 +485,27 @@ export default abstract class iBlockEvent extends iBlockBase { parent.$emit(eventName, this, ...args); parent.$emit(getComponentEventName(eventName), this, ...args); parent.$emit(wrappedEventName, ...args); - logFromParent(parent, `event:${eventName}`); + + if (log) { + logFromParent(parent, `event:${eventName}`); + } } else { parent.$emit(normalizeEventName(`${componentName}::${eventName}`), this, ...args); parent.$emit(normalizeEventName(`${componentName}::${wrappedEventName}`), ...args); - logFromParent(parent, `event:${componentName}::${eventName}`); + + if (log) { + logFromParent(parent, `event:${componentName}::${eventName}`); + } if (globalName != null) { parent.$emit(normalizeEventName(`${globalName}::${eventName}`), this, ...args); parent.$emit(normalizeEventName(`${globalName}::${wrappedEventName}`), ...args); + + if (log) { + logFromParent(parent, `event:${componentName}::${eventName}`); + } + logFromParent(parent, `event:${globalName}::${eventName}`); } } @@ -575,7 +618,7 @@ export default abstract class iBlockEvent extends iBlockBase { @watch({ path: 'proxyCall', immediate: true, - shouldInit: (ctx) => ctx.proxyCall != null + shouldInit: (o) => o.proxyCall != null }) protected initCallChildListener(enable: boolean): void { @@ -600,28 +643,37 @@ export default abstract class iBlockEvent extends iBlockBase { } } -function normalizeEvent(event: ComponentEvent | string): ComponentEvent { +function normalizeEvent(event: ComponentEvent | string): string | ComponentEvent { if (Object.isString(event)) { - return { - event: normalizeEventName(event), - logLevel: 'info' - }; + return normalizeEventName(event); } return { - ...event, + logLevel: event.logLevel, event: normalizeEventName(event.event) }; } +function normalizeStrictEvent(event: ComponentEvent | string): ComponentEvent { + if (Object.isString(event)) { + return {event, logLevel: 'info'}; + } + + return event; +} + function getWrappedEventName(event: string): string { - return normalizeEventName(`on-${event}`); + return `on${event[0].toUpperCase()}${event.slice(1)}`; } function getComponentEventName(event: string): string { - return normalizeEventName(`${event}:component`); + return `${event}:component`; } function normalizeEventName(event: string): string { - return event.camelize(false); + if (event.includes('-')) { + return event.camelize(false); + } + + return event; } diff --git a/src/components/super/i-block/event/interface.ts b/src/components/super/i-block/event/interface.ts index 2c398cac0f..34a3ccc786 100644 --- a/src/components/super/i-block/event/interface.ts +++ b/src/components/super/i-block/event/interface.ts @@ -9,27 +9,29 @@ import type { LogLevel } from 'core/log'; export type InferEvents< - I extends Array<[string, ...any[]]>, - P extends Dictionary = {}, - R extends Dictionary = {} + Scheme extends Array<[string, ...any[]]>, + Parent extends Dictionary = {} +> = _InferEvents, Parent>; + +export type _InferEvents< + Scheme extends any[], + Parent extends Dictionary = {}, + Result extends Dictionary = {} > = { - 0: InferEvents, P, (TB.Head extends [infer E, ...infer A] ? - E extends string ? { - Args: {[K in E]: A}; + 0: _InferEvents, Parent, (TB.Head extends [infer E, ...infer A] ? E extends string ? { + on(event: E, cb: (...args: A) => void): void; + once(event: E, cb: (...args: A) => void): void; + promisifyOnce(event: E): Promise>>; - on(event: E, cb: (...args: A) => void): void; - once(event: E, cb: (...args: A) => void): void; - promisifyOnce(event: E): Promise>>; + off(event: E | string, handler?: Function): void; - off(event: E | string, handler?: Function): void; + strictEmit(event: E, ...args: A): void; + emit(event: E, ...args: A): void; + emit(event: string, ...args: unknown[]): void; + } : {} : {}) & Result>; - strictEmit(event: E, ...args: A): void; - emit(event: E, ...args: A): void; - emit(event: string, ...args: unknown[]): void; - } : {} : {}) & R>; - - 1: R & P; -}[TB.Length extends 0 ? 1 : 0]; + 1: Result & Parent; +}[TB.Length extends 0 ? 1 : 0]; export interface ComponentEvent { event: E; @@ -37,50 +39,51 @@ export interface ComponentEvent { } export type InferComponentEvents< - C, - I extends Array<[string, ...any[]]>, - P extends Dictionary = {}, - R extends Dictionary = {} + Scheme extends Array<[string, ...any[]]>, + Parent extends Dictionary = {} +> = _InferComponentEvents, Parent>; + +export type _InferComponentEvents< + Scheme extends any[], + Parent extends Dictionary = {}, + Result extends Dictionary = {} > = { - 0: InferComponentEvents, P, (TB.Head extends [infer E, ...infer A] ? E extends string ? { - Args: {[K in E]: A}; - - on(event: `on${Capitalize}`, cb: (...args: A) => void): void; - on(event: E | `${E}:component`, cb: (component: C, ...args: A) => void): void; + 0: _InferComponentEvents< + TB.Tail, - once(event: `on${Capitalize}`, cb: (...args: A) => void): void; - once(event: E | `${E}:component`, cb: (component: C, ...args: A) => void): void; + Parent, - promisifyOnce(event: `on${Capitalize}`): Promise>>; - promisifyOnce(event: E | `${E}:component`): Promise>; + (TB.Head extends [infer E, ...infer A] ? E extends string ? { + on(event: `on${Capitalize}`, cb: (...args: A) => void): void; + on(this: T, event: E | `${E}:component`, cb: (component: T, ...args: A) => void): void; - off(event: E | `${E}:component` | `on${Capitalize}` | string, handler?: Function): void; + once(event: `on${Capitalize}`, cb: (...args: A) => void): void; + once(this: T, event: E | `${E}:component`, cb: (component: T, ...args: A) => void): void; - strictEmit(event: E | ComponentEvent, ...args: A): void; - emit(event: E | ComponentEvent, ...args: A): void; - emit(event: string | ComponentEvent, ...args: unknown[]): void; - } : {} : {}) & R>; + promisifyOnce(event: `on${Capitalize}`): Promise>>; + promisifyOnce(this: T, event: E | `${E}:component`): Promise>; - 1: R & OverrideParentComponentEvents; -}[TB.Length extends 0 ? 1 : 0]; + off(event: E | `${E}:component` | `on${Capitalize}` | string, handler?: Function): void; -export type OverrideParentComponentEvents = A extends Record ? { - [E in keyof A]: E extends string ? { - Args: A; + strictEmit(event: E | ComponentEvent, ...args: A): void; + emit(event: E | ComponentEvent, ...args: A): void; + emit(event: string | ComponentEvent, ...args: unknown[]): void; + } : {} : {}) & Result>; - on(event: `on${Capitalize}`, cb: (...args: A[E]) => void): void; - on(event: E | `${E}:component`, cb: (component: C, ...args: A[E]) => void): void; + 1: Parent & Result; +}[TB.Length extends 0 ? 1 : 0]; - once(event: `on${Capitalize}`, cb: (...args: A[E]) => void): void; - once(event: E | `${E}:component`, cb: (component: C, ...args: A[E]) => void): void; +type UnionToIntersection = (T extends any ? (k: T) => void : never) extends (k: infer R) => void ? R : never; - promisifyOnce(event: `on${Capitalize}`): Promise>>; - promisifyOnce(event: E | `${E}:component`): Promise>; +type EventToTuple = + UnionToIntersection Event : never> extends (() => infer T) ? + [...EventToTuple, Result>, [T, ...Result]] : + []; - off(event: E | `${E}:component` | `on${Capitalize}` | string, handler?: Function): void; +export type FlatEvents = { + 0: TB.Head extends [infer E, ...infer A] ? + FlatEvents, [...EventToTuple, ...Result]> : + []; - strictEmit(event: E | ComponentEvent, ...args: A[E]): void; - emit(event: E | ComponentEvent, ...args: A[E]): void; - emit(event: string | ComponentEvent, ...args: unknown[]): void; - } : {}; -}[keyof A] : {}; + 1: Result; +}[TB.Length extends 0 ? 1 : 0]; diff --git a/src/components/super/i-block/event/test/unit/api.ts b/src/components/super/i-block/event/test/unit/api.ts index d70a1b84b1..8578053663 100644 --- a/src/components/super/i-block/event/test/unit/api.ts +++ b/src/components/super/i-block/event/test/unit/api.ts @@ -28,10 +28,6 @@ test.describe(' event API', () => { const scan = await target.evaluate((ctx) => { const res: any[] = []; - ctx.on('onFoo_bar', (...args: any[]) => { - res.push(...args); - }); - ctx.on('onFoo-bar', (...args: any[]) => { res.push(...args); }); @@ -40,12 +36,12 @@ test.describe(' event API', () => { res.push(...args); }); - ctx.emit('foo bar', 1); + ctx.emit('foo-bar', 1); return res; }); - test.expect(scan).toEqual([1, 1, 1]); + test.expect(scan).toEqual([1, 1]); }); test('the `emit` method should fire 3 events', async ({page}) => { diff --git a/src/components/super/i-block/i-block.ts b/src/components/super/i-block/i-block.ts index 10ff183b28..fea1f89ac3 100644 --- a/src/components/super/i-block/i-block.ts +++ b/src/components/super/i-block/i-block.ts @@ -94,15 +94,13 @@ export default abstract class iBlock extends iBlockProviders { */ @watch({ path: 'r.shouldMountTeleports', - flush: 'post' + flush: 'post', + shouldInit: (o) => o.r.shouldMountTeleports === false }) @hook('before:mounted') protected onMountTeleports(): void { - const { - $el: originalNode, - $async: $a - } = this; + const {$el: originalNode, $async: $a} = this; if (originalNode == null) { return; @@ -130,7 +128,7 @@ export default abstract class iBlock extends iBlockProviders { this.watch('$attrs', {deep: true}, mountAttrs); } - function mountAttrs(attrs: Dictionary) { + function mountAttrs(attrs: Dictionary) { const mountedAttrsGroup = {group: 'mountedAttrs'}; $a.terminateWorker(mountedAttrsGroup); @@ -139,34 +137,41 @@ export default abstract class iBlock extends iBlockProviders { } attrsStore ??= new Set(); - const mountedAttrs = attrsStore; - Object.entries(attrs).forEach(([name, attr]) => { - if (attr == null) { - return; + const + mountedAttrs = attrsStore, + attrNames = Object.keys(attrs); + + for (let i = 0; i < attrNames.length; i++) { + const + attrName = attrNames[i], + attrVal = attrs[attrName]; + + if (attrVal == null) { + continue; } - if (name === 'class') { - attr.split(/\s+/).forEach((val) => { - node.classList.add(val); - mountedAttrs.add(`class.${val}`); - }); + if (attrName === 'class') { + for (const className of (attrVal).split(/\s+/)) { + node.classList.add(className); + mountedAttrs.add(`class.${className}`); + } - } else if (originalNode.hasAttribute(name)) { - node.setAttribute(name, attr); - mountedAttrs.add(name); + } else if (originalNode.hasAttribute(attrName)) { + node.setAttribute(attrName, attrVal); + mountedAttrs.add(attrName); } - }); + } $a.worker(() => { - mountedAttrs.forEach((attr) => { - if (attr.startsWith('class.')) { - node.classList.remove(attr.split('.')[1]); + for (const attrName of mountedAttrs) { + if (attrName.startsWith('class.')) { + node.classList.remove(attrName.split('.')[1]); } else { - node.removeAttribute(attr); + node.removeAttribute(attrName); } - }); + } mountedAttrs.clear(); }, mountedAttrsGroup); diff --git a/src/components/super/i-block/mods/README.md b/src/components/super/i-block/mods/README.md index 48590ec463..c8cecd7bfe 100644 --- a/src/components/super/i-block/mods/README.md +++ b/src/components/super/i-block/mods/README.md @@ -183,7 +183,7 @@ Or just pass modifiers as regular props. < b-example :visible = true ``` -To disable modifier inheritance, pass the `inheridMods: false` option when creating the component. +To disable modifier inheritance, pass the `inheritMods: false` option when creating the component. ```typescript import iBlock, { component } from 'components/super/i-block/i-block'; @@ -192,7 +192,7 @@ import iBlock, { component } from 'components/super/i-block/i-block'; class bExample extends iBlock {} ``` -### Getting a Component's Modifier Value +### Getting a Component Modifier Value All component's applied modifiers are stored in the `mods` read-only property. Therefore, to get the value of any modifier, simply access the desired key. @@ -201,7 +201,7 @@ Note that the key should be in kebab case, i.e., normalized. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -221,7 +221,7 @@ This property can be observed using the watch API. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -258,14 +258,14 @@ If you want to use modifiers within a component template, then use the `m` gette ... ``` -### Setting a New Component's Modifier Value +### Setting a New Component Modifier Value To set a new modifier value or remove an existing one, you can use the `setMod` and `removeMod` methods. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -299,7 +299,7 @@ These events provide a way to react to changes in modifiers and perform any nece ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -336,7 +336,7 @@ it can be more convenient than handling each event separately. ```typescript import iBlock, { component, ModsDecl } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { static mods: ModsDecl = { theme: [ @@ -410,7 +410,7 @@ Note that the method returns the normalized value of the modifier. ```typescript import iBlock, { component } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { mounted() { this.setRootMod('foo', 'blaBar'); @@ -431,7 +431,7 @@ The method uses the component's `globalName` prop if provided, otherwise it uses ```typescript import iBlock, { component } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { mounted() { // this.componentName === 'b-button' && this.globalName === undefined @@ -453,7 +453,7 @@ The method uses the component `globalName` prop if it's provided. Otherwise, the ```typescript import iBlock, { component } from 'components/super/i-block/i-block'; -@component({inheritMods: false}) +@component() class bExample extends iBlock { mounted() { this.setRootMod('foo', 'bla'); diff --git a/src/components/super/i-block/mods/index.ts b/src/components/super/i-block/mods/index.ts index 75390ea9f2..c2de18d90f 100644 --- a/src/components/super/i-block/mods/index.ts +++ b/src/components/super/i-block/mods/index.ts @@ -23,16 +23,11 @@ export * from 'components/super/i-block/mods/interface'; @component({partial: 'iBlock'}) export default abstract class iBlockMods extends iBlockEvent { - @system({merge: mergeMods, init: initMods}) + @system({atom: true, merge: mergeMods, init: initMods}) override readonly mods!: ModsDict; + @computed({cache: 'forever'}) override get sharedMods(): CanNull { - const m = this.mods; - - if (m.theme != null) { - return {theme: m.theme}; - } - return null; } diff --git a/src/components/super/i-block/modules/lfc/index.ts b/src/components/super/i-block/modules/lfc/index.ts index ee125b136e..4257dbfcbe 100644 --- a/src/components/super/i-block/modules/lfc/index.ts +++ b/src/components/super/i-block/modules/lfc/index.ts @@ -16,11 +16,12 @@ import SyncPromise from 'core/promise/sync'; import type Async from 'core/async'; import type { AsyncOptions } from 'core/async'; +import { beforeHooks, Hook } from 'core/component'; + import Friend from 'components/friends/friend'; import { statuses } from 'components/super/i-block/const'; -import type { Hook } from 'core/component'; import type { Cb } from 'components/super/i-block/modules/lfc/interface'; export * from 'components/super/i-block/modules/lfc/interface'; @@ -44,14 +45,9 @@ export default class Lfc extends Friend { * ``` */ isBeforeCreate(...skip: Hook[]): boolean { - const beforeHooks = { - beforeRuntime: true, - beforeCreate: true, - beforeDataCreate: true - }; - - skip.forEach((hook) => beforeHooks[hook] = false); - return Boolean(beforeHooks[this.hook]); + const hooks = {...beforeHooks}; + skip.forEach((hook) => hooks[hook] = false); + return Boolean(hooks[this.hook]); } /** @@ -117,7 +113,7 @@ export default class Lfc extends Friend { * ``` */ execCbAfterBlockReady(cb: Cb, opts?: AsyncOptions): CanUndef> { - if (this.ctx.block) { + if ('block' in this.ctx) { if (statuses[this.componentStatus] >= 0) { return cb.call(this.component); } diff --git a/src/components/super/i-block/modules/listeners/index.ts b/src/components/super/i-block/modules/listeners/index.ts index bb1885280f..93fe87a599 100644 --- a/src/components/super/i-block/modules/listeners/index.ts +++ b/src/components/super/i-block/modules/listeners/index.ts @@ -31,13 +31,14 @@ export function initGlobalListeners(component: iBlock, resetListener?: boolean): // eslint-disable-next-line @v4fire/unbound-method baseInitLoad ??= iBlock.prototype.initLoad; - const - ctx = component.unsafe; + const ctx = component.unsafe; const { async: $a, + globalName, globalEmitter: $e, + state: $s, state: {needRouterSync} } = ctx; @@ -45,7 +46,7 @@ export function initGlobalListeners(component: iBlock, resetListener?: boolean): $e.once(`destroy.${ctx.remoteState.appProcessId}`, ctx.$destroy.bind(ctx)); resetListener = Boolean( - (resetListener ?? baseInitLoad !== ctx.instance.initLoad) || + (resetListener ?? baseInitLoad !== ctx.constructor.prototype.initLoad) || (globalName ?? needRouterSync) ); diff --git a/src/components/super/i-block/modules/mods/index.ts b/src/components/super/i-block/modules/mods/index.ts index cf8324053f..7c22fe7e75 100644 --- a/src/components/super/i-block/modules/mods/index.ts +++ b/src/components/super/i-block/modules/mods/index.ts @@ -27,91 +27,184 @@ export * from 'components/super/i-block/modules/mods/interface'; export function initMods(component: iBlock['unsafe']): ModsDict { const declMods = component.meta.component.mods; + type RemoteMods = Array<[string, () => CanUndef]>; + const - attrMods: Array<[string, () => CanUndef]> = [], - modVal = (val: unknown) => val != null ? String(val) : undefined; + sharedMods = initSharedMods(), + attrMods = initAttrMods(); - Object.keys(component.$attrs).forEach((attrName) => { - const modName = attrName.camelize(false); + return component.sync.link(link)!; - if (modName in declMods) { - let el: Nullable; + function link(modsProp: CanUndef): ModsDict { + const isModsInitialized = Object.isDictionary(component.mods); - component.watch(`$attrs.${attrName}`, (attrs: Dictionary = {}) => { - el ??= component.$el; + const mods = isModsInitialized ? component.mods : {...declMods}; - if (el instanceof Element) { - el.removeAttribute(attrName); + linkSharedMods(); + linkModsProp(); + linkAttrMods(); + linkExpMods(); + + return initMods(); + + function initMods() { + const modNames = Object.keys(mods); + + for (let i = 0; i < modNames.length; i++) { + const modName = modNames[i]; + + const modVal = resolveModVal(mods[modName]); + mods[modName] = modVal; + + if (component.hook !== 'beforeDataCreate') { + void component.setMod(modName, modVal); } + } - void component.setMod(modName, modVal(attrs[attrName])); - }); + return mods; + } - component.meta.hooks['before:mounted'].push({ - fn: () => { - el = component.$el; + function linkSharedMods() { + for (let i = 0; i < sharedMods.length; i++) { + const [modName, getModValue] = sharedMods[i]; - if (el instanceof Element) { - el.removeAttribute(attrName); + const modVal = getModValue(); + + if (isModsInitialized || modVal != null) { + mods[modName] = modVal; + } + } + } + + function linkModsProp() { + if (modsProp != null) { + const modNames = Object.keys(modsProp); + + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modVal = modsProp[modNames[i]]; + + if (modVal != null || mods[modName] == null) { + mods[modName] = resolveModVal(modVal); } } - }); + } + } + + function linkAttrMods() { + for (let i = 0; i < attrMods.length; i++) { + const [modName, getModValue] = attrMods[i]; + + const modVal = getModValue(); - attrMods.push([modName, () => modVal(component.$attrs[attrName])]); + if (isModsInitialized || modVal != null) { + mods[modName] = modVal; + } + } } - }); - return Object.cast(component.sync.link(link)); + function linkExpMods() { + const {experiments} = component.r.remoteState; - function link(propMods: CanUndef): ModsDict { - const - isModsInitialized = Object.isDictionary(component.mods), - mods = isModsInitialized ? component.mods : {...declMods}; + if (Object.isArray(experiments)) { + for (let i = 0; i < experiments.length; i++) { + const + exp = experiments[i], + expMods = exp.meta?.mods; - if (propMods != null) { - Object.entries(propMods).forEach(([key, val]) => { - if (val != null || mods[key] == null) { - mods[key] = modVal(val); + if (!Object.isDictionary(expMods)) { + continue; + } + + const expModNames = Object.keys(expMods); + + for (let i = 0; i < expModNames.length; i++) { + const + modName = expModNames[i], + modVal = expMods[modName]; + + if (modVal != null || mods[modName] == null) { + mods[modName] = resolveModVal(modVal); + } + } } - }); + } + } + } + + function initSharedMods() { + const remoteMods: RemoteMods = []; + + if (!component.inheritMods) { + return remoteMods; } - attrMods.forEach(([name, getter]) => { - const val = getter(); + const + parent = component.$parent, + sharedMods = parent?.sharedMods; + + if (sharedMods != null) { + const modNames = Object.keys(sharedMods); - if (isModsInitialized || val != null) { - mods[name] = val; + for (let i = 0; i < modNames.length; i++) { + const modName = modNames[i]; + + component.watch(`$parent.mods.${modName}`, (mods: ModsDict) => { + void component.setMod(modName, mods[modName]); + }); + + remoteMods.push([modName, () => parent!.mods[modName]]); } - }); + } - const {experiments} = component.r.remoteState; + return remoteMods; + } - if (Object.isArray(experiments)) { - experiments.forEach((exp) => { - const experimentMods = exp.meta?.mods; + function initAttrMods() { + const + remoteMods: RemoteMods = [], + attrNames = Object.keys(component.$attrs); - if (!Object.isDictionary(experimentMods)) { - return; - } + let el: Nullable; - Object.entries(experimentMods).forEach(([name, val]) => { - if (val != null || mods[name] == null) { - mods[name] = modVal(val); + for (let i = 0; i < attrNames.length; i++) { + const + attrName = attrNames[i], + modName = attrName.camelize(false); + + if (modName in declMods) { + component.watch(`$attrs.${attrName}`, (attrs: Dictionary = {}) => { + el ??= component.$el; + + if (el instanceof Element) { + el.removeAttribute(attrName); } + + void component.setMod(modName, resolveModVal(attrs[attrName])); }); - }); + + remoteMods.push([modName, () => resolveModVal(component.$attrs[attrName])]); + } } - Object.entries(mods).forEach(([name, val]) => { - val = modVal(mods[name]); - mods[name] = val; + component.meta.hooks['before:mounted'].push({ + fn: () => { + el = component.$el; - if (component.hook !== 'beforeDataCreate') { - void component.setMod(name, val); + if (el instanceof Element) { + for (let i = 0; i < remoteMods.length; i++) { + el.removeAttribute(remoteMods[i][0]); + } + } } }); - return mods; + return remoteMods; + } + + function resolveModVal(val: unknown) { + return val != null ? String(val) : undefined; } } @@ -122,73 +215,62 @@ export function initMods(component: iBlock['unsafe']): ModsDict { * * @param component * @param oldComponent - * @param name - the field name that is merged when the component is re-created (this will be `mods`) - * @param [link] - the reference name which takes its value based on the current field */ -export function mergeMods( - component: iBlock['unsafe'], - oldComponent: iBlock['unsafe'], - name: string, - link?: string -): void { - if (link == null) { - return; - } - - const cache = component.$syncLinkCache.get(link); - - if (cache == null) { - return; - } +export function mergeMods(component: iBlock['unsafe'], oldComponent: iBlock['unsafe']): void { + const + currentModsProps = component.modsProp, + oldModsProps = oldComponent.modsProp; - const l = cache[name]; + const + currentAttrs = component.$attrs, + oldAttrs = oldComponent.$attrs; - if (l == null) { - return; - } + const + currentMods = component.mods, + oldMods = oldComponent.mods; const - modsProp = getExpandedModsProp(component), - mods = {...oldComponent.mods}; + mergedMods = {...currentMods}, + isModsPropsPassed = currentModsProps != null && oldModsProps != null; - Object.keys(mods).forEach((key) => { - if (component.sync.syncModCache[key] != null) { - delete mods[key]; - } - }); + const modNames = Object.keys(oldMods); - if (Object.fastCompare(modsProp, getExpandedModsProp(oldComponent))) { - l.sync(mods); + for (let i = 0; i < modNames.length; i++) { + const modName = modNames[i]; - } else { - l.sync(Object.assign(mods, modsProp)); - } + const + currentModVal = currentMods[modName], + oldModVal = oldMods[modName]; - function getExpandedModsProp(component: iBlock['unsafe']): ModsDict { - if (link == null) { - return {}; - } + // True if the modifier value needs to be taken from the old component + let shouldSetFromOld = + currentModVal !== oldModVal && - const modsProp = component.$props[link]; + // Do not merge modifiers that receive their value through `sync.mod` + component.sync.syncModCache[modName] == null; - if (!Object.isDictionary(modsProp)) { - return {}; - } + if (shouldSetFromOld) { + // If a modifier was passed through the `modsProp` prop, but the value in the prop has changed + if (isModsPropsPassed && currentModsProps[modName] !== oldMods[modName]) { + shouldSetFromOld = false; - const - declMods = component.meta.component.mods, - res = {...modsProp}; + } else { + const attrName = modName.dasherize(); - Object.entries(component.$attrs).forEach(([name, attr]) => { - if (name in declMods) { - if (attr != null) { - res[name] = attr; + // If a modifier was passed through the component's attributes, but its value has changed + if (currentAttrs[attrName] !== oldAttrs[attrName]) { + shouldSetFromOld = false; } } - }); - return res; + if (shouldSetFromOld) { + mergedMods[modName] = oldModVal; + } + } } + + // @ts-ignore (readonly) + component.mods = mergedMods; } /** @@ -198,27 +280,32 @@ export function mergeMods( export function getReactiveMods(component: iBlock): Readonly { const watchMods = {}, - watchers = component.field.get('reactiveModsStore')!, - systemMods = component.mods; + watchers = component.field.get('reactiveModsStore')!; + + const modNames = Object.keys(component.mods); - Object.entries(systemMods).forEach(([name, val]) => { - if (name in watchers) { - watchMods[name] = val; + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modVal = component.mods[modName]; + + if (modName in watchers) { + watchMods[modName] = modVal; } else { - Object.defineProperty(watchMods, name, { + Object.defineProperty(watchMods, modName, { configurable: true, enumerable: true, get: () => { - if (!(name in watchers)) { - Object.getPrototypeOf(watchers)[name] = val; + if (!(modName in watchers)) { + Object.getPrototypeOf(watchers)[modName] = modVal; } - return watchers[name]; + return watchers[modName]; } }); } - }); + } return Object.freeze(watchMods); } diff --git a/src/components/super/i-block/props.ts b/src/components/super/i-block/props.ts index 5671eff74f..e4abcf24e6 100644 --- a/src/components/super/i-block/props.ts +++ b/src/components/super/i-block/props.ts @@ -57,6 +57,9 @@ export default abstract class iBlockProps extends ComponentInterface { @prop({type: Object, required: false, forceUpdate: false}) override readonly modsProp?: ModsProp; + @prop({type: Boolean, required: false}) + override readonly inheritMods?: boolean; + /** * If set to true, the component will be activated by default. * A deactivated component will not retrieve data from providers during initialization. @@ -112,7 +115,7 @@ export default abstract class iBlockProps extends ComponentInterface { /** * If set to false, the component will not render its content during SSR */ - @prop({type: Boolean, forceDefault: true}) + @prop({type: Boolean}) readonly ssrRenderingProp: boolean = true; /** @@ -309,7 +312,7 @@ export default abstract class iBlockProps extends ComponentInterface { @prop({type: Object, required: false}) override readonly styles?: Dictionary | Dictionary>; - @prop({type: Boolean, forceDefault: true}) + @prop({type: Boolean}) override readonly canFunctional: boolean = false; @prop({type: Function, required: false}) @@ -319,5 +322,5 @@ export default abstract class iBlockProps extends ComponentInterface { override readonly getParent?: () => this['$parent']; @prop({type: Function, required: false}) - override readonly getPassedProps?: () => Set; + override readonly getPassedProps?: () => Dictionary; } diff --git a/src/components/super/i-block/providers/README.md b/src/components/super/i-block/providers/README.md index 56f9ba1cd9..39b3d240fb 100644 --- a/src/components/super/i-block/providers/README.md +++ b/src/components/super/i-block/providers/README.md @@ -35,7 +35,7 @@ class bExample extends iBlock { If true, the component is marked as a removed provider. This means that the parent component will wait for the current component to load. -#### [dontWaitRemoteProvidersProp] +#### [dontWaitRemoteProviders = `false`] If true, the component will skip waiting for remote providers to avoid redundant re-rendering. This prop can help optimize your non-functional component when it does not contain any remote providers. @@ -48,11 +48,6 @@ By default, this prop is automatically calculated based on component dependencie A list of additional dependencies to load during the component's initialization. The parameter is tied with the `dependenciesProp` prop. -#### dontWaitRemoteProviders - -If true, the component will skip waiting for remote providers to avoid redundant re-rendering. -The parameter is tied with the `dontWaitRemoteProvidersProp` prop. - ### Methods #### initLoad diff --git a/src/components/super/i-block/providers/index.ts b/src/components/super/i-block/providers/index.ts index 7575fe297b..25fff1e762 100644 --- a/src/components/super/i-block/providers/index.ts +++ b/src/components/super/i-block/providers/index.ts @@ -11,7 +11,6 @@ * @packageDocumentation */ -import config from 'config'; import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; @@ -27,35 +26,31 @@ import type iData from 'components/super/i-data/i-data'; import type iBlock from 'components/super/i-block/i-block'; import { statuses } from 'components/super/i-block/const'; -import { system, hook } from 'components/super/i-block/decorators'; +import { hook } from 'components/super/i-block/decorators'; import type { InitLoadCb, InitLoadOptions } from 'components/super/i-block/interface'; import iBlockState from 'components/super/i-block/state'; + +import type { InferComponentEvents } from 'components/super/i-block/event'; import type { DataProviderProp } from 'components/super/i-block/providers/interface'; export * from 'components/super/i-block/providers/interface'; -const - $$ = symbolGenerator(); +const $$ = symbolGenerator(); @component({partial: 'iBlock'}) export default abstract class iBlockProviders extends iBlockState { - /** {@link iBlock.dontWaitRemoteProvidersProp} */ - @system((o) => o.sync.link((val) => { - if (val == null) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (o.dontWaitRemoteProviders != null) { - return o.dontWaitRemoteProviders; - } - - return !config.components[o.componentName]?.dependencies.some((dep) => dep.includes('remote-provider')); - } - - return val; - })) + /** @inheritDoc */ + declare readonly SelfEmitter: InferComponentEvents<[ + ['initLoadStart', InitLoadOptions], + [event: 'initLoad', data: unknown, opts: InitLoadOptions] + ], iBlockState['SelfEmitter']>; - dontWaitRemoteProviders!: boolean; + /** {@link iBlock.dontWaitRemoteProvidersProp} */ + get dontWaitRemoteProviders(): boolean { + return this.dontWaitRemoteProvidersProp ?? this.dontWaitRemoteProvidersHint(); + } /** * Loads component initialization data. @@ -109,7 +104,7 @@ export default abstract class iBlockProviders extends iBlockState { try { if (opts.emitStartEvent !== false) { - this.emit('initLoadStart', opts); + this.strictEmit('initLoadStart', opts); } if (!opts.silent) { @@ -179,7 +174,7 @@ export default abstract class iBlockProviders extends iBlockState { route: this.route, globalName: component.globalName, component: component.componentName, - dataProvider: (component).dataProvider?.provider.constructor.name + dataProvider: Object.cast(component).dataProvider?.provider.constructor.name } } ); @@ -228,7 +223,7 @@ export default abstract class iBlockProviders extends iBlockState { } function emitInitLoad() { - that.emit('initLoad', get(), opts); + that.strictEmit('initLoad', get(), opts); } } @@ -361,4 +356,12 @@ export default abstract class iBlockProviders extends iBlockState { super.initBaseAPI(); this.createDataProviderInstance = this.instance.createDataProviderInstance.bind(this); } + + /** + * Returns a hint on whether the component initialization mode can be used without waiting for remote providers. + * This method is overridden by a transformer at build time. + */ + protected dontWaitRemoteProvidersHint(): boolean { + return true; + } } diff --git a/src/components/super/i-block/state/README.md b/src/components/super/i-block/state/README.md index 691eaa2b0a..afaba80fde 100644 --- a/src/components/super/i-block/state/README.md +++ b/src/components/super/i-block/state/README.md @@ -38,8 +38,7 @@ the [[iBlock]] class incorporates the following code. ``` @hook('beforeRuntime') protected initBaseAPI() { - const - i = this.instance; + const i = this.instance; this.syncStorageState = i.syncStorageState.bind(this); this.syncRouterState = i.syncRouterState.bind(this); diff --git a/src/components/super/i-block/state/index.ts b/src/components/super/i-block/state/index.ts index 43ff23c4b9..0eeac4928f 100644 --- a/src/components/super/i-block/state/index.ts +++ b/src/components/super/i-block/state/index.ts @@ -30,12 +30,26 @@ import { readyStatuses } from 'components/super/i-block/modules/activation'; import { field, system, computed, wait, hook, WaitDecoratorOptions } from 'components/super/i-block/decorators'; import type iBlock from 'components/super/i-block/i-block'; + +import type { InferComponentEvents } from 'components/super/i-block/event'; import type { Stage, ComponentStatus, ComponentStatuses } from 'components/super/i-block/interface'; import iBlockMods from 'components/super/i-block/mods'; @component({partial: 'iBlock'}) export default abstract class iBlockState extends iBlockMods { + /** @inheritDoc */ + declare readonly SelfEmitter: InferComponentEvents<[ + [`hook:${Hook}`, Hook, Hook], + ['hookChange', Hook, Hook], + + [`componentStatus:${ComponentStatus}`, ComponentStatus, ComponentStatus], + ['componentStatusChange', ComponentStatus, ComponentStatus], + + [`stage:${string}`, CanUndef, CanUndef], + ['stageChange', CanUndef, CanUndef] + ], iBlockMods['SelfEmitter']>; + /** * A list of additional dependencies to load during the component's initialization * {@link iBlock.dependenciesProp} @@ -197,7 +211,7 @@ export default abstract class iBlockState extends iBlockMods { value === 'ready' && oldValue === 'beforeReady' || value === 'inactive' && !this.renderOnActivation || - (this.instance.constructor).shadowComponentStatuses[value]; + (this.constructor).shadowComponentStatuses[value]; if (isShadowStatus) { this.shadowComponentStatusStore = value; @@ -217,8 +231,8 @@ export default abstract class iBlockState extends iBlockMods { } } - this.emit(`componentStatus:${value}`, value, oldValue); - this.emit('componentStatusChange', value, oldValue); + this.strictEmit(`componentStatus:${value}`, value, oldValue); + this.strictEmit('componentStatusChange', value, oldValue); } // eslint-disable-next-line jsdoc/require-param @@ -290,10 +304,10 @@ export default abstract class iBlockState extends iBlockMods { } if (value != null) { - this.emit(`stage:${value}`, value, oldValue); + this.strictEmit(`stage:${value}`, value, oldValue); } - this.emit('stageChange', value, oldValue); + this.strictEmit('stageChange', value, oldValue); } /** @@ -377,8 +391,8 @@ export default abstract class iBlockState extends iBlockMods { this.hookStore = value; if ('lfc' in this && !this.lfc.isBeforeCreate('beforeDataCreate')) { - this.emit(`hook:${value}`, value, oldValue); - this.emit('hookChange', value, oldValue); + this.strictEmit(`hook:${value}`, value, oldValue); + this.strictEmit('hookChange', value, oldValue); } } @@ -594,21 +608,6 @@ export default abstract class iBlockState extends iBlockMods { this.syncRouterState = i.syncRouterState.bind(this); } - /** - * Initializes the theme modifier and attaches a listener to monitor changes of the theme - */ - @hook('created') - protected initThemeModListener(): void { - const theme = this.remoteState.theme.get(); - void this.setMod('theme', theme.value); - - this.async.on( - this.remoteState.theme.emitter, - 'theme.change', - (theme: Theme) => this.setMod('theme', theme.value) - ); - } - /** * Stores a boolean flag in the hydrationStore during SSR, * which determines whether the content of components should be rendered during hydration @@ -642,6 +641,9 @@ export default abstract class iBlockState extends iBlockMods { const v = this.stage; return v == null ? v : String(v); }); + + this.sync.mod('theme', 'remoteState.theme.emitter:theme.change', {immediate: true}, (theme?: Theme) => + (theme ?? this.remoteState.theme.get()).value); } /** diff --git a/src/components/super/i-block/test/unit/teleports.ts b/src/components/super/i-block/test/unit/teleports.ts index daa2e2c84a..38a0d75f45 100644 --- a/src/components/super/i-block/test/unit/teleports.ts +++ b/src/components/super/i-block/test/unit/teleports.ts @@ -45,7 +45,7 @@ test.describe(' using the root teleport', () => { const attrs = await target.evaluate((ctx) => ctx.unsafe.$refs.component.$el!.className); - test.expect(attrs).toBe('i-block-helper u1e705d34abc46a b-bottom-slide b-bottom-slide_opened_false b-bottom-slide_stick_true b-bottom-slide_events_false b-bottom-slide_height-mode_full b-bottom-slide_visible_false b-bottom-slide_theme_light b-bottom-slide_hidden_true'); + test.expect(attrs).toBe('i-block-helper u1e705d34abc46a b-bottom-slide b-bottom-slide_opened_false b-bottom-slide_stick_true b-bottom-slide_events_false b-bottom-slide_height-mode_full b-bottom-slide_theme_light b-bottom-slide_visible_false b-bottom-slide_hidden_true'); }); }); diff --git a/src/components/super/i-data/data.ts b/src/components/super/i-data/data.ts index 3a27bef06a..5d0b883d80 100644 --- a/src/components/super/i-data/data.ts +++ b/src/components/super/i-data/data.ts @@ -7,7 +7,7 @@ */ import symbolGenerator from 'core/symbol'; -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import type iData from 'components/super/i-data/i-data'; import type DataProvider from 'components/friends/data-provider'; @@ -22,9 +22,11 @@ import iBlock, { prop, field, system, + computed, watch, - ModsDecl + ModsDecl, + InferComponentEvents } from 'components/super/i-block/i-block'; @@ -49,6 +51,12 @@ interface iDataData extends Trait {} @derive(iDataProvider) abstract class iDataData extends iBlock implements iDataProvider { + /** @inheritDoc */ + declare readonly SelfEmitter: InferComponentEvents<[ + ['dbCanChange', CanUndef], + ['dbChange', CanUndef], + ], iBlock['SelfEmitter']>; + /** * Type: the raw provider data */ @@ -88,15 +96,16 @@ abstract class iDataData extends iBlock implements iDataProvider { * These functions step by step transform the original provider data before storing it in `db`. * {@link iDataProvider.dbConverter} */ - @system((o) => o.sync.link('dbConverter', (val) => { - if (val == null) { + @computed({dependencies: ['dbConverter']}) + get dbConverters(): ComponentConverter[] { + const propVal = this.dbConverter; + + if (propVal == null) { return []; } - return Object.isIterable(val) ? [...val] : [val]; - })) - - dbConverters!: ComponentConverter[]; + return Object.isIterable(propVal) ? [...propVal] : [propVal]; + } /** * Converter(s) from the raw `db` to the component field. @@ -114,15 +123,16 @@ abstract class iDataData extends iBlock implements iDataProvider { * A list of converters from the raw `db` to the component field * {@link iDataProvider.componentConverter} */ - @system((o) => o.sync.link('componentConverter', (val) => { - if (val == null) { + @computed({dependencies: ['componentConverter']}) + get componentConverters(): ComponentConverter[] { + const propVal = this.componentConverter; + + if (propVal == null) { return []; } - return Object.isIterable(val) ? [...val] : [val]; - })) - - componentConverters!: ComponentConverter[]; + return Object.isIterable(propVal) ? [...propVal] : [propVal]; + } /** * A function to filter all "default" requests: all requests that were created implicitly, as the initial @@ -155,8 +165,8 @@ abstract class iDataData extends iBlock implements iDataProvider { readonly checkDBEquality: CheckDBEquality = true; /** {@link iDataProvider.requestParams} */ - @system({merge: true}) - readonly requestParams: RequestParams = {get: {}}; + @system({merge: true, init: () => ({get: {}})}) + readonly requestParams!: RequestParams; /** * The raw component data from the data provider @@ -173,7 +183,7 @@ abstract class iDataData extends iBlock implements iDataProvider { * @emits `dbChange(value: CanUndef)` */ set db(value: CanUndef) { - this.emit('dbCanChange', value); + this.strictEmit('dbCanChange', value); if (value === this.db) { return; @@ -194,7 +204,7 @@ abstract class iDataData extends iBlock implements iDataProvider { }); } - this.emit('dbChange', value); + this.strictEmit('dbChange', value); } static override readonly mods: ModsDecl = { @@ -216,11 +226,9 @@ abstract class iDataData extends iBlock implements iDataProvider { protected convertDataToDB(data: unknown): O; protected convertDataToDB(data: unknown): this['DB']; protected convertDataToDB(data: unknown): O | this['DB'] { - const - {dbConverters} = this; + const {dbConverters} = this; - let - convertedData = data; + let convertedData = data; if (dbConverters.length > 0) { const rawData = Object.isArray(convertedData) || Object.isDictionary(convertedData) ? @@ -230,8 +238,7 @@ abstract class iDataData extends iBlock implements iDataProvider { convertedData = dbConverters.reduce((val, converter) => converter(val, Object.cast(this)), rawData); } - const - {db, checkDBEquality} = this; + const {db, checkDBEquality} = this; const canKeepOldData = Object.isFunction(checkDBEquality) ? Object.isTruly(checkDBEquality.call(this, convertedData, db)) : @@ -249,11 +256,9 @@ abstract class iDataData extends iBlock implements iDataProvider { * @param data */ protected convertDBToComponent(data: unknown): O | this['DB'] { - const - {componentConverters} = this; + const {componentConverters} = this; - let - convertedData = data; + let convertedData = data; if (componentConverters.length > 0) { const rawData = Object.isArray(convertedData) || Object.isDictionary(convertedData) ? @@ -273,7 +278,7 @@ abstract class iDataData extends iBlock implements iDataProvider { */ @watch({ path: 'componentConverter', - shouldInit: (ctx) => ctx.componentConverter != null + shouldInit: (o) => o.componentConverter != null }) protected initRemoteData(): CanUndef { @@ -282,7 +287,7 @@ abstract class iDataData extends iBlock implements iDataProvider { protected override initModEvents(): void { super.initModEvents(); - iDataProvider.initModEvents(this); + iDataProvider.initModEvents(this); } } diff --git a/src/components/super/i-data/handlers.ts b/src/components/super/i-data/handlers.ts index c0a9a0d0bc..e541a72473 100644 --- a/src/components/super/i-data/handlers.ts +++ b/src/components/super/i-data/handlers.ts @@ -143,13 +143,13 @@ export default abstract class iDataHandlers extends iDataData { { path: 'dataProviderProp', provideArgs: false, - shouldInit: (ctx) => ctx.dataProviderProp != null + shouldInit: (o) => o.dataProviderProp != null }, { path: 'dataProviderOptions', provideArgs: false, - shouldInit: (ctx) => ctx.dataProviderOptions != null + shouldInit: (o) => o.dataProviderOptions != null } ]) diff --git a/src/components/super/i-data/i-data.ts b/src/components/super/i-data/i-data.ts index fae22504b4..448c22f22a 100644 --- a/src/components/super/i-data/i-data.ts +++ b/src/components/super/i-data/i-data.ts @@ -26,6 +26,7 @@ import { InitLoadCb, InitLoadOptions, + UnsafeGetter } from 'components/super/i-block/i-block'; @@ -55,8 +56,7 @@ export { export * from 'components/super/i-block/i-block'; export * from 'components/super/i-data/interface'; -const - $$ = symbolGenerator(); +const $$ = symbolGenerator(); @component({functional: null}) export default abstract class iData extends iDataHandlers { @@ -84,7 +84,7 @@ export default abstract class iData extends iDataHandlers { try { if (opts.emitStartEvent !== false) { - this.emit('initLoadStart', opts); + this.strictEmit('initLoadStart', opts); } if (this.dataProviderProp != null && this.dataProvider == null) { diff --git a/src/components/super/i-input/fields.ts b/src/components/super/i-input/fields.ts index ccc4d0de2b..86024c2f3d 100644 --- a/src/components/super/i-input/fields.ts +++ b/src/components/super/i-input/fields.ts @@ -71,7 +71,7 @@ export default abstract class iInputFields extends iInputProps { * A map of available component validators */ get validatorsMap(): typeof iInputFields['validators'] { - return (this.instance.constructor).validators; + return (this.constructor).validators; } /** diff --git a/src/components/super/i-input/i-input.ts b/src/components/super/i-input/i-input.ts index a5f101fbdc..338c744dd9 100644 --- a/src/components/super/i-input/i-input.ts +++ b/src/components/super/i-input/i-input.ts @@ -334,11 +334,8 @@ export default abstract class iInput extends iInputHandlers implements iVisible, * @param [value] */ protected resolveValue(value?: this['Value']): this['Value'] { - const - i = this.instance; - if (value === undefined && this.lfc.isBeforeCreate()) { - return i['defaultGetter'].call(this); + return this.instance['defaultGetter'].call(this); } return value; diff --git a/src/components/super/i-static-page/CHANGELOG.md b/src/components/super/i-static-page/CHANGELOG.md index 3b653b257a..40030552c3 100644 --- a/src/components/super/i-static-page/CHANGELOG.md +++ b/src/components/super/i-static-page/CHANGELOG.md @@ -100,7 +100,7 @@ Changelog #### :rocket: New Feature -* Added the ability to manipulate meta-information of a page +* Added the ability to manipulate metainformation of a page ## v3.29.0 (2022-09-13) diff --git a/src/components/traits/README.md b/src/components/traits/README.md index f79232b99b..d5c2b2bbb8 100644 --- a/src/components/traits/README.md +++ b/src/components/traits/README.md @@ -1,7 +1,7 @@ # components/traits The module provides a set of traits for components. -A trait in TypeScript is an interface-like abstract class that serves as an interface. +A trait in TypeScript is an abstract class that serves as an interface. But why do we need it? Unlike Java or Kotlin, TypeScript interfaces cannot have default method implementations. Consequently, in TypeScript, we have to implement every method in our classes, @@ -99,7 +99,7 @@ We have defined a trait. We can now proceed to implement it in a basic class. specify any traits we want to automatically implement. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; interface DuckLike extends Trait {} @@ -116,7 +116,7 @@ We have defined a trait. We can now proceed to implement it in a basic class. 4. Profit! Now TS will automatically understand the methods of the interface, and they will work at runtime. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; interface DuckLike extends Trait {} @@ -136,7 +136,7 @@ We have defined a trait. We can now proceed to implement it in a basic class. 5. Of course, we can implement more than one trait in a component. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; interface DuckLike extends Trait, Trait {} diff --git a/src/components/traits/i-access/README.md b/src/components/traits/i-access/README.md index 691cd1369e..278df4ce7f 100644 --- a/src/components/traits/i-access/README.md +++ b/src/components/traits/i-access/README.md @@ -14,7 +14,7 @@ or disabling. * The trait can be automatically derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iAccess from 'components/traits/i-access/i-access'; import iBlock, { component } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-active-items/README.md b/src/components/traits/i-active-items/README.md index 4855143645..5f956db8fe 100644 --- a/src/components/traits/i-active-items/README.md +++ b/src/components/traits/i-active-items/README.md @@ -14,7 +14,7 @@ Take a look at [[bTree]] or [[bList]] to see more. * The trait can be partially derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iActiveItems from 'traits/i-active-items/i-active-items'; import iBlock, { component } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-control-list/README.md b/src/components/traits/i-control-list/README.md index 5c5a409014..2ac7f1240f 100644 --- a/src/components/traits/i-control-list/README.md +++ b/src/components/traits/i-control-list/README.md @@ -13,7 +13,7 @@ This module provides a trait with helpers for a component that renders a list of * The trait can be automatically derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iControlList, { Control } from 'components/traits/i-control-list/i-control-list'; import iBlock, { component } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-control-list/test/b-traits-i-control-list-dummy/b-traits-i-control-list-dummy.ts b/src/components/traits/i-control-list/test/b-traits-i-control-list-dummy/b-traits-i-control-list-dummy.ts index c12aff6cec..e40d1c059e 100644 --- a/src/components/traits/i-control-list/test/b-traits-i-control-list-dummy/b-traits-i-control-list-dummy.ts +++ b/src/components/traits/i-control-list/test/b-traits-i-control-list-dummy/b-traits-i-control-list-dummy.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import iControlList, { Control } from 'components/traits/i-control-list/i-control-list'; import iBlock, { component, prop } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-lock-page-scroll/README.md b/src/components/traits/i-lock-page-scroll/README.md index a5b6830a86..2913b46f51 100644 --- a/src/components/traits/i-lock-page-scroll/README.md +++ b/src/components/traits/i-lock-page-scroll/README.md @@ -12,7 +12,7 @@ It is useful if you have an issue with page scrolling under popups or other over * The trait can be automatically derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iLockPageScroll from 'components/traits/i-lock-page-scroll/i-lock-page-scroll'; import iBlock, { component, wait } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-lock-page-scroll/test/b-traits-i-lock-page-scroll-dummy/b-traits-i-lock-page-scroll-dummy.ts b/src/components/traits/i-lock-page-scroll/test/b-traits-i-lock-page-scroll-dummy/b-traits-i-lock-page-scroll-dummy.ts index 3732d5daa2..81f2fd738b 100644 --- a/src/components/traits/i-lock-page-scroll/test/b-traits-i-lock-page-scroll-dummy/b-traits-i-lock-page-scroll-dummy.ts +++ b/src/components/traits/i-lock-page-scroll/test/b-traits-i-lock-page-scroll-dummy/b-traits-i-lock-page-scroll-dummy.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import bDummy, { component } from 'components/dummies/b-dummy/b-dummy'; import iLockPageScroll from 'components/traits/i-lock-page-scroll/i-lock-page-scroll'; diff --git a/src/components/traits/i-observe-dom/README.md b/src/components/traits/i-observe-dom/README.md index 48f6469ffd..6958ef45a2 100644 --- a/src/components/traits/i-observe-dom/README.md +++ b/src/components/traits/i-observe-dom/README.md @@ -11,7 +11,7 @@ This module provides a trait for a component to observe DOM changes by using [`M * The trait can be automatically derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iObserveDOM from 'components/traits/i-observe-dom/i-observe-dom'; import iBlock, { component, wait } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-observe-dom/test/b-traits-i-observe-dom-dummy/b-traits-i-observe-dom-dummy.ts b/src/components/traits/i-observe-dom/test/b-traits-i-observe-dom-dummy/b-traits-i-observe-dom-dummy.ts index 7c84f19100..e059f4a2b3 100644 --- a/src/components/traits/i-observe-dom/test/b-traits-i-observe-dom-dummy/b-traits-i-observe-dom-dummy.ts +++ b/src/components/traits/i-observe-dom/test/b-traits-i-observe-dom-dummy/b-traits-i-observe-dom-dummy.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { derive } from 'core/functools/trait'; +import { derive } from 'components/traits'; import bDummy, { component, hook, wait } from 'components/dummies/b-dummy/b-dummy'; import iObserveDOM from 'components/traits/i-observe-dom/i-observe-dom'; diff --git a/src/components/traits/i-open-toggle/README.md b/src/components/traits/i-open-toggle/README.md index 8b6367c0a0..b3cdc6d742 100644 --- a/src/components/traits/i-open-toggle/README.md +++ b/src/components/traits/i-open-toggle/README.md @@ -13,7 +13,7 @@ This module provides a trait for a component that extends the "opening/closing" * The trait can be automatically derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iOpenToggle from 'components/traits/i-open-toggle/i-open-toggle'; import iBlock, { component } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-open/README.md b/src/components/traits/i-open/README.md index f90b6af1f7..ca96a123e1 100644 --- a/src/components/traits/i-open/README.md +++ b/src/components/traits/i-open/README.md @@ -11,7 +11,7 @@ This module provides a trait for a component that needs to implement the "openin * The trait can be automatically derived. ```typescript - import { derive } from 'core/functools/trait'; + import { derive } from 'components/traits'; import iOpen from 'components/traits/i-open/i-open'; import iBlock, { component } from 'components/super/i-block/i-block'; diff --git a/src/components/traits/i-open/i-open.ts b/src/components/traits/i-open/i-open.ts index bed33aba85..22ba960ce0 100644 --- a/src/components/traits/i-open/i-open.ts +++ b/src/components/traits/i-open/i-open.ts @@ -53,8 +53,7 @@ export default abstract class iOpen { /** {@link iOpen.prototype.onTouchClose} */ static onTouchClose: AddSelf = async (component, e) => { - const - target = >e.target; + const target = >e.target; if (target == null) { return; @@ -80,10 +79,7 @@ export default abstract class iOpen { events: CloseHelperEvents = {}, eventOpts: AddEventListenerOptions = {} ): void { - const { - async: $a, - localEmitter: $e - } = component.unsafe; + const {async: $a, localEmitter: $e} = component.unsafe; const helpersGroup = {group: 'closeHelpers'}, diff --git a/src/components/traits/index.ts b/src/components/traits/index.ts new file mode 100644 index 0000000000..a0b4e88e4b --- /dev/null +++ b/src/components/traits/index.ts @@ -0,0 +1,159 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:components/traits/README.md]] + * @packageDocumentation + */ + +import { initEmitter, ComponentDescriptor } from 'core/component'; + +import { registeredComponent } from 'core/component/decorators'; +import { regMethod, MethodType } from 'core/component/decorators/method'; + +/** + * Derives the provided traits to a component. + * The function is used to organize multiple implementing interfaces with the support of default methods. + * + * @decorator + * @param traits + * + * @example + * ```typescript + * import { derive } from 'components/traits'; + * import iBlock, { component } from 'components/super/i-block/i-block'; + * + * abstract class iOpen { + * /** + * * This method has a default implementation. + * * The implementation is provided as a static method. + * *\/ + * open(): void { + * return Object.throw(); + * }; + * + * /** + * * The default implementation for iOpen.open. + * * The method takes a context as its first parameter. + * * + * * @see iOpen['open'] + * *\/ + * static open: AddSelf = (self) => { + * self.setMod('opened', true); + * }; + * } + * + * abstract class iSize { + * abstract sizes(): string[]; + * } + * + * interface bExample extends Trait, Trait { + * + * } + * + * @component() + * @derive(iOpen, iSize) + * class bExample extends iBlock implements iOpen, iSize { + * sizes() { + * return ['xs', 's', 'm', 'l', 'xl']; + * } + * } + * + * console.log(new bExample().open()); + * ``` + */ +export function derive(...traits: Function[]) { + return (target: Function): void => { + if (registeredComponent.event == null) { + return; + } + + initEmitter.once(registeredComponent.event, ({meta}: ComponentDescriptor) => { + const proto = target.prototype; + + for (let i = 0; i < traits.length; i++) { + const + originalTrait = traits[i], + chain = getTraitChain(originalTrait); + + for (let i = 0; i < chain.length; i++) { + const [trait, keys] = chain[i]; + + for (let i = 0; i < keys.length; i++) { + const + key = keys[i], + defMethod = Object.getOwnPropertyDescriptor(trait, key), + traitMethod = Object.getOwnPropertyDescriptor(trait.prototype, key); + + const canDerive = + defMethod != null && + traitMethod != null && + !(key in proto) && + + Object.isFunction(defMethod.value) && ( + Object.isFunction(traitMethod.value) || + + // eslint-disable-next-line @v4fire/unbound-method + Object.isFunction(traitMethod.get) || Object.isFunction(traitMethod.set) + ); + + if (canDerive) { + let type: MethodType; + + const newDescriptor: PropertyDescriptor = { + enumerable: false, + configurable: true + }; + + if (Object.isFunction(traitMethod.value)) { + Object.assign(newDescriptor, { + writable: true, + + // eslint-disable-next-line func-name-matching + value: function defaultMethod(...args: unknown[]) { + return originalTrait[key](this, ...args); + } + }); + + type = 'method'; + + } else { + Object.assign(newDescriptor, { + get() { + return originalTrait[key](this); + }, + + set(value: unknown) { + originalTrait[key](this, value); + } + }); + + type = 'accessor'; + } + + Object.defineProperty(proto, key, newDescriptor); + regMethod(key, type, meta, proto); + } + } + } + } + + function getTraitChain>( + trait: Nullable, + methods: T = Object.cast([]) + ): T { + if (!Object.isFunction(trait) || trait === Function.prototype) { + return methods; + } + + methods.push([trait, Object.getOwnPropertyNames(trait)]); + return getTraitChain(Object.getPrototypeOf(trait), methods); + } + }); + }; +} diff --git a/src/config/CHANGELOG.md b/src/config/CHANGELOG.md index fa05e02801..ae34f3111b 100644 --- a/src/config/CHANGELOG.md +++ b/src/config/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :boom: Breaking Change + +* Removed the `components` property + ## v4.0.0-beta.122 (2024-08-06) #### :rocket: New Feature diff --git a/src/config/index.ts b/src/config/index.ts index 5cdcf3828d..a927b781ff 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -41,14 +41,5 @@ extend({ USE_PROFILES: { html: true } - }, - - components: (() => { - try { - return COMPONENTS; - - } catch { - return {}; - } - })() + } }); diff --git a/src/config/interface.ts b/src/config/interface.ts index e70641aa87..bd7a469603 100644 --- a/src/config/interface.ts +++ b/src/config/interface.ts @@ -90,6 +90,4 @@ export interface Config extends SuperConfig { * For more information, see `components/directives/safe-html`. */ safeHtml: SanitizedOptions; - - components: typeof COMPONENTS; } diff --git a/src/core/component/accessor/CHANGELOG.md b/src/core/component/accessor/CHANGELOG.md index 0f62609cad..b88e47a051 100644 --- a/src/core/component/accessor/CHANGELOG.md +++ b/src/core/component/accessor/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :house: Internal + +* The observation of accessor dependencies is now initialized only if the accessor has been used at least once + ## v4.0.0-beta.138.dsl-speedup (2024-10-01) #### :rocket: New Feature diff --git a/src/core/component/accessor/index.ts b/src/core/component/accessor/index.ts index 8920cf4634..3b0c4177cf 100644 --- a/src/core/component/accessor/index.ts +++ b/src/core/component/accessor/index.ts @@ -16,10 +16,12 @@ import * as gc from 'core/component/gc'; import { deprecate } from 'core/functools/deprecation'; import { beforeHooks } from 'core/component/const'; +import { getPropertyInfo } from 'core/component/reflect'; + import { getFieldsStore } from 'core/component/field'; import { cacheStatus } from 'core/component/watch'; -import type { ComponentInterface } from 'core/component/interface'; +import type { ComponentInterface, Hook } from 'core/component/interface'; /** * Attaches accessors and computed fields from a component's tied metaobject to the specified component instance. @@ -69,136 +71,234 @@ export function attachAccessorsFromMeta(component: ComponentInterface): void { meta, // eslint-disable-next-line deprecation/deprecation - meta: {params: {deprecatedProps}, fields, tiedFields}, + meta: {params: {deprecatedProps}, tiedFields, hooks}, $destructors } = component.unsafe; const isFunctional = meta.params.functional === true; - Object.entries(meta.accessors).forEach(([name, accessor]) => { + // eslint-disable-next-line guard-for-in + for (const name in meta.accessors) { + const accessor = meta.accessors[name]; + + if (accessor == null) { + continue; + } + + const tiedWith = tiedFields[name]; + + // In the `tiedFields` dictionary, + // the names of the getters themselves are also stored as keys with their related fields as values. + // This is done for convenience. + // However, watchers for the getter observation of the getter will be created for all keys in `tiedFields`. + // Since it's not possible to watch the getter itself, we need to remove the key with its name. + delete tiedFields[name]; + const canSkip = - accessor == null || - component[name] != null || + name in component || !SSR && isFunctional && accessor.functional === false; if (canSkip) { - const tiedWith = tiedFields[name]; - + // If the getter is not initialized, + // then the related fields should also be removed to avoid registering a watcher for the getter observation, + // as it will not be used if (tiedWith != null) { delete tiedFields[tiedWith]; - delete tiedFields[name]; } - return; + continue; } - delete tiedFields[name]; + let getterInitialized = false; + + // eslint-disable-next-line func-style + const get = function get(this: typeof component): unknown { + if (!getterInitialized) { + getterInitialized = true; + + const {watchers, watchDependencies} = meta; + + const deps = watchDependencies.get(name); + + if (deps != null && deps.length > 0 || tiedWith != null) { + onCreated(this.hook, () => { + if (deps != null) { + for (let i = 0; i < deps.length; i++) { + const + dep = deps[i], + path = Object.isArray(dep) ? dep.join('.') : String(dep), + info = getPropertyInfo(path, component); + + // If a computed property has a field or system field as a dependency + // and the host component does not have any watchers to this field, + // we need to register a "fake" watcher to enforce watching + const needForceWatch = + (info.type === 'system' || info.type === 'field') && + + watchers[info.name] == null && + watchers[info.originalPath] == null && + watchers[info.path] == null; + + if (needForceWatch) { + this.$watch(info, {deep: true, immediate: true}, fakeHandler); + } + } + } + + if (tiedWith != null) { + // If a computed property is tied with a field or system field + // and the host component does not have any watchers to this field, + // we need to register a "fake" watcher to enforce watching + const needForceWatch = watchers[tiedWith] == null && accessor.dependencies?.length !== 0; + + if (needForceWatch) { + this.$watch(tiedWith, {deep: true, immediate: true}, fakeHandler); + } + } + }); + } + } + + return accessor.get!.call(this); + }; Object.defineProperty(component, name, { configurable: true, enumerable: true, - get: accessor.get, + get: accessor.get != null ? get : undefined, set: accessor.set }); - }); + } const cachedAccessors = new Set(); - Object.entries(meta.computedFields).forEach(([name, computed]) => { + // eslint-disable-next-line guard-for-in + for (const name in meta.computedFields) { + const computed = meta.computedFields[name]; + + if (computed == null) { + continue; + } + + const tiedWith = tiedFields[name]; + + // In the `tiedFields` dictionary, + // the names of the getters themselves are also stored as keys with their related fields as values. + // This is done for convenience. + // However, watchers for cache invalidation of the getter will be created for all keys in `tiedFields`. + // Since it's not possible to watch the getter itself, we need to remove the key with its name. + delete tiedFields[name]; + const canSkip = - computed == null || - component[name] != null || + name in component || computed.cache === 'auto' || !SSR && isFunctional && computed.functional === false; if (canSkip) { - const tiedWith = tiedFields[name]; - // If the getter is not initialized, // then the related fields should also be removed to avoid registering a watcher for cache invalidation, // as it will not be used if (tiedWith != null) { delete tiedFields[tiedWith]; - delete tiedFields[name]; } - return; + continue; } - // In the `tiedFields` dictionary, - // the names of the getters themselves are also stored as keys with their related fields as values. - // This is done for convenience. - // However, watchers for cache invalidation of the getter will be created for all keys in `tiedFields`. - // Since it's not possible to watch the getter itself, we need to remove the key with its name. - delete tiedFields[name]; + const + canUseForeverCache = computed.cache === 'forever', + effects: Function[] = []; + + let getterInitialized = canUseForeverCache; // eslint-disable-next-line func-style const get = function get(this: typeof component): unknown { - const {unsafe, hook} = this; - - const canUseForeverCache = computed.cache === 'forever'; - - // We should not use the getter's cache until the component is fully created. - // Because until that moment, we cannot track changes to dependent entities and reset the cache when they change. - // This can lead to hard-to-detect errors. - // Please note that in case of forever caching, we cache immediately. - const canUseCache = canUseForeverCache || beforeHooks[hook] == null; - - if (canUseCache && cacheStatus in get) { - // If a getter already has a cached result and is used inside a template, - // it is not possible to track its effect, as the value is not recalculated. - // This can lead to a problem where one of the entities on which the getter depends is updated, - // but the template is not. - // To avoid this problem, we explicitly touch all dependent entities. - // For functional components, this problem does not exist, as no change in state can trigger their re-render. - const needEffect = !canUseForeverCache && !isFunctional && hook !== 'created'; - - if (needEffect) { - meta.watchDependencies.get(name)?.forEach((path) => { - let firstChunk: string; - - if (Object.isString(path)) { - if (path.includes('.')) { - const chunks = path.split('.'); - - firstChunk = path[0]; - - if (chunks.length === 1) { - path = firstChunk; + if (!getterInitialized) { + getterInitialized = true; + + const {watchers, watchDependencies} = meta; + + const deps = watchDependencies.get(name); + + if (deps != null && deps.length > 0 || tiedWith != null) { + onCreated(this.hook, () => { + if (deps != null) { + for (let i = 0; i < deps.length; i++) { + const + dep = deps[i], + path = Object.isArray(dep) ? dep.join('.') : String(dep), + info = getPropertyInfo(path, component); + + // If a getter already has a cached result and is used inside a template, + // it is not possible to track its effect, as the value is not recalculated. + // This can lead to a problem where one of the entities on which the getter depends is updated, + // but the template is not. + // To avoid this problem, we explicitly touch all dependent entities. + // For functional components, this problem does not exist, + // as no change in state can trigger their re-render. + if (!isFunctional && info.type !== 'system') { + effects.push(() => { + const store = info.type === 'field' ? getFieldsStore(Object.cast(info.ctx)) : info.ctx; + + if (info.path.includes('.')) { + void Object.get(store, path); + + } else if (path in store) { + // @ts-ignore (effect) + void store[path]; + } + }); } - } else { - firstChunk = path; - } + // If a computed property has a field or system field as a dependency + // and the host component does not have any watchers to this field, + // we need to register a "fake" watcher to enforce watching + const needToForceWatching = + (info.type === 'system' || info.type === 'field') && - } else { - firstChunk = path[0]; + watchers[info.name] == null && + watchers[info.originalPath] == null && + watchers[info.path] == null; - if (path.length === 1) { - path = firstChunk; + if (needToForceWatching) { + this.$watch(info, {deep: true, immediate: true}, fakeHandler); + } } } - const store = fields[firstChunk] != null ? getFieldsStore(unsafe) : unsafe; + if (tiedWith != null) { + effects.push(() => { + if (tiedWith in this) { + // @ts-ignore (effect) + void this[tiedWith]; + } + }); - if (Object.isArray(path)) { - void Object.get(store, path); + // If a computed property is tied with a field or system field + // and the host component does not have any watchers to this field, + // we need to register a "fake" watcher to enforce watching + const needToForceWatching = watchers[tiedWith] == null && computed.dependencies?.length !== 0; - } else if (path in store) { - // @ts-ignore (effect) - void store[path]; + if (needToForceWatching) { + this.$watch(tiedWith, {deep: true, immediate: true}, fakeHandler); + } } }); + } + } - ['Store', 'Prop'].forEach((postfix) => { - const path = name + postfix; + // We should not use the getter's cache until the component is fully created. + // Because until that moment, we cannot track changes to dependent entities and reset the cache when they change. + // This can lead to hard-to-detect errors. + // Please note that in case of forever caching, we cache immediately. + const canUseCache = canUseForeverCache || beforeHooks[this.hook] == null; - if (path in this) { - // @ts-ignore (effect) - void this[path]; - } - }); + if (canUseCache && cacheStatus in get) { + if (this.hook !== 'created') { + for (let i = 0; i < effects.length; i++) { + effects[i](); + } } return get[cacheStatus]; @@ -220,24 +320,26 @@ export function attachAccessorsFromMeta(component: ComponentInterface): void { get: computed.get != null ? get : undefined, set: computed.set }); - }); + } // Register a worker to clean up memory upon component destruction $destructors.push(() => { // eslint-disable-next-line require-yield gc.add(function* destructor() { - cachedAccessors.forEach((getter) => { + for (const getter of cachedAccessors) { delete getter[cacheStatus]; - }); + } cachedAccessors.clear(); }()); }); if (deprecatedProps != null) { - Object.entries(deprecatedProps).forEach(([name, renamedTo]) => { + for (const name of Object.keys(deprecatedProps)) { + const renamedTo = deprecatedProps[name]; + if (renamedTo == null) { - return; + continue; } Object.defineProperty(component, name, { @@ -253,6 +355,19 @@ export function attachAccessorsFromMeta(component: ComponentInterface): void { component[renamedTo] = val; } }); - }); + } + } + + function fakeHandler() { + // Loopback + } + + function onCreated(hook: Nullable, cb: Function) { + if (hook == null || beforeHooks[hook] != null) { + hooks['before:created'].push({fn: cb}); + + } else { + cb(); + } } } diff --git a/src/core/component/const/symbols.ts b/src/core/component/const/symbols.ts index 8cc5c0efb5..dc36e66a9a 100644 --- a/src/core/component/const/symbols.ts +++ b/src/core/component/const/symbols.ts @@ -11,11 +11,6 @@ */ export const V4_COMPONENT = Symbol('This is a V4Fire component'); -/** - * A symbol used as a flag to mark a function as a generated default wrapper - */ -export const DEFAULT_WRAPPER = Symbol('This function is the generated default wrapper'); - /** * A placeholder object used to refer to the parent instance in a specific context */ diff --git a/src/core/component/decorators/CHANGELOG.md b/src/core/component/decorators/CHANGELOG.md index 0dcd3e7e9a..be21132bae 100644 --- a/src/core/component/decorators/CHANGELOG.md +++ b/src/core/component/decorators/CHANGELOG.md @@ -9,13 +9,27 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :rocket: New Feature + +* Added new decorators, defaultValue and method, for the class-based DSL. + These decorators are used during code generation by the TS transformer DSL. + +* The prop, field, and system decorators can now accept a default value for the field as a second argument. + This argument is used during code generation by the TS transformer DSL. + +#### :house: Internal + +* The decorators from `core/component/decorators` no longer use a single factory module. Now, each decorator is implemented independently. + ## v4.0.0-beta.153 (2024-11-15) #### :bug: Bug Fix * Fixed endless attempts to load a component template that is not in use. Added a 10-second limit for attempts to load the template. -* Default `forceUpdate` param of a property no longer overrides its value inherited from parent component +* Default `forceUpdate` param of a property no longer overrides its value inherited from the parent component * Fixed typo: `"prop"` -> `"props"` when inheriting parent properties ## v4.0.0-beta.144 (2024-10-09) diff --git a/src/core/component/decorators/README.md b/src/core/component/decorators/README.md index 77cf922aac..77f455fcee 100644 --- a/src/core/component/decorators/README.md +++ b/src/core/component/decorators/README.md @@ -22,6 +22,6 @@ export default class bUser extends iBlock { * `@prop` to declare a component's input property (aka "prop"); * `@field` to declare a component's field; * `@system` to declare a component's system field (system field mutations never cause components to re-render); -* `@computed` to attach meta information to a component's computed field or accessor; +* `@computed` to attach metainformation to a component's computed field or accessor; * `@hook` to attach a hook listener; * `@watch` to attach a watcher. diff --git a/src/core/component/decorators/component/README.md b/src/core/component/decorators/component/README.md index 07d60aa106..0584628d9b 100644 --- a/src/core/component/decorators/component/README.md +++ b/src/core/component/decorators/component/README.md @@ -185,11 +185,6 @@ class bLink extends iData { < b-button-functional ``` -### [defaultProps = `true`] - -If set to false, all default values for the input properties of the component will be disregarded. -This parameter may be inherited from the parent component. - ### [deprecatedProps] A dictionary that specifies deprecated component props along with their recommended alternatives. diff --git a/src/core/component/decorators/component/index.ts b/src/core/component/decorators/component/index.ts index 31246bc3b5..d5ca8adf32 100644 --- a/src/core/component/decorators/component/index.ts +++ b/src/core/component/decorators/component/index.ts @@ -33,8 +33,7 @@ import { inheritMods, inheritParams, - attachTemplatesToMeta, - addMethodsToMeta + attachTemplatesToMeta } from 'core/component/meta'; @@ -44,6 +43,8 @@ import { getComponent, ComponentEngine, AsyncComponentOptions } from 'core/compo import { getComponentMods, getInfoFromConstructor } from 'core/component/reflect'; import { registerComponent, registerParentComponents } from 'core/component/init'; +import { registeredComponent } from 'core/component/decorators/const'; + import type { ComponentConstructor, ComponentOptions } from 'core/component/interface'; const logger = log.namespace('core/component'); @@ -77,6 +78,12 @@ const OVERRIDDEN = Symbol('This class is overridden in the child layer'); */ export function component(opts?: ComponentOptions): Function { return (target: ComponentConstructor) => { + if (registeredComponent.event == null) { + return; + } + + const regComponentEvent = registeredComponent.event; + const componentInfo = getInfoFromConstructor(target, opts), componentParams = componentInfo.params, @@ -91,12 +98,6 @@ export function component(opts?: ComponentOptions): Function { Object.defineProperty(componentInfo.parent, OVERRIDDEN, {value: true}); } - // Add information about the layer in which the component is described - // to correctly handle situations where the component is overridden in child layers of the application - const regEvent = `constructor.${componentNormalizedName}.${componentInfo.layer}`; - - initEmitter.emit('bindConstructor', componentNormalizedName, regEvent); - if (isPartial) { pushToInitList(() => { // Partial classes reuse the same metaobject @@ -106,10 +107,6 @@ export function component(opts?: ComponentOptions): Function { meta = createMeta(componentInfo); components.set(componentFullName, meta); } - - initEmitter.once(regEvent, () => { - addMethodsToMeta(components.get(componentFullName)!, target); - }); }); return; @@ -124,9 +121,6 @@ export function component(opts?: ComponentOptions): Function { if (needRegisterImmediate) { registerComponent(componentFullName); - - } else { - requestIdleCallback(registerComponent.bind(null, componentFullName)); } // If we have a smart component, @@ -215,7 +209,7 @@ export function component(opts?: ComponentOptions): Function { components.set(target, meta); } - initEmitter.emit(regEvent, { + initEmitter.emit(regComponentEvent, { meta, parentMeta: componentInfo.parentMeta }); diff --git a/src/core/component/decorators/computed/README.md b/src/core/component/decorators/computed/README.md index 4905a088de..38112444e0 100644 --- a/src/core/component/decorators/computed/README.md +++ b/src/core/component/decorators/computed/README.md @@ -1,6 +1,6 @@ # core/component/decorators/computed -The decorator assigns meta-information to a computed field or an accessor within a component. +The decorator assigns metainformation to a computed field or an accessor within a component. ```typescript import iBlock, {component, prop, computed} from 'components/super/i-block/i-block'; diff --git a/src/core/component/decorators/computed/decorator.ts b/src/core/component/decorators/computed/decorator.ts new file mode 100644 index 0000000000..b5ef9763a4 --- /dev/null +++ b/src/core/component/decorators/computed/decorator.ts @@ -0,0 +1,101 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { createComponentDecorator3, normalizeFunctionalParams } from 'core/component/decorators/helpers'; + +import type { ComponentAccessor } from 'core/component/interface'; + +import type { PartDecorator } from 'core/component/decorators/interface'; + +import type { DecoratorComputed } from 'core/component/decorators/computed/interface'; + +/** + * Assigns metainformation to a computed field or an accessor within a component + * + * @decorator + * @param [params] - an object with accessor parameters + * + * @example + * ```typescript + * import iBlock, { component, computed } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @computed({cache: true}) + * get hashCode(): number { + * return Math.random(); + * } + * } + * ``` + */ +export function computed(params?: DecoratorComputed): PartDecorator { + return createComponentDecorator3(({meta}, accessorName) => { + params = {...params}; + + if (meta.props[accessorName] != null) { + meta.props[accessorName] = undefined; + delete meta.component.props[accessorName]; + } + + if (meta.fields[accessorName] != null) { + meta.fields[accessorName] = undefined; + } + + if (meta.systemFields[accessorName] != null) { + meta.systemFields[accessorName] = undefined; + } + + let cluster: 'accessors' | 'computedFields' = 'accessors'; + + if ( + params.cache === true || + params.cache === 'auto' || + params.cache === 'forever' || + params.cache !== false && (Object.isArray(params.dependencies) || meta.computedFields[accessorName] != null) + ) { + cluster = 'computedFields'; + } + + const store = meta[cluster]; + + let accessor: ComponentAccessor = store[accessorName] ?? { + src: meta.componentName, + cache: false + }; + + const needOverrideComputed = cluster === 'accessors' ? + meta.computedFields[accessorName] != null : + !('cache' in params) && meta.accessors[accessorName] != null; + + if (needOverrideComputed) { + const computed = meta.computedFields[accessorName]; + + accessor = normalizeFunctionalParams({ + ...computed, + ...params, + src: computed?.src ?? accessor.src, + cache: false + }, meta); + + } else { + accessor = normalizeFunctionalParams({ + ...accessor, + ...params, + cache: cluster === 'computedFields' ? params.cache ?? true : false + }, meta); + } + + meta[cluster === 'computedFields' ? 'accessors' : 'computedFields'][accessorName] = undefined; + + store[accessorName] = accessor; + + if (params.dependencies != null && params.dependencies.length > 0) { + meta.watchDependencies.set(accessorName, params.dependencies); + } + }); +} diff --git a/src/core/component/decorators/computed/index.ts b/src/core/component/decorators/computed/index.ts index 8c5350e17e..6cdb4c08d7 100644 --- a/src/core/component/decorators/computed/index.ts +++ b/src/core/component/decorators/computed/index.ts @@ -11,25 +11,5 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/factory'; -import type { DecoratorComponentAccessor } from 'core/component/decorators/interface'; - -/** - * Assigns meta-information to a computed field or an accessor within a component - * - * @decorator - * - * @example - * ```typescript - * import iBlock, { component, computed } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @computed({cache: true}) - * get hashCode(): number { - * return Math.random(); - * } - * } - * ``` - */ -export const computed = paramsFactory(null); +export * from 'core/component/decorators/computed/decorator'; +export * from 'core/component/decorators/computed/interface'; diff --git a/src/core/component/decorators/interface/accessor.ts b/src/core/component/decorators/computed/interface.ts similarity index 94% rename from src/core/component/decorators/interface/accessor.ts rename to src/core/component/decorators/computed/interface.ts index 0d332d59c2..5d9403b764 100644 --- a/src/core/component/decorators/interface/accessor.ts +++ b/src/core/component/decorators/computed/interface.ts @@ -6,10 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { WatchPath, ComponentAccessorCacheType } from 'core/component/interface'; -import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; +import type { WatchPath } from 'core/object/watch'; -export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { +import type { ComponentAccessorCacheType } from 'core/component/interface'; +import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface'; + +export interface DecoratorComputed extends DecoratorFunctionalOptions { /** * If set to true, the accessor value will be cached after the first touch. * diff --git a/src/core/component/decorators/const.ts b/src/core/component/decorators/const.ts index b297c7844f..aaf31b5013 100644 --- a/src/core/component/decorators/const.ts +++ b/src/core/component/decorators/const.ts @@ -6,13 +6,24 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export const invertedFieldMap = Object.createDict({ - props: ['fields', 'systemFields'], - fields: ['props', 'systemFields'], - systemFields: ['props', 'fields'] -}); +import type { RegisteredComponent } from 'core/component/decorators/interface'; -export const tiedFieldMap = Object.createDict({ - fields: true, - systemFields: true -}); +// Descriptor of the currently registered DSL component. +// It is initialized for each component file during the project's build phase. +// ```typescript +// import { registeredComponent } from 'core/component/decorators/const'; +// +// import iBlock, { component } from 'components/super/i-block/i-block'; +// +// registeredComponent.name = 'bExample'; +// registeredComponent.layer = '@v4fire/client'; +// registeredComponent.event = 'constructor.b-example.@v4fire/client'; +// +// @component() +// class bExample extends iBlock {} +// ``` +export const registeredComponent: RegisteredComponent = { + name: undefined, + layer: undefined, + event: undefined +}; diff --git a/src/core/component/decorators/default-value/README.md b/src/core/component/decorators/default-value/README.md new file mode 100644 index 0000000000..609e3ffa9f --- /dev/null +++ b/src/core/component/decorators/default-value/README.md @@ -0,0 +1,22 @@ +# core/component/decorators/default-value + +The decorator sets a default value for any prop or field of a component. + +Typically, this decorator does not need to be used explicitly, +as it will be automatically added in the appropriate places during the build process. + +```typescript +import { defaultValue } from 'core/component/decorators/default-value'; +import iBlock, { component, prop, system } from 'components/super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @defaultValue(0) + @prop(Number) + id!: number; + + @defaultValue(() => ({})) + @system() + opts: Dictionary; +} +``` diff --git a/src/core/component/decorators/default-value/decorator.ts b/src/core/component/decorators/default-value/decorator.ts new file mode 100644 index 0000000000..06e67a8428 --- /dev/null +++ b/src/core/component/decorators/default-value/decorator.ts @@ -0,0 +1,80 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { regProp } from 'core/component/decorators/prop'; +import { regField } from 'core/component/decorators/system'; + +import { createComponentDecorator3 } from 'core/component/decorators/helpers'; + +import type { PartDecorator } from 'core/component/decorators/interface'; + +/** + * Sets a default value for the specified prop or component field. + * + * Typically, this decorator does not need to be used explicitly, + * as it will be automatically added in the appropriate places during the build process. + * + * @decorator + * @param [getter] - a function that returns the default value for a prop or field + * + * @example + * ```typescript + * import { defaultValue } from 'core/component/decorators/default-value'; + * import iBlock, { component, prop, system } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @defaultValue(0) + * @prop(Number) + * id!: number; + * + * @defaultValue(() => ({})) + * @system() + * opts: Dictionary; + * } + * ``` + */ +export function defaultValue(getter: unknown): PartDecorator { + return createComponentDecorator3(({meta}, key) => { + const isFunction = Object.isFunction(getter); + + if (meta.props[key] != null) { + regProp(key, {default: isFunction ? getter() : getter}, meta); + + } else { + const + isField = key in meta.fields, + isSystemField = !isField && key in meta.systemFields; + + if (isField || isSystemField) { + const cluster = isField ? 'fields' : 'systemFields'; + + const params = isFunction ? + {init: getter, default: undefined} : + {init: undefined, default: getter}; + + regField(key, cluster, params, meta); + + } else if (isFunction) { + // Registration of methods that are described as properties, such as: + // ``` + // on: typeof this['selfEmitter']['on'] = + // function on(this: iBlockEvent, event: string, handler: Function, opts?: AsyncOptions): object { + // return this.selfEmitter.on(event, handler, opts); + // }; + // ``` + Object.defineProperty(meta.constructor.prototype, key, { + configurable: true, + enumerable: false, + writable: true, + value: getter() + }); + } + } + }); +} diff --git a/src/core/component/decorators/default-value/index.ts b/src/core/component/decorators/default-value/index.ts new file mode 100644 index 0000000000..7819c16673 --- /dev/null +++ b/src/core/component/decorators/default-value/index.ts @@ -0,0 +1,14 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/decorators/default-value/README.md]] + * @packageDocumentation + */ + +export * from 'core/component/decorators/default-value/decorator'; diff --git a/src/core/component/decorators/factory.ts b/src/core/component/decorators/factory.ts deleted file mode 100644 index f1bb699ac8..0000000000 --- a/src/core/component/decorators/factory.ts +++ /dev/null @@ -1,303 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { defProp } from 'core/const/props'; - -import { isStore } from 'core/component/reflect'; -import { initEmitter } from 'core/component/event'; - -import { componentDecoratedKeys } from 'core/component/const'; -import { invertedFieldMap, tiedFieldMap } from 'core/component/decorators/const'; - -import type { ComponentMeta, ComponentProp, ComponentField } from 'core/component/interface'; - -import type { - - DecoratorFunctionalOptions, - ParamsFactoryTransformer, - FactoryTransformer - -} from 'core/component/decorators/interface'; - -/** - * Factory for creating component property decorators - * - * @param cluster - the property cluster to decorate, like `fields` or `systemFields` - * @param [transformer] - a transformer for the passed decorator parameters - */ -export function paramsFactory( - cluster: Nullable, - transformer?: ParamsFactoryTransformer -): FactoryTransformer { - return (params: Dictionary = {}) => (_: object, key: string, desc?: PropertyDescriptor) => { - initEmitter.once('bindConstructor', (componentName, regEvent) => { - const decoratedKeys = componentDecoratedKeys[componentName] ?? new Set(); - componentDecoratedKeys[componentName] = decoratedKeys; - - decoratedKeys.add(key); - initEmitter.once(regEvent, decorate); - }); - - function decorate({meta}: {meta: ComponentMeta}): void { - delete meta.tiedFields[key]; - - let p = params; - - if (desc != null) { - decorateMethodOrAccessor(); - - } else { - decorateProperty(); - } - - function decorateMethodOrAccessor() { - if (desc == null) { - return; - } - - delete meta.props[key]; - delete meta.fields[key]; - delete meta.systemFields[key]; - - let metaKey: string; - - if (cluster != null) { - metaKey = cluster; - - } else if ('value' in desc) { - metaKey = 'methods'; - - } else if ( - p.cache === true || - p.cache === 'auto' || - p.cache === 'forever' || - p.cache !== false && (Object.isArray(p.dependencies) || key in meta.computedFields) - ) { - metaKey = 'computedFields'; - - } else { - metaKey = 'accessors'; - } - - if (transformer) { - p = transformer(p, metaKey); - } - - const - metaCluster = meta[metaKey], - info = metaCluster[key] ?? {src: meta.componentName}; - - if (metaKey === 'methods') { - decorateMethod(); - - } else { - decorateAccessor(); - } - - function decorateMethod() { - const name = key; - - let {watchers, hooks} = info; - - if (p.watch != null) { - watchers ??= {}; - - Array.toArray(p.watch).forEach((watcher) => { - if (Object.isPlainObject(watcher)) { - const path = String(watcher.path ?? watcher.field); - watchers[path] = wrapOpts({...p.watchParams, ...watcher, path}); - - } else { - watchers[watcher] = wrapOpts({...p.watchParams, path: watcher}); - } - }); - } - - if (p.hook != null) { - hooks ??= {}; - - Array.toArray(p.hook).forEach((hook) => { - if (Object.isSimpleObject(hook)) { - const - hookName = Object.keys(hook)[0], - hookInfo = hook[hookName]; - - hooks[hookName] = wrapOpts({ - ...hookInfo, - name, - hook: hookName, - after: hookInfo.after != null ? new Set([].concat(hookInfo.after)) : undefined - }); - - } else { - hooks[hook] = wrapOpts({name, hook}); - } - }); - } - - metaCluster[key] = wrapOpts({...info, ...p, watchers, hooks}); - } - - function decorateAccessor() { - delete meta.accessors[key]; - delete meta.computedFields[key]; - - const needOverrideComputed = metaKey === 'accessors' ? - key in meta.computedFields : - !('cache' in p) && key in meta.accessors; - - if (needOverrideComputed) { - metaCluster[key] = wrapOpts({...meta.computedFields[key], ...p, cache: false}); - - } else { - metaCluster[key] = wrapOpts({ - ...info, - ...p, - cache: metaKey === 'computedFields' ? p.cache ?? true : false - }); - } - - if (p.dependencies != null && p.dependencies.length > 0) { - meta.watchDependencies.set(key, p.dependencies); - } - } - } - - function decorateProperty() { - delete meta.methods[key]; - delete meta.accessors[key]; - delete meta.computedFields[key]; - - const accessors = meta.accessors[key] ? - meta.accessors : - meta.computedFields; - - if (accessors[key]) { - Object.defineProperty(meta.constructor.prototype, key, defProp); - delete accessors[key]; - } - - const - metaKey = cluster ?? (key in meta.props ? 'props' : 'fields'), - metaCluster: ComponentProp | ComponentField = meta[metaKey]; - - inheritFromParent(); - - if (transformer != null) { - p = transformer(p, metaKey); - } - - const info = metaCluster[key] ?? {src: meta.componentName}; - - let {watchers, after} = info; - - if (p.after != null) { - after = new Set([].concat(p.after)); - } - - if (p.watch != null) { - Array.toArray(p.watch).forEach((watcher) => { - watchers ??= new Map(); - - if (Object.isPlainObject(watcher)) { - watchers.set(watcher.handler ?? watcher.fn, wrapOpts({...watcher, handler: watcher.handler})); - - } else { - watchers.set(watcher, wrapOpts({handler: watcher})); - } - }); - } - - const desc = wrapOpts({ - ...info, - ...p, - - after, - watchers, - - meta: { - ...info.meta, - ...p.meta - } - }); - - if (metaKey === 'props') { - desc.forceUpdate ??= true; - } - - metaCluster[key] = desc; - - if (metaKey === 'props' && desc.forceUpdate === false) { - // A special system property used to observe props with the option `forceUpdate: false`. - // This is because `forceUpdate: false` props are passed as attributes, - // i.e., they are accessible via `$attrs`. - // Moreover, all such attributes are readonly for the component. - // However, we need a system property that will be synchronized with this attribute - // and will update whenever this attribute is updated from the outside. - // Therefore, we introduce a special private system field formatted as `[[${fieldName}]]`. - meta.systemFields[`[[${key}]]`] = { - ...info, - watchers, - - meta: { - ...info.meta, - ...p.meta - } - }; - } - - if (tiedFieldMap[metaKey] != null && isStore.test(key)) { - const tiedWith = isStore.replace(key); - meta.tiedFields[key] = tiedWith; - meta.tiedFields[tiedWith] = key; - } - - function inheritFromParent() { - const invertedMetaKeys: CanUndef = invertedFieldMap[metaKey]; - - invertedMetaKeys?.some((invertedMetaKey) => { - const invertedMetaCluster = meta[invertedMetaKey]; - - if (key in invertedMetaCluster) { - const info = {...invertedMetaCluster[key]}; - delete info.functional; - - if (invertedMetaKey === 'props') { - if (Object.isFunction(info.default)) { - (info).init = info.default; - delete info.default; - } - - } else if (metaKey === 'props') { - delete (info).init; - } - - metaCluster[key] = info; - delete invertedMetaCluster[key]; - - return true; - } - - return false; - }); - } - } - - function wrapOpts(opts: T): T { - const p = meta.params; - - // eslint-disable-next-line eqeqeq - if (opts.functional === undefined && p.functional === null) { - opts.functional = false; - } - - return opts; - } - } - }; -} diff --git a/src/core/component/decorators/field/decorator.ts b/src/core/component/decorators/field/decorator.ts new file mode 100644 index 0000000000..699935db4d --- /dev/null +++ b/src/core/component/decorators/field/decorator.ts @@ -0,0 +1,41 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { PartDecorator } from 'core/component/decorators/interface'; + +import { system } from 'core/component/decorators/system'; +import type { InitFieldFn, DecoratorField } from 'core/component/decorators/field/interface'; + +/** + * Marks a class property as a component field. + * In non-functional components, field property mutations typically cause the component to re-render. + * + * @decorator + * @param [initOrParams] - a function to initialize the field value or an object with field parameters + * @param [initOrDefault] - a function to initialize the field value or the field default value + * + * @example + * ```typescript + * import iBlock, { component, field } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @field() + * bla: number = 0; + * + * @field(() => Math.random()) + * baz?: number; + * } + * ``` + */ +export function field( + initOrParams?: InitFieldFn | DecoratorField, + initOrDefault?: InitFieldFn | DecoratorField['default'] +): PartDecorator { + return system(initOrParams, initOrDefault, 'fields'); +} diff --git a/src/core/component/decorators/field/index.ts b/src/core/component/decorators/field/index.ts index 1f40a3376b..5db0cd9f7a 100644 --- a/src/core/component/decorators/field/index.ts +++ b/src/core/component/decorators/field/index.ts @@ -11,33 +11,5 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/factory'; -import type { InitFieldFn, DecoratorField } from 'core/component/decorators/interface'; - -/** - * Marks a class property as a component field. - * In non-functional components, field property mutations typically cause the component to re-render. - * - * @decorator - * - * @example - * ```typescript - * import iBlock, { component, field } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @field() - * bla: number = 0; - * - * @field(() => Math.random()) - * baz?: number; - * } - * ``` - */ -export const field = paramsFactory('fields', (p) => { - if (Object.isFunction(p)) { - return {init: p}; - } - - return p; -}); +export * from 'core/component/decorators/field/decorator'; +export * from 'core/component/decorators/field/interface'; diff --git a/src/core/component/field/interface.ts b/src/core/component/decorators/field/interface.ts similarity index 54% rename from src/core/component/field/interface.ts rename to src/core/component/decorators/field/interface.ts index d8b501cb03..328e4a8aab 100644 --- a/src/core/component/field/interface.ts +++ b/src/core/component/decorators/field/interface.ts @@ -6,6 +6,4 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentField } from 'core/component/interface'; - -export type SortedFields = Array<[string, CanUndef]>; +export * from 'core/component/decorators/system'; diff --git a/src/core/component/decorators/helpers.ts b/src/core/component/decorators/helpers.ts new file mode 100644 index 0000000000..2621219059 --- /dev/null +++ b/src/core/component/decorators/helpers.ts @@ -0,0 +1,88 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { initEmitter } from 'core/component/event'; + +import type { ComponentMeta } from 'core/component/meta'; + +import { registeredComponent } from 'core/component/decorators/const'; + +import type { + + PartDecorator, + + ComponentPartDecorator3, + ComponentPartDecorator4, + + ComponentDescriptor, + DecoratorFunctionalOptions + +} from 'core/component/decorators/interface'; + +/** + * Creates a decorator for a component's property or method based on the provided decorator function. + * The decorator function expects three input arguments (excluding the object descriptor). + * + * @param decorator + */ +export function createComponentDecorator3(decorator: ComponentPartDecorator3): PartDecorator { + return (proto: object, partKey: string) => { + createComponentDecorator(decorator, partKey, undefined, proto); + }; +} + +/** + * Creates a decorator for a component's property or method based on the provided decorator function. + * The decorator function expects four input arguments (including the object descriptor). + * + * @param decorator + */ +export function createComponentDecorator4(decorator: ComponentPartDecorator4): PartDecorator { + return (proto: object, partKey: string, partDesc?: PropertyDescriptor) => { + createComponentDecorator(decorator, partKey, partDesc, proto); + }; +} + +function createComponentDecorator( + decorator: ComponentPartDecorator3 | ComponentPartDecorator4, + partKey: string, + partDesc: CanUndef, + proto: object +): void { + if (registeredComponent.event == null) { + return; + } + + initEmitter.once(registeredComponent.event, (componentDesc: ComponentDescriptor) => { + if (decorator.length <= 3) { + (decorator)(componentDesc, partKey, proto); + + } else { + (decorator)(componentDesc, partKey, partDesc, proto); + } + }); +} + +/** + * Accepts decorator parameters and a component metaobject, + * and normalizes the value of the functional option based on these parameters + * + * @param params + * @param meta + */ +export function normalizeFunctionalParams( + params: T, + meta: ComponentMeta +): T { + // eslint-disable-next-line eqeqeq + if (params.functional === undefined && meta.params.functional === null) { + params.functional = false; + } + + return params; +} diff --git a/src/core/component/decorators/hook/README.md b/src/core/component/decorators/hook/README.md index 561073c548..eaae6b696d 100644 --- a/src/core/component/decorators/hook/README.md +++ b/src/core/component/decorators/hook/README.md @@ -55,8 +55,7 @@ However, to use some methods before the `created` hook, the [[iBlock]] class has ``` @hook('beforeRuntime') protected initBaseAPI() { - const - i = this.instance; + const i = this.instance; this.syncStorageState = i.syncStorageState.bind(this); this.syncRouterState = i.syncRouterState.bind(this); @@ -243,7 +242,7 @@ then the rest will wait for its resolving to preserve the initialization order. ### [after] -A method name or a list of names after which this handler should be invoked on a registered hook event. +A method name or a list of method names after which this handler should be invoked during a registered hook event. ```typescript import iBlock, { component, hook } from 'components/super/i-block/i-block'; diff --git a/src/core/component/decorators/hook/decorator.ts b/src/core/component/decorators/hook/decorator.ts new file mode 100644 index 0000000000..ef7deb93d4 --- /dev/null +++ b/src/core/component/decorators/hook/decorator.ts @@ -0,0 +1,98 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { createComponentDecorator3, normalizeFunctionalParams } from 'core/component/decorators/helpers'; + +import type { ComponentMethod } from 'core/component/interface'; + +import type { PartDecorator } from 'core/component/decorators/interface'; + +import type { DecoratorHook } from 'core/component/decorators/hook/interface'; + +/** + * Attaches a hook listener to a component method. + * This means that when the component switches to the specified hook(s), the method will be called. + * + * @decorator + * @param [hook] - the hook name, an array of hooks, or an object with hook parameters + * + * @example + * ```typescript + * import iBlock, { component, hook } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @hook('mounted') + * onMounted() { + * + * } + * } + * ``` + */ +export function hook(hook: DecoratorHook): PartDecorator { + return createComponentDecorator3(({meta}, methodName) => { + const methodHooks = Array.toArray(hook); + + let method: ComponentMethod; + + const alreadyDefined = meta.methods.hasOwnProperty(methodName); + + if (alreadyDefined) { + method = meta.methods[methodName]!; + + } else { + const parent = meta.methods[methodName]; + + if (parent != null) { + method = { + ...parent, + src: meta.componentName + }; + + Object.assign(method, parent); + + if (parent.hooks != null) { + method.hooks = Object.create(parent.hooks); + } + + } else { + method = { + src: meta.componentName, + fn: Object.throw + }; + } + } + + const {hooks = {}} = method; + + for (const hook of methodHooks) { + if (Object.isSimpleObject(hook)) { + const + hookName = Object.keys(hook)[0], + hookParams = hook[hookName]; + + hooks[hookName] = normalizeFunctionalParams({ + ...hookParams, + name: methodName, + hook: hookName, + after: hookParams.after != null ? new Set(Array.toArray(hookParams.after)) : undefined + }, meta); + + } else { + hooks[hook] = normalizeFunctionalParams({name: methodName, hook}, meta); + } + } + + if (alreadyDefined) { + method.hooks = hooks; + + } else { + meta.methods[methodName] = normalizeFunctionalParams({...method, hooks}, meta); + } + }); +} diff --git a/src/core/component/decorators/hook/index.ts b/src/core/component/decorators/hook/index.ts index 216689bf71..754593fa2b 100644 --- a/src/core/component/decorators/hook/index.ts +++ b/src/core/component/decorators/hook/index.ts @@ -11,26 +11,5 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/factory'; -import type { DecoratorHook } from 'core/component/decorators/interface'; - -/** - * Attaches a hook listener to a component method. - * This means that when the component switches to the specified hook(s), the method will be called. - * - * @decorator - * - * @example - * ```typescript - * import iBlock, { component, hook } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @hook('mounted') - * onMounted() { - * - * } - * } - * ``` - */ -export const hook = paramsFactory(null, (hook) => ({hook})); +export * from 'core/component/decorators/hook/decorator'; +export * from 'core/component/decorators/hook/interface'; diff --git a/src/core/component/decorators/interface/hook.ts b/src/core/component/decorators/hook/interface.ts similarity index 86% rename from src/core/component/decorators/interface/hook.ts rename to src/core/component/decorators/hook/interface.ts index 3000cf94cf..938b220d3d 100644 --- a/src/core/component/decorators/interface/hook.ts +++ b/src/core/component/decorators/hook/interface.ts @@ -7,7 +7,7 @@ */ import type { Hook } from 'core/component/interface'; -import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; +import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface'; export type DecoratorHook = CanArray | @@ -16,7 +16,8 @@ export type DecoratorHook = export type DecoratorHookOptions = { [hook in Hook]?: DecoratorFunctionalOptions & { /** - * A method name or a list of names after which this handler should be invoked on a registered hook event + * A method name or a list of method names after which + * this handler should be invoked during a registered hook event * * @example * ```typescript diff --git a/src/core/component/decorators/index.ts b/src/core/component/decorators/index.ts index c8f0e2d071..bd4395b37e 100644 --- a/src/core/component/decorators/index.ts +++ b/src/core/component/decorators/index.ts @@ -12,7 +12,6 @@ */ export * from 'core/component/decorators/component'; -export * from 'core/component/decorators/factory'; export * from 'core/component/decorators/prop'; export * from 'core/component/decorators/field'; @@ -22,4 +21,5 @@ export * from 'core/component/decorators/computed'; export * from 'core/component/decorators/hook'; export * from 'core/component/decorators/watch'; +export * from 'core/component/decorators/const'; export * from 'core/component/decorators/interface'; diff --git a/src/core/component/decorators/interface.ts b/src/core/component/decorators/interface.ts new file mode 100644 index 0000000000..f1b803b244 --- /dev/null +++ b/src/core/component/decorators/interface.ts @@ -0,0 +1,40 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentMeta } from 'core/component/meta'; + +export interface DecoratorFunctionalOptions { + /** + * If set to false, this value can't be used with a functional component + * @default `true` + */ + functional?: boolean; +} + +export interface ComponentDescriptor { + meta: ComponentMeta; + parentMeta: CanNull; +} + +export interface ComponentPartDecorator3 { + (component: ComponentDescriptor, partKey: string, proto: object): void; +} + +export interface ComponentPartDecorator4 { + (component: ComponentDescriptor, partKey: string, partDesc: CanUndef, proto: object): void; +} + +export interface PartDecorator { + (target: object, partKey: string, partDesc?: PropertyDescriptor): void; +} + +export interface RegisteredComponent { + name?: string; + layer?: string; + event?: string; +} diff --git a/src/core/component/decorators/interface/index.ts b/src/core/component/decorators/interface/index.ts deleted file mode 100644 index f5bb1431a3..0000000000 --- a/src/core/component/decorators/interface/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export * from 'core/component/decorators/interface/watcher'; -export * from 'core/component/decorators/interface/hook'; -export * from 'core/component/decorators/interface/prop'; -export * from 'core/component/decorators/interface/field'; -export * from 'core/component/decorators/interface/method'; -export * from 'core/component/decorators/interface/accessor'; -export * from 'core/component/decorators/interface/types'; diff --git a/src/core/component/decorators/interface/method.ts b/src/core/component/decorators/interface/method.ts deleted file mode 100644 index 1b58419ee6..0000000000 --- a/src/core/component/decorators/interface/method.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { ComponentInterface, MethodWatcher } from 'core/component/interface'; -import type { DecoratorHook } from 'core/component/decorators/interface/hook'; - -export interface DecoratorMethod { - /** - * A path specifies the property to watch, or you can provide a list of such paths. - * Whenever a mutation occurs in any one of the specified properties, this method will be invoked. - * You can also set additional parameters for watching. - * - * The `core/watch` module is used to make objects watchable. - * Therefore, for more information, please refer to its documentation. - * - * @example - * ```typescript - * import iBlock, { component, field, system, watch } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @system() - * i: number = 0; - * - * @field() - * opts: Dictionary = {a: {b: 1}}; - * - * @watch(['i', {path: 'opts.a.b', flush: 'sync'}]) - * onIncrement(val, oldVal, info) { - * console.log(val, oldVal, info); - * } - * } - * ``` - */ - watch?: DecoratorMethodWatcher; - - /** - * Watch parameters that are applied for all watchers. - * - * The `core/watch` module is used to make objects watchable. - * Therefore, for more information, please refer to its documentation. - */ - watchParams?: MethodWatcher; - - /** - * A component lifecycle hook or a list of such hooks on which this method should be called - */ - hook?: DecoratorHook; -} - -export type DecoratorMethodWatcher = - string | - MethodWatcher | - Array>; diff --git a/src/core/component/decorators/interface/types.ts b/src/core/component/decorators/interface/types.ts deleted file mode 100644 index 52658aa0be..0000000000 --- a/src/core/component/decorators/interface/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export interface DecoratorFunctionalOptions { - /** - * If set to false, this value can't be used with a functional component - * @default `true` - */ - functional?: boolean; -} - -export interface ParamsFactoryTransformer { - (params: object, cluster: string): Dictionary; -} - -export interface FactoryTransformer { - (params?: T): Function; -} diff --git a/src/core/component/decorators/method/README.md b/src/core/component/decorators/method/README.md new file mode 100644 index 0000000000..6c48d54892 --- /dev/null +++ b/src/core/component/decorators/method/README.md @@ -0,0 +1,24 @@ +# core/component/decorators/method + +The decorator marks a class method or accessor as a component part. + +Typically, this decorator does not need to be used explicitly, +as it will be automatically added in the appropriate places during the build process. + +```typescript +import { method } from 'core/component/decorators/method'; +import iBlock, { component, prop, system } from 'components/super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @method('accessor') + get answer() { + return 42; + } + + @method('method') + just() { + return 'do it'; + } +} +``` diff --git a/src/core/component/decorators/method/decorator.ts b/src/core/component/decorators/method/decorator.ts new file mode 100644 index 0000000000..3dbd28c2b3 --- /dev/null +++ b/src/core/component/decorators/method/decorator.ts @@ -0,0 +1,253 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { defProp } from 'core/const/props'; + +import { createComponentDecorator3 } from 'core/component/decorators/helpers'; + +import type { ComponentMeta } from 'core/component/meta'; +import type { ComponentAccessor, ComponentMethod } from 'core/component/interface'; + +import type { PartDecorator } from 'core/component/decorators/interface'; +import type { MethodType } from 'core/component/decorators/method/interface'; + +/** + * Marks a class method or accessor as a component part. + * + * Typically, this decorator does not need to be used explicitly, + * as it will be automatically added in the appropriate places during the build process. + * + * @decorator + * @param type - the type of the member: `method` or `accessor` + * + * @example + * ```typescript + * import { method } from 'core/component/decorators/method'; + * import iBlock, { component, prop, system } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @method('accessor') + * get answer() { + * return 42; + * } + * + * @method('method') + * just() { + * return 'do it'; + * } + * } + * ``` + */ +export function method(type: MethodType): PartDecorator { + return createComponentDecorator3((desc, name, proto) => { + regMethod(name, type, desc.meta, proto); + }); +} + +/** + * Registers a method or accessor in the specified metaobject + * + * @param name - the name of the method or accessor to be registered + * @param type - the type of the member: `method` or `accessor` + * @param meta - the metaobject where the member is registered + * @param proto - the prototype of the class where the method or accessor is defined + */ +export function regMethod(name: string, type: MethodType, meta: ComponentMeta, proto: object): void { + const {componentName: src} = meta; + + if (type === 'method') { + regMethod(); + + } else { + regAccessor(); + } + + function regMethod() { + const {methods} = meta; + + const fn = proto[name]; + + let method: ComponentMethod; + + if (methods.hasOwnProperty(name)) { + method = methods[name]!; + method.fn = fn; + + } else { + const parent = methods[name]; + + if (parent != null) { + method = {...parent, src, fn}; + + if (parent.hooks != null) { + method.hooks = Object.create(parent.hooks); + } + + if (parent.watchers != null) { + method.watchers = Object.create(parent.watchers); + } + + } else { + method = {src, fn}; + } + } + + methods[name] = method; + + const {hooks, watchers} = method; + + if (hooks != null || watchers != null) { + meta.metaInitializers.set(name, (meta) => { + const isFunctional = meta.params.functional === true; + + if (hooks != null) { + // eslint-disable-next-line guard-for-in + for (const hookName in hooks) { + const hook = hooks[hookName]; + + if (hook == null || isFunctional && hook.functional === false) { + continue; + } + + meta.hooks[hookName].push({...hook, fn}); + } + } + + if (watchers != null) { + // eslint-disable-next-line guard-for-in + for (const watcherName in watchers) { + const watcher = watchers[watcherName]; + + if (watcher == null || isFunctional && watcher.functional === false) { + continue; + } + + const watcherListeners = meta.watchers[watcherName] ?? []; + meta.watchers[watcherName] = watcherListeners; + + watcherListeners.push({ + ...watcher, + method: name, + args: Array.toArray(watcher.args), + handler: fn + }); + } + } + }); + } + } + + function regAccessor() { + const desc = Object.getOwnPropertyDescriptor(proto, name); + + if (desc == null) { + return; + } + + const {props, fields, systemFields} = meta; + + const + propKey = `${name}Prop`, + storeKey = `${name}Store`; + + let + type: 'accessors' | 'computedFields' = 'accessors', + tiedWith: CanNull = null; + + // Computed fields are cached by default + if ( + meta.computedFields[name] != null || + meta.accessors[name] == null && (tiedWith = props[propKey] ?? fields[storeKey] ?? systemFields[storeKey]) + ) { + type = 'computedFields'; + } + + let field: Dictionary; + + if (props[name] != null) { + field = props; + + } else if (fields[name] != null) { + field = fields; + + } else { + field = systemFields; + } + + const store = meta[type]; + + // If we already have a property by this key, like a prop or field, + // we need to delete it to correct override + if (field[name] != null) { + Object.defineProperty(proto, name, defProp); + field[name] = undefined; + } + + const + old = store[name], + + // eslint-disable-next-line @v4fire/unbound-method + set = desc.set ?? old?.set, + + // eslint-disable-next-line @v4fire/unbound-method + get = desc.get ?? old?.get; + + // To use `super` within the setter, we also create a new method with a name `${key}Setter` + if (set != null) { + const nm = `${name}Setter`; + + proto[nm] = set; + meta.methods[nm] = {src, fn: set, accessor: true}; + } + + // To using `super` within the getter, we also create a new method with a name `${key}Getter` + if (get != null) { + const nm = `${name}Getter`; + + proto[nm] = get; + meta.methods[nm] = {src, fn: get, accessor: true}; + } + + let accessor: ComponentAccessor; + + if (store.hasOwnProperty(name)) { + accessor = store[name]!; + + } else { + const parent = store[name]; + + accessor = {src, cache: false}; + + if (parent != null) { + Object.assign(accessor, parent); + } + } + + accessor.get = get; + accessor.set = set; + + store[name] = accessor; + + if (accessor.cache === 'auto') { + meta.component.computed[name] = { + get: accessor.get, + set: accessor.set + }; + } + + // eslint-disable-next-line eqeqeq + if (accessor.functional === undefined && meta.params.functional === null) { + accessor.functional = false; + } + + if (tiedWith != null) { + accessor.tiedWith = tiedWith; + } + } +} diff --git a/src/core/component/decorators/method/index.ts b/src/core/component/decorators/method/index.ts new file mode 100644 index 0000000000..8f76300c5d --- /dev/null +++ b/src/core/component/decorators/method/index.ts @@ -0,0 +1,15 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/decorators/method/README.md]] + * @packageDocumentation + */ + +export * from 'core/component/decorators/method/decorator'; +export * from 'core/component/decorators/method/interface'; diff --git a/src/core/component/decorators/method/interface.ts b/src/core/component/decorators/method/interface.ts new file mode 100644 index 0000000000..f47824c399 --- /dev/null +++ b/src/core/component/decorators/method/interface.ts @@ -0,0 +1,9 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export type MethodType = 'method' | 'accessor'; diff --git a/src/core/component/decorators/prop/README.md b/src/core/component/decorators/prop/README.md index ebcd2f46ae..5ecaac26bc 100644 --- a/src/core/component/decorators/prop/README.md +++ b/src/core/component/decorators/prop/README.md @@ -392,9 +392,3 @@ class bExample extends iBlock { ### [functional = `true`] If set to false, the prop can't be passed to a functional component. - -### [forceDefault = `false`] - -If set to true, the prop always uses its own default value when needed. -This option is actually used when the `defaultProps` property is set to false for the described component -(via the `@component` decorator) and we want to override this behavior for a particular prop. diff --git a/src/core/component/decorators/prop/decorator.ts b/src/core/component/decorators/prop/decorator.ts new file mode 100644 index 0000000000..c083f9fff2 --- /dev/null +++ b/src/core/component/decorators/prop/decorator.ts @@ -0,0 +1,268 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { defProp } from 'core/const/props'; + +import { isBinding } from 'core/component/reflect'; +import { createComponentDecorator3, normalizeFunctionalParams } from 'core/component/decorators/helpers'; + +import type { ComponentMeta } from 'core/component/meta'; +import type { ComponentProp, ComponentField } from 'core/component/interface'; + +import type { PartDecorator } from 'core/component/decorators/interface'; +import type { DecoratorProp, PropType } from 'core/component/decorators/prop/interface'; + +/** + * Marks a class property as a component prop + * + * @decorator + * @param [typeOrParams] - a constructor of the prop type or an object with prop parameters + * @param [defaultValue] - default prop value + * + * @example + * ```typescript + * import iBlock, { component, prop } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @prop(Number) + * bla: number = 0; + * + * @prop({type: Number, required: false}) + * baz?: number; + * + * @prop({type: Number, default: () => Math.random()}) + * bar!: number; + * } + * ``` + */ +export function prop( + typeOrParams?: PropType | DecoratorProp, + defaultValue?: DecoratorProp['default'] +): PartDecorator { + return createComponentDecorator3((desc, propName) => { + const hasDefault = Object.isDictionary(typeOrParams) && 'default' in typeOrParams; + + let params = typeOrParams; + + if (defaultValue !== undefined && !hasDefault) { + if (Object.isDictionary(params)) { + params.default = defaultValue; + + } else { + params = {default: defaultValue}; + + if (typeOrParams !== undefined) { + params.type = typeOrParams; + } + } + } + + regProp(propName, params, desc.meta); + }); +} + +/** + * Registers a component prop in the specified metaobject + * + * @param propName - the name of the property + * @param typeOrParams - a constructor of the property type or an object with property parameters + * @param meta - the metaobject where the property is registered + */ +export function regProp(propName: string, typeOrParams: Nullable, meta: ComponentMeta): void { + const params: DecoratorProp = Object.isFunction(typeOrParams) || Object.isArray(typeOrParams) ? + {type: typeOrParams} : + {...typeOrParams}; + + let prop: ComponentProp; + + const alreadyDefined = meta.props.hasOwnProperty(propName); + + if (alreadyDefined) { + prop = meta.props[propName]!; + + } else { + if (meta.methods[propName] != null) { + meta.methods[propName] = undefined; + } + + const accessors = meta.accessors[propName] != null ? + meta.accessors : + meta.computedFields; + + if (accessors[propName] != null) { + Object.defineProperty(meta.constructor.prototype, propName, defProp); + accessors[propName] = undefined; + delete meta.component.computed[propName]; + } + + // Handling the situation when a field changes type during inheritance, + // for example, it was a @system in the parent component and became a @prop + for (const anotherType of ['fields', 'systemFields']) { + const cluster = meta[anotherType]; + + if (cluster[propName] != null) { + const field: ComponentField = {...cluster[propName]}; + + // Do not inherit the `functional` option in this case + delete field.functional; + + // The option `init` cannot be converted to `default` + delete field.init; + + meta.props[propName] = {...field, forceUpdate: true}; + + cluster[propName] = undefined; + + break; + } + } + + const parent = meta.props[propName]; + + if (parent != null) { + prop = { + ...parent, + meta: {...parent.meta} + }; + + if (parent.watchers != null) { + prop.watchers = new Map(parent.watchers); + } + + } else { + prop = { + forceUpdate: true, + meta: {} + }; + } + } + + let {watchers} = prop; + + if (params.watch != null) { + watchers ??= new Map(); + + for (const fieldWatcher of Array.toArray(params.watch)) { + if (Object.isPlainObject(fieldWatcher)) { + // FIXME: remove Object.cast + watchers.set(fieldWatcher.handler, Object.cast(normalizeFunctionalParams({...fieldWatcher}, meta))); + + } else { + // FIXME: remove Object.cast + watchers.set(fieldWatcher, Object.cast(normalizeFunctionalParams({handler: fieldWatcher}, meta))); + } + } + } + + if (alreadyDefined) { + const {meta} = prop; + + if (params.meta != null) { + Object.assign(meta, params.meta); + } + + Object.assign(prop, { + ...params, + watchers, + meta + }); + + } else { + prop = normalizeFunctionalParams({ + ...prop, + ...params, + + watchers, + + meta: { + ...prop.meta, + ...params.meta + } + }, meta); + + meta.props[propName] = prop; + } + + let defaultValue: unknown; + + const + isRoot = meta.params.root === true, + isFunctional = meta.params.functional === true; + + if (prop.default !== undefined) { + defaultValue = prop.default; + } + + if (!isRoot || defaultValue !== undefined) { + const {component} = meta; + + (prop.forceUpdate ? component.props : component.attrs)[propName] = { + type: prop.type, + required: prop.required !== false && defaultValue === undefined, + + default: defaultValue, + functional: prop.functional, + + // eslint-disable-next-line @v4fire/unbound-method + validator: prop.validator + }; + } + + const canWatchProps = !SSR && !isRoot && !isFunctional; + + if (canWatchProps || watchers != null && watchers.size > 0) { + meta.metaInitializers.set(propName, (meta) => { + const {watchPropDependencies} = meta; + + const + isFunctional = meta.params.functional === true, + canWatchProps = !SSR && !isRoot && !isFunctional; + + const watcherListeners = meta.watchers[propName] ?? []; + meta.watchers[propName] = watcherListeners; + + if (watchers != null) { + for (const watcher of watchers.values()) { + if (isFunctional && watcher.functional === false || !canWatchProps && !watcher.immediate) { + continue; + } + + watcherListeners.push(watcher); + } + } + + if (canWatchProps) { + const normalizedName = isBinding.test(propName) ? isBinding.replace(propName) : propName; + + if ((meta.computedFields[normalizedName] ?? meta.accessors[normalizedName]) != null) { + const props = watchPropDependencies.get(normalizedName) ?? new Set(); + + props.add(propName); + watchPropDependencies.set(normalizedName, props); + + } else { + for (const [path, deps] of meta.watchDependencies) { + for (const dep of deps) { + const pathChunks = Object.isArray(dep) ? dep : dep.split('.', 1); + + if (pathChunks[0] === propName) { + const props = watchPropDependencies.get(path) ?? new Set(); + + props.add(propName); + watchPropDependencies.set(path, props); + + break; + } + } + } + } + } + }); + } +} diff --git a/src/core/component/decorators/prop/index.ts b/src/core/component/decorators/prop/index.ts index e544854a7d..45f45d2b72 100644 --- a/src/core/component/decorators/prop/index.ts +++ b/src/core/component/decorators/prop/index.ts @@ -11,45 +11,11 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/factory'; -import type { DecoratorProp } from 'core/component/decorators/interface'; - //#if runtime has dummyComponents import('core/component/decorators/prop/test/b-effect-prop-wrapper-dummy'); import('core/component/decorators/prop/test/b-effect-prop-dummy'); import('core/component/decorators/prop/test/b-non-effect-prop-dummy'); //#endif -/** - * Marks a class property as a component prop - * - * @decorator - * - * @example - * ```typescript - * import iBlock, { component, prop } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @prop(Number) - * bla: number = 0; - * - * @prop({type: Number, required: false}) - * baz?: number; - * - * @prop({type: Number, default: () => Math.random()}) - * bar!: number; - * } - * ``` - */ -export const prop = paramsFactory< - CanArray | - ObjectConstructor | - DecoratorProp ->('props', (p) => { - if (Object.isFunction(p) || Object.isArray(p)) { - return {type: p}; - } - - return p; -}); +export * from 'core/component/decorators/prop/decorator'; +export * from 'core/component/decorators/prop/interface'; diff --git a/src/core/component/decorators/interface/prop.ts b/src/core/component/decorators/prop/interface.ts similarity index 93% rename from src/core/component/decorators/interface/prop.ts rename to src/core/component/decorators/prop/interface.ts index 6f89b33c13..4434a46910 100644 --- a/src/core/component/decorators/interface/prop.ts +++ b/src/core/component/decorators/prop/interface.ts @@ -7,7 +7,7 @@ */ import type { ComponentInterface } from 'core/component/interface'; -import type { DecoratorFieldWatcher } from 'core/component/decorators/interface/watcher'; +import type { DecoratorFieldWatcher } from 'core/component/decorators/watch'; /** * Options of a component prop @@ -130,15 +130,6 @@ export interface DecoratorProp< */ forceUpdate?: boolean; - /** - * If set to true, the prop always uses its own default value when needed. - * This option is actually used when the `defaultProps` property is set to false for the described component - * (via the `@component` decorator), and we want to override this behavior for a particular prop. - * - * @default `false` - */ - forceDefault?: boolean; - /** * A watcher or a list of watchers for the current prop. * The watcher can be defined as a component method to invoke, callback function, or watch handle. @@ -214,7 +205,7 @@ export interface DecoratorProp< export type Prop = {(): T} | - {new(...args: any[]): T & object} | - {new(...args: string[]): Function}; + {new (...args: any[]): T & object} | + {new (...args: string[]): Function}; export type PropType = CanArray>; diff --git a/src/core/component/decorators/prop/test/unit/force-update.ts b/src/core/component/decorators/prop/test/unit/force-update.ts index eecdc9a684..38e6344d36 100644 --- a/src/core/component/decorators/prop/test/unit/force-update.ts +++ b/src/core/component/decorators/prop/test/unit/force-update.ts @@ -35,32 +35,6 @@ test.describe('contracts for props effects', () => { await test.expect(res).resolves.toBe(42); }); - - test.describe('an accessor error', () => { - test.beforeEach(async ({consoleTracker}) => { - await consoleTracker.setMessageFilters({ - 'No accessors are defined for the prop "dataProp".': (msg) => msg.args()[3].evaluate((err) => err.message) - }); - }); - - test('should be thrown if the value is not an accessor', async ({consoleTracker}) => { - await target.evaluate((ctx) => { - const vnode = ctx.vdom.create('b-non-effect-prop-dummy', {attrs: {dataProp: {a: 1}}}); - ctx.vdom.render(vnode); - }); - - await test.expect(consoleTracker.getMessages()).resolves.toHaveLength(1); - }); - - test('should not be thrown if the value is `undefined`', async ({consoleTracker}) => { - await target.evaluate((ctx) => { - const vnode = ctx.vdom.create('b-non-effect-prop-dummy', {attrs: {dataProp: undefined}}); - ctx.vdom.render(vnode); - }); - - await test.expect(consoleTracker.getMessages()).resolves.toHaveLength(0); - }); - }); }); test.describe('changing the value of the prop with `forceUpdate: false`', () => { diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md index 23bfd6699a..3260b25334 100644 --- a/src/core/component/decorators/system/README.md +++ b/src/core/component/decorators/system/README.md @@ -1,7 +1,7 @@ # core/component/decorators/system The decorator marks a class property as a system field. -System property mutations never cause components to re-render. +Mutations to a system field never cause components to re-render. ```typescript import iBlock, { component, system } from 'components/super/i-block/i-block'; diff --git a/src/core/component/decorators/system/decorator.ts b/src/core/component/decorators/system/decorator.ts new file mode 100644 index 0000000000..06f360a309 --- /dev/null +++ b/src/core/component/decorators/system/decorator.ts @@ -0,0 +1,304 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { defProp } from 'core/const/props'; +import { isStore } from 'core/component/reflect'; + +import { createComponentDecorator3, normalizeFunctionalParams } from 'core/component/decorators/helpers'; + +import type { ComponentField } from 'core/component/interface'; +import type { ComponentMeta } from 'core/component/meta'; + +import type { PartDecorator } from 'core/component/decorators/interface'; + +import type { + + FieldCluster, + InitFieldFn, + + DecoratorSystem, + DecoratorField + +} from 'core/component/decorators/system/interface'; + +const INIT = Symbol('The field initializer'); + +/** + * Marks a class property as a system field. + * Mutations to a system field never cause components to re-render. + * + * @decorator + * + * @param [initOrParams] - a function to initialize the field value or an object with field parameters + * @param [initOrDefault] - a function to initialize the field value or the field default value + * @param [cluster] - the cluster for the registered field: `systemFields` or `fields` + * + * @example + * ```typescript + * import iBlock, { component, system } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @system() + * bla: number = 0; + * + * @system(() => Math.random()) + * baz!: number; + * } + * ``` + */ +export function system( + initOrParams?: InitFieldFn | DecoratorSystem, + initOrDefault?: InitFieldFn | DecoratorSystem['default'], + cluster?: 'systemFields' +): PartDecorator; + +/** + * Marks a class property as a field. + * + * @param [initOrParams] - a function to initialize the field value or an object with field parameters + * @param [initOrDefault] - a function to initialize the field value or the field default value + * @param [cluster] - the cluster for the registered field: `systemFields` or `fields` + */ +export function system( + initOrParams: CanUndef, + initOrDefault: InitFieldFn | DecoratorSystem['default'], + cluster: 'fields' +): PartDecorator; + +export function system( + initOrParams?: InitFieldFn | DecoratorSystem | DecoratorField, + initOrDefault?: InitFieldFn | DecoratorSystem['default'], + cluster: FieldCluster = 'systemFields' +): PartDecorator { + return createComponentDecorator3((desc, fieldName) => { + const hasInitOrDefault = + Object.isFunction(initOrParams) || + Object.isDictionary(initOrParams) && ('init' in initOrParams || 'default' in initOrParams); + + let params = initOrParams; + + if (initOrDefault !== undefined && !hasInitOrDefault) { + if (Object.isFunction(initOrDefault)) { + if (Object.isDictionary(params)) { + params.init = initOrDefault; + + } else { + params = initOrDefault; + } + + } else if (Object.isDictionary(params)) { + params.default = initOrDefault; + + } else { + params = {default: initOrDefault}; + } + } + + regField(fieldName, cluster, params, desc.meta); + }); +} + +/** + * Registers a component field in the specified metaobject + * + * @param fieldName - the name of the field + * @param cluster - the cluster for the registered field: `systemFields` or `fields` + * @param initOrParams - a function to initialize the field value or an object with field parameters + * @param meta - the metaobject where the field is registered + */ +export function regField( + fieldName: string, + cluster: FieldCluster, + initOrParams: Nullable, + meta: ComponentMeta +): void { + const params = Object.isFunction(initOrParams) ? {init: initOrParams} : {...initOrParams}; + + let field: ComponentField; + + const + store = meta[cluster], + alreadyDefined = store.hasOwnProperty(fieldName); + + if (alreadyDefined) { + field = store[fieldName]!; + + } else { + if (meta.methods[fieldName] != null) { + meta.methods[fieldName] = undefined; + } + + const accessors = meta.accessors[fieldName] != null ? + meta.accessors : + meta.computedFields; + + if (accessors[fieldName] != null) { + Object.defineProperty(meta.constructor.prototype, fieldName, defProp); + accessors[fieldName] = undefined; + delete meta.component.computed[fieldName]; + } + + // Handling the situation when a field changes type during inheritance, + // for example, it was a @prop in the parent component and became a @system + for (const anotherType of ['props', cluster === 'fields' ? 'systemFields' : 'fields']) { + const anotherStore = meta[anotherType]; + + if (anotherStore[fieldName] != null) { + const field: ComponentField = {...anotherStore[fieldName]}; + + // Do not inherit the `functional` option in this case + delete field.functional; + + if (anotherType === 'props') { + delete meta.component.props[fieldName]; + + if (Object.isFunction(field.default)) { + field.init = field.default; + delete field.default; + } + } + + store[fieldName] = field; + anotherStore[fieldName] = undefined; + + break; + } + } + + const parent = store[fieldName]; + + if (parent != null) { + field = { + ...parent, + src: meta.componentName, + meta: {...parent.meta} + }; + + if (parent.watchers != null) { + field.watchers = new Map(parent.watchers); + } + + } else { + field = { + src: meta.componentName, + meta: {} + }; + } + } + + let {watchers, after} = field; + + if (params.after != null) { + after = new Set(Array.toArray(params.after)); + } + + if (params.watch != null) { + watchers ??= new Map(); + + for (const fieldWatcher of Array.toArray(params.watch)) { + if (Object.isPlainObject(fieldWatcher)) { + // FIXME: remove Object.cast + watchers.set(fieldWatcher.handler, Object.cast(normalizeFunctionalParams({...fieldWatcher}, meta))); + + } else { + // FIXME: remove Object.cast + watchers.set(fieldWatcher, Object.cast(normalizeFunctionalParams({handler: fieldWatcher}, meta))); + } + } + } + + if (alreadyDefined) { + const {meta} = field; + + if (params.meta != null) { + Object.assign(meta, params.meta); + } + + Object.assign(field, { + ...params, + after, + watchers, + meta + }); + + } else { + field = normalizeFunctionalParams({ + ...field, + ...params, + + after, + watchers, + + meta: { + ...field.meta, + ...params.meta + } + }, meta); + + store[fieldName] = field; + + if (isStore.test(fieldName)) { + const tiedWith = isStore.replace(fieldName); + meta.tiedFields[fieldName] = tiedWith; + meta.tiedFields[tiedWith] = fieldName; + } + } + + if (field.init == null || !(INIT in field.init)) { + const defValue = field.default; + + if (field.init != null) { + const customInit = field.init; + + field.init = (ctx, store) => { + const val = customInit.call(ctx, ctx, store); + + if (val === undefined && defValue !== undefined) { + if (store[fieldName] === undefined) { + return defValue; + } + + return undefined; + } + + return val; + }; + + } else if (defValue !== undefined) { + field.init = (_, store) => { + if (store[fieldName] === undefined) { + return defValue; + } + + return undefined; + }; + } + + if (field.init != null) { + Object.defineProperty(field.init, INIT, {value: true}); + } + } + + if (watchers != null && watchers.size > 0) { + meta.metaInitializers.set(fieldName, (meta) => { + const isFunctional = meta.params.functional === true; + + for (const watcher of watchers!.values()) { + if (isFunctional && watcher.functional === false) { + continue; + } + + const watcherListeners = meta.watchers[fieldName] ?? []; + meta.watchers[fieldName] = watcherListeners; + + watcherListeners.push(watcher); + } + }); + } +} diff --git a/src/core/component/decorators/system/index.ts b/src/core/component/decorators/system/index.ts index 7a77aa7bfc..07eb63a339 100644 --- a/src/core/component/decorators/system/index.ts +++ b/src/core/component/decorators/system/index.ts @@ -11,33 +11,5 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/factory'; -import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/interface'; - -/** - * Marks a class property as a system field. - * System field mutations never cause components to re-render. - * - * @decorator - * - * @example - * ```typescript - * import iBlock, { component, system } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @system() - * bla: number = 0; - * - * @system(() => Math.random()) - * baz!: number; - * } - * ``` - */ -export const system = paramsFactory('systemFields', (p) => { - if (Object.isFunction(p)) { - return {init: p}; - } - - return p; -}); +export * from 'core/component/decorators/system/decorator'; +export * from 'core/component/decorators/system/interface'; diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/system/interface.ts similarity index 91% rename from src/core/component/decorators/interface/field.ts rename to src/core/component/decorators/system/interface.ts index 93e869f91f..63a324ac5e 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/system/interface.ts @@ -7,11 +7,13 @@ */ import type { ComponentInterface } from 'core/component/interface'; -import type { DecoratorFieldWatcher } from 'core/component/decorators/interface/watcher'; -import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; +import type { DecoratorFieldWatcher } from 'core/component/decorators/watch'; +import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface'; + +export type FieldCluster = 'fields' | 'systemFields'; export interface DecoratorSystem< - CTX extends ComponentInterface = ComponentInterface, + Ctx extends ComponentInterface = ComponentInterface, A = unknown, B = A > extends DecoratorFunctionalOptions { @@ -25,7 +27,7 @@ export interface DecoratorSystem< * * @default `false` */ - unique?: boolean | UniqueFieldFn; + unique?: boolean | UniqueFieldFn; /** * This option allows you to set the default value of the field. @@ -85,7 +87,7 @@ export interface DecoratorSystem< * } * ``` */ - init?: InitFieldFn; + init?: InitFieldFn; /** * A name or a list of names after which this property should be initialized. @@ -183,7 +185,7 @@ export interface DecoratorSystem< * } * ``` */ - watch?: DecoratorFieldWatcher; + watch?: DecoratorFieldWatcher; /** * If set to false, the field can't be watched if created inside a functional component. @@ -207,7 +209,7 @@ export interface DecoratorSystem< * * @default `false` */ - merge?: MergeFieldFn | boolean; + merge?: MergeFieldFn | boolean; /** * A dictionary with some extra information of the field. @@ -233,10 +235,10 @@ export interface DecoratorSystem< } export interface DecoratorField< - CTX extends ComponentInterface = ComponentInterface, + Ctx extends ComponentInterface = ComponentInterface, A = unknown, B = A -> extends DecoratorSystem { +> extends DecoratorSystem { /** * If set to true, property changes will cause the template to be guaranteed to be re-rendered. * Be aware that enabling this property may result in redundant redrawing. @@ -246,14 +248,14 @@ export interface DecoratorField< forceUpdate?: boolean; } -export interface InitFieldFn { - (ctx: CTX['unsafe'], data: Dictionary): unknown; +export interface InitFieldFn { + (ctx: Ctx['unsafe'], data: Dictionary): unknown; } -export interface MergeFieldFn { - (ctx: CTX['unsafe'], oldCtx: CTX['unsafe'], field: string, link?: string): unknown; +export interface MergeFieldFn { + (ctx: Ctx['unsafe'], oldCtx: Ctx['unsafe'], field: string, link?: string): unknown; } -export interface UniqueFieldFn { - (ctx: CTX['unsafe'], oldCtx: CTX['unsafe']): AnyToBoolean; +export interface UniqueFieldFn { + (ctx: Ctx['unsafe'], oldCtx: Ctx['unsafe']): AnyToBoolean; } diff --git a/src/core/component/decorators/watch/README.md b/src/core/component/decorators/watch/README.md index 9018993d87..a8a2bb1bdd 100644 --- a/src/core/component/decorators/watch/README.md +++ b/src/core/component/decorators/watch/README.md @@ -112,7 +112,7 @@ class bExample extends iBlock { } ``` -## Additional options +## Additional Options The `@watch` decorator can accept any options compatible with the watch function from the `core/object/watch` module. For a more detailed list of these options, please refer to that module's documentation. @@ -203,7 +203,7 @@ export default class bExample extends iBlock { @prop({required: false}) params?: Dictionary; - @watch({path: 'params', shouldInit: (ctx) => ctx.params != null}) + @watch({path: 'params', shouldInit: (o) => o.params != null}) watcher() { } diff --git a/src/core/component/decorators/watch/decorator.ts b/src/core/component/decorators/watch/decorator.ts new file mode 100644 index 0000000000..9120a4f250 --- /dev/null +++ b/src/core/component/decorators/watch/decorator.ts @@ -0,0 +1,250 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { createComponentDecorator4, normalizeFunctionalParams } from 'core/component/decorators/helpers'; + +import type { ComponentProp, ComponentField, ComponentMethod } from 'core/component/interface'; + +import type { PartDecorator } from 'core/component/decorators/interface'; + +import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/component/decorators/watch/interface'; + +/** + * Attaches a watcher of a component property/event to a component method or property. + * + * When you observe a property's alteration, + * the handler function can accept a second argument that refers to the property's old value. + * If the object being watched isn't primitive, the old value will be cloned from the original old value. + * This helps avoid issues that may arise from having two references to the same object. + * + * @decorator + * @param watcher - parameters for observation + * + * @example + * + * ```typescript + * import iBlock, { component, field, watch } from 'components/super/i-block/i-block'; + * + * @component() + * class Foo extends iBlock { + * @field() + * list: Dictionary[] = []; + * + * @watch('list') + * onListChange(value: Dictionary[], oldValue: Dictionary[]): void { + * // true + * console.log(value !== oldValue); + * console.log(value[0] !== oldValue[0]); + * } + * + * // If you don't specify a second argument in a watcher, + * // the property's old value won't be cloned. + * @watch('list') + * onListChangeWithoutCloning(value: Dictionary[]): void { + * // true + * console.log(value === arguments[1]); + * console.log(value[0] === oldValue[0]); + * } + * + * // If you're deep-watching a property and declare a second argument in a watcher, + * // the old value of the property will be deep-cloned. + * @watch({path: 'list', deep: true}) + * onListChangeWithDeepCloning(value: Dictionary[], oldValue: Dictionary[]): void { + * // true + * console.log(value !== oldValue); + * console.log(value[0] !== oldValue[0]); + * } + * + * created() { + * this.list.push({}); + * this.list[0].foo = 1; + * } + * } + * ``` + * + * To listen to an event, you should use the special delimiter `:` within a watch path. + * You can also specify an event emitter to listen to by writing a link before `:`. + * Here are some examples: + * + * 1. `:onChange` - the component will listen to its own event `onChange`. + * 2. `localEmitter:onChange` - the component will listen to an `onChange` event from `localEmitter`. + * 3. `$parent.localEmitter:onChange` - the component will listen to an `onChange` event from `$parent.localEmitter`. + * 4. `document:scroll` - the component will listen to a `scroll` event from `window.document`. + * + * A link to the event emitter is taken either from the component properties or from the global object. + * An empty link `''` refers to the component itself. + * + * If you are listening to an event, you can manage when to start listening to the event by using special characters at + * the beginning of a watch path: + * + * 1. `'!'` - start to listen to an event on the `beforeCreate` hook, e.g., `!rootEmitter:reset`. + * 2. `'?'` - start to listen to an event on the `mounted` hook, e.g., `?$el:click`. + * + * By default, all events start being listened to on the `created` hook. + * + * ```typescript + * import iBlock, { component, field, watch } from 'components/super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @field() + * foo: Dictionary = {bla: 0}; + * + * // Watch "foo" for any changes + * @watch('foo') + * watcher1() { + * + * } + * + * // Deeply watch "foo" for any changes + * @watch({path: 'foo', deep: true}) + * watcher2() { + * + * } + * + * // Watch "foo.bla" for any changes + * @watch('foo.bla') + * watcher3() { + * + * } + * + * // Listen to the component's "onChange" event + * @watch(':onChange') + * watcher3() { + * + * } + * + * // Listen to "onChange" event from the parentEmitter component + * @watch('parentEmitter:onChange') + * watcher4() { + * + * } + * } + * ``` + */ +export function watch(watcher: DecoratorFieldWatcher | DecoratorMethodWatcher): PartDecorator { + return createComponentDecorator4(({meta}, key, desc) => { + if (desc == null) { + decorateField(); + + } else { + decorateMethod(); + } + + function decorateMethod() { + let method: ComponentMethod; + + const alreadyDefined = meta.methods.hasOwnProperty(key); + + if (alreadyDefined) { + method = meta.methods[key]!; + + } else { + const parent = meta.methods[key]; + + if (parent != null) { + method = { + ...parent, + src: meta.componentName + }; + + if (parent.watchers != null) { + method.watchers = Object.create(parent.watchers); + } + + } else { + method = { + src: meta.componentName, + fn: Object.throw + }; + } + } + + const {watchers = {}} = method; + + for (const methodWatcher of Array.toArray(watcher)) { + if (Object.isString(methodWatcher)) { + watchers[methodWatcher] = normalizeFunctionalParams({path: methodWatcher}, meta); + + } else { + watchers[methodWatcher.path] = normalizeFunctionalParams({...methodWatcher}, meta); + } + } + + if (alreadyDefined) { + method.watchers = watchers; + + } else { + meta.methods[key] = normalizeFunctionalParams({...method, watchers}, meta); + } + } + + function decorateField() { + const fieldWatchers = Array.toArray(watcher); + + let store: typeof meta['props'] | typeof meta['fields']; + + if (meta.props[key] != null) { + store = meta.props; + + } else if (meta.fields[key] != null) { + store = meta.fields; + + } else { + store = meta.systemFields; + } + + let field: ComponentProp | ComponentField; + + const alreadyDefined = store.hasOwnProperty(key); + + if (alreadyDefined) { + field = store[key]!; + + } else { + const parent = store[key]; + + if (parent != null) { + field = { + ...parent, + src: meta.componentName, + meta: {...parent.meta} + }; + + if (parent.watchers != null) { + field.watchers = new Map(parent.watchers); + } + + } else { + field = { + src: meta.componentName, + meta: {} + }; + } + } + + const {watchers = new Map()} = field; + + for (const fieldWatcher of fieldWatchers) { + if (Object.isPlainObject(fieldWatcher)) { + watchers.set(fieldWatcher.handler, normalizeFunctionalParams({...fieldWatcher}, meta)); + + } else { + watchers.set(watcher, normalizeFunctionalParams({handler: watcher}, meta)); + } + } + + if (alreadyDefined) { + field.watchers = watchers; + + } else { + store[key] = normalizeFunctionalParams({...field, watchers}, meta); + } + } + }); +} diff --git a/src/core/component/decorators/watch/index.ts b/src/core/component/decorators/watch/index.ts index 43c696941a..a536160ded 100644 --- a/src/core/component/decorators/watch/index.ts +++ b/src/core/component/decorators/watch/index.ts @@ -11,118 +11,5 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/factory'; -import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/component/decorators/interface'; - -/** - * Attaches a watcher of a component property/event to a component method or property. - * - * When you observe a property's alteration, - * the handler function can accept a second argument that refers to the property's old value. - * If the object being watched isn't primitive, the old value will be cloned from the original old value. - * This helps avoid issues that may arise from having two references to the same object. - * - * ```typescript - * import iBlock, { component, field, watch } from 'components/super/i-block/i-block'; - * - * @component() - * class Foo extends iBlock { - * @field() - * list: Dictionary[] = []; - * - * @watch('list') - * onListChange(value: Dictionary[], oldValue: Dictionary[]): void { - * // true - * console.log(value !== oldValue); - * console.log(value[0] !== oldValue[0]); - * } - * - * // If you don't specify a second argument in a watcher, - * // the property's old value won't be cloned. - * @watch('list') - * onListChangeWithoutCloning(value: Dictionary[]): void { - * // true - * console.log(value === arguments[1]); - * console.log(value[0] === oldValue[0]); - * } - * - * // If you're deep-watching a property and declare a second argument in a watcher, - * // the old value of the property will be deep-cloned. - * @watch({path: 'list', deep: true}) - * onListChangeWithDeepCloning(value: Dictionary[], oldValue: Dictionary[]): void { - * // true - * console.log(value !== oldValue); - * console.log(value[0] !== oldValue[0]); - * } - * - * created() { - * this.list.push({}); - * this.list[0].foo = 1; - * } - * } - * ``` - * - * To listen to an event, you should use the special delimiter `:` within a watch path. - * You can also specify an event emitter to listen to by writing a link before `:`. - * Here are some examples: - * - * 1. `:onChange` - the component will listen to its own event `onChange`. - * 2. `localEmitter:onChange` - the component will listen to an `onChange` event from `localEmitter`. - * 3. `$parent.localEmitter:onChange` - the component will listen to an `onChange` event from `$parent.localEmitter`. - * 4. `document:scroll` - the component will listen to a `scroll` event from `window.document`. - * - * A link to the event emitter is taken either from the component properties or from the global object. - * An empty link `''` refers to the component itself. - * - * If you are listening to an event, you can manage when to start listening to the event by using special characters at - * the beginning of a watch path: - * - * 1. `'!'` - start to listen to an event on the `beforeCreate` hook, e.g., `!rootEmitter:reset`. - * 2. `'?'` - start to listen to an event on the `mounted` hook, e.g., `?$el:click`. - * - * By default, all events start being listened to on the `created` hook. - * - * ```typescript - * import iBlock, { component, field, watch } from 'components/super/i-block/i-block'; - * - * @component() - * class bExample extends iBlock { - * @field() - * foo: Dictionary = {bla: 0}; - * - * // Watch "foo" for any changes - * @watch('foo') - * watcher1() { - * - * } - * - * // Deeply watch "foo" for any changes - * @watch({path: 'foo', deep: true}) - * watcher2() { - * - * } - * - * // Watch "foo.bla" for any changes - * @watch('foo.bla') - * watcher3() { - * - * } - * - * // Listen to the component's "onChange" event - * @watch(':onChange') - * watcher3() { - * - * } - * - * // Listen to "onChange" event from the parentEmitter component - * @watch('parentEmitter:onChange') - * watcher4() { - * - * } - * } - * ``` - */ -export const watch = paramsFactory< - DecoratorFieldWatcher | - DecoratorMethodWatcher ->(null, (watch) => ({watch})); +export * from 'core/component/decorators/watch/decorator'; +export * from 'core/component/decorators/watch/interface'; diff --git a/src/core/component/decorators/interface/watcher.ts b/src/core/component/decorators/watch/interface.ts similarity index 63% rename from src/core/component/decorators/interface/watcher.ts rename to src/core/component/decorators/watch/interface.ts index 3431f708cf..2705f0c6a5 100644 --- a/src/core/component/decorators/interface/watcher.ts +++ b/src/core/component/decorators/watch/interface.ts @@ -6,10 +6,26 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentInterface, WatchOptions, WatchHandlerParams } from 'core/component/interface'; +import type { ComponentInterface, MethodWatcher, WatchHandlerParams, WatchOptions } from 'core/component/interface'; + +export type DecoratorMethodWatcher = + string | + MethodWatcher & {path: string} | + Array & {path: string}>; + +export type DecoratorFieldWatcher = + string | + DecoratorFieldWatcherObject | + DecoratorWatchHandler | + Array | DecoratorWatchHandler>; + +export interface DecoratorWatchHandler { + (ctx: Ctx['unsafe'], a: A, b: B, params?: WatchHandlerParams): unknown; + (ctx: Ctx['unsafe'], ...args: A[]): unknown; +} export interface DecoratorFieldWatcherObject< - CTX extends ComponentInterface = ComponentInterface, + Ctx extends ComponentInterface = ComponentInterface, A = unknown, B = A > extends WatchOptions { @@ -32,7 +48,7 @@ export interface DecoratorFieldWatcherObject< * } * ``` */ - handler: string | DecoratorWatchHandler; + handler: string | DecoratorWatchHandler; /** * If set to false, a handler that gets invoked due to a watcher event won't take any arguments from the event @@ -63,16 +79,5 @@ export interface DecoratorFieldWatcherObject< * * @param ctx */ - shouldInit?(ctx: CTX): boolean; -} - -export interface DecoratorWatchHandler { - (ctx: CTX['unsafe'], a: A, b: B, params?: WatchHandlerParams): unknown; - (ctx: CTX['unsafe'], ...args: A[]): unknown; + shouldInit?(ctx: Ctx): boolean; } - -export type DecoratorFieldWatcher = - string | - DecoratorFieldWatcherObject | - DecoratorWatchHandler | - Array | DecoratorWatchHandler>; diff --git a/src/core/component/directives/async-target/index.ts b/src/core/component/directives/async-target/index.ts index 1fe29f3ce5..bd8bc7a9c1 100644 --- a/src/core/component/directives/async-target/index.ts +++ b/src/core/component/directives/async-target/index.ts @@ -20,8 +20,7 @@ export * from 'core/component/directives/async-target/interface'; ComponentEngine.directive('async-target', { beforeCreate(params: DirectiveParams, vnode: VNode): void { - const - ctx = getDirectiveContext(params, vnode); + const ctx = getDirectiveContext(params, vnode); if (ctx == null || params.value === false) { return; diff --git a/src/core/component/directives/attrs/README.md b/src/core/component/directives/attrs/README.md index 60d26bf98c..9ebbfd5c8a 100644 --- a/src/core/component/directives/attrs/README.md +++ b/src/core/component/directives/attrs/README.md @@ -7,7 +7,7 @@ the provided dictionary. < .example v-attrs = {'@click': console.log, class: classes, 'v-show': condition} ``` -## Why is This Directive Needed? +## Why is This Directive Necessary? Often, there are situations where we need to dynamically apply a set of parameters to an element or component. While we have extended versions of the `v-on` and `v-bind` directives for events and attributes, diff --git a/src/core/component/directives/attrs/helpers.ts b/src/core/component/directives/attrs/helpers.ts index dc750d13f0..c68899c221 100644 --- a/src/core/component/directives/attrs/helpers.ts +++ b/src/core/component/directives/attrs/helpers.ts @@ -69,13 +69,13 @@ export function normalizePropertyAttribute(name: string): string { export function normalizeDirectiveModifiers(rawModifiers: string): Record { const modifiers = {}; - rawModifiers.split('.').forEach((modifier) => { - modifier = modifier.trim(); + for (const rawModifier of rawModifiers.split('.')) { + const modifier = rawModifier.trim(); if (modifier !== '') { modifiers[modifier] = true; } - }); + } return modifiers; } diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index 1b6e0ff7b9..773f9ab5a3 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -65,8 +65,7 @@ ComponentEngine.directive('attrs', { r = ctx.$renderEngine.r; } - let - attrs = {...params.value}; + let attrs = {...params.value}; if (componentMeta != null) { attrs = normalizeComponentAttrs(attrs, vnode.dynamicProps, componentMeta)!; @@ -113,21 +112,17 @@ ComponentEngine.directive('attrs', { } function parseDirective(attrName: string, attrVal: unknown) { - const - decl = directiveRgxp.exec(attrName); + const decl = directiveRgxp.exec(attrName); - let - value = attrVal; + let value = attrVal; if (decl == null) { throw new SyntaxError('Invalid directive declaration'); } - const - [, name, arg = '', rawModifiers = ''] = decl; + const [, name, arg = '', rawModifiers = ''] = decl; - let - dir: CanUndef; + let dir: CanUndef; switch (name) { case 'show': { @@ -137,9 +132,9 @@ ComponentEngine.directive('attrs', { case 'on': { if (Object.isDictionary(value)) { - Object.entries(value).forEach(([name, handler]) => { - attachEvent(name, handler); - }); + for (const name of Object.keys(value)) { + attachEvent(name, value[name]); + } } return; @@ -147,10 +142,10 @@ ComponentEngine.directive('attrs', { case 'bind': { if (Object.isDictionary(value)) { - Object.entries(value).forEach(([name, val]) => { - attrs[name] = val; + for (const name of Object.keys(value)) { + attrs[name] = value[name]; attrsKeys.push(name); - }); + } } return; @@ -165,8 +160,7 @@ ComponentEngine.directive('attrs', { handlerCache = getHandlerStore(), handlerKey = `onUpdate:${modelProp}:${modelValLink}`; - let - handler = handlerCache.get(handlerKey); + let handler = handlerCache.get(handlerKey); if (handler == null) { handler = (newVal: unknown) => { @@ -231,8 +225,7 @@ ComponentEngine.directive('attrs', { if (Object.isDictionary(dir)) { if (Object.isFunction(dir.beforeCreate)) { - const - newVnode = dir.beforeCreate(binding, vnode); + const newVnode = dir.beforeCreate(binding, vnode); if (newVnode != null) { vnode = newVnode; @@ -287,8 +280,7 @@ ComponentEngine.directive('attrs', { props: Dictionary = vnode?.props ?? {}, componentMeta = ctx?.meta; - let - attrs = {...params.value}; + let attrs = {...params.value}; if (vnode != null) { vnode.props ??= props; @@ -298,30 +290,29 @@ ComponentEngine.directive('attrs', { attrs = normalizeComponentAttrs(attrs, null, componentMeta)!; } - Object.entries(attrs).forEach(([name, value]) => { + for (const name of Object.keys(attrs)) { + const value = attrs[name]; + if (name.startsWith('v-')) { parseDirective(name, value); } else if (!name.startsWith('@') || isPropGetter.test(name)) { patchProps(props, normalizePropertyAttribute(name), value); } - }); + } return props; function parseDirective(attrName: string, attrVal: unknown) { - const - decl = directiveRgxp.exec(attrName); + const decl = directiveRgxp.exec(attrName); if (decl == null) { throw new SyntaxError('Invalid directive declaration'); } - const - [, name, arg = '', rawModifiers = ''] = decl; + const [, name, arg = '', rawModifiers = ''] = decl; - let - dir: CanUndef; + let dir: CanUndef; switch (name) { case 'show': { @@ -331,9 +322,9 @@ ComponentEngine.directive('attrs', { case 'bind': { if (Object.isDictionary(attrVal)) { - Object.entries(attrVal).forEach(([name, val]) => { - props[name] = val; - }); + for (const name of Object.keys(attrVal)) { + props[name] = attrVal[name]; + } } return; diff --git a/src/core/component/directives/helpers.ts b/src/core/component/directives/helpers.ts index 09388d4953..8da7c7ab73 100644 --- a/src/core/component/directives/helpers.ts +++ b/src/core/component/directives/helpers.ts @@ -23,8 +23,7 @@ import { vOnKeyModifiers, vOnModifiers } from 'core/component/directives/const'; * @param idsCache - the store for the registered elements */ export function getElementId(el: Element, idsCache: WeakMap): string { - let - id = idsCache.get(el); + let id = idsCache.get(el); if (id == null) { id = Object.fastHash(Math.random()); @@ -145,7 +144,7 @@ export function patchVnodeEventListener( // For the transmission of accessors, `forceUpdate: false` props use events. // For example, `@:value = createPropAccessors(() => someValue)`. // A distinctive feature of such events is the prefix `@:` or `on:`. - // Such events are processed in a special way. + // Such events are processed specially. const isSystemGetter = isPropGetter.test(event); props[event] = attrVal; diff --git a/src/core/component/directives/hook/README.md b/src/core/component/directives/hook/README.md index 8df9285e38..8b37641467 100644 --- a/src/core/component/directives/hook/README.md +++ b/src/core/component/directives/hook/README.md @@ -16,7 +16,7 @@ from the component. } . ``` -## Why is This Directive Needed? +## Why is This Directive Necessary? This directive is typically used with functional components as they do not initially possess their own lifecycle API and can easily be supplemented with it using this directive. diff --git a/src/core/component/directives/ref/README.md b/src/core/component/directives/ref/README.md index ae2882a080..414e0f43da 100644 --- a/src/core/component/directives/ref/README.md +++ b/src/core/component/directives/ref/README.md @@ -13,7 +13,7 @@ This directive is used in conjunction with the standard `ref` directive. < b-button :ref = $resolveRef('button') | v-ref = 'button' ``` -## Why is This Directive Needed? +## Why is This Directive Necessary? V4Fire supports two types of components: regular and functional. From the perspective of the rendering library used, functional components are regular functions that return VNodes. diff --git a/src/core/component/directives/ref/index.ts b/src/core/component/directives/ref/index.ts index 8331939622..efcc83c21a 100644 --- a/src/core/component/directives/ref/index.ts +++ b/src/core/component/directives/ref/index.ts @@ -30,10 +30,7 @@ ComponentEngine.directive('ref', { }); function updateRef(el: Element | ComponentElement, opts: DirectiveOptions, vnode: VNode): void { - const { - value, - instance - } = opts; + const {value, instance} = opts; let ctx = getDirectiveContext(opts, vnode); ctx = Object.cast(ctx?.meta.params.functional === true ? ctx : instance); @@ -61,8 +58,7 @@ function updateRef(el: Element | ComponentElement, opts: DirectiveOptions, vnode refs = ctx.$refs; if (vnode.virtualComponent != null) { - const - refVal = getRefVal(); + const refVal = getRefVal(); if (Object.isArray(refVal)) { refVal[REF_ID] ??= Math.random(); @@ -103,11 +99,9 @@ function updateRef(el: Element | ComponentElement, opts: DirectiveOptions, vnode } function resolveRefVal(key?: PropertyKey) { - const - refVal = getRefVal(); + const refVal = getRefVal(); - let - ref: unknown; + let ref: unknown; if (Object.isArray(refVal)) { if (key != null) { diff --git a/src/core/component/directives/render/README.md b/src/core/component/directives/render/README.md index 1f053c9426..3a0e3b36a0 100644 --- a/src/core/component/directives/render/README.md +++ b/src/core/component/directives/render/README.md @@ -7,7 +7,7 @@ The directive supports several modes of operation: 2. The new VNodes are inserted as child content of the node where the directive is applied. 3. The new VNodes are inserted as a component slot (if the directive is applied to a component). -## Why is This Directive Needed? +## Why is This Directive Necessary? To decompose the template of one component into multiple render functions and utilize their composition. This approach is extremely useful when we have a large template that cannot be divided into independent components. diff --git a/src/core/component/directives/render/index.ts b/src/core/component/directives/render/index.ts index 1d9ef28bc6..b212998035 100644 --- a/src/core/component/directives/render/index.ts +++ b/src/core/component/directives/render/index.ts @@ -21,8 +21,7 @@ export * from 'core/component/directives/render/interface'; ComponentEngine.directive('render', { beforeCreate(params: DirectiveParams, vnode: VNode): CanUndef { - const - ctx = getDirectiveContext(params, vnode); + const ctx = getDirectiveContext(params, vnode); const newVNode = Object.cast>>(params.value), @@ -76,14 +75,15 @@ ComponentEngine.directive('render', { } else { if (Object.isArray(newVNode)) { if (isSlot(newVNode[0])) { - newVNode.forEach((vnode) => { + for (let i = 0; i < newVNode.length; i++) { const + vnode = newVNode[i], slot = vnode.props?.slot; if (slot != null) { slots[slot] = () => vnode.children ?? getDefaultSlotFromChildren(slot); } - }); + } return; } @@ -117,8 +117,7 @@ ComponentEngine.directive('render', { return; } - const - {r} = ctx.$renderEngine; + const {r} = ctx.$renderEngine; return r.createVNode.call(ctx, 'ssr-fragment', { innerHTML: getSSRInnerHTML(content) @@ -131,8 +130,7 @@ ComponentEngine.directive('render', { function getDefaultSlotFromChildren(slotName: string): unknown { if (Object.isPlainObject(originalChildren)) { - const - slot = originalChildren[slotName]; + const slot = originalChildren[slotName]; if (Object.isFunction(slot)) { return slot(); diff --git a/src/core/component/directives/tag/README.md b/src/core/component/directives/tag/README.md index 96970d6d49..0e7dae948b 100644 --- a/src/core/component/directives/tag/README.md +++ b/src/core/component/directives/tag/README.md @@ -7,7 +7,7 @@ for dynamically specifying the name of the element tag to which the directive is < div v-tag = 'span' ``` -## Why is This Directive Needed? +## Why is This Directive Necessary? Unlike the component `:is directive`, which can be used for both creating components and regular elements, this directive can only be applied to regular elements, and the passed name is always treated as a regular name, diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index a1b1997f42..91bc957610 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -56,7 +56,6 @@ export function getComponent(meta: ComponentMeta): ComponentOptionsfunction App(component: Component & {el?: Element}, rootProps: Nullable) { const app = Object.create((ssrContext ? createSSRApp : createApp)(component, rootProps)); @@ -65,9 +64,9 @@ const Vue = makeLazy( call: { component: (contexts, ...args) => { if (args.length === 1) { - contexts.forEach((ctx) => { + for (const ctx of contexts) { ctx.component.apply(ctx, Object.cast(args)); - }); + } return; } @@ -79,9 +78,9 @@ const Vue = makeLazy( directive: (contexts, ...args: any[]) => { if (args.length === 1) { - contexts.forEach((ctx) => { + for (const ctx of contexts) { ctx.directive.apply(ctx, Object.cast(args)); - }); + } return; } @@ -92,15 +91,15 @@ const Vue = makeLazy( }, mixin: (contexts, ...args) => { - contexts.forEach((ctx) => { + for (const ctx of contexts) { ctx.mixin.apply(ctx, Object.cast(args)); - }); + } }, provide: (contexts, ...args) => { - contexts.forEach((ctx) => { + for (const ctx of contexts) { ctx.provide.apply(ctx, Object.cast(args)); - }); + } } } } diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 51acbbc3f0..1ed6a4fc06 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -59,8 +59,8 @@ import { wrapWithDirectives, wrapResolveDirective, wrapMergeProps, - wrapWithCtx, + wrapWithCtx, wrapWithModifiers } from 'core/component/render'; @@ -233,8 +233,8 @@ export function render(vnode: CanArray, parent?: ComponentInterface, grou gc.add(function* destructor() { const vnodes = Array.toArray(vnode); - for (const vnode of vnodes) { - destroy(vnode); + for (let i = 0; i < vnodes.length; i++) { + destroy(vnodes[i]); yield; } @@ -312,7 +312,10 @@ export function destroy(node: VNode | Node): void { } if (Object.isArray(vnode)) { - vnode.forEach(removeVNode); + for (let i = 0; i < vnode.length; i++) { + removeVNode(vnode[i]); + } + return; } @@ -323,11 +326,15 @@ export function destroy(node: VNode | Node): void { destroyedVNodes.add(vnode); if (Object.isArray(vnode.children)) { - vnode.children.forEach(removeVNode); + for (let i = 0; i < vnode.children.length; i++) { + removeVNode(vnode.children[i]); + } } - if (Object.isArray(vnode['dynamicChildren'])) { - vnode['dynamicChildren'].forEach((vnode) => removeVNode(Object.cast(vnode))); + if ('dynamicChildren' in vnode && Object.isArray(vnode.dynamicChildren)) { + for (let i = 0; i < vnode.dynamicChildren.length; i++) { + removeVNode(vnode.dynamicChildren[i]); + } } gc.add(function* destructor() { @@ -340,13 +347,13 @@ export function destroy(node: VNode | Node): void { yield; - ['dirs', 'children', 'dynamicChildren', 'dynamicProps'].forEach((key) => { + for (const key of ['dirs', 'children', 'dynamicChildren', 'dynamicProps']) { vnode[key] = []; - }); + } - ['el', 'ctx', 'ref', 'virtualComponent', 'virtualContext'].forEach((key) => { + for (const key of ['el', 'ctx', 'ref', 'virtualComponent', 'virtualContext']) { vnode[key] = null; - }); + } }()); } } diff --git a/src/core/component/event/component.ts b/src/core/component/event/component.ts index 066c22ccfe..da3c8b3980 100644 --- a/src/core/component/event/component.ts +++ b/src/core/component/event/component.ts @@ -59,8 +59,8 @@ export function resetComponents(type?: ComponentResetType): void { */ export function implementEventEmitterAPI(component: object): void { const - ctx = Object.cast(component), - nativeEmit = Object.cast>(ctx.$emit); + unsafe = Object.cast(component), + nativeEmit = Object.cast>(unsafe.$emit); const regularEmitter = new EventEmitter({ maxListeners: 1e3, @@ -68,7 +68,7 @@ export function implementEventEmitterAPI(component: object): void { wildcard: true }); - const wrappedEmitter = ctx.$async.wrapEventEmitter(regularEmitter); + const wrappedEmitter = unsafe.$async.wrapEventEmitter(regularEmitter); const reversedEmitter = Object.cast({ on: (...args: Parameters) => regularEmitter.prependListener(...args), @@ -78,46 +78,47 @@ export function implementEventEmitterAPI(component: object): void { const wrappedReversedEmitter = Object.cast(reversedEmitter); - Object.defineProperty(ctx, '$emit', { + Object.defineProperty(unsafe, '$emit', { configurable: true, enumerable: false, writable: false, value(event: string, ...args: unknown[]) { - if (!event.startsWith('[[')) { - nativeEmit?.(event, ...args); + if (nativeEmit != null && !event.startsWith('[[')) { + nativeEmit(event, ...args); } regularEmitter.emit(event, ...args); + return this; } }); - Object.defineProperty(ctx, '$on', { + Object.defineProperty(unsafe, '$on', { configurable: true, enumerable: false, writable: false, value: getMethod('on') }); - Object.defineProperty(ctx, '$once', { + Object.defineProperty(unsafe, '$once', { configurable: true, enumerable: false, writable: false, value: getMethod('once') }); - Object.defineProperty(ctx, '$off', { + Object.defineProperty(unsafe, '$off', { configurable: true, enumerable: false, writable: false, value: getMethod('off') }); - ctx.$destructors.push(() => { + unsafe.$destructors.push(() => { gc.add(function* destructor() { for (const key of ['$emit', '$on', '$once', '$off']) { - Object.defineProperty(ctx, key, { + Object.defineProperty(unsafe, key, { configurable: true, enumerable: true, writable: false, @@ -148,7 +149,9 @@ export function implementEventEmitterAPI(component: object): void { emitter = Object.cast(opts.rawEmitter ? reversedEmitter : wrappedReversedEmitter); } - Array.toArray(event).forEach((event) => { + const events = Array.toArray(event); + + for (const event of events) { if (method === 'off' && cb == null) { emitter.removeAllListeners(event); @@ -159,7 +162,7 @@ export function implementEventEmitterAPI(component: object): void { links.push(Object.cast(opts.rawEmitter ? cb : link)); } } - }); + } if (isOnLike) { return Object.isArray(event) ? links : links[0]; diff --git a/src/core/component/event/emitter.ts b/src/core/component/event/emitter.ts index d39a7be133..7de1e6162f 100644 --- a/src/core/component/event/emitter.ts +++ b/src/core/component/event/emitter.ts @@ -23,7 +23,7 @@ const originalEmit = globalEmitter.emit.bind(globalEmitter); globalEmitter.emit = (event: string, ...args) => { const res = originalEmit(event, ...args); - log(`global:event:${event.replace(/\./g, ':')}`, ...args); + log(`global:event:${event.replaceAll('.', ':')}`, ...args); return res; }; diff --git a/src/core/component/field/const.ts b/src/core/component/field/const.ts deleted file mode 100644 index cec2b48a8d..0000000000 --- a/src/core/component/field/const.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import type { ComponentField } from 'core/component/interface'; -import type { SortedFields } from 'core/component/field/interface'; - -/** - * A cache for sorted component fields - */ -export const sortedFields = new WeakMap, SortedFields>(); diff --git a/src/core/component/field/index.ts b/src/core/component/field/index.ts index 2bbbec88f6..531a4cb11e 100644 --- a/src/core/component/field/index.ts +++ b/src/core/component/field/index.ts @@ -13,4 +13,3 @@ export * from 'core/component/field/init'; export * from 'core/component/field/store'; -export * from 'core/component/field/interface'; diff --git a/src/core/component/field/init.ts b/src/core/component/field/init.ts index 567cc03d4f..4e70850e93 100644 --- a/src/core/component/field/init.ts +++ b/src/core/component/field/init.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { sortFields } from 'core/component/field/helpers'; -import type { ComponentInterface, ComponentField } from 'core/component/interface'; +import type { ComponentMeta } from 'core/component/meta'; +import type { ComponentInterface } from 'core/component/interface'; /** * Initializes all fields of a given component instance. @@ -19,7 +19,7 @@ import type { ComponentInterface, ComponentField } from 'core/component/interfac * @param [store] - the store for initialized fields */ export function initFields( - from: Dictionary, + from: ComponentMeta['fieldInitializers'], component: ComponentInterface, store: Dictionary = {} ): Dictionary { @@ -27,55 +27,22 @@ export function initFields( component ); - const { - params, - instance - } = unsafe.meta; + for (let i = 0; i < from.length; i++) { + const [name, field] = from[i]; - const isFunctional = params.functional === true; - - sortFields(from).forEach(([name, field]) => { const sourceVal = store[name]; - const canSkip = - field == null || sourceVal !== undefined || - !SSR && isFunctional && field.functional === false || - field.init == null && field.default === undefined && instance[name] === undefined; - - if (field == null || canSkip) { + if (sourceVal !== undefined || field?.init == null) { store[name] = sourceVal; - return; + continue; } unsafe.$activeField = name; - let val: unknown; - - if (field.init != null) { - val = field.init(component.unsafe, store); - } - - if (val === undefined) { - if (store[name] === undefined) { - // To prevent linking to the same type of component for non-primitive values, - // it's important to clone the default value from the component constructor. - if (field.default !== undefined) { - val = field.default; - - } else { - const defValue = instance[name]; - val = Object.isPrimitive(defValue) ? defValue : Object.fastClone(defValue); - } - - store[name] = val; - } - - } else { - store[name] = val; - } - }); + store[name] = field.init(unsafe, store); - unsafe.$activeField = undefined; + unsafe.$activeField = undefined; + } return store; } diff --git a/src/core/component/functional/context/create.ts b/src/core/component/functional/context/create.ts index 1a67d35ab7..4485cc2c39 100644 --- a/src/core/component/functional/context/create.ts +++ b/src/core/component/functional/context/create.ts @@ -10,6 +10,7 @@ import * as init from 'core/component/init'; import { saveRawComponentContext } from 'core/component/context'; import { forkMeta, ComponentMeta } from 'core/component/meta'; + import { initProps } from 'core/component/prop'; import type { ComponentInterface } from 'core/component/interface'; @@ -41,8 +42,14 @@ export function createVirtualContext( const handlers: Array<[string, boolean, Function]> = []; if (props != null) { - Object.entries(props).forEach(([name, prop]) => { - const normalizedName = name.camelize(false); + const keys = Object.keys(props); + + for (let i = 0; i < keys.length; i++) { + const name = keys[i]; + + const + prop = props[name], + normalizedName = name.camelize(false); if (normalizedName in meta.props) { $props[normalizedName] = prop; @@ -62,16 +69,13 @@ export function createVirtualContext( $attrs[name] = prop; } - }); + } } let $options: {directives: Dictionary; components: Dictionary}; if ('$options' in parent) { - const { - directives = {}, - components = {} - } = parent.$options; + const {directives = {}, components = {}} = parent.$options; $options = { directives: Object.create(directives), @@ -88,6 +92,8 @@ export function createVirtualContext( const virtualCtx = Object.cast({ componentName: meta.componentName, + render: meta.component.render, + meta, get instance(): typeof meta['instance'] { @@ -135,18 +141,17 @@ export function createVirtualContext( }); init.beforeCreateState(virtualCtx, meta, { - addMethods: true, implementEventAPI: true }); - handlers.forEach(([event, once, handler]) => { + for (const [event, once, handler] of handlers) { if (once) { unsafe.$once(event, handler); } else { unsafe.$on(event, handler); } - }); + } init.beforeDataCreateState(virtualCtx); return initDynamicComponentLifeCycle(virtualCtx); diff --git a/src/core/component/functional/context/inherit.ts b/src/core/component/functional/context/inherit.ts index aabe9883e3..21d3b82fd8 100644 --- a/src/core/component/functional/context/inherit.ts +++ b/src/core/component/functional/context/inherit.ts @@ -32,47 +32,60 @@ export function inheritContext( parentCtx.$destroy({recursive: false, shouldUnmountVNodes: false}); const - props = ctx.$props, - parentProps = parentCtx.$props, + parentProps = parentCtx.getPassedProps?.(), linkedFields = {}; - Object.keys(parentProps).forEach((prop) => { - const linked = parentCtx.$syncLinkCache.get(prop); + if (parentProps != null) { + const propNames = Object.keys(parentProps); - if (linked == null) { - return; - } + for (let i = 0; i < propNames.length; i++) { + const + propName = propNames[i], + linked = parentCtx.$syncLinkCache.get(propName); + + if (linked != null) { + const links = Object.values(linked); + + for (let j = 0; j < links.length; j++) { + const link = links[j]; - Object.values(linked).forEach((link) => { - if (link != null) { - linkedFields[link.path] = prop; + if (link != null) { + linkedFields[link.path] = propName; + } + } } - }); - }); + } + } - const fields = [ - parentCtx.meta.systemFields, - parentCtx.meta.fields + const parentMeta = parentCtx.meta; + + const clusters = [ + [parentMeta.systemFields, parentMeta.systemFieldInitializers], + [parentMeta.fields, parentMeta.fieldInitializers] ]; - fields.forEach((cluster) => { - Object.entries(cluster).forEach(([name, field]) => { + for (const [cluster, fields] of clusters) { + for (let i = 0; i < fields.length; i++) { + const + fieldName = fields[i][0], + field = cluster[fieldName]; + if (field == null) { - return; + continue; } - const link = linkedFields[name]; + const link = linkedFields[fieldName]; const - val = ctx[name], - oldVal = parentCtx[name]; + val = ctx[fieldName], + oldVal = parentCtx[fieldName]; const needMerge = - ctx.$modifiedFields[name] !== true && + ctx.$modifiedFields[fieldName] !== true && ( Object.isFunction(field.unique) ? - !Object.isTruly(field.unique(ctx, Object.cast(parentCtx))) : + !Object.isTruly(field.unique(ctx, parentCtx)) : !field.unique ) && @@ -80,32 +93,30 @@ export function inheritContext( ( link == null || - Object.fastCompare(props[link], parentProps[link]) + Object.fastCompare(ctx[link], parentCtx[link]) ); if (needMerge) { - if (Object.isTruly(field.merge)) { - if (field.merge === true) { - let newVal = oldVal; + if (field.merge === true) { + let newVal = oldVal; - if (Object.isDictionary(val) || Object.isDictionary(oldVal)) { - // eslint-disable-next-line prefer-object-spread - newVal = Object.assign({}, val, oldVal); + if (Object.isDictionary(val) || Object.isDictionary(oldVal)) { + // eslint-disable-next-line prefer-object-spread + newVal = Object.assign({}, val, oldVal); - } else if (Object.isArray(val) || Object.isArray(oldVal)) { - newVal = Object.assign([], val, oldVal); - } + } else if (Object.isArray(val) || Object.isArray(oldVal)) { + newVal = Object.assign([], val, oldVal); + } - ctx[name] = newVal; + ctx[fieldName] = newVal; - } else if (Object.isFunction(field.merge)) { - field.merge(ctx, Object.cast(parentCtx), name, link); - } + } else if (Object.isFunction(field.merge)) { + field.merge(ctx, parentCtx, fieldName, link); } else { - ctx[name] = parentCtx[name]; + ctx[fieldName] = parentCtx[fieldName]; } } - }); - }); + } + } } diff --git a/src/core/component/gc/const.ts b/src/core/component/gc/const.ts index d68742b5b7..b5ac2025c9 100644 --- a/src/core/component/gc/const.ts +++ b/src/core/component/gc/const.ts @@ -31,9 +31,9 @@ export const add = (task: Iterator): number => { const l = queue.push(task); if (newTaskHandlersQueue.length > 0) { - newTaskHandlersQueue.splice(0, newTaskHandlersQueue.length).forEach((handler) => { + for (const handler of newTaskHandlersQueue.splice(0, newTaskHandlersQueue.length)) { handler(); - }); + } } return l; diff --git a/src/core/component/hook/index.ts b/src/core/component/hook/index.ts index 4cc02220f1..2f679371b4 100644 --- a/src/core/component/hook/index.ts +++ b/src/core/component/hook/index.ts @@ -80,8 +80,10 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn if (hooks.some((hook) => hook.after != null && hook.after.size > 0)) { const emitter = new QueueEmitter(); - hooks.forEach((hook, i) => { - const nm = hook.name; + for (let i = 0; i < hooks.length; i++) { + const + hook = hooks[i], + hookName = hook.name; if (hook.once) { toDelete ??= []; @@ -92,16 +94,16 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn const res = args.length > 0 ? hook.fn.apply(component, args) : hook.fn.call(component); if (Object.isPromise(res)) { - return res.then(() => nm != null ? emitter.emit(nm) : undefined); + return res.then(() => hookName != null ? emitter.emit(hookName) : undefined); } - const tasks = nm != null ? emitter.emit(nm) : null; + const tasks = hookName != null ? emitter.emit(hookName) : null; if (tasks != null) { return tasks; } }); - }); + } removeFromHooks(toDelete); @@ -114,7 +116,9 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn } else { let tasks: CanNull>> = null; - hooks.forEach((hook, i) => { + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i]; + let res: unknown; switch (args.length) { @@ -139,11 +143,10 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn tasks ??= []; tasks.push(res); } - }); + } removeFromHooks(toDelete); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (tasks != null) { return Promise.all(tasks).then(() => undefined); } @@ -155,9 +158,9 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn function removeFromHooks(toDelete: CanNull) { if (toDelete != null) { - toDelete.reverse().forEach((i) => { + for (const i of toDelete.reverse()) { hooks.splice(i, 1); - }); + } } } } diff --git a/src/core/component/index.ts b/src/core/component/index.ts index 110aa04ed0..dc5244087a 100644 --- a/src/core/component/index.ts +++ b/src/core/component/index.ts @@ -32,6 +32,10 @@ export { isComponent, rootComponents, + beforeHooks, + beforeRenderHooks, + beforeMountHooks, + V4_COMPONENT, ASYNC_RENDER_ID, PARENT diff --git a/src/core/component/init/component.ts b/src/core/component/init/component.ts index fae4173e5f..2a852beb95 100644 --- a/src/core/component/init/component.ts +++ b/src/core/component/init/component.ts @@ -46,7 +46,10 @@ export function registerParentComponents(component: ComponentConstructorInfo): b const regParentComponent = componentRegInitializers[parentName]; if (regParentComponent != null) { - regParentComponent.forEach((reg) => reg()); + for (const reg of regParentComponent) { + reg(); + } + delete componentRegInitializers[parentName]; return true; } @@ -73,7 +76,10 @@ export function registerComponent(name: CanUndef): CanNull reg()); + for (const reg of regComponent) { + reg(); + } + delete componentRegInitializers[name]; } diff --git a/src/core/component/init/interface.ts b/src/core/component/init/interface.ts index 0d321517d8..308aa3d0e0 100644 --- a/src/core/component/init/interface.ts +++ b/src/core/component/init/interface.ts @@ -7,6 +7,5 @@ */ export interface InitBeforeCreateStateOptions { - addMethods?: boolean; implementEventAPI?: boolean; } diff --git a/src/core/component/init/states/before-create.ts b/src/core/component/init/states/before-create.ts index d88aa62b84..80c09af384 100644 --- a/src/core/component/init/states/before-create.ts +++ b/src/core/component/init/states/before-create.ts @@ -15,7 +15,6 @@ import { V4_COMPONENT } from 'core/component/const'; import { getComponentContext } from 'core/component/context'; import { forkMeta } from 'core/component/meta'; -import { getPropertyInfo, PropertyInfo } from 'core/component/reflect'; import { getNormalParent } from 'core/component/traverse'; import { initProps, attachAttrPropsListeners } from 'core/component/prop'; @@ -51,8 +50,6 @@ export function beforeCreateState( ): void { meta = forkMeta(meta); - const isFunctional = meta.params.functional === true; - // To avoid TS errors marks all properties as editable const unsafe = Object.cast>(component); unsafe[V4_COMPONENT] = true; @@ -68,6 +65,12 @@ export function beforeCreateState( get: () => meta.instance }); + Object.defineProperty(unsafe, 'constructor', { + configurable: true, + enumerable: true, + value: meta.constructor + }); + unsafe.$fields = {}; unsafe.$systemFields = {}; unsafe.$modifiedFields = {}; @@ -116,7 +119,9 @@ export function beforeCreateState( return ctx[$getRoot]; } - const fn = () => ('getRoot' in ctx ? ctx.getRoot?.() : null) ?? ctx.$root; + let fn = () => ('getRoot' in ctx ? ctx.getRoot?.() : null) ?? ctx.$root; + + fn = fn.once(); Object.defineProperty(ctx, $getRoot, { configurable: true, @@ -129,14 +134,18 @@ export function beforeCreateState( }) }); + let r: CanNull = null; + Object.defineProperty(unsafe, 'r', { configurable: true, enumerable: true, get: () => { - const r = ('getRoot' in unsafe ? unsafe.getRoot?.() : null) ?? unsafe.$root; + if (r == null) { + r = ('getRoot' in unsafe ? unsafe.getRoot?.() : null) ?? unsafe.$root; - if ('$remoteParent' in r.unsafe) { - return r.unsafe.$remoteParent!.$root; + if ('$remoteParent' in r.unsafe) { + r = r.unsafe.$remoteParent!.$root; + } } return r; @@ -170,6 +179,8 @@ export function beforeCreateState( fn = () => ctx; } + fn = fn.once(); + Object.defineProperty(targetCtx, $getParent, { configurable: true, enumerable: true, @@ -208,7 +219,7 @@ export function beforeCreateState( unsafe.$normalParent = getNormalParent(component); - ['$root', '$parent', '$normalParent'].forEach((key) => { + for (const key of ['$root', '$parent', '$normalParent']) { const val = unsafe[key]; if (val != null) { @@ -219,7 +230,7 @@ export function beforeCreateState( value: getComponentContext(Object.cast(val)) }); } - }); + } Object.defineProperty(unsafe, '$children', { configurable: true, @@ -252,9 +263,7 @@ export function beforeCreateState( }()); }); - if (opts?.addMethods) { - attachMethodsFromMeta(component); - } + attachMethodsFromMeta(component); if (opts?.implementEventAPI) { implementEventEmitterAPI(component); @@ -288,86 +297,7 @@ export function beforeCreateState( runHook('beforeRuntime', component).catch(stderr); - const { - systemFields, - tiedFields, - - computedFields, - accessors, - - watchDependencies, - watchers - } = meta; - - initFields(systemFields, component, unsafe); - - const fakeHandler = () => undefined; - - if (watchDependencies.size > 0) { - const watchSet = new Set(); - - watchDependencies.forEach((deps) => { - deps.forEach((dep) => { - const - path = Object.isArray(dep) ? dep.join('.') : String(dep), - info = getPropertyInfo(path, component); - - if (info.type === 'system' || isFunctional && info.type === 'field') { - watchSet.add(info); - } - }); - }); - - // If a computed property has a field or system field as a dependency - // and the host component does not have any watchers to this field, - // we need to register a "fake" watcher to enforce watching - watchSet.forEach((info) => { - const needToForceWatching = - watchers[info.name] == null && - watchers[info.originalPath] == null && - watchers[info.path] == null; - - if (needToForceWatching) { - watchers[info.name] = [ - { - deep: true, - immediate: true, - provideArgs: false, - handler: fakeHandler - } - ]; - } - }); - } - - // If a computed property is tied with a field or system field - // and the host component does not have any watchers to this field, - // we need to register a "fake" watcher to enforce watching - Object.entries(tiedFields).forEach(([name, normalizedName]) => { - if (normalizedName == null) { - return; - } - - const - accessor = accessors[normalizedName], - computed = accessor == null ? computedFields[normalizedName] : null; - - const needForceWatch = watchers[name] == null && ( - accessor != null && accessor.dependencies?.length !== 0 || - computed != null && computed.cache !== 'forever' && computed.dependencies?.length !== 0 - ); - - if (needForceWatch) { - watchers[name] = [ - { - deep: true, - immediate: true, - provideArgs: false, - handler: fakeHandler - } - ]; - } - }); + initFields(meta.systemFieldInitializers, component, unsafe); runHook('beforeCreate', component).catch(stderr); callMethodFromComponent(component, 'beforeCreate'); diff --git a/src/core/component/init/states/before-data-create.ts b/src/core/component/init/states/before-data-create.ts index 3c2a1c1c27..560de43ac3 100644 --- a/src/core/component/init/states/before-data-create.ts +++ b/src/core/component/init/states/before-data-create.ts @@ -18,7 +18,7 @@ import type { ComponentInterface } from 'core/component/interface'; */ export function beforeDataCreateState(component: ComponentInterface): void { const {meta, $fields} = component.unsafe; - initFields(meta.fields, component, $fields); + initFields(meta.fieldInitializers, component, $fields); // In functional components, the watching of fields can be initialized in lazy mode if (SSR || meta.params.functional === true) { diff --git a/src/core/component/init/states/before-destroy.ts b/src/core/component/init/states/before-destroy.ts index 76513140a2..86f3dfe7a2 100644 --- a/src/core/component/init/states/before-destroy.ts +++ b/src/core/component/init/states/before-destroy.ts @@ -40,7 +40,10 @@ export function beforeDestroyState(component: ComponentInterface, opts: Componen unsafe.async.clearAll().locked = true; unsafe.$async.clearAll().locked = true; - unsafe.$destructors.forEach((destructor) => destructor()); + + for (let i = 0; i < unsafe.$destructors.length; i++) { + unsafe.$destructors[i](); + } if ($el != null && $el.component === component) { delete $el.component; diff --git a/src/core/component/interface/CHANGELOG.md b/src/core/component/interface/CHANGELOG.md index e5b0920fbd..3b34ff0462 100644 --- a/src/core/component/interface/CHANGELOG.md +++ b/src/core/component/interface/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :boom: Breaking Change + +* The `getPassedProps` method now returns a dictionary instead of a set + ## v4.0.0-beta.139.dsl-speedup-2 (2024-10-03) #### :rocket: New Feature diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 8e00ecb288..056d6f38b2 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -78,6 +78,12 @@ export abstract class ComponentInterface { */ abstract readonly globalName?: string; + /** + * If set to true, the component will inherit modifiers from the parent `sharedMods` property. + * This prop is set automatically during the build. + */ + abstract readonly inheritMods?: boolean; + /** * True if the component renders as a regular one, but can be rendered as a functional. * This parameter is used during SSR and when hydrating the page. @@ -163,9 +169,9 @@ export abstract class ComponentInterface { abstract readonly getParent?: () => this['$parent']; /** - * The getter is used to get a set of props that were passed to the component directly through the template + * The getter is used to get a dictionary of props that were passed to the component directly through the template */ - abstract readonly getPassedProps?: () => Set; + abstract readonly getPassedProps?: () => Dictionary; /** * A string value indicating the lifecycle hook that the component is currently in. @@ -251,7 +257,7 @@ export abstract class ComponentInterface { /** * A dictionary containing component attributes that are not identified as input properties */ - protected readonly $attrs!: Dictionary; + protected readonly $attrs!: Dictionary; /** * A dictionary containing the watchable component fields that can trigger a re-rendering of the component diff --git a/src/core/component/interface/watch.ts b/src/core/component/interface/watch.ts index 96e7ad9549..56fd7f60e0 100644 --- a/src/core/component/interface/watch.ts +++ b/src/core/component/interface/watch.ts @@ -45,7 +45,7 @@ export interface FieldWatcher< /** * This handler is called when a watcher event occurs */ - handler: WatchHandler; + handler: string | WatchHandler; /** * If set to false, the watcher will not be registered for functional components @@ -63,7 +63,7 @@ export interface FieldWatcher< } export interface WatchObject< - CTX extends ComponentInterface = ComponentInterface, + Ctx extends ComponentInterface = ComponentInterface, A = unknown, B = A > extends WatchOptions { @@ -133,7 +133,7 @@ export interface WatchObject< * } * ``` */ - wrapper?: WatchWrapper; + wrapper?: WatchWrapper; /** * The name of a component method that is registered as a handler for the watcher @@ -152,11 +152,11 @@ export interface WatchObject< * * @param ctx */ - shouldInit?(ctx: CTX): boolean; + shouldInit?(ctx: Ctx): boolean; } export interface MethodWatcher< - CTX extends ComponentInterface = ComponentInterface, + Ctx extends ComponentInterface = ComponentInterface, A = unknown, B = A > extends WatchOptions { @@ -190,7 +190,7 @@ export interface MethodWatcher< * * @param ctx */ - shouldInit?(ctx: CTX): boolean; + shouldInit?(ctx: Ctx): boolean; /** * An object with additional settings for the event emitter @@ -228,7 +228,7 @@ export interface MethodWatcher< * } * ``` */ - wrapper?: WatchWrapper; + wrapper?: WatchWrapper; } export type WatchPath = @@ -236,9 +236,9 @@ export type WatchPath = PropertyInfo | {ctx: object; path?: RawWatchPath}; -export interface RawWatchHandler { +export interface RawWatchHandler { (a: A, b?: B, params?: WatchHandlerParams): void; - (this: CTX, a: A, b?: B, params?: WatchHandlerParams): void; + (this: Ctx, a: A, b?: B, params?: WatchHandlerParams): void; } export interface WatchHandler { diff --git a/src/core/component/meta/CHANGELOG.md b/src/core/component/meta/CHANGELOG.md index 803648c0e8..53e515a6d2 100644 --- a/src/core/component/meta/CHANGELOG.md +++ b/src/core/component/meta/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-beta.?? (2024-??-??) + +#### :house: Internal + +* When inheriting metaobjects, prototype chains are now used instead of full copying. + This optimizes the process of creating metaobjects. +* Methods and accessors are now added to the metaobject via the `method` decorator instead of runtime reflection. + This decorator is automatically added during the build process. +* Optimized creation of metaobjects. + ## v4.0.0-beta.138.dsl-speedup (2024-10-01) #### :rocket: New Feature diff --git a/src/core/component/meta/README.md b/src/core/component/meta/README.md index 6085170a51..8db117ccb8 100644 --- a/src/core/component/meta/README.md +++ b/src/core/component/meta/README.md @@ -202,11 +202,6 @@ This function modifies the original object. Populates the passed metaobject with methods and properties from the specified component class constructor. -### addMethodsToMeta - -Loops through the prototype of the passed component constructor and -adds methods and accessors to the specified metaobject. - ### attachTemplatesToMeta Attaches templates to the specified metaobject. diff --git a/src/core/component/meta/create.ts b/src/core/component/meta/create.ts index b5bd7e2523..f33ef53fbc 100644 --- a/src/core/component/meta/create.ts +++ b/src/core/component/meta/create.ts @@ -12,6 +12,8 @@ import { inheritMeta } from 'core/component/meta/inherit'; import type { ComponentMeta, ComponentConstructorInfo } from 'core/component/interface'; +const INSTANCE = Symbol('The component instance'); + /** * Creates a component metaobject based on the information from its constructor, and then returns this object * @param component - information obtained from the component constructor using the `getInfoFromConstructor` function @@ -19,21 +21,34 @@ import type { ComponentMeta, ComponentConstructorInfo } from 'core/component/int export function createMeta(component: ComponentConstructorInfo): ComponentMeta { const meta: ComponentMeta = { name: component.name, - layer: component.layer, componentName: component.componentName, + layer: component.layer, + params: component.params, parentMeta: component.parentMeta, + constructor: component.constructor, - instance: {}, - params: component.params, + get instance() { + const {constructor} = this; + + if (!constructor.hasOwnProperty(INSTANCE)) { + Object.defineProperty(constructor, INSTANCE, {value: Object.create(constructor.prototype)}); + } + + return constructor[INSTANCE]; + }, props: {}, mods: component.params.partial == null ? getComponentMods(component) : {}, fields: {}, - tiedFields: {}, + fieldInitializers: [], + systemFields: {}, + systemFieldInitializers: [], + + tiedFields: {}, computedFields: {}, methods: {}, @@ -65,15 +80,15 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { renderTriggered: [] }, + metaInitializers: new Map(), + component: { name: component.name, mods: {}, props: {}, attrs: {}, - computed: {}, - methods: {}, render() { throw new ReferenceError(`The render function for the component "${component.componentName}" is not specified`); diff --git a/src/core/component/field/helpers.ts b/src/core/component/meta/field.ts similarity index 63% rename from src/core/component/field/helpers.ts rename to src/core/component/meta/field.ts index edcf59c9d9..b8741cbaae 100644 --- a/src/core/component/field/helpers.ts +++ b/src/core/component/meta/field.ts @@ -6,10 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { sortedFields } from 'core/component/field/const'; - -import type { ComponentField } from 'core/component/interface'; -import type { SortedFields } from 'core/component/field/interface'; +import type { ComponentField, ComponentFieldInitializers } from 'core/component/meta'; /** * Returns the weight of a specified field from a given scope. @@ -26,14 +23,14 @@ export function getFieldWeight(field: CanUndef, scope: Dictionar return 0; } - const {after} = field; - let weight = 0; + const {after} = field; + if (after != null) { weight += after.size; - after.forEach((name) => { + for (const name of after) { const dep = scope[name]; if (dep == null) { @@ -41,7 +38,7 @@ export function getFieldWeight(field: CanUndef, scope: Dictionar } weight += getFieldWeight(dep, scope); - }); + } } if (!field.atom) { @@ -55,20 +52,28 @@ export function getFieldWeight(field: CanUndef, scope: Dictionar * Sorts the specified fields and returns an array that is ordered and ready for initialization * @param fields */ -export function sortFields(fields: Dictionary): SortedFields { - let val = sortedFields.get(fields); +export function sortFields(fields: Dictionary): ComponentFieldInitializers { + const list: Array<[string, ComponentField]> = []; - if (val == null) { - val = Object.entries(Object.cast>(fields)).sort(([_1, a], [_2, b]) => { - const - aWeight = getFieldWeight(a, fields), - bWeight = getFieldWeight(b, fields); + // eslint-disable-next-line guard-for-in + for (const name in fields) { + const field = fields[name]; - return aWeight - bWeight; - }); + if (field == null) { + continue; + } - sortedFields.set(fields, val); + list.push([name, field]); } - return val; + // The for-in loop first iterates over the object's own properties, and then over those from the prototypes, + // which means the initialization order will be reversed. + // To fix this, we need to reverse the list of fields before sorting. + return list.reverse().sort(([aName], [bName]) => { + const + aWeight = getFieldWeight(fields[aName], fields), + bWeight = getFieldWeight(fields[bName], fields); + + return aWeight - bWeight; + }); } diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 26be73be5a..b23431fbec 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -6,16 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { DEFAULT_WRAPPER } from 'core/component/const'; +import { isAbstractComponent } from 'core/component/reflect'; -import { getComponentContext } from 'core/component/context'; -import { isAbstractComponent, isBinding } from 'core/component/reflect'; -import { addMethodsToMeta } from 'core/component/meta/method'; +import { sortFields } from 'core/component/meta/field'; -import type { ComponentConstructor, ComponentMeta, ModVal } from 'core/component/interface'; +import type { ComponentConstructor, ModVal } from 'core/component/interface'; +import type { ComponentMeta } from 'core/component/meta/interface'; const - INSTANCE = Symbol('The component instance'), BLUEPRINT = Symbol('The metaobject blueprint'), ALREADY_FILLED = Symbol('This constructor has already been used to populate the metaobject'); @@ -26,8 +24,6 @@ const * @param [constructor] - the component constructor */ export function fillMeta(meta: ComponentMeta, constructor: ComponentConstructor = meta.constructor): ComponentMeta { - addMethodsToMeta(meta, constructor); - if (isAbstractComponent.test(meta.componentName)) { return meta; } @@ -44,14 +40,19 @@ export function fillMeta(meta: ComponentMeta, constructor: ComponentConstructor }); } - const blueprint: CanNull> = meta[BLUEPRINT]; + type Blueprint = Pick; + + const blueprint: CanNull = meta[BLUEPRINT]; if (blueprint != null) { const hooks = {}; - Object.entries(blueprint.hooks).forEach(([name, handlers]) => { - hooks[name] = handlers.slice(); - }); + const hookNames = Object.keys(blueprint.hooks); + + for (let i = 0; i < hookNames.length; i++) { + const name = hookNames[i]; + hooks[name] = blueprint.hooks[name].slice(); + } Object.assign(meta, { hooks, @@ -59,246 +60,42 @@ export function fillMeta(meta: ComponentMeta, constructor: ComponentConstructor }); } - const { - component, - params, - - methods, - accessors, - computedFields, - - watchers, - hooks, - - watchDependencies, - watchPropDependencies - } = meta; - - Object.defineProperty(meta, 'instance', { - enumerable: true, - configurable: true, - - get() { - if (!constructor.hasOwnProperty(INSTANCE)) { - Object.defineProperty(constructor, INSTANCE, {value: new constructor()}); - } - - return constructor[INSTANCE]; - } - }); - - // Creating an instance of a component is not a free operation. - // If it is not immediately necessary, we execute it in the background during idle time. - requestIdleCallback(() => { - void meta.instance; - }); - - const - isRoot = params.root === true, - isFunctional = params.functional === true; - - // Props - - const - defaultProps = params.defaultProps !== false, - canWatchProps = !SSR && !isRoot && !isFunctional; - - Object.entries(meta.props).forEach(([propName, prop]) => { - if (prop == null) { - return; - } - - if (isFirstFill) { - const skipDefault = !defaultProps && !prop.forceDefault; - - let defaultValue: unknown; - - if (!skipDefault) { - if (prop.default !== undefined) { - defaultValue = prop.default; - - } else { - const defaultInstanceValue = meta.instance[propName]; - - let getDefault = defaultInstanceValue; - - // If the default value of a prop is set via a default value for a class property, - // it is necessary to clone this value for each new component instance - // to ensure that they do not share the same value - if (prop.type !== Function && defaultInstanceValue != null && typeof defaultInstanceValue === 'object') { - getDefault = () => Object.isPrimitive(defaultInstanceValue) ? - defaultInstanceValue : - Object.fastClone(defaultInstanceValue); - - (getDefault)[DEFAULT_WRAPPER] = true; - } - - defaultValue = getDefault; - } - } - - if (!isRoot || defaultValue !== undefined) { - (prop.forceUpdate ? component.props : component.attrs)[propName] = { - type: prop.type, - required: prop.required !== false && defaultProps && defaultValue === undefined, - - default: defaultValue, - functional: prop.functional, - - // eslint-disable-next-line @v4fire/unbound-method - validator: prop.validator - }; - } - } - - if (prop.watchers != null && Object.size(prop.watchers) > 0) { - const watcherListeners = watchers[propName] ?? []; - watchers[propName] = watcherListeners; - - prop.watchers.forEach((watcher) => { - if (isFunctional && watcher.functional === false || !canWatchProps && !watcher.immediate) { - return; - } - - watcherListeners.push(watcher); - }); - } - - if (canWatchProps) { - const normalizedName = isBinding.test(propName) ? isBinding.replace(propName) : propName; - - if ((computedFields[normalizedName] ?? accessors[normalizedName]) != null) { - const props = watchPropDependencies.get(normalizedName) ?? new Set(); - - props.add(propName); - watchPropDependencies.set(normalizedName, props); - - } else { - watchDependencies.forEach((deps, path) => { - deps.some((dep) => { - const pathChunks = Object.isArray(dep) ? dep : dep.split('.', 1); - - if (pathChunks[0] === propName) { - const props = watchPropDependencies.get(path) ?? new Set(); - - props.add(propName); - watchPropDependencies.set(path, props); - - return true; - } - - return false; - }); - }); - } - } - }); - - // Fields - - [meta.systemFields, meta.fields].forEach((field) => { - Object.entries(field).forEach(([fieldName, field]) => { - field?.watchers?.forEach((watcher) => { - if (isFunctional && watcher.functional === false) { - return; - } - - const watcherListeners = watchers[fieldName] ?? []; - - watchers[fieldName] = watcherListeners; - watcherListeners.push(watcher); - }); - }); - }); - - // Computed fields + const {component} = meta; if (isFirstFill) { - Object.entries(computedFields).forEach(([name, computed]) => { - if (computed == null || computed.cache !== 'auto') { - return; - } - - component.computed[name] = { - get: computed.get, - set: computed.set - }; - }); + meta.fieldInitializers = sortFields(meta.fields); + meta.systemFieldInitializers = sortFields(meta.systemFields); } - // Methods - - Object.entries(methods).forEach(([methodName, method]) => { - if (method == null) { - return; - } - - if (isFirstFill) { - component.methods[methodName] = wrapper; - - if (wrapper.length !== method.fn.length) { - Object.defineProperty(wrapper, 'length', {get: () => method.fn.length}); - } - } - - if (method.watchers != null) { - Object.entries(method.watchers).forEach(([watcherName, watcher]) => { - if (watcher == null || isFunctional && watcher.functional === false) { - return; - } - - const watcherListeners = watchers[watcherName] ?? []; - watchers[watcherName] = watcherListeners; - - watcherListeners.push({ - ...watcher, - method: methodName, - args: Array.toArray(watcher.args), - handler: Object.cast(method.fn) - }); - }); - } - - // Method hooks - - if (method.hooks) { - Object.entries(method.hooks).forEach(([hookName, hook]) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (hook == null || isFunctional && hook.functional === false) { - return; - } - - hooks[hookName].push({...hook, fn: method.fn}); - }); - } - - function wrapper(this: object) { - // eslint-disable-next-line prefer-rest-params - return method!.fn.apply(getComponentContext(this), arguments); - } - }); - - // Modifiers + for (const init of meta.metaInitializers.values()) { + init(meta); + } if (isFirstFill) { const {mods} = component; - Object.entries(meta.mods).forEach(([modsName, mod]) => { + const modNames = Object.keys(meta.mods); + + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + mod = meta.mods[modName]; + let defaultValue: CanUndef; if (mod != null) { - mod.some((val) => { + for (let i = 0; i < mod.length; i++) { + const val = mod[i]; + if (Object.isArray(val)) { defaultValue = val; - return true; + break; } + } - return false; - }); - - mods[modsName] = defaultValue !== undefined ? String(defaultValue[0]) : undefined; + mods[modName] = defaultValue !== undefined ? String(defaultValue[0]) : undefined; } - }); + } } Object.defineProperty(constructor, ALREADY_FILLED, {value: true}); diff --git a/src/core/component/meta/fork.ts b/src/core/component/meta/fork.ts index 18ffec176e..c184cb12b0 100644 --- a/src/core/component/meta/fork.ts +++ b/src/core/component/meta/fork.ts @@ -21,17 +21,26 @@ export function forkMeta(base: ComponentMeta): ComponentMeta { meta.tiedFields = {...meta.tiedFields}; meta.hooks = {}; - Object.entries(base.hooks).forEach(([name, handlers]) => { - meta.hooks[name] = handlers.slice(); - }); + const hookNames = Object.keys(base.hooks); + + for (let i = 0; i < hookNames.length; i++) { + const name = hookNames[i]; + meta.hooks[name] = base.hooks[name].slice(); + } meta.watchers = {}; - Object.entries(base.watchers).forEach(([name, watchers]) => { + const watcherNames = Object.keys(base.watchers); + + for (let i = 0; i < watcherNames.length; i++) { + const + name = watcherNames[i], + watchers = base.watchers[name]; + if (watchers != null) { meta.watchers[name] = watchers.slice(); } - }); + } return meta; } diff --git a/src/core/component/meta/index.ts b/src/core/component/meta/index.ts index d9e97d9fb5..5d1378ff67 100644 --- a/src/core/component/meta/index.ts +++ b/src/core/component/meta/index.ts @@ -15,6 +15,5 @@ export * from 'core/component/meta/interface'; export * from 'core/component/meta/create'; export * from 'core/component/meta/fill'; export * from 'core/component/meta/fork'; -export * from 'core/component/meta/method'; export * from 'core/component/meta/tpl'; export * from 'core/component/meta/inherit'; diff --git a/src/core/component/meta/inherit.ts b/src/core/component/meta/inherit.ts index 823db3ac46..5d3ca69a64 100644 --- a/src/core/component/meta/inherit.ts +++ b/src/core/component/meta/inherit.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { componentDecoratedKeys, PARENT } from 'core/component/const'; +import { PARENT } from 'core/component/const'; -import type { ModDeclVal, FieldWatcher } from 'core/component/interface'; +import type { ModDeclVal } from 'core/component/interface'; import type { ComponentMeta } from 'core/component/meta/interface'; /** @@ -19,121 +19,35 @@ import type { ComponentMeta } from 'core/component/meta/interface'; * @param parentMeta */ export function inheritMeta(meta: ComponentMeta, parentMeta: ComponentMeta): ComponentMeta { - const decoratedKeys = componentDecoratedKeys[meta.componentName]; + meta.tiedFields = {...parentMeta.tiedFields}; - Object.assign(meta.tiedFields, parentMeta.tiedFields); + if (parentMeta.metaInitializers.size > 0) { + meta.metaInitializers = new Map(parentMeta.metaInitializers); + } if (parentMeta.watchDependencies.size > 0) { meta.watchDependencies = new Map(parentMeta.watchDependencies); } inheritParams(meta, parentMeta); - inheritProp(meta.props, parentMeta.props); - inheritField(meta.fields, parentMeta.fields); - inheritField(meta.systemFields, parentMeta.systemFields); + meta.props = Object.create(parentMeta.props); + meta.fields = Object.create(parentMeta.fields); + meta.systemFields = Object.create(parentMeta.systemFields); - inheritAccessors(meta.accessors, parentMeta.accessors); - inheritAccessors(meta.computedFields, parentMeta.computedFields); + meta.accessors = Object.create(parentMeta.accessors); + meta.computedFields = Object.create(parentMeta.computedFields); + meta.methods = Object.create(parentMeta.methods); - inheritMethods(meta.methods, parentMeta.methods); + meta.component.props = {...parentMeta.component.props}; + meta.component.attrs = {...parentMeta.component.attrs}; + meta.component.computed = {...parentMeta.component.computed}; if (meta.params.partial == null) { inheritMods(meta, parentMeta); } return meta; - - function inheritProp(current: ComponentMeta['props'], parent: ComponentMeta['props']) { - Object.entries(parent).forEach(([propName, parent]) => { - if (parent == null) { - return; - } - - if (decoratedKeys == null || !decoratedKeys.has(propName)) { - current[propName] = parent; - return; - } - - let watchers: CanUndef>; - - parent.watchers?.forEach((watcher: FieldWatcher) => { - watchers ??= new Map(); - watchers.set(watcher.handler, {...watcher}); - }); - - current[propName] = {...parent, watchers}; - }); - } - - function inheritField(current: ComponentMeta['fields'], parent: ComponentMeta['fields']) { - Object.entries(parent).forEach(([fieldName, parent]) => { - if (parent == null) { - return; - } - - if (decoratedKeys == null || !decoratedKeys.has(fieldName)) { - current[fieldName] = parent; - return; - } - - let - after: CanUndef>, - watchers: CanUndef>; - - parent.watchers?.forEach((watcher: FieldWatcher) => { - watchers ??= new Map(); - watchers.set(watcher.handler, {...watcher}); - }); - - parent.after?.forEach((name: string) => { - after ??= new Set(); - after.add(name); - }); - - current[fieldName] = {...parent, after, watchers}; - }); - } - - function inheritAccessors(current: ComponentMeta['accessors'], parent: ComponentMeta['accessors']) { - Object.entries(parent).forEach(([accessorName, parent]) => { - current[accessorName] = {...parent!}; - }); - } - - function inheritMethods(current: ComponentMeta['methods'], parent: ComponentMeta['methods']) { - Object.entries(parent).forEach(([methodName, parent]) => { - if (parent == null) { - return; - } - - if (decoratedKeys == null || !decoratedKeys.has(methodName)) { - current[methodName] = {...parent}; - return; - } - - const - watchers = {}, - hooks = {}; - - if (parent.watchers != null) { - Object.entries(parent.watchers).forEach(([key, val]) => { - watchers[key] = {...val}; - }); - } - - if (parent.hooks != null) { - Object.entries(parent.hooks).forEach(([key, hook]) => { - hooks[key] = { - ...hook, - after: Object.size(hook.after) > 0 ? new Set(hook.after) : undefined - }; - }); - } - - current[methodName] = {...parent, watchers, hooks}; - }); - } } /** @@ -175,7 +89,13 @@ export function inheritParams(meta: ComponentMeta, parentMeta: ComponentMeta): v export function inheritMods(meta: ComponentMeta, parentMeta: ComponentMeta): void { const {mods} = meta; - Object.entries(parentMeta.mods).forEach(([modName, parentModValues]) => { + const keys = Object.keys(parentMeta.mods); + + for (let i = 0; i < keys.length; i++) { + const + modName = keys[i], + parentModValues = parentMeta.mods[modName]; + const currentModValues = mods[modName], forkedParentModValues = parentModValues?.slice() ?? []; @@ -183,7 +103,7 @@ export function inheritMods(meta: ComponentMeta, parentMeta: ComponentMeta): voi if (currentModValues != null) { const values = Object.createDict(); - currentModValues.slice().forEach((modVal, i) => { + for (const [i, modVal] of currentModValues.slice().entries()) { if (modVal !== PARENT) { const modName = String(modVal); @@ -191,14 +111,14 @@ export function inheritMods(meta: ComponentMeta, parentMeta: ComponentMeta): voi values[modName] = >modVal; } - return; + continue; } const hasDefault = currentModValues.some((el) => Object.isArray(el)); let appliedDefault = !hasDefault; - forkedParentModValues.forEach((modVal) => { + for (const modVal of forkedParentModValues) { const modsName = String(modVal); if (!(modsName in values)) { @@ -209,10 +129,10 @@ export function inheritMods(meta: ComponentMeta, parentMeta: ComponentMeta): voi forkedParentModValues[i] = modVal[0]; appliedDefault = true; } - }); + } currentModValues.splice(i, 1, ...forkedParentModValues); - }); + } mods[modName] = Object .values(values) @@ -221,5 +141,5 @@ export function inheritMods(meta: ComponentMeta, parentMeta: ComponentMeta): voi } else if (!(modName in mods)) { mods[modName] = forkedParentModValues; } - }); + } } diff --git a/src/core/component/meta/interface/index.ts b/src/core/component/meta/interface/index.ts index ad5459d07a..d21f8f9e8d 100644 --- a/src/core/component/meta/interface/index.ts +++ b/src/core/component/meta/interface/index.ts @@ -6,200 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { PropOptions } from 'core/component/decorators'; -import type { RenderFunction, WritableComputedOptions } from 'core/component/engines'; - -import type { WatchObject, ComponentConstructor, ModsDecl } from 'core/component/interface'; -import type { ComponentOptions } from 'core/component/meta/interface/options'; - -import type { - - ComponentProp, - ComponentField, - - ComponentMethod, - ComponentAccessor, - ComponentHooks, - - ComponentDirectiveOptions, - ComponentWatchDependencies, - ComponentWatchPropDependencies - -} from 'core/component/meta/interface/types'; - +export * from 'core/component/meta/interface/meta'; export * from 'core/component/meta/interface/options'; export * from 'core/component/meta/interface/types'; - -/** - * An abstract component representation - */ -export interface ComponentMeta { - /** - * The full name of the component, which may include a `-functional` postfix if the component is smart - */ - name: string; - - /** - * The name of the NPM package in which the component is defined or overridden - */ - layer?: string; - - /** - * Component name without any special suffixes - */ - componentName: string; - - /** - * A link to the component's constructor - */ - constructor: ComponentConstructor; - - /** - * A link to the component's class instance - */ - instance: Dictionary; - - /** - * A dictionary containing the parameters provided to the `@component` decorator for the component - */ - params: ComponentOptions; - - /** - * A link to the metaobject of the parent component - */ - parentMeta: CanNull; - - /** - * A dictionary containing the input properties (props) for the component - */ - props: Dictionary; - - /** - * A dictionary containing the available component modifiers. - * Modifiers are a way to alter the behavior or appearance of a component without changing its underlying - * functionality. - * They can be used to customize components for specific use cases, or to extend their capabilities. - * The modifiers may include options such as size, color, placement, and other configurations. - */ - mods: ModsDecl; - - /** - * A dictionary containing the component fields that can trigger a re-rendering of the component - */ - fields: Dictionary; - - /** - * A dictionary containing the component fields that do not cause a re-rendering of the component when they change. - * These fields are typically used for internal bookkeeping or for caching computed values, - * and do not affect the visual representation of the component. - * Examples include variables used for storing data or for tracking the component's internal state, - * and helper functions or methods that do not modify any reactive properties. - * It's important to identify and distinguish these non-reactive fields from the reactive ones, - * and to use them appropriately to optimize the performance of the component. - */ - systemFields: Dictionary; - - /** - * A dictionary containing the component properties as well as properties that are related to them. - * For example: - * - * `foo → fooStore` - * `fooStore → foo` - */ - tiedFields: Dictionary; - - /** - * A dictionary containing the accessor methods of the component that support caching or watching - */ - computedFields: Dictionary; - - /** - * A dictionary containing the simple component accessors, - * which are typically used for retrieving or modifying the value of a non-reactive property - * that does not require caching or watching - */ - accessors: Dictionary; - - /** - * A dictionary containing the component methods - */ - methods: Dictionary; - - /** - * A dictionary with the component watchers - */ - watchers: Dictionary; - - /** - * A dictionary containing the component dependencies to watch to invalidate the cache of computed fields - */ - watchDependencies: ComponentWatchDependencies; - - /** - * A dictionary containing the component prop dependencies to watch to invalidate the cache of computed fields - */ - watchPropDependencies: ComponentWatchPropDependencies; - - /** - * A dictionary containing the component hook listeners, - * which are essentially functions that are executed at specific stages in the V4Fire component's lifecycle - */ - hooks: ComponentHooks; - - /** - * A less abstract representation of the component would typically include the following elements, - * which are useful for building component libraries: - */ - component: { - /** - * The full name of the component, which may include a `-functional` postfix if the component is smart - */ - name: string; - - /** - * A dictionary with registered component props - */ - props: Dictionary; - - /** - * A dictionary with registered component attributes. - * Unlike props, changing attributes does not lead to re-rendering of the component template. - */ - attrs: Dictionary; - - /** - * A dictionary containing the default component modifiers - */ - mods: Dictionary; - - /** - * A dictionary containing the accessor methods of the component that support caching or watching - */ - computed: Dictionary>>; - - /** - * A dictionary containing the component methods - */ - methods: Dictionary; - - /** - * A dictionary containing the available component directives - */ - directives?: Dictionary; - - /** - * A dictionary containing the available local components - */ - components?: Dictionary; - - /** - * The component's render function - */ - render?: RenderFunction; - - /** - * The component's render function for use with SSR - */ - ssrRender?: RenderFunction; - }; -} diff --git a/src/core/component/meta/interface/meta.ts b/src/core/component/meta/interface/meta.ts new file mode 100644 index 0000000000..b35e1e8bc2 --- /dev/null +++ b/src/core/component/meta/interface/meta.ts @@ -0,0 +1,215 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { WatchPath } from 'core/object/watch'; + +import type { PropOptions } from 'core/component/decorators'; +import type { RenderFunction, WritableComputedOptions } from 'core/component/engines'; + +import type { ComponentConstructor, WatchObject, ModsDecl } from 'core/component/interface'; +import type { ComponentOptions } from 'core/component/meta/interface/options'; + +import type { + + ComponentProp, + ComponentField, + ComponentSystemField, + ComponentFieldInitializers, + + ComponentMethod, + ComponentAccessor, + ComponentHooks, + + ComponentDirectiveOptions + +} from 'core/component/meta/interface/types'; + +/** + * An abstract component representation + */ +export interface ComponentMeta { + /** + * The full name of the component, which may include a `-functional` postfix if the component is smart + */ + name: string; + + /** + * Component name without any special suffixes + */ + componentName: string; + + /** + * The name of the NPM package in which the component is defined or overridden + */ + layer?: string; + + /** + * A link to the component's constructor + */ + constructor: ComponentConstructor; + + /** + * A link to the component's class instance + */ + instance: Dictionary; + + /** + * A dictionary containing the parameters provided to the `@component` decorator for the component + */ + params: ComponentOptions; + + /** + * A link to the metaobject of the parent component + */ + parentMeta: CanNull; + + /** + * A dictionary containing the input properties (props) for the component + */ + props: Dictionary; + + /** + * A dictionary containing the available component modifiers. + * Modifiers are a way to alter the behavior or appearance of a component without changing its underlying + * functionality. + * They can be used to customize components for specific use cases, or to extend their capabilities. + * The modifiers may include options such as size, color, placement, and other configurations. + */ + mods: ModsDecl; + + /** + * A dictionary containing the component fields that can trigger a re-rendering of the component + */ + fields: Dictionary; + + /** + * A sorted array of fields and functions for their initialization on the component + */ + fieldInitializers: ComponentFieldInitializers; + + /** + * A dictionary containing the component fields that do not cause a re-rendering of the component when they change. + * These fields are typically used for internal bookkeeping or for caching computed values, + * and do not affect the visual representation of the component. + * Examples include variables used for storing data or for tracking the component's internal state, + * and helper functions or methods that do not modify any reactive properties. + * It's important to identify and distinguish these non-reactive fields from the reactive ones, + * and to use them appropriately to optimize the performance of the component. + */ + systemFields: Dictionary; + + /** + * A sorted array of system fields and functions for their initialization on the component + */ + systemFieldInitializers: ComponentFieldInitializers; + + /** + * A dictionary containing the component properties as well as properties that are related to them. + * For example: + * + * `foo → fooStore` + * `fooStore → foo` + */ + tiedFields: Dictionary; + + /** + * A dictionary containing the accessor methods of the component that support caching or watching + */ + computedFields: Dictionary; + + /** + * A dictionary containing the simple component accessors, + * which are typically used for retrieving or modifying the value of a non-reactive property + * that does not require caching or watching + */ + accessors: Dictionary; + + /** + * A dictionary containing the component methods + */ + methods: Dictionary; + + /** + * A dictionary containing the component watchers + */ + watchers: Dictionary; + + /** + * A dictionary containing the component dependencies to watch to invalidate the cache of computed fields + */ + watchDependencies: Map; + + /** + * A dictionary containing the component prop dependencies to watch to invalidate the cache of computed fields + */ + watchPropDependencies: Map>; + + /** + * A dictionary containing the component hook listeners, + * which are essentially functions that are executed at specific stages in the V4Fire component's lifecycle + */ + hooks: ComponentHooks; + + /** + * A dictionary containing functions to initialize the component metaobject. + * The keys in the dictionary are the component entities: props, fields, methods, etc. + */ + metaInitializers: Map void>; + + /** + * A less abstract representation of the component would typically include the following elements, + * which are useful for building component libraries: + */ + component: { + /** + * The full name of the component, which may include a `-functional` postfix if the component is smart + */ + name: string; + + /** + * A dictionary with registered component props + */ + props: Dictionary; + + /** + * A dictionary with registered component attributes. + * Unlike props, changing attributes does not lead to re-rendering of the component template. + */ + attrs: Dictionary; + + /** + * A dictionary containing the default component modifiers + */ + mods: Dictionary; + + /** + * A dictionary containing the accessor methods of the component that support caching or watching + */ + computed: Dictionary>>; + + /** + * A dictionary containing the available component directives + */ + directives?: Dictionary; + + /** + * A dictionary containing the available local components + */ + components?: Dictionary; + + /** + * The component's render function + */ + render?: RenderFunction; + + /** + * The component's render function for use with SSR + */ + ssrRender?: RenderFunction; + }; +} diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts index 8d5e726f87..a3362b885f 100644 --- a/src/core/component/meta/interface/options.ts +++ b/src/core/component/meta/interface/options.ts @@ -159,14 +159,6 @@ export interface ComponentOptions { */ functional?: Nullable | Dictionary>; - /** - * If set to false, all default values for the input properties of the component will be disregarded. - * This parameter may be inherited from the parent component. - * - * @default `true` - */ - defaultProps?: boolean; - /** * A dictionary that specifies deprecated component props along with their recommended alternatives. * The keys in the dictionary represent the deprecated props, @@ -220,9 +212,7 @@ export interface ComponentOptions { inheritAttrs?: boolean; /** - * If set to true, the component will automatically inherit base modifiers from its parent component. - * This parameter may be inherited from the parent component. - * + * If set to true, the component will inherit modifiers from the parent `sharedMods` property * @default `true` */ inheritMods?: boolean; diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index d43e616e66..51978444ff 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -9,19 +9,21 @@ import type { WatchPath } from 'core/object/watch'; import type { WritableComputedOptions, DirectiveBinding } from 'core/component/engines'; + import type { PropOptions, InitFieldFn, MergeFieldFn, UniqueFieldFn } from 'core/component/decorators'; import type { ComponentInterface, FieldWatcher, MethodWatcher, Hook } from 'core/component/interface'; export interface ComponentProp extends PropOptions { forceUpdate: boolean; - forceDefault?: boolean; + watchers?: Map; default?: unknown; + meta: Dictionary; } -export interface ComponentSystemField { +export interface ComponentSystemField { src: string; meta: Dictionary; @@ -29,20 +31,23 @@ export interface ComponentSystemField; default?: unknown; - unique?: boolean | UniqueFieldFn; + unique?: boolean | UniqueFieldFn; functional?: boolean; functionalWatching?: boolean; - init?: InitFieldFn; - merge?: MergeFieldFn | boolean; + init?: InitFieldFn; + merge?: MergeFieldFn | boolean; + + watchers?: Map; } -export interface ComponentField extends ComponentSystemField { +export interface ComponentField extends ComponentSystemField { forceUpdate?: boolean; - watchers?: Map; } +export type ComponentFieldInitializers = Array<[string, CanUndef]>; + export type ComponentAccessorCacheType = boolean | 'forever' | @@ -64,7 +69,7 @@ export interface ComponentMethod { src?: string; wrapper?: boolean; - functional?: boolean; + accessor?: boolean; watchers?: Dictionary; hooks?: ComponentMethodHooks; @@ -94,6 +99,3 @@ export type ComponentMethodHooks = { }; export interface ComponentDirectiveOptions extends DirectiveBinding {} - -export type ComponentWatchDependencies = Map; -export type ComponentWatchPropDependencies = Map>; diff --git a/src/core/component/meta/method.ts b/src/core/component/meta/method.ts deleted file mode 100644 index 849d5e0442..0000000000 --- a/src/core/component/meta/method.ts +++ /dev/null @@ -1,149 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { defProp } from 'core/const/props'; -import type { ComponentMeta } from 'core/component/interface'; - -const ALREADY_PASSED = Symbol('This target is passed'); - -/** - * Loops through the prototype of the passed component constructor and - * adds methods and accessors to the specified metaobject - * - * @param meta - * @param [constructor] - */ -export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = meta.constructor): void { - // For smart components, this method can be called more than once - if (constructor.hasOwnProperty(ALREADY_PASSED)) { - return; - } - - Object.defineProperty(constructor, ALREADY_PASSED, {value: true}); - - const { - componentName: src, - props, - fields, - computedFields, - systemFields, - accessors, - methods - } = meta; - - const - proto = constructor.prototype, - descriptors = Object.getOwnPropertyDescriptors(proto); - - Object.entries(descriptors).forEach(([name, desc]) => { - if (name === 'constructor') { - return; - } - - // Methods - if ('value' in desc) { - const fn = desc.value; - - if (!Object.isFunction(fn)) { - return; - } - - methods[name] = Object.assign(methods[name] ?? {watchers: {}, hooks: {}}, {src, fn}); - - // Accessors - } else { - const - propKey = `${name}Prop`, - storeKey = `${name}Store`; - - let - metaKey: string, - tiedWith: CanUndef; - - // Computed fields are cached by default - if ( - name in computedFields || - !(name in accessors) && (tiedWith = props[propKey] ?? fields[storeKey] ?? systemFields[storeKey]) - ) { - metaKey = 'computedFields'; - - } else { - metaKey = 'accessors'; - } - - let field: Dictionary; - - if (props[name] != null) { - field = props; - - } else if (fields[name] != null) { - field = fields; - - } else { - field = systemFields; - } - - const store = meta[metaKey]; - - // If we already have a property by this key, like a prop or field, - // we need to delete it to correct override - if (field[name] != null) { - Object.defineProperty(proto, name, defProp); - delete field[name]; - } - - const - old = store[name], - set = desc.set ?? old?.set, - get = desc.get ?? old?.get; - - // To use `super` within the setter, we also create a new method with a name `${key}Setter` - if (set != null) { - const nm = `${name}Setter`; - proto[nm] = set; - - meta.methods[nm] = { - src, - fn: set, - watchers: {}, - hooks: {} - }; - } - - // To using `super` within the getter, we also create a new method with a name `${key}Getter` - if (get != null) { - const nm = `${name}Getter`; - proto[nm] = get; - - meta.methods[nm] = { - src, - fn: get, - watchers: {}, - hooks: {} - }; - } - - const acc = Object.assign(store[name] ?? {}, { - src, - get: desc.get ?? old?.get, - set - }); - - store[name] = acc; - - // eslint-disable-next-line eqeqeq - if (acc.functional === undefined && meta.params.functional === null) { - acc.functional = false; - } - - if (tiedWith != null) { - acc.tiedWith = tiedWith; - } - } - }); -} diff --git a/src/core/component/method/index.ts b/src/core/component/method/index.ts index 450e209782..b8ded54961 100644 --- a/src/core/component/method/index.ts +++ b/src/core/component/method/index.ts @@ -11,26 +11,29 @@ * @packageDocumentation */ -import type { ComponentInterface } from 'core/component/interface'; +import type { ComponentInterface, UnsafeComponentInterface } from 'core/component/interface'; /** * Attaches methods to the passed component instance, taken from its associated metaobject * @param component */ export function attachMethodsFromMeta(component: ComponentInterface): void { - const {meta, meta: {methods}} = component.unsafe; + const {meta, meta: {methods}} = Object.cast(component); - const isFunctional = meta.params.functional === true; + // eslint-disable-next-line guard-for-in + for (const methodName in methods) { + const method = methods[methodName]; - Object.entries(methods).forEach(([name, method]) => { - if (method == null || !SSR && isFunctional && method.functional === false) { - return; + // Methods for accessors, such as fooGetter/fooSetter, + // are used only with super, so it's not necessary to initialize them as a method on the component + if (method == null || method.accessor) { + continue; } - component[name] = method.fn.bind(component); - }); + component[methodName] = method.fn.bind(component); + } - if (isFunctional) { + if (meta.params.functional === true) { component.render = Object.cast(meta.component.render); } } diff --git a/src/core/component/prop/helpers.ts b/src/core/component/prop/helpers.ts index 07b0da05e5..dfd8a2e8bf 100644 --- a/src/core/component/prop/helpers.ts +++ b/src/core/component/prop/helpers.ts @@ -36,31 +36,30 @@ export function attachAttrPropsListeners(component: ComponentInterface): void { el = unsafe.$el, propValuesToUpdate: string[][] = []; - Object.entries(unsafe.$attrs).forEach(([attrName, attrVal]) => { - const propPrefix = 'on:'; - - if (meta.props[attrName]?.forceUpdate === false) { - const getterName = propPrefix + attrName; - - if (attrVal !== undefined && !Object.isFunction(unsafe.$attrs[getterName])) { - throw new Error(`No accessors are defined for the prop "${attrName}". To set the accessors, pass them as ":${attrName} = propValue | @:${attrName} = createPropAccessors(() => propValue)()" or "v-attrs = {'@:${attrName}': createPropAccessors(() => propValue)}".`); - } - } - - if (!isPropGetter.test(attrName)) { - return; - } - - const propName = isPropGetter.replace(attrName); - - if (meta.props[propName]?.forceUpdate === false) { - propValuesToUpdate.push([propName, attrName]); - - if (el instanceof Element) { - el.removeAttribute(propName); + const attrNames = Object.keys(unsafe.$attrs); + + for (let i = 0; i < attrNames.length; i++) { + const + // The name of an attribute can be either the name of a component prop, + // the name of a regular DOM node attribute, + // or an event handler (in which case the attribute name will start with `on`) + attrName = attrNames[i], + prop = meta.props[attrName]; + + if (prop == null && isPropGetter.test(attrName)) { + const propName = isPropGetter.replace(attrName); + + // If an accessor is provided for a prop with `forceUpdate: false`, + // it is included in the list of synchronized props + if (meta.props[propName]?.forceUpdate === false) { + propValuesToUpdate.push([propName, attrName]); + + if (el instanceof Element) { + el.removeAttribute(propName); + } } } - }); + } if (propValuesToUpdate.length > 0) { nonFunctionalParent.$on('hook:beforeUpdate', updatePropsValues); @@ -79,13 +78,13 @@ export function attachAttrPropsListeners(component: ComponentInterface): void { await parent.$nextTick(); } - propValuesToUpdate.forEach(([propName, getterName]) => { + for (const [propName, getterName] of propValuesToUpdate) { const getter = unsafe.$attrs[getterName]; if (Object.isFunction(getter)) { unsafe[`[[${propName}]]`] = getter()[0]; } - }); + } } } } diff --git a/src/core/component/prop/init.ts b/src/core/component/prop/init.ts index 2ba8478076..f47d94c4a4 100644 --- a/src/core/component/prop/init.ts +++ b/src/core/component/prop/init.ts @@ -6,8 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { DEFAULT_WRAPPER } from 'core/component/const'; - import type { ComponentInterface } from 'core/component/interface'; import type { InitPropsObjectOptions } from 'core/component/prop/interface'; @@ -38,29 +36,36 @@ export function initProps( ...opts }; - const { - store, - from - } = p; + const {store, from} = p; const isFunctional = meta.params.functional === true, source: typeof props = p.forceUpdate ? props : attrs; - Object.entries(source).forEach(([propName, prop]) => { + const + propNames = Object.keys(source), + passedProps = unsafe.getPassedProps?.(); + + for (let i = 0; i < propNames.length; i++) { + const + propName = propNames[i], + prop = source[propName]; + const canSkip = prop == null || !SSR && isFunctional && prop.functional === false; if (canSkip) { - return; + continue; } unsafe.$activeField = propName; let propValue = (from ?? component)[propName]; - const getAccessors = unsafe.$attrs[`on:${propName}`]; + const + accessorName = `on:${propName}`, + getAccessors = unsafe.$attrs[accessorName]; if (propValue === undefined && Object.isFunction(getAccessors)) { propValue = getAccessors()[0]; @@ -71,7 +76,7 @@ export function initProps( if (propValue === undefined && prop.default !== undefined) { propValue = prop.default; - if (Object.isFunction(propValue) && (opts.saveToStore === true || propValue[DEFAULT_WRAPPER] !== true)) { + if (Object.isFunction(propValue) && opts.saveToStore) { propValue = prop.type === Function ? propValue : propValue(component); if (Object.isFunction(propValue)) { @@ -96,7 +101,7 @@ export function initProps( if (needSaveToStore) { const privateField = `[[${propName}]]`; - if (!opts.forceUpdate) { + if (!opts.forceUpdate && passedProps?.hasOwnProperty(accessorName)) { // Set the property as enumerable so that it can be deleted in the destructor later Object.defineProperty(store, privateField, { configurable: true, @@ -106,13 +111,23 @@ export function initProps( }); } - Object.defineProperty(store, propName, { - configurable: true, - enumerable: true, - get: () => opts.forceUpdate ? propValue : store[privateField] - }); + if (opts.forceUpdate) { + Object.defineProperty(store, propName, { + configurable: true, + enumerable: true, + writable: false, + value: propValue + }); + + } else { + Object.defineProperty(store, propName, { + configurable: true, + enumerable: true, + get: () => Object.hasOwn(store, privateField) ? store[privateField] : propValue + }); + } } - }); + } unsafe.$activeField = undefined; return store; diff --git a/src/core/component/queue-emitter/index.ts b/src/core/component/queue-emitter/index.ts index c9bd3c29e9..086a8e1bd8 100644 --- a/src/core/component/queue-emitter/index.ts +++ b/src/core/component/queue-emitter/index.ts @@ -35,11 +35,11 @@ export default class QueueEmitter { */ on(event: Nullable>, handler: Function): void { if (event != null && event.size > 0) { - event.forEach((name) => { + for (const name of event) { const listeners = this.listeners[name] ?? []; listeners.push({event, handler}); this.listeners[name] = listeners; - }); + } return; } @@ -63,10 +63,12 @@ export default class QueueEmitter { const tasks: Array> = []; - queue.forEach((el) => { + for (let i = 0; i < queue.length; i++) { + const el = queue[i]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (el == null) { - return; + continue; } const ev = el.event; @@ -79,7 +81,7 @@ export default class QueueEmitter { tasks.push(task); } } - }); + } if (tasks.length > 0) { return Promise.all(tasks).then(() => undefined); @@ -96,13 +98,13 @@ export default class QueueEmitter { const tasks: Array> = []; - queue.forEach((el) => { - const task = el(); + for (let i = 0; i < queue.length; i++) { + const task = queue[i](); if (Object.isPromise(task)) { tasks.push(task); } - }); + } if (tasks.length > 0) { return Promise.all(tasks).then(() => undefined); diff --git a/src/core/component/reflect/mod.ts b/src/core/component/reflect/mod.ts index 24df579830..04f87cfb6a 100644 --- a/src/core/component/reflect/mod.ts +++ b/src/core/component/reflect/mod.ts @@ -38,10 +38,7 @@ import type { ComponentConstructorInfo } from 'core/component/reflect/interface' * ``` */ export function getComponentMods(component: ComponentConstructorInfo): ModsDecl { - const { - constructor, - componentName - } = component; + const {constructor, componentName} = component; const mods = {}, @@ -49,21 +46,31 @@ export function getComponentMods(component: ComponentConstructorInfo): ModsDecl modsFromConstructor: ModsDecl = {...constructor['mods']}; if (Object.isDictionary(modsFromDS)) { - Object.entries(modsFromDS).forEach(([name, dsModDecl]) => { - const modDecl = modsFromConstructor[name]; - modsFromConstructor[name] = Object.cast(Array.toArray(modDecl, dsModDecl)); - }); + const modNames = Object.keys(modsFromDS); + + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modDecl = modsFromConstructor[modName]; + + modsFromConstructor[modName] = Object.cast(Array.toArray(modDecl, modsFromDS[modName])); + } } - Object.entries(modsFromConstructor).forEach(([modName, modDecl]) => { - const modValues: Array = []; + const modNames = Object.keys(modsFromConstructor); - if (modDecl != null && modDecl.length > 0) { - const cache = new Map(); + for (let i = 0; i < modNames.length; i++) { + const + modName = modNames[i], + modDecl = modsFromConstructor[modName], + modValues: Array = []; + if (modDecl != null && modDecl.length > 0) { let active: CanUndef; - modDecl.forEach((modVal) => { + const cache = new Map(); + + for (const modVal of modDecl) { if (Object.isArray(modVal)) { if (active !== undefined) { cache.set(active, active); @@ -73,23 +80,21 @@ export function getComponentMods(component: ComponentConstructorInfo): ModsDecl cache.set(active, [active]); } else { - const normalizedModVal = Object.isDictionary(modVal) ? - modVal : - String(modVal); + const normalizedModVal = Object.isDictionary(modVal) ? modVal : String(modVal); if (!cache.has(normalizedModVal)) { cache.set(normalizedModVal, normalizedModVal); } } - }); + } - cache.forEach((val) => { + for (const val of cache.values()) { modValues.push(val); - }); + } } mods[modName.camelize(false)] = modValues; - }); + } return mods; } diff --git a/src/core/component/reflect/property.ts b/src/core/component/reflect/property.ts index da62d366cc..4b15e128aa 100644 --- a/src/core/component/reflect/property.ts +++ b/src/core/component/reflect/property.ts @@ -66,9 +66,11 @@ export function getPropertyInfo(path: string, component: ComponentInterface): Pr let obj: Nullable = component; - chunks.some((chunk, i, chunks) => { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + if (obj == null) { - return true; + break; } if (Object.isMap(obj)) { @@ -82,9 +84,7 @@ export function getPropertyInfo(path: string, component: ComponentInterface): Pr component = obj; rootI = i === chunks.length - 1 ? i : i + 1; } - - return false; - }); + } path = chunks.slice(rootI).join('.'); topPath = chunks.slice(0, rootI + 1).join('.'); diff --git a/src/core/component/render/daemon/index.ts b/src/core/component/render/daemon/index.ts index c1301f892f..78669ff8fb 100644 --- a/src/core/component/render/daemon/index.ts +++ b/src/core/component/render/daemon/index.ts @@ -74,8 +74,7 @@ function run(): void { done = opts.weightPerTick; } - const - w = val.weight ?? 1; + const w = val.weight ?? 1; if (done - w < 0 && done !== opts.weightPerTick) { continue; diff --git a/src/core/component/render/helpers/attrs.ts b/src/core/component/render/helpers/attrs.ts index c3b564dd30..b0c61273e4 100644 --- a/src/core/component/render/helpers/attrs.ts +++ b/src/core/component/render/helpers/attrs.ts @@ -60,7 +60,9 @@ export function resolveAttrs(this: ComponentInterface, vnode: T } if (Object.isArray(children)) { - children.forEach((child) => resolveAttrs.call(this, Object.cast(child))); + for (let i = 0; i < children.length; i++) { + resolveAttrs.call(this, Object.cast(children[i])); + } } if (Object.isArray(dynamicChildren) && dynamicChildren.length > 0) { @@ -146,16 +148,20 @@ export function resolveAttrs(this: ComponentInterface, vnode: T const dynamicProps = vnode.dynamicProps ?? []; vnode.dynamicProps = dynamicProps; - Object.keys(props).forEach((prop) => { - if (isHandler.test(prop)) { - if (SSR && !isPropGetter.test(prop)) { - delete props![prop]; + const propNames = Object.keys(props); + + for (let i = 0; i < propNames.length; i++) { + const propName = propNames[i]; + + if (isHandler.test(propName)) { + if (SSR && !isPropGetter.test(propName)) { + delete props[propName]; } else { - dynamicProps.push(prop); + dynamicProps.push(propName); } } - }); + } } delete props[key]; @@ -180,15 +186,19 @@ export function resolveAttrs(this: ComponentInterface, vnode: T names = props[key]; if (names != null) { - names.split(' ').forEach((name: string) => { + const nameChunks = names.split(' '); + + for (let i = 0; i < nameChunks.length; i++) { + const name = nameChunks[i]; + if ('classes' in this && this.classes?.[name] != null) { - Object.assign(props, mergeProps({class: props?.class}, {class: this.classes[name]})); + Object.assign(props, mergeProps({class: props.class}, {class: this.classes[name]})); } if ('styles' in this && this.styles?.[name] != null) { - Object.assign(props, mergeProps({style: props?.style}, {style: this.styles[name]})); + Object.assign(props, mergeProps({style: props.style}, {style: this.styles[name]})); } - }); + } delete props[key]; } diff --git a/src/core/component/render/helpers/flags.ts b/src/core/component/render/helpers/flags.ts index bab6c549c0..601ce663d7 100644 --- a/src/core/component/render/helpers/flags.ts +++ b/src/core/component/render/helpers/flags.ts @@ -46,7 +46,9 @@ type PatchFlags = Exclude; * ``` */ export function setVNodePatchFlags(vnode: VNode, ...flags: Flags): void { - flags.forEach((flag) => { + for (let i = 0; i < flags.length; i++) { + const flag = flags[i]; + const val = flagValues[flag], dest = flagDest[flag]; @@ -55,7 +57,7 @@ export function setVNodePatchFlags(vnode: VNode, ...flags: Flags): void { if ((vnode[dest] & val) === 0) { vnode[dest] += val; } - }); + } } /** diff --git a/src/core/component/render/helpers/normalizers.ts b/src/core/component/render/helpers/normalizers.ts index 3a52adc497..10b8f81cea 100644 --- a/src/core/component/render/helpers/normalizers.ts +++ b/src/core/component/render/helpers/normalizers.ts @@ -23,20 +23,26 @@ export function normalizeClass(classes: CanArray): string { classesStr = classes; } else if (Object.isArray(classes)) { - classes.forEach((className) => { - const normalizedClass = normalizeClass(className); + for (let i = 0; i < classes.length; i += 1) { + const + className = classes[i], + normalizedClass = normalizeClass(className); if (normalizedClass !== '') { classesStr += `${normalizedClass} `; } - }); + } } else if (Object.isDictionary(classes)) { - Object.entries(classes).forEach(([className, has]) => { - if (Object.isTruly(has)) { + const keys = Object.keys(classes); + + for (let i = 0; i < keys.length; i++) { + const className = keys[i]; + + if (Object.isTruly(classes[className])) { classesStr += `${className} `; } - }); + } } return classesStr.trim(); @@ -50,15 +56,22 @@ export function normalizeStyle(styles: CanArray>): s if (Object.isArray(styles)) { const normalizedStyles = {}; - styles.forEach((style) => { + for (let i = 0; i < styles.length; i++) { + const style = styles[i]; + const normalizedStyle = Object.isString(style) ? parseStringStyle(style) : normalizeStyle(style); - if (Object.size(normalizedStyle) > 0) { - Object.entries(normalizedStyle).forEach(([name, style]) => normalizedStyles[name] = style); + if (Object.isDictionary(normalizedStyle)) { + const keys = Object.keys(normalizedStyle); + + for (let i = 0; i < keys.length; i++) { + const name = keys[i]; + normalizedStyles[name] = normalizedStyle[name]; + } } - }); + } return normalizedStyles; } @@ -85,17 +98,19 @@ const export function parseStringStyle(style: string): Dictionary { const styles = {}; - style.split(listDelimiterRgxp).forEach((singleStyle) => { - singleStyle = singleStyle.trim(); + const styleRules = style.split(listDelimiterRgxp); - if (singleStyle !== '') { - const chunks = singleStyle.split(propertyDelimiterRgxp, 2); + for (let i = 0; i < styleRules.length; i++) { + const style = styleRules[i].trim(); + + if (style !== '') { + const chunks = style.split(propertyDelimiterRgxp, 2); if (chunks.length > 1) { styles[chunks[0].trim()] = chunks[1].trim(); } } - }); + } return styles; } @@ -130,12 +145,18 @@ export function normalizeComponentAttrs( normalizedAttrs['v-attrs'] = normalizeComponentAttrs(normalizedAttrs['v-attrs'], dynamicProps, component); } - Object.entries(normalizedAttrs).forEach(normalizeAttr); + const attrNames = Object.keys(normalizedAttrs); + + for (let i = 0; i < attrNames.length; i++) { + const attrName = attrNames[i]; + normalizeAttr(attrName, normalizedAttrs[attrName]); + } + modifyDynamicPath(); return normalizedAttrs; - function normalizeAttr([attrName, value]: [string, unknown]) { + function normalizeAttr(attrName: string, value: unknown) { let propName = `${attrName}Prop`.camelize(false); if (attrName === 'ref' || attrName === 'ref_for') { @@ -165,7 +186,7 @@ export function normalizeComponentAttrs( tiedPropValue = value()[0]; normalizedAttrs[tiedPropName] = tiedPropValue; - normalizeAttr([tiedPropName, tiedPropValue]); + normalizeAttr(tiedPropName, tiedPropValue); dynamicProps.push(tiedPropName); } else if (isGetter) { @@ -207,8 +228,7 @@ export function normalizeComponentAttrs( return; } - // eslint-disable-next-line vars-on-top, no-var - for (var i = dynamicProps.length - 1; i >= 0; i--) { + for (let i = dynamicProps.length - 1; i >= 0; i--) { const prop = dynamicProps[i], path = dynamicPropsPatches.get(prop); @@ -228,9 +248,8 @@ export function normalizeComponentAttrs( } /** - * Normalizes the props with `forceUpdate` set to `false` for a child component - * using the parent context. The function returns a new object of normalized props - * for the child component. + * Normalizes the props with forceUpdate set to false for a child component using the parent context. + * The function returns a new object containing the normalized props for the child component. * * @param parentCtx - the context of the parent component * @param componentName - the name of the child component @@ -247,18 +266,23 @@ export function normalizeComponentForceUpdateProps( return props; } - const normalizedProps = {}; + const + normalizedProps = {}, + propNames = Object.keys(props); - Object.entries(props).forEach(([key, value]) => { - const propInfo = meta.props[key] ?? meta.props[`${key}Prop`]; + for (let i = 0; i < propNames.length; i++) { + const + propName = propNames[i], + propVal = props[propName], + propInfo = meta.props[propName] ?? meta.props[`${propName}Prop`]; if (propInfo?.forceUpdate === false) { - normalizedProps[`@:${key}`] = parentCtx.unsafe.createPropAccessors(() => value); + normalizedProps[`@:${propName}`] = parentCtx.unsafe.createPropAccessors(() => propVal); } else { - normalizedProps[key] = value; + normalizedProps[propName] = propVal; } - }); + } return normalizedProps; } diff --git a/src/core/component/render/helpers/props.ts b/src/core/component/render/helpers/props.ts index 197c6af2e4..e807c6be53 100644 --- a/src/core/component/render/helpers/props.ts +++ b/src/core/component/render/helpers/props.ts @@ -28,7 +28,9 @@ export const isHandler = { export function mergeProps(...args: Dictionary[]): Dictionary { const props: Dictionary = {}; - args.forEach((toMerge) => { + for (let i = 0; i < args.length; i += 1) { + const toMerge = args[i]; + for (const key in toMerge) { if (key === 'class') { if (props.class !== toMerge.class) { @@ -47,14 +49,14 @@ export function mergeProps(...args: Dictionary[]): Dictionary { existing !== incoming && !(Object.isArray(existing) && existing.includes(incoming)) ) { - props[key] = Object.isTruly(existing) ? ([]).concat(existing, incoming) : incoming; + props[key] = Object.isTruly(existing) ? Array.toArray(existing, incoming) : incoming; } } else if (key !== '') { props[key] = toMerge[key]; } } - }); + } return props; } diff --git a/src/core/component/render/helpers/test/unit/flags.ts b/src/core/component/render/helpers/test/unit/flags.ts index 6ce0c16d52..b1a0bee0f0 100644 --- a/src/core/component/render/helpers/test/unit/flags.ts +++ b/src/core/component/render/helpers/test/unit/flags.ts @@ -30,6 +30,7 @@ test.describe('core/component/render/helpers/flags', () => { await renderDummy(page, true); const vnode = page.getByTestId('vnode'); + // FIXME: don't use private API const patchFlag = await vnode.evaluate( (ctx) => (<{__vnode?: VNode}>ctx).__vnode?.patchFlag ?? 0 ); diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index 6679b9e4a1..087a88a45b 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -140,8 +140,8 @@ export function wrapCreateBlock(original: T): T { vnode.virtualParent.value : this; - let passedProps: CanNull> = null; - props.getPassedProps ??= () => passedProps ??= new Set(attrs != null ? Object.keys(attrs) : []); + let passedProps: Nullable = null; + props.getPassedProps ??= () => passedProps ??= attrs; // For refs within functional components, // it is necessary to explicitly set a reference to the instance of the component @@ -163,15 +163,19 @@ export function wrapCreateBlock(original: T): T { const virtualCtx = createVirtualContext(component, {parent: this, props: attrs, slots}); vnode.virtualComponent = virtualCtx; + const filteredAttrs = {}; + const declaredProps = component.props, - filteredAttrs = {}; + propKeys = Object.keys(props); + + for (let i = 0; i < propKeys.length; i++) { + const propName = propKeys[i]; - Object.entries(props).forEach(([key, val]) => { - if (declaredProps[key.camelize(false)] == null) { - filteredAttrs[key] = val; + if (declaredProps[propName.camelize(false)] == null) { + filteredAttrs[propName] = props[propName]; } - }); + } const functionalVNode = virtualCtx.render(virtualCtx, []); @@ -208,15 +212,19 @@ export function wrapCreateBlock(original: T): T { } if (!SSR && functionalVNode.dynamicProps != null && functionalVNode.dynamicProps.length > 0) { + const functionalProps = functionalVNode.dynamicProps; + const dynamicProps = vnode.dynamicProps ?? []; vnode.dynamicProps = dynamicProps; - functionalVNode.dynamicProps.forEach((propName) => { + for (let i = 0; i < functionalProps.length; i++) { + const propName = functionalProps[i]; + if (isHandler.test(propName)) { dynamicProps.push(propName); setVNodePatchFlags(vnode, 'props'); } - }); + } } functionalVNode.ignore = true; @@ -398,7 +406,9 @@ export function wrapWithDirectives(_: T): T { Object.cast(this.$normalParent) : this; - dirs.forEach((decl) => { + for (let i = 0; i < dirs.length; i++) { + const decl = dirs[i]; + const [dir, value, arg, modifiers] = decl; const binding: DirectiveBinding = { @@ -416,7 +426,8 @@ export function wrapWithDirectives(_: T): T { }; if (!Object.isDictionary(dir)) { - return bindings.push(binding); + bindings.push(binding); + continue; } if (Object.isFunction(dir.beforeCreate)) { @@ -434,7 +445,7 @@ export function wrapWithDirectives(_: T): T { } else if (Object.keys(dir).length > 0) { bindings.push(binding); } - }); + } return vnode; @@ -536,7 +547,9 @@ export function wrapAPI(this: ComponentInterface, path: st async function unrollBuffer(buf: BufItems): Promise { let res = ''; - for (let val of buf) { + for (let i = 0; i < buf.length; i++) { + let val = buf[i]; + if (Object.isPromise(val)) { val = await val; } diff --git a/src/core/component/watch/bind.ts b/src/core/component/watch/bind.ts index d1cc17ed43..f28c8fb42f 100644 --- a/src/core/component/watch/bind.ts +++ b/src/core/component/watch/bind.ts @@ -37,97 +37,97 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR {unsafe} = component, {$watch, meta, hook, meta: {hooks}} = unsafe; + const $a = p.async ?? unsafe.$async; + const - componentAsync = unsafe.$async, - $a = p.async ?? componentAsync; + allWatchers = p.watchers ?? meta.watchers, + watcherKeys = Object.keys(allWatchers); const // True if the component is currently deactivated isDeactivated = hook === 'deactivated', // True if the component has not been created yet - isBeforeCreate = Boolean(beforeHooks[hook]); - - const - watchersMap = p.watchers ?? meta.watchers, + isBeforeCreate = Boolean(beforeHooks[hook]), // True if the method has been invoked with passing the custom async instance as a property - customAsync = $a !== unsafe.$async; + isCustomAsync = $a !== unsafe.$async; // Iterate over all registered watchers and listeners and initialize their - Object.entries(watchersMap).forEach(([watchPath, watchers]) => { + for (let i = 0; i < watcherKeys.length; i++) { + let watchPath = watcherKeys[i]; + + const watchers = allWatchers[watchPath]; + if (watchers == null) { - return; + continue; } - let - // A link to the context of the watcher; - // by default, a component is passed to the function - watcherCtx = component, + // A link to the context of the watcher; + // by default, a component is passed to the function + let watcherCtx = component; - // True if this watcher can initialize only when the component is created - watcherNeedCreated = true, + // True if this watcher can initialize only when the component is created + let attachWatcherOnCreated = true; - // True if this watcher can initialize only when the component is mounted - watcherNeedMounted = false; + // True if this watcher can initialize only when the component is mounted + let attachWatcherOnMounted = false; // Custom watchers look like ':foo', 'bla:foo', '?bla:foo' - // and are used to listen to custom events instead of property mutations. + // and are used to listen to custom events instead of property mutations const customWatcher = isCustomWatcher.test(watchPath) ? customWatcherRgxp.exec(watchPath) : null; if (customWatcher != null) { - const m = customWatcher[1]; - watcherNeedCreated = m === ''; - watcherNeedMounted = m === '?'; - } - - // Add a listener to a component's created hook if the component has not yet been created - if (watcherNeedCreated && isBeforeCreate) { - hooks['before:created'].push({fn: attachWatcher}); - return; - } - - // Add a listener to a component's mounted/activated hook if the component has not yet been mounted or activated - if (watcherNeedMounted && (isBeforeCreate || component.$el == null)) { - hooks[isDeactivated ? 'activated' : 'mounted'].unshift({fn: attachWatcher}); - return; + const hookMod = customWatcher[1]; + attachWatcherOnCreated = hookMod === ''; + attachWatcherOnMounted = hookMod === '?'; } - attachWatcher(); - - function attachWatcher() { + const attachWatcher = () => { // If we have a custom watcher, we need to find a link to the event emitter. // For instance: // ':foo' -> watcherCtx == ctx; key = 'foo' // 'document:foo' -> watcherCtx == document; key = 'foo' if (customWatcher != null) { - const l = customWatcher[2]; + const pathToEmitter = customWatcher[2]; - if (l !== '') { - watcherCtx = Object.get(component, l) ?? Object.get(globalThis, l) ?? component; - watchPath = customWatcher[3].toString(); + if (pathToEmitter !== '') { + watcherCtx = Object.get(component, pathToEmitter) ?? Object.get(globalThis, pathToEmitter) ?? component; } else { watcherCtx = component; - watchPath = customWatcher[3].dasherize(); } + + watchPath = customWatcher[3]; } + let propInfo: typeof p.info = p.info; + // Iterates over all registered handlers for this watcher - watchers!.forEach((watchInfo) => { + for (let i = 0; i < watchers.length; i++) { + const watchInfo = watchers[i]; + if (watchInfo.shouldInit?.(component) === false) { - return; + continue; + } + + if (customWatcher == null) { + propInfo ??= getPropertyInfo(watchPath, component); + + if (canSkipWatching(propInfo, watchInfo)) { + continue; + } } const rawHandler = watchInfo.handler; const asyncParams = { - label: watchInfo.label, group: watchInfo.group, + label: watchInfo.label, join: watchInfo.join }; - if (!customAsync) { + if (!isCustomAsync) { if (asyncParams.label == null && !watchInfo.immediate) { let defLabel: string; @@ -257,24 +257,22 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR } if (customWatcher) { - // True if the component itself can listen to an event, - // because watcherCtx does not appear to be an event emitter - const needDefEmitter = + const needUse$on = watcherCtx === component && - !Object.isFunction(watcherCtx['on']) && - !Object.isFunction(watcherCtx['addListener']); + !('on' in watcherCtx) && + !('addListener' in watcherCtx); - if (needDefEmitter) { + if (needUse$on) { unsafe.$on(watchPath, handler); } else { - const addListener = (watcherCtx: ComponentInterface & EventEmitterLike) => + const addListener = (watcherCtx: ComponentInterface & EventEmitterLike) => { $a.on(watcherCtx, watchPath, handler, eventParams, ...watchInfo.args ?? []); + }; if (Object.isPromise(watcherCtx)) { - $a.promise(Object.cast(watcherCtx), asyncParams) - .then(addListener) - .catch(stderr); + type PromiseType = ComponentInterface & EventEmitterLike; + $a.promise(Object.cast(watcherCtx), asyncParams).then(addListener).catch(stderr); } else { addListener(watcherCtx); @@ -284,12 +282,6 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR return; } - const propInfo = p.info ?? getPropertyInfo(watchPath, component); - - if (canSkipWatching(propInfo, watchInfo)) { - return; - } - /* eslint-disable prefer-const */ let @@ -299,14 +291,8 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR /* eslint-enable prefer-const */ const emitter: EventEmitterLikeP = (_, wrappedHandler) => { - handler = Object.cast(wrappedHandler); - - $a.worker(() => { - if (link != null) { - $a.off(link); - } - }, asyncParams); - + handler = wrappedHandler; + $a.worker(() => link != null && $a.off(link), asyncParams); return () => unwatch?.(); }; @@ -316,24 +302,22 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR } else { if (customWatcher) { - // True if the component itself can listen to an event, - // because watcherCtx does not appear to be an event emitter - const needDefEmitter = + const needUse$on = watcherCtx === component && - !Object.isFunction(watcherCtx['on']) && - !Object.isFunction(watcherCtx['addListener']); + !('on' in watcherCtx) && + !('addListener' in watcherCtx); - if (needDefEmitter) { + if (needUse$on) { unsafe.$on(watchPath, handler); } else { - const addListener = (watcherCtx: ComponentInterface & EventEmitterLike) => + const addListener = (watcherCtx: ComponentInterface & EventEmitterLike) => { $a.on(watcherCtx, watchPath, handler, eventParams, ...watchInfo.args ?? []); + }; if (Object.isPromise(watcherCtx)) { - $a.promise(Object.cast(watcherCtx), asyncParams) - .then(addListener) - .catch(stderr); + type PromiseType = ComponentInterface & EventEmitterLike; + $a.promise(Object.cast(watcherCtx), asyncParams).then(addListener).catch(stderr); } else { addListener(watcherCtx); @@ -343,12 +327,6 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR return; } - const propInfo = p.info ?? getPropertyInfo(watchPath, component); - - if (canSkipWatching(propInfo, watchInfo)) { - return; - } - /* eslint-disable prefer-const */ let @@ -359,20 +337,26 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR const emitter: EventEmitterLikeP = (_, wrappedHandler) => { handler = Object.cast(wrappedHandler); - - $a.worker(() => { - if (link != null) { - $a.off(link); - } - }, asyncParams); - + $a.worker(() => link != null && $a.off(link), asyncParams); return () => unwatch?.(); }; link = $a.on(emitter, 'mutation', handler, wrapWithSuspending(asyncParams, 'watchers')); unwatch = $watch.call(component, propInfo, watchInfo, handler); } - }); + } + }; + + // Add a listener to the component's created hook if the component has not been created yet + if (attachWatcherOnCreated && isBeforeCreate) { + hooks['before:created'].push({fn: attachWatcher}); + + // Add a listener to the component's mounted/activated hook if the component has not been mounted or activated yet + } else if (attachWatcherOnMounted && (isBeforeCreate || component.$el == null)) { + hooks[isDeactivated ? 'activated' : 'mounted'].unshift({fn: attachWatcher}); + + } else { + attachWatcher(); } - }); + } } diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index c6bfae70c3..e76528e591 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -85,14 +85,13 @@ export function implementComponentWatchAPI(component: ComponentInterface): void withProto: true }; - watchDependencies.forEach((deps, path) => { - const - newDeps: typeof deps = []; + for (const [path, deps] of watchDependencies) { + const newDeps: typeof deps = []; - let - needForkDeps = false; + let needForkDeps = false; - deps.forEach((dep, i) => { + for (let i = 0; i < deps.length; i++) { + const dep = deps[i]; newDeps[i] = dep; const @@ -102,7 +101,7 @@ export function implementComponentWatchAPI(component: ComponentInterface): void if (watchInfo.ctx === component && !watchDependencies.has(dep)) { needForkDeps = true; newDeps[i] = watchInfo.path; - return; + continue; } const invalidateCache = (value, oldValue, info) => { @@ -132,10 +131,11 @@ export function implementComponentWatchAPI(component: ComponentInterface): void mutations = [Object.cast([mutations, ...args])]; } - const - modifiedMutations: Array<[unknown, unknown, WatchHandlerParams]> = []; + const modifiedMutations: Array<[unknown, unknown, WatchHandlerParams]> = []; + + for (let i = 0; i < mutations.length; i++) { + const [value, oldValue, info] = mutations[i]; - mutations.forEach(([value, oldValue, info]) => { modifiedMutations.push([ value, oldValue, @@ -150,19 +150,18 @@ export function implementComponentWatchAPI(component: ComponentInterface): void parent: {value, oldValue, info} }) ]); - }); + } broadcastAccessorMutations(modifiedMutations); }; attachDynamicWatcher(component, watchInfo, watchOpts, broadcastMutations, dynamicHandlers); - }); + } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (needForkDeps) { watchDependencies.set(path, newDeps); } - }); + } } // Watcher of fields @@ -274,10 +273,10 @@ export function implementComponentWatchAPI(component: ComponentInterface): void invalidateComputedCache[tiedWatchers] = tiedLinks; broadcastAccessorMutations[tiedWatchers] = tiedLinks; - props.forEach((prop) => { + for (const prop of props) { unsafe.$watch(prop, {...propWatchOpts, flush: 'sync'}, invalidateComputedCache); unsafe.$watch(prop, propWatchOpts, broadcastAccessorMutations); - }); + } } } } @@ -360,9 +359,11 @@ export function implementComponentWatchAPI(component: ComponentInterface): void ctx = invalidateComputedCache[tiedWatchers] != null ? component : info.root[toComponentObject] ?? component, currentDynamicHandlers = immediateDynamicHandlers.get(ctx)?.[rootKey]; - currentDynamicHandlers?.forEach((handler) => { - handler(val, oldVal, info); - }); + if (currentDynamicHandlers != null) { + for (const handler of currentDynamicHandlers) { + handler(val, oldVal, info); + } + } }; } @@ -374,20 +375,20 @@ export function implementComponentWatchAPI(component: ComponentInterface): void mutations = [Object.cast([mutations, ...args])]; } - mutations.forEach(([val, oldVal, info]) => { - const - {path} = info; + for (let i = 0; i < mutations.length; i++) { + const [val, oldVal, info] = mutations[i]; + + const {path} = info; if (path[path.length - 1] === '__proto__') { - return; + continue; } if (info.parent != null) { - const - {path: parentPath} = info.parent.info; + const {path: parentPath} = info.parent.info; if (parentPath[parentPath.length - 1] === '__proto__') { - return; + continue; } } @@ -396,24 +397,26 @@ export function implementComponentWatchAPI(component: ComponentInterface): void ctx = emitAccessorEvents[tiedWatchers] != null ? component : info.root[toComponentObject] ?? component, currentDynamicHandlers = dynamicHandlers.get(ctx)?.[rootKey]; - currentDynamicHandlers?.forEach((handler) => { - // Because we register several watchers (props, fields, etc.) at the same time, - // we need to control that every dynamic handler must be invoked no more than one time per tick - if (usedHandlers.has(handler)) { - return; + if (currentDynamicHandlers != null) { + for (const handler of currentDynamicHandlers) { + // Because we register several watchers (props, fields, etc.) at the same time, + // we need to control that every dynamic handler must be invoked no more than one time per tick + if (usedHandlers.has(handler)) { + continue; + } + + handler(val, oldVal, info); + usedHandlers.add(handler); + + if (timerId == null) { + timerId = setImmediate(() => { + timerId = undefined; + usedHandlers.clear(); + }); + } } - - handler(val, oldVal, info); - usedHandlers.add(handler); - - if (timerId == null) { - timerId = setImmediate(() => { - timerId = undefined; - usedHandlers.clear(); - }); - } - }); - }); + } + } }; } } diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index 33b9ce3189..b827c12484 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -258,8 +258,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface return null; } - let - proxy = watchInfo?.value; + let proxy = watchInfo?.value; if (proxy != null) { if (watchInfo == null) { @@ -269,8 +268,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface switch (info.type) { case 'field': case 'system': { - const - propCtx = info.ctx.unsafe; + const propCtx = info.ctx.unsafe; if (!Object.getOwnPropertyDescriptor(propCtx, info.name)?.get) { proxy[watcherInitializer]?.(); @@ -308,11 +306,9 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface } case 'attr': { - const - attr = info.name; + const attr = info.name; - let - unwatch: Function; + let unwatch: Function; if ('watch' in watchInfo) { unwatch = watchInfo.watch(attr, (value: object, oldValue: object) => { @@ -338,7 +334,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface case 'prop': { const - prop = info.name, + propName = info.name, pathChunks = info.path.split('.'), slicedPathChunks = pathChunks.slice(1); @@ -347,19 +343,19 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface destructors: Function[] = []; const attachDeepProxy = (forceUpdate = true) => { - const getAccessors: CanUndef> = Object.cast( - this.$attrs[`on:${prop}`] - ); - - let accessors: Nullable>>; + let accessors: Nullable>>; if (!forceUpdate) { + const getAccessors: CanUndef> = Object.cast( + this.$attrs[`on:${propName}`] + ); + accessors = getAccessors?.(); } const parent = component.$parent, - propVal = forceUpdate ? proxy[prop] : accessors?.[0]; + propVal = forceUpdate || accessors == null ? proxy[propName] : accessors[0]; if (parent == null || getProxyType(propVal) == null) { return; @@ -401,9 +397,11 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface const tiedLinks = handler[tiedWatchers]; if (Object.isArray(tiedLinks)) { - tiedLinks.forEach((path) => { + for (let i = 0; i < tiedLinks.length; i++) { + const path = tiedLinks[i]; + if (!Object.isArray(path)) { - return; + continue; } const modifiedInfo: WatchHandlerParams = { @@ -413,20 +411,27 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface }; handler.call(this, value, oldValue, modifiedInfo); - }); + } } else { handler.call(this, value, oldValue, info); } }; - const watcher = forceUpdate ? - watch(propVal, info.path, normalizedOpts, watchHandler) : - accessors?.[1](info.path, normalizedOpts, watchHandler); + let watcher: ReturnType; + + if (forceUpdate) { + watcher = watch(propVal, info.path, normalizedOpts, watchHandler); - if (watcher != null) { - destructors.push(watcher.unwatch.bind(watcher)); + } else { + if (accessors == null) { + throw new Error(`Accessors for observing the "${propName}" prop are not defined. To set the accessors, pass them as ":${propName} = propValue | @:${propName} = createPropAccessors(() => propValue)()" or "v-attrs = {'@:${propName}': createPropAccessors(() => propValue)}"`); + } + + watcher = accessors[1](info.path, normalizedOpts, watchHandler); } + + destructors.push(watcher.unwatch.bind(watcher)); }; const externalWatchHandler = (value: unknown, oldValue: unknown, i?: WatchHandlerParams) => { @@ -434,7 +439,9 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface // This situation occurs when the root observable object has changed, // and we need to remove the watchers of all its "nested parts", but leave the root watcher intact - destructors.splice(1, destructors.length).forEach((destroy) => destroy()); + for (const destroy of destructors.splice(1, destructors.length)) { + destroy(); + } if (fromSystem) { i.path = [isPrivateField.replace(String(i.path[0])), ...i.path.slice(1)]; @@ -463,12 +470,12 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface let unwatch: Function; if (forceUpdate && 'watch' in watchInfo) { - unwatch = watchInfo.watch(prop, (value: object, oldValue?: object) => { + unwatch = watchInfo.watch(propName, (value: object, oldValue?: object) => { const info: WatchHandlerParams = { obj: component, root: component, - path: [prop], - originalPath: [prop], + path: [propName], + originalPath: [propName], top: value, fromProto: false }; @@ -476,9 +483,11 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface const tiedLinks = handler[tiedWatchers]; if (Object.isArray(tiedLinks)) { - tiedLinks.forEach((path) => { + for (let i = 0; i < tiedLinks.length; i++) { + const path = tiedLinks[i]; + if (!Object.isArray(path)) { - return; + continue; } const modifiedInfo: WatchHandlerParams = { @@ -488,7 +497,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface }; externalWatchHandler(value, oldValue, modifiedInfo); - }); + } } else { externalWatchHandler(value, oldValue, info); @@ -504,7 +513,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface if (forceUpdate) { // eslint-disable-next-line @v4fire/unbound-method - unwatch = watch(proxy, prop, topOpts, Object.cast(externalWatchHandler)).unwatch; + unwatch = watch(proxy, propName, topOpts, Object.cast(externalWatchHandler)).unwatch; } else { if (topOpts.immediate) { @@ -512,7 +521,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface delete topOpts.immediate; } - unwatch = watchFn.call(this, `[[${prop}]]`, topOpts, externalWatchHandler); + unwatch = watchFn.call(this, `[[${propName}]]`, topOpts, externalWatchHandler); } } @@ -520,8 +529,9 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface attachDeepProxy(forceUpdate); return wrapDestructor(() => { - destructors.forEach((destroy) => destroy()); - destructors.splice(0, destructors.length); + for (const destroy of destructors.splice(0, destructors.length)) { + destroy(); + } }); } diff --git a/src/core/component/watch/helpers.ts b/src/core/component/watch/helpers.ts index 77c298da80..8157c3b819 100644 --- a/src/core/component/watch/helpers.ts +++ b/src/core/component/watch/helpers.ts @@ -56,7 +56,7 @@ export function canSkipWatching( prop = meta.props[propInfo.name], propName = prop?.forceUpdate !== false ? propInfo.name : `on:${propInfo.name}`; - skipWatching = ctx.getPassedProps?.().has(propName) === false; + skipWatching = ctx.getPassedProps?.().hasOwnProperty(propName) === false; } } else { @@ -116,9 +116,8 @@ export function attachDynamicWatcher( const filteredMutations: unknown[] = []; - mutations.forEach((mutation) => { - const - [value, oldValue, info] = mutation; + for (const mutation of mutations) { + const [value, oldValue, info] = mutation; if ( // We don't watch deep mutations @@ -130,11 +129,11 @@ export function attachDynamicWatcher( // The mutation has been already fired watchOpts.eventFilter && !Object.isTruly(watchOpts.eventFilter(value, oldValue, info)) ) { - return; + continue; } filteredMutations.push(mutation); - }); + } if (filteredMutations.length > 0) { if (isPacked) { @@ -149,8 +148,7 @@ export function attachDynamicWatcher( let destructor: Function; if (prop.type === 'mounted') { - let - watcher: Watcher; + let watcher: Watcher; if (Object.size(prop.path) > 0) { watcher = watch(prop.ctx, prop.path, watchOpts, wrapper); diff --git a/src/core/prelude/test-env/components/index.ts b/src/core/prelude/test-env/components/index.ts index 0a2eab65b4..cc5c077605 100644 --- a/src/core/prelude/test-env/components/index.ts +++ b/src/core/prelude/test-env/components/index.ts @@ -14,8 +14,7 @@ import type { ComponentElement } from 'components/super/i-static-page/i-static-p import { expandedParse } from 'core/prelude/test-env/components/json'; -const - createdComponents = Symbol('A set of created components'); +const createdComponents = Symbol('A set of created components'); globalThis.renderComponents = ( componentName: string, @@ -29,11 +28,9 @@ globalThis.renderComponents = ( } } - const - ID_ATTR = 'data-dynamic-component-id'; + const ID_ATTR = 'data-dynamic-component-id'; - const - ctx = >app.component; + const ctx = >app.component; if (ctx == null) { throw new ReferenceError('The root context for rendering is not defined'); @@ -68,13 +65,12 @@ globalThis.renderComponents = ( }; globalThis.removeCreatedComponents = () => { - const - components = globalThis[createdComponents]; + const components = globalThis[createdComponents]; if (Object.isSet(components)) { - Object.cast>(components).forEach((node) => { - node.component?.unsafe.$destroy(); - node.remove(); + Object.cast>>(components).forEach((node) => { + node?.component?.unsafe.$destroy(); + node?.remove(); }); components.clear(); diff --git a/tests/helpers/component-object/README.md b/tests/helpers/component-object/README.md index cce58fc90e..e6eb732f97 100644 --- a/tests/helpers/component-object/README.md +++ b/tests/helpers/component-object/README.md @@ -274,11 +274,11 @@ Here's an example of setting up a spy to track the `emit` method of a component const myComponent = new MyComponentObject(page, 'b-component'); await myComponent.withProps({ - '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'emit'), + '@hook:beforeDataCreate': (ctx) => jestMock.spy(ctx, 'strictEmit'), }); // Extract the spy -const spy = await myComponent.component.getSpy((ctx) => ctx.emit); +const spy = await myComponent.component.getSpy((ctx) => ctx.strictEmit); // Access the spy console.log(await spy.calls);