From 22c1bea23a13c46c52db0b40e862e7b6d34ae591 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 30 Sep 2021 14:32:14 +0300 Subject: [PATCH 0001/2313] Added vue3 engine --- components-lock.json | 78 +------- config/default.js | 7 +- package-lock.json | 164 +++++++++++++++-- package.json | 5 +- src/core/component/engines/index.ts | 4 + src/core/component/engines/vue3/CHANGELOG.md | 58 ++++++ src/core/component/engines/vue3/README.md | 3 + src/core/component/engines/vue3/component.ts | 181 +++++++++++++++++++ src/core/component/engines/vue3/config.ts | 72 ++++++++ src/core/component/engines/vue3/const.ts | 83 +++++++++ src/core/component/engines/vue3/index.ts | 30 +++ src/core/component/engines/vue3/lib.ts | 9 + src/core/component/engines/vue3/vnode.ts | 57 ++++++ src/core/init/semaphore.ts | 2 + src/super/i-static-page/deps.js | 4 + 15 files changed, 660 insertions(+), 97 deletions(-) create mode 100644 src/core/component/engines/vue3/CHANGELOG.md create mode 100644 src/core/component/engines/vue3/README.md create mode 100644 src/core/component/engines/vue3/component.ts create mode 100644 src/core/component/engines/vue3/config.ts create mode 100644 src/core/component/engines/vue3/const.ts create mode 100644 src/core/component/engines/vue3/index.ts create mode 100644 src/core/component/engines/vue3/lib.ts create mode 100644 src/core/component/engines/vue3/vnode.ts diff --git a/components-lock.json b/components-lock.json index 4527cc7079..e6003562a4 100644 --- a/components-lock.json +++ b/components-lock.json @@ -1,5 +1,5 @@ { - "hash": "79a92e80fd890025320770cfeeb2e10fa947975ccbb684bc4e55770a3713aa93", + "hash": "370c991e308f60783b80984989d4b312220a966e2e3da7615497a6321489a005", "data": { "%data": "%data:Map", "%data:Map": [ @@ -1962,84 +1962,12 @@ "declaration": { "name": "p-v4-components-demo", "parent": "i-static-page", - "dependencies": [ - "b-v4-component-demo", - "p-v4-dynamic-page1", - "p-v4-dynamic-page2", - "p-v4-dynamic-page3", - "b-remote-provider", - "b-router", - "b-dynamic-page", - "b-tree", - "b-list", - "b-form", - "b-button", - "b-checkbox", - "b-radio-button", - "b-input-hidden", - "b-input", - "b-textarea", - "b-select", - "b-select-date", - "b-virtual-scroll", - "b-window", - "b-sidebar", - "b-slider", - "b-bottom-slide", - "b-icon", - "b-image", - "b-dummy", - "b-dummy-text", - "b-dummy-async-render", - "b-dummy-module-loader", - "b-dummy-lfc", - "b-dummy-watch", - "b-dummy-sync", - "b-dummy-state", - "b-dummy-control-list", - "b-dummy-decorators" - ], + "dependencies": [], "libs": [] }, "name": "p-v4-components-demo", "parent": "i-static-page", - "dependencies": [ - "b-v4-component-demo", - "p-v4-dynamic-page1", - "p-v4-dynamic-page2", - "p-v4-dynamic-page3", - "b-remote-provider", - "b-router", - "b-dynamic-page", - "b-tree", - "b-list", - "b-form", - "b-button", - "b-checkbox", - "b-radio-button", - "b-input-hidden", - "b-input", - "b-textarea", - "b-select", - "b-select-date", - "b-virtual-scroll", - "b-window", - "b-sidebar", - "b-slider", - "b-bottom-slide", - "b-icon", - "b-image", - "b-dummy", - "b-dummy-text", - "b-dummy-async-render", - "b-dummy-module-loader", - "b-dummy-lfc", - "b-dummy-watch", - "b-dummy-sync", - "b-dummy-state", - "b-dummy-control-list", - "b-dummy-decorators" - ], + "dependencies": [], "libs": [], "resolvedLibs": { "%data": "%data:Set", diff --git a/config/default.js b/config/default.js index 08062ae2a5..1fde3c2597 100644 --- a/config/default.js +++ b/config/default.js @@ -240,13 +240,14 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * @param {string=} [def] - default value * @returns {string} */ - engine(def = 'vue') { + engine(def = 'vue3') { return o('engine', { env: true, default: def, validate(v) { return Boolean({ vue: true, + vue3: true, zero: true }[v]); } @@ -443,7 +444,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * Value of `externals` */ externals: { - vue: 'Vue', + vue: 'root Vue', eventemitter2: 'EventEmitter2', setimmediate: 'setImmediate' }, @@ -892,7 +893,7 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { return { client: this.extend(super.snakeskin(), { - adapter: 'ss2vue', + adapter: 'ss2vue3', adapterOptions: {transpiler: true}, tagFilter: 'tagFilter', tagNameFilter: 'tagNameFilter', diff --git a/package-lock.json b/package-lock.json index 8a2ded8774..ff34b83ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1790,6 +1790,126 @@ } } }, + "@vue/compiler-core": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.19.tgz", + "integrity": "sha512-8dOPX0YOtaXol0Zf2cfLQ4NU/yHYl2H7DCKsLEZ7gdvPK6ZSEwGLJ7IdghhY2YEshEpC5RB9QKdC5I07z8Dtjg==", + "requires": { + "@babel/parser": "^7.15.0", + "@vue/shared": "3.2.19", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + }, + "dependencies": { + "@babel/parser": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", + "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==" + } + } + }, + "@vue/compiler-dom": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.19.tgz", + "integrity": "sha512-WzQoE8rfkFjPtIioc7SSgTsnz9g2oG61DU8KHnzPrRS7fW/lji6H2uCYJfp4Z6kZE8GjnHc1Ljwl3/gxDes0cw==", + "requires": { + "@vue/compiler-core": "3.2.19", + "@vue/shared": "3.2.19" + } + }, + "@vue/compiler-sfc": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.19.tgz", + "integrity": "sha512-pLlbgkO1UHTO02MSpa/sFOXUwIDxSMiKZ1ozE5n71CY4DM+YmI+G3gT/ZHZ46WBId7f3VTF/D8pGwMygcQbrQA==", + "requires": { + "@babel/parser": "^7.15.0", + "@vue/compiler-core": "3.2.19", + "@vue/compiler-dom": "3.2.19", + "@vue/compiler-ssr": "3.2.19", + "@vue/ref-transform": "3.2.19", + "@vue/shared": "3.2.19", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + }, + "dependencies": { + "@babel/parser": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", + "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==" + } + } + }, + "@vue/compiler-ssr": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.19.tgz", + "integrity": "sha512-oLon0Cn3O7WEYzzmzZavGoqXH+199LT+smdjBT3Uf3UX4HwDNuBFCmvL0TsqV9SQnIgKvBRbQ7lhbpnd4lqM3w==", + "requires": { + "@vue/compiler-dom": "3.2.19", + "@vue/shared": "3.2.19" + } + }, + "@vue/reactivity": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.19.tgz", + "integrity": "sha512-FtachoYs2SnyrWup5UikP54xDX6ZJ1s5VgHcJp4rkGoutU3Ry61jhs+nCX7J64zjX992Mh9gGUC0LqTs8q9vCA==", + "requires": { + "@vue/shared": "3.2.19" + } + }, + "@vue/ref-transform": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/ref-transform/-/ref-transform-3.2.19.tgz", + "integrity": "sha512-03wwUnoIAeKti5IGGx6Vk/HEBJ+zUcm5wrUM3+PQsGf7IYnXTbeIfHHpx4HeSeWhnLAjqZjADQwW8uA4rBmVbg==", + "requires": { + "@babel/parser": "^7.15.0", + "@vue/compiler-core": "3.2.19", + "@vue/shared": "3.2.19", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + }, + "dependencies": { + "@babel/parser": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", + "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==" + } + } + }, + "@vue/runtime-core": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.19.tgz", + "integrity": "sha512-qArZSWKxWsgKfxk9BelZ32nY0MZ31CAW2kUUyVJyxh4cTfHaXGbjiQB5JgsvKc49ROMNffv9t3/qjasQqAH+RQ==", + "requires": { + "@vue/reactivity": "3.2.19", + "@vue/shared": "3.2.19" + } + }, + "@vue/runtime-dom": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.19.tgz", + "integrity": "sha512-hIRboxXwafeHhbZEkZYNV0MiJXPNf4fP0X6hM2TJb0vssz8BKhD9cF92BkRgZztTQevecbhk0gu4uAPJ3dxL9A==", + "requires": { + "@vue/runtime-core": "3.2.19", + "@vue/shared": "3.2.19", + "csstype": "^2.6.8" + } + }, + "@vue/server-renderer": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.19.tgz", + "integrity": "sha512-A9FNT7fgQJXItwdzWREntAgWKVtKYuXHBKGev/H4+ByTu8vB7gQXGcim01QxaJshdNg4dYuH2tEBZXCNCNx+/w==", + "requires": { + "@vue/compiler-ssr": "3.2.19", + "@vue/shared": "3.2.19" + } + }, + "@vue/shared": { + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.19.tgz", + "integrity": "sha512-Knqhx7WieLdVgwCAZgTVrDCXZ50uItuecLh9JdLC8O+a5ayaSyIQYveUK3hCRNC7ws5zalHmZwfdLMGaS8r4Ew==" + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -4887,6 +5007,11 @@ } } }, + "csstype": { + "version": "2.6.18", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz", + "integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -6488,6 +6613,11 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -11386,7 +11516,6 @@ "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "optional": true, "requires": { "sourcemap-codec": "^1.4.4" } @@ -13631,7 +13760,6 @@ "version": "8.3.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", - "optional": true, "requires": { "colorette": "^1.2.2", "nanoid": "^3.1.23", @@ -13641,14 +13769,12 @@ "colorette": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", - "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", - "optional": true + "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==" }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", - "optional": true + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" } } }, @@ -16169,8 +16295,7 @@ "source-map-js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", - "optional": true + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" }, "source-map-resolve": { "version": "0.5.3", @@ -16207,8 +16332,7 @@ "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "optional": true + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, "sparkles": { "version": "1.0.1", @@ -16298,10 +16422,9 @@ "lpad-align": "^1.0.1" } }, - "ss2vue": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ss2vue/-/ss2vue-1.1.0.tgz", - "integrity": "sha1-EMMh7GrICPfDBqdkSQsYbD4k238=", + "ss2vue3": { + "version": "github:snakeskintpl/ss2vue3#118a05375297fe3ce59fc5695e16edc3ab129192", + "from": "github:snakeskintpl/ss2vue3", "optional": true }, "sshpk": { @@ -19249,9 +19372,16 @@ "optional": true }, "vue": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", - "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" + "version": "3.2.19", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.19.tgz", + "integrity": "sha512-6KAMdIfAtlK+qohTIUE4urwAv4A3YRuo8uAbByApUmiB0CziGAAPs6qVugN6oHPia8YIafHB/37K0O6KZ7sGmA==", + "requires": { + "@vue/compiler-dom": "3.2.19", + "@vue/compiler-sfc": "3.2.19", + "@vue/runtime-dom": "3.2.19", + "@vue/server-renderer": "3.2.19", + "@vue/shared": "3.2.19" + } }, "vue-template-compiler": { "version": "2.6.10", diff --git a/package.json b/package.json index 8e64668ea3..2c7e6604bc 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@v4fire/core": "^3.59.0", + "@vue/compiler-sfc": "^3.2.19", "escaper": "^3.0.6", "eventemitter2": "^6.4.4", "jsdom": "^16.7.0", @@ -45,7 +46,7 @@ "snakeskin": "^7.5.1", "svg-transform-loader": "^2.0.13", "tslib": "2.3.1", - "vue": "2.6.10" + "vue": "^3.2.19" }, "optionalDependencies": { "@pzlr/build-core": "^2.11.10", @@ -101,7 +102,7 @@ "postcss-loader": "^6.1.1", "raw-loader": "^4.0.2", "snakeskin-loader": "^8.0.1", - "ss2vue": "^1.1.0", + "ss2vue3": "github:snakeskintpl/ss2vue3", "string-dasherize": "^1.0.0", "style-loader": "^3.2.1", "stylus": "^0.54.8", diff --git a/src/core/component/engines/index.ts b/src/core/component/engines/index.ts index 213ef05ae5..f8a4f5bf08 100644 --- a/src/core/component/engines/index.ts +++ b/src/core/component/engines/index.ts @@ -15,6 +15,10 @@ export * from 'core/component/engines/vue'; //#endif +//#if runtime.engine = vue3 +export * from 'core/component/engines/vue3'; +//#endif + //#if runtime.engine = zero // @ts-ignore (double export) export * from 'core/component/engines/zero'; diff --git a/src/core/component/engines/vue3/CHANGELOG.md b/src/core/component/engines/vue3/CHANGELOG.md new file mode 100644 index 0000000000..8440d92f35 --- /dev/null +++ b/src/core/component/engines/vue3/CHANGELOG.md @@ -0,0 +1,58 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.0.0-rc.204 (2021-06-23) + +#### :bug: Bug Fix + +* Fixed async rendering with text elements + +## v3.0.0-rc.137 (2021-02-04) + +#### :bug: Bug fix + +* Fixed redundant listening of events + +## v3.0.0-rc.92 (2020-11-03) + +#### :house: Internal + +* Refactoring + +## v3.0.0-rc.66 (2020-09-22) + +#### :bug: Bug Fix + +* [Fixed mixing of directives within of a component root tag](https://github.com/V4Fire/Client/pull/337) + +## v3.0.0-rc.50 (2020-08-03) + +#### :bug: Bug Fix + +* Fixed `getComponentName` + +## v3.0.0-rc.44 (2020-07-30) + +#### :bug: Bug Fix + +* Fixed setting of `staticClass` + +## v3.0.0-rc.40 (2020-07-27) + +#### :house: Internal + +* Logging Vue errors and warnings via the `core/log` module + +## v3.0.0-rc.37 (2020-07-20) + +#### :house: Internal + +* Fixed ESLint warnings diff --git a/src/core/component/engines/vue3/README.md b/src/core/component/engines/vue3/README.md new file mode 100644 index 0000000000..0c91b8488e --- /dev/null +++ b/src/core/component/engines/vue3/README.md @@ -0,0 +1,3 @@ +# core/component/engines/vue + +This module provides an adaptor for Vue.js. diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts new file mode 100644 index 0000000000..046337a432 --- /dev/null +++ b/src/core/component/engines/vue3/component.ts @@ -0,0 +1,181 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import watch, { set, mute, unmute, WatchHandlerParams } from 'core/object/watch'; +import * as init from 'core/component/construct'; + +import { beforeRenderHooks } from 'core/component/const'; +import { fillMeta } from 'core/component/meta'; +import { implementComponentForceUpdateAPI } from 'core/component/render'; + +import { supports, minimalCtx, proxyGetters } from 'core/component/engines/vue3/const'; +import { cloneVNode, patchVNode, renderVNode } from 'core/component/engines/vue3/vnode'; + +import { fakeMapSetCopy } from 'core/component/engines/helpers'; + +import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; +import type { ComponentMeta } from 'core/component/interface'; + +/** + * Returns a component declaration object from the specified component meta object + * @param meta + */ +export function getComponent(meta: ComponentMeta): ComponentOptions { + const + {component} = fillMeta(meta); + + const + p = meta.params, + m = p.model; + + return { + ...(component), + inheritAttrs: p.inheritAttrs, + + model: m && { + prop: m.prop, + event: m.event?.dasherize() ?? '' + }, + + data(): Dictionary { + const + ctx = this, + + // eslint-disable-next-line @typescript-eslint/unbound-method + {$watch, $set, $delete} = this; + + ctx.$vueWatch = $watch; + ctx.$vueSet = $set; + ctx.$vueDelete = $delete; + + init.beforeDataCreateState(ctx); + + const emitter = (_, handler) => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const {unwatch} = watch(ctx.$fields, {deep: true, immediate: true}, handler); + return unwatch; + }; + + ctx.$async.on(emitter, 'mutation', watcher, { + group: 'watchers:suspend' + }); + + return ctx.$fields; + + function watcher(value: unknown, oldValue: unknown, info: WatchHandlerParams): void { + const + {path} = info; + + ctx.lastSelfReasonToRender = { + path, + value, + oldValue + }; + + if (beforeRenderHooks[ctx.hook] === true) { + return; + } + + if (meta.fields[String(path[0])]?.forceUpdate !== false) { + ctx.$forceUpdate(); + } + + let + {obj} = info; + + if (path.length > 1) { + if (Object.isDictionary(obj)) { + const + key = String(path[path.length - 1]), + desc = Object.getOwnPropertyDescriptor(obj, key); + + // If we register a new property, we must register it to Vue too + if (desc?.get == null) { + // For correct registering of a property with Vue, + // we need to remove it from a proxy and original object + delete obj[key]; + + // Get a link to a proxy object + obj = Object.get(ctx.$fields, path.slice(0, -1)) ?? {}; + delete obj[key]; + + // Finally we can register a Vue watcher + $set.call(ctx, obj, key, value); + + // Don't forget to restore the original watcher + mute(obj); + set(obj, key, value); + unmute(obj); + } + + // Because Vue doesn't see changes from Map/Set structures, we must use this hack + } else if (Object.isSet(obj) || Object.isMap(obj) || Object.isWeakMap(obj) || Object.isWeakSet(obj)) { + Object.set(ctx, path.slice(0, -1), fakeMapSetCopy(obj)); + } + } + } + }, + + beforeCreate(): void { + const + ctx = this; + + ctx.$renderEngine = { + supports, + minimalCtx, + proxyGetters, + cloneVNode, + patchVNode, + renderVNode + }; + + init.beforeCreateState(ctx, meta); + implementComponentForceUpdateAPI(ctx, this.$forceUpdate.bind(this)); + }, + + created(this: any): void { + init.createdState(this); + }, + + beforeMount(this: any): void { + init.beforeMountState(this); + }, + + mounted(this: any): void { + init.mountedState(this); + }, + + beforeUpdate(this: any): void { + init.beforeUpdateState(this); + }, + + updated(this: any): void { + init.updatedState(this); + }, + + activated(this: any): void { + init.activatedState(this); + }, + + deactivated(this: any): void { + init.deactivatedState(this); + }, + + beforeDestroy(this: any): void { + init.beforeDestroyState(this); + }, + + destroyed(this: any): void { + init.destroyedState(this); + }, + + errorCaptured(this: any): void { + init.errorCapturedState(this); + } + }; +} diff --git a/src/core/component/engines/vue3/config.ts b/src/core/component/engines/vue3/config.ts new file mode 100644 index 0000000000..e630f95a7a --- /dev/null +++ b/src/core/component/engines/vue3/config.ts @@ -0,0 +1,72 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Vue from 'core/component/engines/vue3/lib'; +import log from 'core/log'; + +console.log(Vue); + +import type { ComponentInterface } from 'core/component/interface'; + +const + logger = log.namespace('vue'); + +/* +Vue.config.errorHandler = (err, vm, info) => { + logger.error('errorHandler', err, info, getComponentInfo(vm)); +}; + +Vue.config.warnHandler = (msg, vm, trace) => { + logger.warn('warnHandler', msg, trace, getComponentInfo(vm)); +}; +*/ + +const + UNRECOGNIZED_COMPONENT_NAME = 'unrecognized-component', + ROOT_COMPONENT_NAME = 'root-component'; + +/** + * Returns component info to log + * @param component + */ +function getComponentInfo(component: Vue | ComponentInterface): Dictionary { + try { + if ('componentName' in component) { + return { + name: getComponentName(component), + hook: component.hook, + status: component.unsafe.componentStatus + }; + } + + return { + name: getComponentName(component) + }; + + } catch { + return { + name: UNRECOGNIZED_COMPONENT_NAME + }; + } +} + +/** + * Returns a name of the specified component + * @param component + */ +function getComponentName(component: Vue | ComponentInterface): string { + if ('componentName' in component) { + return component.componentName; + } + + if (component.$root === component) { + return ROOT_COMPONENT_NAME; + } + + return Object.get(component, '$options.name') ?? UNRECOGNIZED_COMPONENT_NAME; +} diff --git a/src/core/component/engines/vue3/const.ts b/src/core/component/engines/vue3/const.ts new file mode 100644 index 0000000000..954e890c09 --- /dev/null +++ b/src/core/component/engines/vue3/const.ts @@ -0,0 +1,83 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Vue from 'core/component/engines/vue3/lib'; + +export const supports = { + regular: true, + functional: true, + composite: true, + + ssr: false, + boundCreateElement: true +}; + +export const minimalCtx = (() => { + const + obj = Vue.prototype, + ctx = {}; + + for (const key in obj) { + if (key.length === 2) { + ctx[key] = obj[key]; + } + } + + return ctx; +})(); + +export const proxyGetters = Object.createDict({ + prop: (ctx) => ({ + key: '_props', + + get value(): typeof ctx._props { + return ctx._props; + }, + + watch: (path, handler) => ctx.$vueWatch(path, (val, oldVal) => { + if (val !== oldVal) { + handler(val, oldVal); + } + }) + }), + + attr: (ctx) => ({ + key: '$attrs', + + get value(): typeof ctx.$attrs { + return ctx.$attrs; + }, + + watch: (path, handler) => ctx.$vueWatch(path, (val, oldVal) => { + if (val !== oldVal) { + handler(val, oldVal); + } + }) + }), + + field: (ctx) => ({ + key: '$fields', + get value(): typeof ctx.$fields { + return ctx.$fields; + } + }), + + system: (ctx) => ({ + key: '$systemFields', + get value(): typeof ctx.$systemFields { + return ctx.$systemFields; + } + }), + + mounted: (ctx) => ({ + key: null, + get value(): typeof ctx { + return ctx; + } + }) +}); diff --git a/src/core/component/engines/vue3/index.ts b/src/core/component/engines/vue3/index.ts new file mode 100644 index 0000000000..e9927e83fe --- /dev/null +++ b/src/core/component/engines/vue3/index.ts @@ -0,0 +1,30 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/engines/vue3/README.md]] + * @packageDocumentation + */ + +import Vue from 'core/component/engines/vue3/lib'; +import 'core/component/engines/vue3/config'; + +export * from 'vue'; + +/** @deprecated */ +export { Vue as ComponentDriver }; +export { Vue as ComponentEngine }; +export { Vue as default }; + +export * from 'core/component/engines/vue3/const'; +export * from 'core/component/engines/vue3/vnode'; +export * from 'core/component/engines/vue3/component'; + +//#if VueInterfaces +export { VNode } from 'vue'; +//#endif diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts new file mode 100644 index 0000000000..d7b5fdf385 --- /dev/null +++ b/src/core/component/engines/vue3/lib.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 default require('vue'); diff --git a/src/core/component/engines/vue3/vnode.ts b/src/core/component/engines/vue3/vnode.ts new file mode 100644 index 0000000000..d27f09bd87 --- /dev/null +++ b/src/core/component/engines/vue3/vnode.ts @@ -0,0 +1,57 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode } from 'vue'; +import Vue from 'core/component/engines/vue3/lib'; + +import { patchComponentVData } from 'core/component/vnode'; +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Clones the specified vnode + * @param vnode + */ +export function cloneVNode(vnode: VNode): VNode { + return vnode; +} + +/** + * Patches the specified VNode by using provided contexts + * + * @param vnode + * @param component - component instance + * @param renderCtx - render context + */ +export function patchVNode(vnode: VNode, component: ComponentInterface, renderCtx: RenderContext): void { + patchComponentVData(vnode.data, renderCtx.data, { + patchAttrs: Boolean(component.unsafe.meta.params.inheritAttrs) + }); +} + +/** + * Renders the specified VNode/s and returns the result + * + * @param vnode + * @param parent - parent component + */ +export function renderVNode(vnode: VNode, parent: ComponentInterface): Node; +export function renderVNode(vnodes: VNode[], parent: ComponentInterface): Node[]; +export function renderVNode(vnode: CanArray, parent: ComponentInterface): CanArray { + const vue = new Vue({ + render: (c) => Object.isArray(vnode) ? c('div', vnode) : vnode + }); + + Object.set(vue, '$root', Object.create(parent.$root)); + Object.set(vue, '$root.$remoteParent', parent); + Object.set(vue, '$root.unsafe', vue.$root); + + const el = document.createElement('div'); + vue.$mount(el); + + return Object.isArray(vnode) ? Array.from(vue.$el.childNodes) : vue.$el; +} diff --git a/src/core/init/semaphore.ts b/src/core/init/semaphore.ts index 3437330488..b43557f00f 100644 --- a/src/core/init/semaphore.ts +++ b/src/core/init/semaphore.ts @@ -26,6 +26,8 @@ export default createsAsyncSemaphore(async () => { throw new ReferenceError('The root component is not found'); } + return; + const getData = component.data, params = JSON.parse(node.getAttribute('data-root-component-params') ?? '{}'); diff --git a/src/super/i-static-page/deps.js b/src/super/i-static-page/deps.js index d614f30099..8e97f1fdc4 100644 --- a/src/super/i-static-page/deps.js +++ b/src/super/i-static-page/deps.js @@ -46,6 +46,10 @@ switch (runtime.engine) { deps.scripts.set('vue', `vue/dist/vue.runtime${config.webpack.mode() === 'production' ? '.min' : ''}.js`); break; + case 'vue3': + deps.scripts.set('vue', `vue/dist/vue.runtime.global${config.webpack.mode() === 'production' ? '.prod' : ''}.js`); + break; + default: if (!runtime.engine) { throw new Error('An engine to use is not specified'); From cbd2224e51916216d939e32db5c73c60f600118c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 12 Jan 2022 19:03:37 +0300 Subject: [PATCH 0002/2313] refactor: wrap Vue with a lazy structure --- src/core/component/engines/vue3/lib.ts | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index d7b5fdf385..464f51059d 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -6,4 +6,32 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export default require('vue'); +import makeLazy from 'core/lazy'; +import { createApp } from 'vue'; + +const Vue = makeLazy(createApp, { + use: Function, + + component: Function, + directive: Function, + + mixin: Function, + provide: Function, + version: '', + + mount: Function, + unmount: Function, + + config: { + performance: false, + + errorHandler: Function, + warnHandler: Function, + + compilerOptions: {}, + globalProperties: {}, + optionMergeStrategies: {} + } +}); + +export default Vue; From 2607d46688f5f8455f827d431cc85977e768a656 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 1 Feb 2022 14:17:00 +0300 Subject: [PATCH 0003/2313] refactor: use Vue3 types --- src/core/component/engines/vue3/config.ts | 39 +++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/core/component/engines/vue3/config.ts b/src/core/component/engines/vue3/config.ts index e630f95a7a..8095ea1ec4 100644 --- a/src/core/component/engines/vue3/config.ts +++ b/src/core/component/engines/vue3/config.ts @@ -6,60 +6,57 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import Vue from 'core/component/engines/vue3/lib'; import log from 'core/log'; -console.log(Vue); +import type { ComponentPublicInstance } from 'vue'; +import Vue from 'core/component/engines/vue3/lib'; import type { ComponentInterface } from 'core/component/interface'; const logger = log.namespace('vue'); -/* Vue.config.errorHandler = (err, vm, info) => { - logger.error('errorHandler', err, info, getComponentInfo(vm)); + logger.error('errorHandler', err, info, getComponentInfoLog(vm)); }; Vue.config.warnHandler = (msg, vm, trace) => { - logger.warn('warnHandler', msg, trace, getComponentInfo(vm)); + logger.warn('warnHandler', msg, trace, getComponentInfoLog(vm)); }; -*/ const UNRECOGNIZED_COMPONENT_NAME = 'unrecognized-component', ROOT_COMPONENT_NAME = 'root-component'; /** - * Returns component info to log + * Returns information of the specified component to log * @param component */ -function getComponentInfo(component: Vue | ComponentInterface): Dictionary { - try { - if ('componentName' in component) { - return { - name: getComponentName(component), - hook: component.hook, - status: component.unsafe.componentStatus - }; - } - +function getComponentInfoLog(component: Nullable): Dictionary { + if (component == null) { return { - name: getComponentName(component) + name: UNRECOGNIZED_COMPONENT_NAME }; + } - } catch { + if ('componentName' in component) { return { - name: UNRECOGNIZED_COMPONENT_NAME + name: getComponentName(component), + hook: component.hook, + status: component.unsafe.componentStatus }; } + + return { + name: getComponentName(component) + }; } /** * Returns a name of the specified component * @param component */ -function getComponentName(component: Vue | ComponentInterface): string { +function getComponentName(component: ComponentPublicInstance | ComponentInterface): string { if ('componentName' in component) { return component.componentName; } From 9666901497e71bf16ec708b7a00f0f1ead3a7331 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 1 Feb 2022 17:16:30 +0300 Subject: [PATCH 0004/2313] refactor: added support for native $emit --- src/core/component/event/component-api.ts | 79 ++++++++++++++++------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/core/component/event/component-api.ts b/src/core/component/event/component-api.ts index 174942f7ce..d6a42d7fc5 100644 --- a/src/core/component/event/component-api.ts +++ b/src/core/component/event/component-api.ts @@ -6,42 +6,73 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { EventEmitter2 as EventEmitter, ListenerFn } from 'eventemitter2'; +import { EventEmitter2 as EventEmitter } from 'eventemitter2'; +import type { UnsafeComponentInterface } from 'core/component/interface'; /** * Implements the base event API to a component instance - * @param component + * @param obj */ -export function implementEventAPI(component: object): void { +export function implementEventAPI(obj: object): void { + /* eslint-disable @typescript-eslint/typedef */ + + const + component = Object.cast(obj); + const $e = new EventEmitter({ - maxListeners: 1e6, + maxListeners: 1e3, newListener: false, wildcard: true }); - // @ts-ignore (access) - component.$emit = $e.emit.bind($e); + const + nativeEmit = Object.cast>(component.$emit); - // @ts-ignore (access) - component.$once = $e.once.bind($e); + Object.defineProperty(component, '$emit', { + configurable: true, + enumerable: false, + writable: false, - // @ts-ignore (access) - component.$on = function $on(e: CanArray, cb: ListenerFn): void { - const - events = Array.concat([], e); - - for (let i = 0; i < events.length; i++) { - $e.on(events[i], cb); + value(event, ...args) { + nativeEmit?.(event, ...args); + $e.emit(event, ...args); + return this; } - }; + }); - // @ts-ignore (access) - component.$off = function $off(e: CanArray, cb: ListenerFn): void { - const - events = Array.concat([], e); + Object.defineProperty(component, '$on', { + configurable: true, + enumerable: false, + writable: false, + value: getMethod('on') + }); - for (let i = 0; i < events.length; i++) { - $e.off(events[i], cb); - } - }; + Object.defineProperty(component, '$once', { + configurable: true, + enumerable: false, + writable: false, + value: getMethod('once') + }); + + Object.defineProperty(component, '$off', { + configurable: true, + enumerable: false, + writable: false, + value: getMethod('off') + }); + + function getMethod(method: 'on' | 'once' | 'off') { + return function wrapper(this: unknown, e, cb) { + const + events = Array.concat([], e); + + for (let i = 0; i < events.length; i++) { + $e[method](events[i], Object.cast(cb)); + } + + return this; + }; + } + + /* eslint-enable @typescript-eslint/typedef */ } From 885ae97dc0c9344cf05c47ebe453037245d0a4e7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 1 Feb 2022 17:16:52 +0300 Subject: [PATCH 0005/2313] refactor: no more than 1e3 listeners --- src/core/component/event/emitter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/event/emitter.ts b/src/core/component/event/emitter.ts index 95d1165178..4d181599be 100644 --- a/src/core/component/event/emitter.ts +++ b/src/core/component/event/emitter.ts @@ -13,7 +13,7 @@ import { EventEmitter2 as EventEmitter } from 'eventemitter2'; * Event emitter to broadcast external events to components */ const emitter = new EventEmitter({ - maxListeners: 1e6, + maxListeners: 1e3, newListener: false, wildcard: true }); From 4ef9d41f1ce0bf289439d16935b0c26e21d264d4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 1 Feb 2022 17:17:19 +0300 Subject: [PATCH 0006/2313] refactor: vue3 --- src/core/component/engines/vue3/component.ts | 18 ++++++------------ src/core/component/engines/vue3/lib.ts | 10 ++++++++-- src/core/init/semaphore.ts | 2 -- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index 046337a432..e7148404a1 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -30,18 +30,12 @@ export function getComponent(meta: ComponentMeta): ComponentOptions(component), + ...Object.cast(component), inheritAttrs: p.inheritAttrs, - model: m && { - prop: m.prop, - event: m.event?.dasherize() ?? '' - }, - data(): Dictionary { const ctx = this, @@ -53,7 +47,7 @@ export function getComponent(meta: ComponentMeta): ComponentOptions { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -134,7 +128,7 @@ export function getComponent(meta: ComponentMeta): ComponentOptionsfunction App(component: Component & {el: Element}, rootProps: Nullable) { + const app = createApp(component, rootProps); + app.mount(component.el); + return app; +}; + +const Vue = makeLazy(App, { use: Function, component: Function, diff --git a/src/core/init/semaphore.ts b/src/core/init/semaphore.ts index b43557f00f..3437330488 100644 --- a/src/core/init/semaphore.ts +++ b/src/core/init/semaphore.ts @@ -26,8 +26,6 @@ export default createsAsyncSemaphore(async () => { throw new ReferenceError('The root component is not found'); } - return; - const getData = component.data, params = JSON.parse(node.getAttribute('data-root-component-params') ?? '{}'); From 519ffd84cb589a31f1631eb3d9673b7897a1d5f8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 15:36:05 +0300 Subject: [PATCH 0007/2313] refactor: moved ds typings to ts-definitions --- index.d.ts | 2 +- .../ds/index.d.ts => ts-definitions/stylus-ds.d.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename build/stylus/ds/index.d.ts => ts-definitions/stylus-ds.d.ts (89%) diff --git a/index.d.ts b/index.d.ts index 6c22fc1254..2741d0c49f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,8 +9,8 @@ */ /// -/// /// +/// declare let __webpack_nonce__: CanUndef; declare let __webpack_public_path__: CanUndef; diff --git a/build/stylus/ds/index.d.ts b/ts-definitions/stylus-ds.d.ts similarity index 89% rename from build/stylus/ds/index.d.ts rename to ts-definitions/stylus-ds.d.ts index 4e020ee4bb..4db2b79d26 100644 --- a/build/stylus/ds/index.d.ts +++ b/ts-definitions/stylus-ds.d.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -interface DesignSystemDeprecatedOptions { +declare interface DesignSystemDeprecatedOptions { /** * Indicates that a style or component was renamed, but its interface still actual, * the value contains a name after renaming @@ -24,7 +24,7 @@ interface DesignSystemDeprecatedOptions { notice?: string; } -interface DesignSystem { +declare interface DesignSystem { meta?: { themes: string[]; @@ -80,7 +80,7 @@ interface DesignSystem { * } * ``` */ -interface DesignSystemVariables extends Dictionary { +declare interface DesignSystemVariables extends Dictionary { /** * Dictionary of couples `[cssVariable, value]`. * It may be separated by groups (e.g., themes). @@ -104,9 +104,9 @@ interface DesignSystemVariables extends Dictionary { map: Dictionary; } -type DesignSystemVariableMapValue = [string, unknown]; +declare type DesignSystemVariableMapValue = [string, unknown]; -interface BuildTimeDesignSystemParams { +declare interface BuildTimeDesignSystemParams { data: DesignSystem; variables: DesignSystemVariables; } From 728a39938088d4a5ce81e3a5b23577e53ac54bf3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 15:50:58 +0300 Subject: [PATCH 0008/2313] refactor: create a wrapper for the raw context; removed Vue2 specific code --- src/core/component/engines/vue3/component.ts | 123 ++++++++----------- 1 file changed, 49 insertions(+), 74 deletions(-) diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index e7148404a1..1a7e1cf782 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import watch, { set, mute, unmute, WatchHandlerParams } from 'core/object/watch'; +import watch, { WatchHandlerParams } from 'core/object/watch'; import * as init from 'core/component/construct'; import { beforeRenderHooks } from 'core/component/const'; @@ -16,21 +16,20 @@ import { implementComponentForceUpdateAPI } from 'core/component/render'; import { supports, minimalCtx, proxyGetters } from 'core/component/engines/vue3/const'; import { cloneVNode, patchVNode, renderVNode } from 'core/component/engines/vue3/vnode'; -import { fakeMapSetCopy } from 'core/component/engines/helpers'; - import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; -import type { ComponentMeta } from 'core/component/interface'; +import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; /** * Returns a component declaration object from the specified component meta object * @param meta */ -export function getComponent(meta: ComponentMeta): ComponentOptions { +export function getComponent(meta: ComponentMeta): ComponentOptions { const {component} = fillMeta(meta); const - p = meta.params; + p = meta.params, + ctxMap = new WeakMap(); return { ...Object.cast(component), @@ -38,16 +37,10 @@ export function getComponent(meta: ComponentMeta): ComponentOptionsthis, - - // eslint-disable-next-line @typescript-eslint/unbound-method - {$watch, $set, $delete} = this; + ctx = getCtx(this); - ctx.$vueWatch = $watch; - ctx.$vueSet = $set; - ctx.$vueDelete = $delete; - - init.beforeDataCreateState(this); + ctx.$vueWatch = this.$watch.bind(this); + init.beforeDataCreateState(ctx); const emitter = (_, handler) => { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -65,111 +58,93 @@ export function getComponent(meta: ComponentMeta): ComponentOptions 1) { - if (Object.isDictionary(obj)) { - const - key = String(path[path.length - 1]), - desc = Object.getOwnPropertyDescriptor(obj, key); - - // If we register a new property, we must register it to Vue too - if (desc?.get == null) { - // For correct registering of a property with Vue, - // we need to remove it from a proxy and original object - delete obj[key]; - - // Get a link to a proxy object - obj = Object.get(ctx.$fields, path.slice(0, -1)) ?? {}; - delete obj[key]; - - // Finally we can register a Vue watcher - $set.call(ctx, obj, key, value); - - // Don't forget to restore the original watcher - mute(obj); - set(obj, key, value); - unmute(obj); - } - - // Because Vue doesn't see changes from Map/Set structures, we must use this hack - } else if (Object.isSet(obj) || Object.isMap(obj) || Object.isWeakMap(obj) || Object.isWeakSet(obj)) { - Object.set(ctx, path.slice(0, -1), fakeMapSetCopy(obj)); - } - } } }, beforeCreate(): void { const - ctx = this; + ctx = getCtx(this); - ctx.$renderEngine = { + Object.set(ctx, '$renderEngine', { supports, minimalCtx, proxyGetters, cloneVNode, patchVNode, renderVNode - }; + }); init.beforeCreateState(ctx, meta, {implementEventAPI: true}); implementComponentForceUpdateAPI(ctx, this.$forceUpdate.bind(this)); }, created(this: any): void { - init.createdState(this); + init.createdState(getCtx(this)); }, beforeMount(this: any): void { - init.beforeMountState(this); + init.beforeMountState(getCtx(this)); }, - mounted(this: any): void { - init.mountedState(this); + mounted(): void { + init.mountedState(getCtx(this)); }, - beforeUpdate(this: any): void { - init.beforeUpdateState(this); + beforeUpdate(): void { + init.beforeUpdateState(getCtx(this)); }, - updated(this: any): void { - init.updatedState(this); + updated(): void { + init.updatedState(getCtx(this)); }, - activated(this: any): void { - init.activatedState(this); + activated(): void { + init.activatedState(getCtx(this)); }, - deactivated(this: any): void { - init.deactivatedState(this); + deactivated(): void { + init.deactivatedState(getCtx(this)); }, - beforeUnmount(this: any): void { - init.beforeDestroyState(this); + beforeUnmount(): void { + init.beforeDestroyState(getCtx(this)); }, - unmounted(this: any): void { - init.destroyedState(this); + unmounted(): void { + init.destroyedState(getCtx(this)); }, - errorCaptured(this: any): void { - init.errorCapturedState(this); + errorCaptured(): void { + init.errorCapturedState(getCtx(this)); } }; + + function getCtx(ctx: object): Dictionary & ComponentInterface['unsafe'] { + let + v = ctxMap.get(ctx); + + if (v == null) { + v = Object.create(ctx); + ctxMap.set(ctx, v); + } + + return v; + } } From 7c453947b0f81b8c21d7cf3421c85c1f6374dd46 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 16:13:40 +0300 Subject: [PATCH 0009/2313] refactor: moved getCtx to helpers --- src/core/component/engines/helpers.ts | 20 ++++++++ src/core/component/engines/index.ts | 1 + src/core/component/engines/vue3/component.ts | 48 ++++++++------------ 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/core/component/engines/helpers.ts b/src/core/component/engines/helpers.ts index 3f856e9149..c03e2be1b6 100644 --- a/src/core/component/engines/helpers.ts +++ b/src/core/component/engines/helpers.ts @@ -7,6 +7,26 @@ */ import { fakeCopyLabel } from 'core/component/watch'; +import type { ComponentInterface } from 'core/component'; + +const + ctxMap = new WeakMap(); + +/** + * Returns a component context object by the specified component instance + * @param component + */ +export function getComponentContext(component: object): Dictionary & ComponentInterface['unsafe'] { + let + v = ctxMap.get(component); + + if (v == null) { + v = Object.create(component); + ctxMap.set(component, v); + } + + return v; +} const toNonFakeObject = Symbol('Link to a non fake object'); diff --git a/src/core/component/engines/index.ts b/src/core/component/engines/index.ts index e96e4eb865..19e6c8a3b2 100644 --- a/src/core/component/engines/index.ts +++ b/src/core/component/engines/index.ts @@ -13,4 +13,5 @@ import 'core/component/engines/directive'; +export * from 'core/component/engines/helpers'; export * from 'core/component/engines/engine'; diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index 1a7e1cf782..fc68a857f4 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -7,18 +7,19 @@ */ import watch, { WatchHandlerParams } from 'core/object/watch'; -import * as init from 'core/component/construct'; +import * as init from 'core/component/construct'; import { beforeRenderHooks } from 'core/component/const'; + import { fillMeta } from 'core/component/meta'; import { implementComponentForceUpdateAPI } from 'core/component/render'; +import { getComponentContext, ComponentEngine, ComponentOptions } from 'core/component/engines'; +import type { ComponentMeta } from 'core/component/interface'; + import { supports, minimalCtx, proxyGetters } from 'core/component/engines/vue3/const'; import { cloneVNode, patchVNode, renderVNode } from 'core/component/engines/vue3/vnode'; -import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; -import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; - /** * Returns a component declaration object from the specified component meta object * @param meta @@ -28,8 +29,7 @@ export function getComponent(meta: ComponentMeta): ComponentOptions Date: Wed, 13 Apr 2022 17:53:47 +0300 Subject: [PATCH 0010/2313] refactor: split states into separated files --- src/core/component/construct/index.ts | 402 +----------------- .../component/construct/states/activated.ts | 25 ++ .../construct/states/before-create.ts | 183 ++++++++ .../construct/states/before-data-create.ts | 46 ++ .../construct/states/before-destroy.ts | 22 + .../construct/states/before-mount.ts | 30 ++ .../construct/states/before-update.ts | 21 + .../component/construct/states/created.ts | 86 ++++ .../component/construct/states/deactivated.ts | 21 + .../component/construct/states/destroyed.ts | 22 + .../construct/states/error-captured.ts | 24 ++ src/core/component/construct/states/index.ts | 20 + .../component/construct/states/mounted.ts | 32 ++ .../component/construct/states/updated.ts | 26 ++ 14 files changed, 559 insertions(+), 401 deletions(-) create mode 100644 src/core/component/construct/states/activated.ts create mode 100644 src/core/component/construct/states/before-create.ts create mode 100644 src/core/component/construct/states/before-data-create.ts create mode 100644 src/core/component/construct/states/before-destroy.ts create mode 100644 src/core/component/construct/states/before-mount.ts create mode 100644 src/core/component/construct/states/before-update.ts create mode 100644 src/core/component/construct/states/created.ts create mode 100644 src/core/component/construct/states/deactivated.ts create mode 100644 src/core/component/construct/states/destroyed.ts create mode 100644 src/core/component/construct/states/error-captured.ts create mode 100644 src/core/component/construct/states/index.ts create mode 100644 src/core/component/construct/states/mounted.ts create mode 100644 src/core/component/construct/states/updated.ts diff --git a/src/core/component/construct/index.ts b/src/core/component/construct/index.ts index b9eaae8c45..dce4b3a6e0 100644 --- a/src/core/component/construct/index.ts +++ b/src/core/component/construct/index.ts @@ -11,405 +11,5 @@ * @packageDocumentation */ -import symbolGenerator from 'core/symbol'; - -import { deprecate } from 'core/functools/deprecation'; -import { unmute } from 'core/object/watch'; - -import Async from 'core/async'; - -import { asyncLabel } from 'core/component/const'; -import { getPropertyInfo, PropertyInfo } from 'core/component/reflection'; - -import { initFields } from 'core/component/field'; -import { attachAccessorsFromMeta } from 'core/component/accessor'; -import { attachMethodsFromMeta, callMethodFromComponent } from 'core/component/method'; - -import { implementEventAPI } from 'core/component/event'; -import { bindRemoteWatchers, implementComponentWatchAPI } from 'core/component/watch'; - -import { runHook } from 'core/component/hook'; -import { resolveRefs } from 'core/component/ref'; - -import { getNormalParent } from 'core/component/traverse'; -import { forkMeta } from 'core/component/meta'; - -import type { ComponentInterface, ComponentMeta, Hook } from 'core/component/interface'; -import type { InitBeforeCreateStateOptions, InitBeforeDataCreateStateOptions } from 'core/component/construct/interface'; - +export * from 'core/component/construct/states'; export * from 'core/component/construct/interface'; - -export const - $$ = symbolGenerator(); - -/** - * Initializes "beforeCreate" state to the specified component instance - * - * @param component - * @param meta - component meta object - * @param [opts] - additional options - */ -export function beforeCreateState( - component: ComponentInterface, - meta: ComponentMeta, - opts?: InitBeforeCreateStateOptions -): void { - meta = forkMeta(meta); - - // @ts-ignore (access) - component.unsafe = component; - - // @ts-ignore (access) - component.meta = meta; - - // @ts-ignore (access) - component['componentName'] = meta.componentName; - - // @ts-ignore (access) - component['instance'] = meta.instance; - - // @ts-ignore (access) - component.$fields = {}; - - // @ts-ignore (access) - component.$systemFields = {}; - - // @ts-ignore (access) - component.$refHandlers = {}; - - // @ts-ignore (access) - component.$modifiedFields = {}; - - // @ts-ignore (access) - component.$async = new Async(component); - - // @ts-ignore (access) - component.$asyncLabel = asyncLabel; - - const - {unsafe, unsafe: {$parent: parent}} = component; - - const - isFunctional = meta.params.functional === true; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (parent != null && parent.componentName == null) { - // @ts-ignore (access) - unsafe['$parent'] = unsafe.$root.unsafe.$remoteParent; - } - - // @ts-ignore (access) - unsafe['$normalParent'] = getNormalParent(component); - - if (opts?.addMethods) { - attachMethodsFromMeta(component); - } - - if (opts?.implementEventAPI) { - implementEventAPI(component); - } - - attachAccessorsFromMeta(component); - 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(); - - for (let o = watchDependencies.values(), el = o.next(); !el.done; el = o.next()) { - const - deps = el.value; - - for (let i = 0; i < deps.length; i++) { - const - dep = deps[i], - info = getPropertyInfo(Object.isArray(dep) ? dep.join('.') : String(dep), component); - - if (info.type === 'system' || isFunctional && info.type === 'field') { - watchSet.add(info); - } - } - } - - // If a computed property a field or system field as a dependency, - // and the host component does not have any watchers to this field, - // we need to register the "fake" watcher to force watching - if (watchSet.size > 0) { - for (let o = watchSet.values(), el = o.next(); !el.done; el = o.next()) { - const - info = el.value; - - 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 the "fake" watcher to force watching - for (let keys = Object.keys(tiedFields), i = 0; i < keys.length; i++) { - const - key = keys[i], - normalizedKey = tiedFields[key]; - - if (normalizedKey == null) { - continue; - } - - const needToForceWatching = watchers[key] == null && ( - accessors[normalizedKey] != null || - computedFields[normalizedKey] != null - ); - - if (needToForceWatching) { - watchers[key] = [ - { - deep: true, - immediate: true, - provideArgs: false, - handler: fakeHandler - } - ]; - } - } - - runHook('beforeCreate', component).catch(stderr); - callMethodFromComponent(component, 'beforeCreate'); -} - -/** - * Initializes "beforeDataCreate" state to the specified component instance - * - * @param component - * @param [opts] - additional options - */ -export function beforeDataCreateState( - component: ComponentInterface, - opts?: InitBeforeDataCreateStateOptions -): void { - const {meta, $fields} = component.unsafe; - initFields(meta.fields, component, $fields); - - // Because in functional components, - // the watching of fields can be initialized in a lazy mode - if (meta.params.functional === true) { - Object.assign(component, $fields); - } - - Object.defineProperty(component, '$$data', { - get(): typeof $fields { - deprecate({name: '$$data', type: 'property', renamedTo: '$fields'}); - return $fields; - } - }); - - runHook('beforeDataCreate', component) - .catch(stderr); - - if (!component.isFlyweight && !component.$renderEngine.supports.ssr) { - implementComponentWatchAPI(component, {tieFields: opts?.tieFields}); - bindRemoteWatchers(component); - } -} - -/** - * Initializes "created" state to the specified component instance - * @param component - */ -export function createdState(component: ComponentInterface): void { - const { - unsafe, - unsafe: {$root: r, $async: $a, $normalParent: parent} - } = component; - - unmute(unsafe.$fields); - unmute(unsafe.$systemFields); - - const - isRegular = unsafe.meta.params.functional !== true && !unsafe.isFlyweight; - - if (parent != null && '$remoteParent' in r) { - const - p = parent.unsafe, - onBeforeDestroy = unsafe.$destroy.bind(unsafe); - - p.$on('on-component-hook:before-destroy', onBeforeDestroy); - $a.worker(() => p.$off('on-component-hook:before-destroy', onBeforeDestroy)); - - if (isRegular) { - const activationHooks: Dictionary = Object.createDict({ - activated: true, - deactivated: true - }); - - const onActivation = (status: Hook) => { - if (!activationHooks[status]) { - return; - } - - if (status === 'deactivated') { - component.deactivate(); - return; - } - - $a.requestIdleCallback(component.activate.bind(component), { - label: $$.remoteActivation, - timeout: 50 - }); - }; - - if (activationHooks[p.hook]) { - onActivation(p.hook); - } - - p.$on('on-component-hook-change', onActivation); - $a.worker(() => p.$off('on-component-hook-change', onActivation)); - } - } - - runHook('created', component).then(() => { - callMethodFromComponent(component, 'created'); - }).catch(stderr); -} - -/** - * Initializes "beforeMount" state to the specified component instance - * @param component - */ -export function beforeMountState(component: ComponentInterface): void { - const - {$el} = component; - - if ($el != null) { - $el.component = component; - } - - if (!component.isFlyweight) { - runHook('beforeMount', component).catch(stderr); - callMethodFromComponent(component, 'beforeMount'); - } -} - -/** - * Initializes "mounted" state to the specified component instance - * @param component - */ -export function mountedState(component: ComponentInterface): void { - const - {$el} = component; - - if ($el != null && $el.component !== component) { - $el.component = component; - } - - resolveRefs(component); - - runHook('mounted', component).then(() => { - callMethodFromComponent(component, 'mounted'); - }).catch(stderr); -} - -/** - * Initializes "beforeUpdate" state to the specified component instance - * @param component - */ -export function beforeUpdateState(component: ComponentInterface): void { - runHook('beforeUpdate', component).catch(stderr); - callMethodFromComponent(component, 'beforeUpdate'); -} - -/** - * Initializes "updated" state to the specified component instance - * @param component - */ -export function updatedState(component: ComponentInterface): void { - runHook('beforeUpdated', component).catch(stderr); - resolveRefs(component); - - runHook('updated', component).then(() => { - callMethodFromComponent(component, 'updated'); - }).catch(stderr); -} - -/** - * Initializes "activated" state to the specified component instance - * @param component - */ -export function activatedState(component: ComponentInterface): void { - runHook('beforeActivated', component).catch(stderr); - resolveRefs(component); - - runHook('activated', component).catch(stderr); - callMethodFromComponent(component, 'activated'); -} - -/** - * Initializes "deactivated" state to the specified component instance - * @param component - */ -export function deactivatedState(component: ComponentInterface): void { - runHook('deactivated', component).catch(stderr); - callMethodFromComponent(component, 'deactivated'); -} - -/** - * Initializes "beforeDestroy" state to the specified component instance - * @param component - */ -export function beforeDestroyState(component: ComponentInterface): void { - runHook('beforeDestroy', component).catch(stderr); - callMethodFromComponent(component, 'beforeDestroy'); - component.unsafe.$async.clearAll().locked = true; -} - -/** - * Initializes "destroyed" state to the specified component instance - * @param component - */ -export function destroyedState(component: ComponentInterface): void { - runHook('destroyed', component).then(() => { - callMethodFromComponent(component, 'destroyed'); - }).catch(stderr); -} - -/** - * Initializes "errorCaptured" state to the specified component instance - * - * @param component - * @param args - additional arguments - */ -export function errorCapturedState(component: ComponentInterface, ...args: unknown[]): void { - runHook('errorCaptured', component, ...args).then(() => { - callMethodFromComponent(component, 'errorCaptured', ...args); - }).catch(stderr); -} diff --git a/src/core/component/construct/states/activated.ts b/src/core/component/construct/states/activated.ts new file mode 100644 index 0000000000..69fa12ade2 --- /dev/null +++ b/src/core/component/construct/states/activated.ts @@ -0,0 +1,25 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { resolveRefs } from 'core/component/ref'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "activated" state to the specified component instance + * @param component + */ +export function activatedState(component: ComponentInterface): void { + runHook('beforeActivated', component).catch(stderr); + resolveRefs(component); + + runHook('activated', component).catch(stderr); + callMethodFromComponent(component, 'activated'); +} diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts new file mode 100644 index 0000000000..a2833bf91c --- /dev/null +++ b/src/core/component/construct/states/before-create.ts @@ -0,0 +1,183 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Async from 'core/async'; + +import { asyncLabel } from 'core/component/const'; +import { getComponentContext } from 'core/component/engines/helpers'; + +import { forkMeta } from 'core/component/meta'; +import { getPropertyInfo, PropertyInfo } from 'core/component/reflection'; +import { getNormalParent } from 'core/component/traverse'; + +import { initFields } from 'core/component/field'; +import { attachAccessorsFromMeta } from 'core/component/accessor'; +import { attachMethodsFromMeta, callMethodFromComponent } from 'core/component/method'; + +import { runHook } from 'core/component/hook'; +import { implementEventAPI } from 'core/component/event'; + +import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; +import type { InitBeforeCreateStateOptions } from 'core/component/construct/interface'; + +/** + * Initializes the "beforeCreate" state to the specified component instance + * + * @param component + * @param meta - component meta object + * @param [opts] - additional options + */ +export function beforeCreateState( + component: ComponentInterface, + meta: ComponentMeta, + opts?: InitBeforeCreateStateOptions +): void { + meta = forkMeta(meta); + + // To avoid TS errors marks all properties as editable + const unsafe = Object.cast>(component); + + unsafe.unsafe = unsafe; + unsafe.meta = meta; + + unsafe.componentName = meta.componentName; + unsafe.instance = Object.cast(meta.instance); + + unsafe.$fields = {}; + unsafe.$systemFields = {}; + unsafe.$modifiedFields = {}; + unsafe.$refHandlers = {}; + + unsafe.$async = new Async(component); + unsafe.$asyncLabel = asyncLabel; + + const + parent = unsafe.$parent, + isFunctional = meta.params.functional === true; + + if (parent != null && parent.componentName == null) { + unsafe.$parent = unsafe.$root.unsafe.$remoteParent; + } + + unsafe.$normalParent = getNormalParent(component); + + for (let keys = ['$root', '$parent', '$normalParent'], i = 0; i < keys.length; i++) { + const + key = keys[i], + val = unsafe[key]; + + if (val != null) { + unsafe[key] = getComponentContext(Object.cast(val)); + } + } + + if (opts?.addMethods) { + attachMethodsFromMeta(component); + } + + if (opts?.implementEventAPI) { + implementEventAPI(component); + } + + attachAccessorsFromMeta(component); + 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(); + + for (let o = watchDependencies.values(), el = o.next(); !el.done; el = o.next()) { + const + deps = el.value; + + for (let i = 0; i < deps.length; i++) { + const + dep = deps[i], + info = getPropertyInfo(Object.isArray(dep) ? dep.join('.') : String(dep), 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 the "fake" watcher to force watching + if (watchSet.size > 0) { + for (let o = watchSet.values(), el = o.next(); !el.done; el = o.next()) { + const + info = el.value; + + 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 the "fake" watcher to force watching + for (let keys = Object.keys(tiedFields), i = 0; i < keys.length; i++) { + const + key = keys[i], + normalizedKey = tiedFields[key]; + + if (normalizedKey == null) { + continue; + } + + const needToForceWatching = watchers[key] == null && ( + accessors[normalizedKey] != null || + computedFields[normalizedKey] != null + ); + + if (needToForceWatching) { + watchers[key] = [ + { + deep: true, + immediate: true, + provideArgs: false, + handler: fakeHandler + } + ]; + } + } + + runHook('beforeCreate', component).catch(stderr); + callMethodFromComponent(component, 'beforeCreate'); +} diff --git a/src/core/component/construct/states/before-data-create.ts b/src/core/component/construct/states/before-data-create.ts new file mode 100644 index 0000000000..ca0419c487 --- /dev/null +++ b/src/core/component/construct/states/before-data-create.ts @@ -0,0 +1,46 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { initFields } from 'core/component/field'; +import { bindRemoteWatchers, implementComponentWatchAPI } from 'core/component/watch'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; +import type { InitBeforeDataCreateStateOptions } from 'core/component/construct/interface'; + +/** + * Initializes the "beforeDataCreate" state to the specified component instance + * + * @param component + * @param [opts] - additional options + */ +export function beforeDataCreateState( + component: ComponentInterface, + opts?: InitBeforeDataCreateStateOptions +): void { + const {meta, $fields} = component.unsafe; + initFields(meta.fields, component, $fields); + + // Because in functional components, + // the watching of fields can be initialized in a lazy mode + if (meta.params.functional === true) { + Object.assign(component, $fields); + } + + runHook('beforeDataCreate', component) + .catch(stderr); + + const hasWatchAPI = + !component.isFlyweight && + !component.$renderEngine.supports.ssr; + + if (hasWatchAPI) { + implementComponentWatchAPI(component, {tieFields: opts?.tieFields}); + bindRemoteWatchers(component); + } +} diff --git a/src/core/component/construct/states/before-destroy.ts b/src/core/component/construct/states/before-destroy.ts new file mode 100644 index 0000000000..80ec5c988c --- /dev/null +++ b/src/core/component/construct/states/before-destroy.ts @@ -0,0 +1,22 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "beforeDestroy" state to the specified component instance + * @param component + */ +export function beforeDestroyState(component: ComponentInterface): void { + runHook('beforeDestroy', component).catch(stderr); + callMethodFromComponent(component, 'beforeDestroy'); + component.unsafe.$async.clearAll().locked = true; +} diff --git a/src/core/component/construct/states/before-mount.ts b/src/core/component/construct/states/before-mount.ts new file mode 100644 index 0000000000..8a3c79122b --- /dev/null +++ b/src/core/component/construct/states/before-mount.ts @@ -0,0 +1,30 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "beforeMount" state to the specified component instance + * @param component + */ +export function beforeMountState(component: ComponentInterface): void { + const + {$el} = component; + + if ($el != null) { + $el.component = component; + } + + if (!component.isFlyweight) { + runHook('beforeMount', component).catch(stderr); + callMethodFromComponent(component, 'beforeMount'); + } +} diff --git a/src/core/component/construct/states/before-update.ts b/src/core/component/construct/states/before-update.ts new file mode 100644 index 0000000000..1eedf5d226 --- /dev/null +++ b/src/core/component/construct/states/before-update.ts @@ -0,0 +1,21 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "beforeUpdate" state to the specified component instance + * @param component + */ +export function beforeUpdateState(component: ComponentInterface): void { + runHook('beforeUpdate', component).catch(stderr); + callMethodFromComponent(component, 'beforeUpdate'); +} diff --git a/src/core/component/construct/states/created.ts b/src/core/component/construct/states/created.ts new file mode 100644 index 0000000000..bb0f3c79b4 --- /dev/null +++ b/src/core/component/construct/states/created.ts @@ -0,0 +1,86 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { unmute } from 'core/object/watch'; + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface, Hook } from 'core/component/interface'; + +const + remoteActivationLabel = Symbol('Remote activation label'); + +/** + * Initializes the "created" state to the specified component instance + * @param component + */ +export function createdState(component: ComponentInterface): void { + const { + unsafe, + unsafe: { + $root: r, + $async: $a, + $normalParent: parent + } + } = component; + + unmute(unsafe.$fields); + unmute(unsafe.$systemFields); + + const isDynamicallyMountedComponent = + parent != null && + '$remoteParent' in r; + + if (isDynamicallyMountedComponent) { + const + p = parent.unsafe, + destroy = unsafe.$destroy.bind(unsafe); + + p.$on('on-component-hook:before-destroy', destroy); + $a.worker(() => p.$off('on-component-hook:before-destroy', destroy)); + + const isRegularComponent = + unsafe.meta.params.functional !== true && + !unsafe.isFlyweight; + + if (isRegularComponent) { + const activationHooks = Object.createDict({ + activated: true, + deactivated: true + }); + + const onActivation = (status: Hook) => { + if (activationHooks[status] == null) { + return; + } + + if (status === 'deactivated') { + component.deactivate(); + return; + } + + $a.requestIdleCallback(component.activate.bind(component), { + label: remoteActivationLabel, + timeout: 50 + }); + }; + + if (activationHooks[p.hook] != null) { + onActivation(p.hook); + } + + p.$on('on-component-hook-change', onActivation); + $a.worker(() => p.$off('on-component-hook-change', onActivation)); + } + } + + runHook('created', component).then(() => { + callMethodFromComponent(component, 'created'); + }).catch(stderr); +} diff --git a/src/core/component/construct/states/deactivated.ts b/src/core/component/construct/states/deactivated.ts new file mode 100644 index 0000000000..e86e26a462 --- /dev/null +++ b/src/core/component/construct/states/deactivated.ts @@ -0,0 +1,21 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "deactivated" state to the specified component instance + * @param component + */ +export function deactivatedState(component: ComponentInterface): void { + runHook('deactivated', component).catch(stderr); + callMethodFromComponent(component, 'deactivated'); +} diff --git a/src/core/component/construct/states/destroyed.ts b/src/core/component/construct/states/destroyed.ts new file mode 100644 index 0000000000..ec0ebc9840 --- /dev/null +++ b/src/core/component/construct/states/destroyed.ts @@ -0,0 +1,22 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "destroyed" state to the specified component instance + * @param component + */ +export function destroyedState(component: ComponentInterface): void { + runHook('destroyed', component).then(() => { + callMethodFromComponent(component, 'destroyed'); + }).catch(stderr); +} diff --git a/src/core/component/construct/states/error-captured.ts b/src/core/component/construct/states/error-captured.ts new file mode 100644 index 0000000000..09dd29932d --- /dev/null +++ b/src/core/component/construct/states/error-captured.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "errorCaptured" state to the specified component instance + * + * @param component + * @param args - additional arguments + */ +export function errorCapturedState(component: ComponentInterface, ...args: unknown[]): void { + runHook('errorCaptured', component, ...args).then(() => { + callMethodFromComponent(component, 'errorCaptured', ...args); + }).catch(stderr); +} diff --git a/src/core/component/construct/states/index.ts b/src/core/component/construct/states/index.ts new file mode 100644 index 0000000000..c3c4a5d905 --- /dev/null +++ b/src/core/component/construct/states/index.ts @@ -0,0 +1,20 @@ +/*! + * 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/construct/states/before-create'; +export * from 'core/component/construct/states/before-data-create'; +export * from 'core/component/construct/states/created'; +export * from 'core/component/construct/states/before-mount'; +export * from 'core/component/construct/states/mounted'; +export * from 'core/component/construct/states/before-update'; +export * from 'core/component/construct/states/updated'; +export * from 'core/component/construct/states/activated'; +export * from 'core/component/construct/states/deactivated'; +export * from 'core/component/construct/states/before-destroy'; +export * from 'core/component/construct/states/destroyed'; +export * from 'core/component/construct/states/error-captured'; diff --git a/src/core/component/construct/states/mounted.ts b/src/core/component/construct/states/mounted.ts new file mode 100644 index 0000000000..28e6d04f20 --- /dev/null +++ b/src/core/component/construct/states/mounted.ts @@ -0,0 +1,32 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { resolveRefs } from 'core/component/ref'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "mounted" state to the specified component instance + * @param component + */ +export function mountedState(component: ComponentInterface): void { + const + {$el} = component; + + if ($el != null && $el.component !== component) { + $el.component = component; + } + + resolveRefs(component); + + runHook('mounted', component).then(() => { + callMethodFromComponent(component, 'mounted'); + }).catch(stderr); +} diff --git a/src/core/component/construct/states/updated.ts b/src/core/component/construct/states/updated.ts new file mode 100644 index 0000000000..dff3c75f66 --- /dev/null +++ b/src/core/component/construct/states/updated.ts @@ -0,0 +1,26 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { resolveRefs } from 'core/component/ref'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "updated" state to the specified component instance + * @param component + */ +export function updatedState(component: ComponentInterface): void { + runHook('beforeUpdated', component).catch(stderr); + resolveRefs(component); + + runHook('updated', component).then(() => { + callMethodFromComponent(component, 'updated'); + }).catch(stderr); +} From 045a52a65d79e5ac0e78a4ab2b912f9b5a702da8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 18:15:40 +0300 Subject: [PATCH 0011/2313] refactor: added wrapping of a context --- src/core/component/ref/index.ts | 65 +++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/core/component/ref/index.ts b/src/core/component/ref/index.ts index 777f7fc51b..bdc86a7d46 100644 --- a/src/core/component/ref/index.ts +++ b/src/core/component/ref/index.ts @@ -11,13 +11,14 @@ * @packageDocumentation */ +import { getComponentContext } from 'core/component/engines/helpers'; import type { ComponentElement, ComponentInterface } from 'core/component/interface'; /** - * Resolves reference from the specified component instance. + * Resolves references from the specified component instance. * * This function replaces refs from component DOM nodes to component instances. - * Also, this function fires events of appearance refs. + * Also, this function fires events of ref appearances. * * @param component */ @@ -33,37 +34,45 @@ export function resolveRefs(component: ComponentInterface): void { for (let keys = Object.keys($refs), i = 0; i < keys.length; i++) { const key = keys[i], - el = $refs[key]; + ref = $refs[key]; - if (el == null) { + if (ref == null) { continue; } - if (Object.isArray(el)) { + if (Object.isArray(ref)) { const - arr = []; + refList = []; let needRewrite = false; - for (let i = 0; i < el.length; i++) { + for (let i = 0; i < ref.length; i++) { const - listEl = el[i]; + nestedRef = ref[i]; let component; - if (listEl instanceof Node) { - component = (listEl).component; - needRewrite = Boolean(component) && component.$el === listEl; + if (nestedRef instanceof Node) { + component = (nestedRef).component; + needRewrite = Boolean(component) && component.$el === nestedRef; } else { - const {$el} = listEl; + const {$el} = nestedRef; component = $el?.component; - needRewrite = listEl !== component; + needRewrite = nestedRef !== component; } - arr.push(needRewrite ? component : listEl); + let + newRef = needRewrite ? component : nestedRef; + + if (!(newRef instanceof Node)) { + needRewrite = true; + newRef = getComponentContext(newRef); + } + + refList.push(newRef); } if (needRewrite) { @@ -71,7 +80,7 @@ export function resolveRefs(component: ComponentInterface): void { configurable: true, enumerable: true, writable: true, - value: arr + value: refList }); } @@ -80,14 +89,22 @@ export function resolveRefs(component: ComponentInterface): void { component, needRewrite = false; - if (el instanceof Node) { - component = (el).component; - needRewrite = Boolean(component) && component.$el === el; + if (ref instanceof Node) { + component = (ref).component; + needRewrite = Boolean(component) && component.$el === ref; } else { - const {$el} = el; + const {$el} = ref; component = $el?.component; - needRewrite = el !== component; + needRewrite = ref !== component; + } + + let + newRef = needRewrite ? component : ref; + + if (!(newRef instanceof Node)) { + needRewrite = true; + newRef = getComponentContext(newRef); } if (needRewrite) { @@ -95,7 +112,7 @@ export function resolveRefs(component: ComponentInterface): void { configurable: true, enumerable: true, writable: true, - value: component + value: newRef }); } } @@ -106,11 +123,11 @@ export function resolveRefs(component: ComponentInterface): void { const key = keys[i], watchers = $refHandlers[key], - el = $refs[key]; + ref = $refs[key]; - if (el != null && watchers) { + if (ref != null && watchers != null) { for (let i = 0; i < watchers.length; i++) { - watchers[i](el); + watchers[i](ref); } delete $refHandlers[key]; From e30f635fafbaf21ade3fcfb160f337a6cd21d54f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 18:19:43 +0300 Subject: [PATCH 0012/2313] fix: fixed an issue when a context can be wrapped twice --- src/core/component/engines/helpers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/component/engines/helpers.ts b/src/core/component/engines/helpers.ts index c03e2be1b6..4c5d521efa 100644 --- a/src/core/component/engines/helpers.ts +++ b/src/core/component/engines/helpers.ts @@ -10,6 +10,7 @@ import { fakeCopyLabel } from 'core/component/watch'; import type { ComponentInterface } from 'core/component'; const + toRaw = Symbol('Link to a RAW component'), ctxMap = new WeakMap(); /** @@ -17,11 +18,14 @@ const * @param component */ export function getComponentContext(component: object): Dictionary & ComponentInterface['unsafe'] { + component = component[toRaw] ?? component; + let v = ctxMap.get(component); if (v == null) { v = Object.create(component); + Object.defineProperty(v, toRaw, {value: component}); ctxMap.set(component, v); } From 33506517f49325f0f22a1ffd2f951d5e2f0d4bb2 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 18:27:34 +0300 Subject: [PATCH 0013/2313] refactor: simple refactoring --- src/core/component/construct/states/created.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/construct/states/created.ts b/src/core/component/construct/states/created.ts index bb0f3c79b4..66153abaf2 100644 --- a/src/core/component/construct/states/created.ts +++ b/src/core/component/construct/states/created.ts @@ -45,11 +45,11 @@ export function createdState(component: ComponentInterface): void { p.$on('on-component-hook:before-destroy', destroy); $a.worker(() => p.$off('on-component-hook:before-destroy', destroy)); - const isRegularComponent = + const isRegular = unsafe.meta.params.functional !== true && !unsafe.isFlyweight; - if (isRegularComponent) { + if (isRegular) { const activationHooks = Object.createDict({ activated: true, deactivated: true From d2228a895a1f34b66924dad620453a6600adb3ad Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 18:27:56 +0300 Subject: [PATCH 0014/2313] chore: removed redundant TS types --- src/core/component/engines/vue3/component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index fc68a857f4..bc06efd7a9 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -95,11 +95,11 @@ export function getComponent(meta: ComponentMeta): ComponentOptions Date: Wed, 13 Apr 2022 18:38:02 +0300 Subject: [PATCH 0015/2313] fix: should set already exists properties using Object.defineProperty --- src/core/component/construct/states/before-create.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts index a2833bf91c..f1a87bcb45 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/construct/states/before-create.ts @@ -72,7 +72,12 @@ export function beforeCreateState( val = unsafe[key]; if (val != null) { - unsafe[key] = getComponentContext(Object.cast(val)); + Object.defineProperty(unsafe, key, { + configurable: true, + enumerable: true, + writable: false, + value: getComponentContext(Object.cast(val)) + }); } } From 263ee2301fa43e3083f8a3300b7df3595e639d1d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 18:49:25 +0300 Subject: [PATCH 0016/2313] refactor: removed @ts-ignore --- src/core/component/field/index.ts | 90 ++++++++++++++++--------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/src/core/component/field/index.ts b/src/core/component/field/index.ts index d1f606d05c..cc77b85089 100644 --- a/src/core/component/field/index.ts +++ b/src/core/component/field/index.ts @@ -13,6 +13,7 @@ import { defProp } from 'core/const/props'; import { fieldQueue } from 'core/component/field/const'; + import type { ComponentInterface, ComponentField } from 'core/component/interface'; export * from 'core/component/field/const'; @@ -34,11 +35,12 @@ export function initFields( component: ComponentInterface, store: Dictionary = {} ): Dictionary { - const { - unsafe, - unsafe: {meta: {componentName, params, instance}}, - isFlyweight - } = component; + const + unsafe = Object.cast>(component.unsafe); + + const + {isFlyweight} = component, + {componentName, params, instance} = unsafe.meta; const ssrMode = component.$renderEngine.supports.ssr, @@ -70,7 +72,7 @@ export function initFields( for (let keys = Object.keys(fields).sort(), i = 0; i < keys.length; i++) { const key = keys[i], - el = fields[key]; + field = fields[key]; let sourceVal = store[key], @@ -78,9 +80,9 @@ export function initFields( if (isFlyweight) { if ( - el != null && ( - el.replace !== true && (Object.isTruly(el.unique) || el.src === componentName) || - el.replace === false + field != null && ( + field.replace !== true && (Object.isTruly(field.unique) || field.src === componentName) || + field.replace === false ) ) { Object.defineProperty(store, key, defField); @@ -90,26 +92,26 @@ export function initFields( } const dontNeedInit = - el == null || + field == null || sourceVal !== undefined || // Don't initialize a property for the functional component unless explicitly required - !ssrMode && isNotRegular && el.functional === false || + !ssrMode && isNotRegular && field.functional === false || - el.init == null && el.default === undefined && instance[key] === undefined; + field.init == null && field.default === undefined && instance[key] === undefined; - if (el == null || dontNeedInit) { + if (field == null || dontNeedInit) { canSkip[key] = true; store[key] = sourceVal; continue; } - if (el.atom) { + if (field.atom) { // If true, then the field does not have any dependencies and can be initialized right now let canInit = true; const - {after} = el; + {after} = field; if (after && after.size > 0) { for (let o = after.values(), val = o.next(); !val.done; val = o.next()) { @@ -134,21 +136,20 @@ export function initFields( store[key] = undefined; } - // @ts-ignore (access) - unsafe['$activeField'] = key; + unsafe.$activeField = key; let val; - if (el.init != null) { - val = el.init(unsafe, store); + if (field.init != null) { + val = field.init(component.unsafe, store); } if (val === undefined) { if (store[key] === undefined) { // We need to clone the default value from a constructor // to prevent linking to the same object for a non-primitive value - val = el.default !== undefined ? el.default : Object.fastClone(instance[key]); + val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); store[key] = val; } @@ -156,8 +157,7 @@ export function initFields( store[key] = val; } - // @ts-ignore (access) - unsafe['$activeField'] = undefined; + unsafe.$activeField = undefined; } } else { @@ -170,12 +170,17 @@ export function initFields( for (let i = 0; i < atomList.length; i++) { const key = nonAtomList[i], - el = key != null ? fields[key] : null; + field = key != null ? fields[key] : null; let isNull = false; - if (el == null || key == null || key in store && !(isNull = store[key] === NULL)) { + const canSkip = + field == null || + key == null || + key in store && !(isNull = store[key] === NULL); + + if (canSkip) { continue; } @@ -183,7 +188,7 @@ export function initFields( let canInit = true; const - {after} = el; + {after} = field; if (after && after.size > 0) { for (let o = after.values(), val = o.next(); !val.done; val = o.next()) { @@ -220,22 +225,21 @@ export function initFields( store[key] = undefined; } - // @ts-ignore (access) - unsafe['$activeField'] = key; + unsafe.$activeField = key; fieldQueue.delete(key); let val; - if (el.init != null) { - val = el.init(unsafe, store); + if (field.init != null) { + val = field.init(component.unsafe, store); } if (val === undefined) { if (store[key] === undefined) { // We need to clone the default value from a constructor // to prevent linking to the same object for a non-primitive value - val = el.default !== undefined ? el.default : Object.fastClone(instance[key]); + val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); store[key] = val; } @@ -243,8 +247,7 @@ export function initFields( store[key] = val; } - // @ts-ignore (access) - unsafe['$activeField'] = undefined; + unsafe.$activeField = undefined; } } @@ -259,12 +262,17 @@ export function initFields( for (let i = 0; i < nonAtomList.length; i++) { const key = nonAtomList[i], - el = key != null ? fields[key] : null; + field = key != null ? fields[key] : null; let isNull = false; - if (el == null || key == null || key in store && !(isNull = store[key] === NULL)) { + const canSkip = + field == null || + key == null || + key in store && !(isNull = store[key] === NULL); + + if (canSkip) { continue; } @@ -272,7 +280,7 @@ export function initFields( let canInit = true; const - {after} = el; + {after} = field; if (after && after.size > 0) { for (let o = after.values(), val = o.next(); !val.done; val = o.next()) { @@ -305,22 +313,21 @@ export function initFields( store[key] = undefined; } - // @ts-ignore (access) - unsafe['$activeField'] = key; + unsafe.$activeField = key; fieldQueue.delete(key); let val; - if (el.init != null) { - val = el.init(unsafe, store); + if (field.init != null) { + val = field.init(component.unsafe, store); } if (val === undefined) { if (store[key] === undefined) { // We need to clone the default value from a constructor // to prevent linking to the same object for a non-primitive value - val = el.default !== undefined ? el.default : Object.fastClone(instance[key]); + val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); store[key] = val; } @@ -328,8 +335,7 @@ export function initFields( store[key] = val; } - // @ts-ignore (access) - unsafe['$activeField'] = undefined; + unsafe.$activeField = undefined; } } From 6f5f77d67b55ec2b6810d33bacc91dfab3280981 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 13 Apr 2022 18:49:57 +0300 Subject: [PATCH 0017/2313] :art: --- src/core/component/field/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/field/README.md b/src/core/component/field/README.md index 30360954a0..f054a41153 100644 --- a/src/core/component/field/README.md +++ b/src/core/component/field/README.md @@ -1,3 +1,3 @@ # core/component/field -This module provides API to initialize component fields and system fields. +This module provides API to initialize component fields. From 47a05a3a3ee204654c469e405141db1b8a89cda7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 12:27:23 +0300 Subject: [PATCH 0018/2313] refactor: moved to a folder --- src/core/component/interface/{component.ts => component/index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/core/component/interface/{component.ts => component/index.ts} (100%) diff --git a/src/core/component/interface/component.ts b/src/core/component/interface/component/index.ts similarity index 100% rename from src/core/component/interface/component.ts rename to src/core/component/interface/component/index.ts From b21cca850b8d352e9ac81314218ee1fa6d180299 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 13:59:37 +0300 Subject: [PATCH 0019/2313] refactor: removed vu2 engine --- src/core/component/engines/vue/CHANGELOG.md | 58 ------- src/core/component/engines/vue/README.md | 3 - src/core/component/engines/vue/component.ts | 182 -------------------- src/core/component/engines/vue/config.ts | 68 -------- src/core/component/engines/vue/const.ts | 89 ---------- src/core/component/engines/vue/index.ts | 30 ---- src/core/component/engines/vue/vnode.ts | 58 ------- 7 files changed, 488 deletions(-) delete mode 100644 src/core/component/engines/vue/CHANGELOG.md delete mode 100644 src/core/component/engines/vue/README.md delete mode 100644 src/core/component/engines/vue/component.ts delete mode 100644 src/core/component/engines/vue/config.ts delete mode 100644 src/core/component/engines/vue/const.ts delete mode 100644 src/core/component/engines/vue/index.ts delete mode 100644 src/core/component/engines/vue/vnode.ts diff --git a/src/core/component/engines/vue/CHANGELOG.md b/src/core/component/engines/vue/CHANGELOG.md deleted file mode 100644 index 8440d92f35..0000000000 --- a/src/core/component/engines/vue/CHANGELOG.md +++ /dev/null @@ -1,58 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.0.0-rc.204 (2021-06-23) - -#### :bug: Bug Fix - -* Fixed async rendering with text elements - -## v3.0.0-rc.137 (2021-02-04) - -#### :bug: Bug fix - -* Fixed redundant listening of events - -## v3.0.0-rc.92 (2020-11-03) - -#### :house: Internal - -* Refactoring - -## v3.0.0-rc.66 (2020-09-22) - -#### :bug: Bug Fix - -* [Fixed mixing of directives within of a component root tag](https://github.com/V4Fire/Client/pull/337) - -## v3.0.0-rc.50 (2020-08-03) - -#### :bug: Bug Fix - -* Fixed `getComponentName` - -## v3.0.0-rc.44 (2020-07-30) - -#### :bug: Bug Fix - -* Fixed setting of `staticClass` - -## v3.0.0-rc.40 (2020-07-27) - -#### :house: Internal - -* Logging Vue errors and warnings via the `core/log` module - -## v3.0.0-rc.37 (2020-07-20) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/core/component/engines/vue/README.md b/src/core/component/engines/vue/README.md deleted file mode 100644 index 0c91b8488e..0000000000 --- a/src/core/component/engines/vue/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core/component/engines/vue - -This module provides an adaptor for Vue.js. diff --git a/src/core/component/engines/vue/component.ts b/src/core/component/engines/vue/component.ts deleted file mode 100644 index 41ca8fbfbf..0000000000 --- a/src/core/component/engines/vue/component.ts +++ /dev/null @@ -1,182 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import watch, { set, mute, unmute, WatchHandlerParams } from 'core/object/watch'; -import * as init from 'core/component/construct'; - -import { beforeRenderHooks } from 'core/component/const'; -import { fillMeta } from 'core/component/meta'; -import { implementComponentForceUpdateAPI } from 'core/component/render'; - -import { supports, minimalCtx, proxyGetters } from 'core/component/engines/vue/const'; -import { cloneVNode, patchVNode, renderVNode } from 'core/component/engines/vue/vnode'; - -import { fakeMapSetCopy } from 'core/component/engines/helpers'; - -import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; -import type { ComponentInterface, UnsafeComponentInterface, ComponentMeta } from 'core/component/interface'; - -/** - * Returns a component declaration object from the specified component meta object - * @param meta - */ -export function getComponent(meta: ComponentMeta): ComponentOptions { - const - {component} = fillMeta(meta); - - const - p = meta.params, - m = p.model; - - return { - ...Object.cast(component), - inheritAttrs: p.inheritAttrs, - - model: m && { - prop: m.prop, - event: m.event?.dasherize() ?? '' - }, - - data(this: ComponentInterface): Dictionary { - const - ctx = Object.cast(this), - - // eslint-disable-next-line @typescript-eslint/unbound-method - {$watch, $set, $delete} = this; - - this['$vueWatch'] = $watch; - this['$vueSet'] = $set; - this['$vueDelete'] = $delete; - - init.beforeDataCreateState(this); - - const emitter = (_, handler) => { - // eslint-disable-next-line @typescript-eslint/unbound-method - const {unwatch} = watch(ctx.$fields, {deep: true, immediate: true}, handler); - return unwatch; - }; - - ctx.$async.on(emitter, 'mutation', watcher, { - group: 'watchers:suspend' - }); - - return ctx.$fields; - - function watcher(value: unknown, oldValue: unknown, info: WatchHandlerParams): void { - const - {path} = info; - - ctx.lastSelfReasonToRender = { - path, - value, - oldValue - }; - - if (beforeRenderHooks[ctx.hook] === true) { - return; - } - - if (meta.fields[String(path[0])]?.forceUpdate !== false) { - ctx.$forceUpdate(); - } - - let - {obj} = info; - - if (path.length > 1) { - if (Object.isDictionary(obj)) { - const - key = String(path[path.length - 1]), - desc = Object.getOwnPropertyDescriptor(obj, key); - - // If we register a new property, we must register it to Vue too - if (desc?.get == null) { - // For correct registering of a property with Vue, - // we need to remove it from a proxy and original object - delete obj[key]; - - // Get a link to a proxy object - obj = Object.get(ctx.$fields, path.slice(0, -1)) ?? {}; - delete obj[key]; - - // Finally, we can register a Vue watcher - $set.call(ctx, obj, key, value); - - // Don't forget to restore the original watcher - mute(obj); - set(obj, key, value); - unmute(obj); - } - - // Because Vue does not see changes from Map/Set structures, we must use this hack - } else if (Object.isSet(obj) || Object.isMap(obj) || Object.isWeakMap(obj) || Object.isWeakSet(obj)) { - Object.set(ctx, path.slice(0, -1), fakeMapSetCopy(obj)); - } - } - } - }, - - beforeCreate(): void { - const - ctx = Object.cast(this), - unsafe = Object.cast(this); - - unsafe.$renderEngine = { - supports, - minimalCtx, - proxyGetters, - cloneVNode, - patchVNode, - renderVNode - }; - - init.beforeCreateState(ctx, meta); - implementComponentForceUpdateAPI(ctx, this.$forceUpdate.bind(this)); - }, - - created(this: ComponentInterface): void { - init.createdState(this); - }, - - beforeMount(this: ComponentInterface): void { - init.beforeMountState(this); - }, - - mounted(this: ComponentInterface): void { - init.mountedState(this); - }, - - beforeUpdate(this: ComponentInterface): void { - init.beforeUpdateState(this); - }, - - updated(this: ComponentInterface): void { - init.updatedState(this); - }, - - activated(this: ComponentInterface): void { - init.activatedState(this); - }, - - deactivated(this: ComponentInterface): void { - init.deactivatedState(this); - }, - - beforeDestroy(this: ComponentInterface): void { - init.beforeDestroyState(this); - }, - - destroyed(this: ComponentInterface): void { - init.destroyedState(this); - }, - - errorCaptured(this: ComponentInterface): void { - init.errorCapturedState(this); - } - }; -} diff --git a/src/core/component/engines/vue/config.ts b/src/core/component/engines/vue/config.ts deleted file mode 100644 index cdb920f026..0000000000 --- a/src/core/component/engines/vue/config.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Vue from 'vue'; -import log from 'core/log'; - -import type { ComponentInterface } from 'core/component/interface'; - -const - logger = log.namespace('vue'); - -Vue.config.errorHandler = (err, vm, info) => { - logger.error('errorHandler', err, info, getComponentInfo(vm)); -}; - -Vue.config.warnHandler = (msg, vm, trace) => { - logger.warn('warnHandler', msg, trace, getComponentInfo(vm)); -}; - -const - UNRECOGNIZED_COMPONENT_NAME = 'unrecognized-component', - ROOT_COMPONENT_NAME = 'root-component'; - -/** - * Returns component info to log - * @param component - */ -function getComponentInfo(component: Vue | ComponentInterface): Dictionary { - try { - if ('componentName' in component) { - return { - name: getComponentName(component), - hook: component.hook, - status: component.unsafe.componentStatus - }; - } - - return { - name: getComponentName(component) - }; - - } catch { - return { - name: UNRECOGNIZED_COMPONENT_NAME - }; - } -} - -/** - * Returns a name of the specified component - * @param component - */ -function getComponentName(component: Vue | ComponentInterface): string { - if ('componentName' in component) { - return component.componentName; - } - - if (component.$root === component) { - return ROOT_COMPONENT_NAME; - } - - return Object.get(component, '$options.name') ?? UNRECOGNIZED_COMPONENT_NAME; -} diff --git a/src/core/component/engines/vue/const.ts b/src/core/component/engines/vue/const.ts deleted file mode 100644 index 50d5af6a42..0000000000 --- a/src/core/component/engines/vue/const.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Vue from 'vue'; -import type { UnsafeComponentInterface, ProxyGetters } from 'core/component/interface'; - -export const supports = { - regular: true, - functional: true, - composite: true, - - ssr: false, - boundCreateElement: true -}; - -export const minimalCtx = (() => { - const - obj = Vue.prototype, - ctx = {}; - - for (const key in obj) { - if (key.length === 2) { - ctx[key] = obj[key]; - } - } - - return ctx; -})(); - -type VueProxyGetters = ProxyGetters; - -export const proxyGetters = Object.createDict({ - prop: (ctx) => ({ - key: '_props', - - get value(): Dictionary { - return ctx._props; - }, - - watch: (path, handler) => ctx.$vueWatch(path, (val, oldVal) => { - if (val !== oldVal) { - handler(val, oldVal); - } - }) - }), - - attr: (ctx) => ({ - key: '$attrs', - - get value(): typeof ctx.$attrs { - return ctx.$attrs; - }, - - watch: (path, handler) => ctx.$vueWatch(path, (val, oldVal) => { - if (val !== oldVal) { - handler(val, oldVal); - } - }) - }), - - field: (ctx) => ({ - key: '$fields', - get value(): typeof ctx.$fields { - return ctx.$fields; - } - }), - - system: (ctx) => ({ - key: '$systemFields', - get value(): typeof ctx.$systemFields { - return ctx.$systemFields; - } - }), - - mounted: (ctx) => ({ - key: null, - get value(): typeof ctx { - return ctx; - } - }) -}); diff --git a/src/core/component/engines/vue/index.ts b/src/core/component/engines/vue/index.ts deleted file mode 100644 index cb5cd8f766..0000000000 --- a/src/core/component/engines/vue/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/engines/vue/README.md]] - * @packageDocumentation - */ - -import Vue from 'vue'; -import 'core/component/engines/vue/config'; - -export * from 'vue'; - -/** @deprecated */ -export { Vue as ComponentDriver }; -export { Vue as ComponentEngine }; -export { Vue as default }; - -export * from 'core/component/engines/vue/const'; -export * from 'core/component/engines/vue/vnode'; -export * from 'core/component/engines/vue/component'; - -//#if VueInterfaces -export { VNode, ScopedSlot, NormalizedScopedSlot } from 'vue/types/vnode'; -//#endif diff --git a/src/core/component/engines/vue/vnode.ts b/src/core/component/engines/vue/vnode.ts deleted file mode 100644 index a2a06064f0..0000000000 --- a/src/core/component/engines/vue/vnode.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Vue, { RenderContext, VNode } from 'vue'; - -import { patchComponentVData } from 'core/component/vnode'; -import type { ComponentInterface } from 'core/component/interface'; - -/** - * Clones the specified vnode - * @param vnode - */ -export function cloneVNode(vnode: VNode): VNode { - return vnode; -} - -/** - * Patches the specified VNode by using provided contexts - * - * @param vnode - * @param component - component instance - * @param renderCtx - render context - */ -export function patchVNode(vnode: VNode, component: ComponentInterface, renderCtx: RenderContext): VNode { - patchComponentVData(vnode.data, renderCtx.data, { - patchAttrs: Boolean(component.unsafe.meta.params.inheritAttrs) - }); - - return vnode; -} - -/** - * Renders the specified VNode/s and returns the result - * - * @param vnode - * @param parent - parent component - */ -export function renderVNode(vnode: VNode, parent: ComponentInterface): Node; -export function renderVNode(vnodes: VNode[], parent: ComponentInterface): Node[]; -export function renderVNode(vnode: CanArray, parent: ComponentInterface): CanArray { - const vue = new Vue({ - render: (c) => Object.isArray(vnode) ? c('div', vnode) : vnode - }); - - Object.set(vue, '$root', Object.create(parent.$root)); - Object.set(vue, '$root.$remoteParent', parent); - Object.set(vue, '$root.unsafe', vue.$root); - - const el = document.createElement('div'); - vue.$mount(el); - - return Object.isArray(vnode) ? Array.from(vue.$el.childNodes) : vue.$el; -} From 87745ea897dfb68b303513eb709cc941e7bfb716 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 14:00:28 +0300 Subject: [PATCH 0020/2313] refactor: split component types into separated files; removed legacy --- .../interface/component/component.ts | 427 +++++++++++ .../component/interface/component/index.ts | 690 +----------------- .../component/interface/component/types.ts | 33 + .../component/interface/component/unsafe.ts | 128 ++++ 4 files changed, 591 insertions(+), 687 deletions(-) create mode 100644 src/core/component/interface/component/component.ts create mode 100644 src/core/component/interface/component/types.ts create mode 100644 src/core/component/interface/component/unsafe.ts diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts new file mode 100644 index 0000000000..707c85b88f --- /dev/null +++ b/src/core/component/interface/component/component.ts @@ -0,0 +1,427 @@ +/* +eslint-disable +@typescript-eslint/no-unused-vars-experimental, +@typescript-eslint/no-empty-function, +@typescript-eslint/unified-signatures +*/ + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { LogMessageOptions } from 'core/log'; + +import type Async from 'core/async'; +import type { BoundFn, ProxyCb } from 'core/async'; + +import type { Slots, ComponentOptions } from 'core/component/engines'; + +import type { + + Hook, + ComponentMeta, + SyncLinkCache, + + WatchPath, + WatchOptions, + RawWatchHandler, + + RenderEngine + +} from 'core/component/interface'; + +import type { ComponentElement } from 'core/component/interface/component/types'; +import type { UnsafeGetter, UnsafeComponentInterface } from 'core/component/interface/component/unsafe'; + +/** + * An abstract class that represents Vue compatible component API + */ +export abstract class ComponentInterface { + /** + * Type: root component + */ + readonly Root!: ComponentInterface; + + /** + * Type: base super class for all components + */ + readonly Component!: ComponentInterface; + + /** + * Unique component string identifier + */ + readonly componentId!: string; + + /** + * Name of the component without special postfixes like `-functional` + */ + readonly componentName!: string; + + /** + * Link to a class instance that is used to describe the component. + * Basically, this parameter is used for `instanceof` checks and to get default values of properties. + * Mind, every kind of components has only the one instance of that kind, + * i.e. one instance is shared between components of the same type. + */ + readonly instance!: this; + + /** + * Name of the active component hook + */ + get hook(): Hook { + return 'beforeRuntime'; + } + + /** + * Switches the component to a new hook + * + * @param value + * @emits `componentHook:{$value}(value: Hook, oldValue: Hook)` + * @emits `componentHookChange(value: Hook, oldValue: Hook) + */ + protected set hook(value: Hook) { + // Loopback + } + + /** + * Name of the active rendering group to use with async rendering + */ + readonly renderGroup?: string; + + /** + * True if the component is attached to the parent render function as a flyweight + */ + readonly isFlyweight?: boolean; + + /** + * API to invoke unsafely of internal properties of the component. + * This parameter helps to avoid TS errors of using protected properties and methods outside from the component class. + * It's useful to create component’ friendly classes. + */ + get unsafe(): UnsafeGetter> { + return Object.cast(this); + } + + /** + * Link to a DOM element that is the root for the component + */ + readonly $el?: ComponentElement; + + /** + * Raw parameters of the component with which it was created by an engine + */ + readonly $options!: ComponentOptions; + + /** + * Map of initialized input properties of the component + */ + readonly $props!: Dictionary; + + /** + * Link to the root component of the application + */ + readonly $root!: this['Root']; + + /** + * Link to a parent component of the current component + */ + readonly $parent?: this['Component']; + + /** + * Link to the closest non-functional parent component of the current component + */ + readonly $normalParent?: this['Component']; + + /** + * API of the used rendering engine + */ + readonly $renderEngine!: RenderEngine; + + /** + * A link to the component meta object. + * This object contains all information of component properties, methods and other stuff. + * It's used to create a "real" component by the used engine and some optimizations based on reflect. + */ + protected readonly meta!: ComponentMeta; + + /** + * Number that increments on every re-rendering of the component + */ + protected renderCounter!: number; + + /** + * Temporary unique component string identifier for functional components + */ + protected readonly $componentId?: string; + + /** + * Link to a parent component if the current component was dynamically created and mounted + */ + protected readonly $remoteParent?: this['Component']; + + /** + * Map of watchable component properties that can force re-rendering + */ + protected readonly $fields!: Dictionary; + + /** + * Map of watchable component properties that can't force re-rendering + */ + protected readonly $systemFields!: Dictionary; + + /** + * Map of component properties that were modified and need to synchronize + * (only for functional components) + */ + protected readonly $modifiedFields!: Dictionary; + + /** + * Map of component attributes that aren't recognized as input properties + */ + protected readonly $attrs!: Dictionary; + + /** + * Map of external listeners of component events + */ + protected readonly $listeners!: Dictionary>; + + /** + * Map of references to elements that have a "ref" attribute + */ + protected readonly $refs!: Dictionary; + + /** + * Map of handlers that wait appearing of references to elements that have a "ref" attribute + */ + protected readonly $refHandlers!: Dictionary; + + /** + * Map of available render slots + */ + protected readonly $slots!: Slots; + + /** + * Name of the active property to initialize + */ + protected readonly $activeField?: string; + + /** + * Cache for component links + */ + protected readonly $syncLinkCache!: SyncLinkCache; + + /** + * API to tie and control async operations + */ + protected readonly $async!: Async; + + /** + * Promise of the component initializing + */ + protected $initializer?: Promise; + + /** + * Logs an event with the specified context + * + * @param ctxOrOpts - logging context or logging options (logLevel, context) + * @param [details] - event details + */ + log?(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void; + + /** + * Activates the component. + * The deactivated component won't load data from providers during initializing. + * + * Basically, you don't need to think about the component activation, + * because it's automatically synchronized with `keep-alive` or the component prop. + * + * @param [force] - if true, then the component will be forced to activate, even if it is already activated + */ + activate(force?: boolean): void {} + + /** + * Deactivates the component. + * The deactivated component won't load data from providers during initializing. + * + * Basically, you don't need to think about the component activation, + * because it's automatically synchronized with `keep-alive` or the component prop. + */ + deactivate(): void {} + + /** + * Forces the component to re-render + */ + $forceUpdate(): void {} + + /** + * Executes the specified function on the next render tick + * @param cb + */ + $nextTick(cb: Function | BoundFn): void; + + /** + * Returns a promise that will be resolved on the next render tick + */ + $nextTick(): Promise; + $nextTick(): CanPromise {} + + /** + * Destroys the component + */ + protected $destroy(): void {} + + /** + * Sets a new reactive value to the specified property of an object + * + * @param object + * @param key + * @param value + */ + protected $set(object: object, key: unknown, value: T): T { + return value; + } + + /** + * Deletes the specified reactive property from an object + * + * @param object + * @param key + */ + protected $delete(object: object, key: unknown): void {} + + /** + * Sets a watcher to a component/object property by the specified path + * + * @param path + * @param handler + * @param opts + */ + protected $watch( + path: WatchPath, + opts: WatchOptions, + handler: RawWatchHandler + ): Nullable; + + /** + * Sets a watcher to a component/object property by the specified path + * + * @param path + * @param handler + */ + protected $watch( + path: WatchPath, + handler: RawWatchHandler + ): Nullable; + + /** + * Sets a watcher to the specified watchable object + * + * @param obj + * @param handler + */ + protected $watch( + obj: object, + handler: RawWatchHandler + ): Nullable; + + /** + * Sets a watcher to the specified watchable object + * + * @param obj + * @param handler + * @param opts + */ + protected $watch( + obj: object, + opts: WatchOptions, + handler: RawWatchHandler + ): Nullable; + + protected $watch(): Nullable { + return null; + } + + /** + * Attaches a listener to the specified component event + * + * @param event + * @param handler + */ + protected $on(event: CanArray, handler: ProxyCb): this { + return this; + } + + /** + * Attaches a disposable listener to the specified component event + * + * @param event + * @param handler + */ + protected $once(event: string, handler: ProxyCb): this { + return this; + } + + /** + * Detaches an event listeners from the component + * + * @param [event] + * @param [handler] + */ + protected $off(event?: CanArray, handler?: Function): this { + return this; + } + + /** + * Emits a component event + * + * @param event + * @param args + */ + protected $emit(event: string, ...args: unknown[]): this { + return this; + } + + /** + * Hook handler: the component has been created + * (only for flyweight components) + */ + protected onCreatedHook(): void { + // Loopback + } + + /** + * Hook handler: the component has been bound + * (only for functional and flyweight components) + */ + protected onBindHook(): void { + // Loopback + } + + /** + * Hook handler: the component has been mounted + * (only for functional and flyweight components) + */ + protected onInsertedHook(): void { + // Loopback + } + + /** + * Hook handler: the component has been updated + * (only for functional and flyweight components) + */ + protected onUpdateHook(): void { + // Loopback + } + + /** + * Hook handler: the component has been unbound + * (only for functional and flyweight components) + */ + protected onUnbindHook(): void { + // Loopback + } +} diff --git a/src/core/component/interface/component/index.ts b/src/core/component/interface/component/index.ts index d20840401d..2d8a2b6d78 100644 --- a/src/core/component/interface/component/index.ts +++ b/src/core/component/interface/component/index.ts @@ -1,10 +1,3 @@ -/* -eslint-disable -@typescript-eslint/no-unused-vars-experimental, -@typescript-eslint/no-empty-function, -@typescript-eslint/unified-signatures -*/ - /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -13,683 +6,6 @@ eslint-disable * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { LogMessageOptions } from 'core/log'; - -import type Async from 'core/async'; -import type { BoundFn, ProxyCb } from 'core/async'; - -import type { - - ComponentEngine, - - ComponentOptions, - FunctionalComponentOptions, - - VNode, - ScopedSlot, - CreateElement - -} from 'core/component/engines'; - -import type { - - Hook, - ComponentMeta, - SyncLinkCache, - - WatchPath, - WatchOptions, - RawWatchHandler, - - RenderEngine - -} from 'core/component/interface'; - -/** - * Component render function - */ -export type RenderFunction = - ComponentOptions['render'] | FunctionalComponentOptions['render']; - -/** - * Base context of a functional component - */ -export interface FunctionalCtx { - componentName: string; - meta: ComponentMeta; - instance: Dictionary; - $options: Dictionary; -} - -export interface ComponentConstructor { - new(): T; -} - -/** - * DOM Element that is tied with a component - */ -export type ComponentElement = Element & { - component?: T; -}; - -export interface RenderReason { - value: unknown; - oldValue: CanUndef; - path: unknown[]; -} - -/** - * A helper structure to pack the unsafe interface: - * it fixes some ambiguous TS warnings - */ -export type UnsafeGetter = - Dictionary & U['CTX'] & U & {unsafe: any}; - -/** - * Abstract class that represents Vue compatible component API - */ -export abstract class ComponentInterface { - /** - * Type: base component - */ - readonly Component!: ComponentInterface; - - /** - * Type: root component - */ - readonly Root!: ComponentInterface; - - /** - * Unique component string identifier - */ - readonly componentId!: string; - - /** - * Name of the component without special postfixes - */ - readonly componentName!: string; - - /** - * Link to a component instance - */ - readonly instance!: this; - - /** - * Name of the active component hook - */ - get hook(): Hook { - return 'beforeRuntime'; - } - - /** - * Switches the component to a new hook - * - * @param value - * @emits `componentHook:{$value}(value: Hook, oldValue: Hook)` - * @emits `componentHookChange(value: Hook, oldValue: Hook) - */ - protected set hook(value: Hook) { - // Loopback - } - - /** - * Name of the active rendering group - * (it's used with async rendering) - */ - readonly renderGroup?: string; - - /** - * API for unsafe invoking of internal properties of the component. - * It can be useful to create component' friendly classes. - */ - get unsafe(): UnsafeGetter> { - return Object.cast(this); - } - - /** - * Link to a DOM element that is tied with the component - */ - // @ts-ignore (ts error) - readonly $el?: ComponentElement; - - /** - * Raw component options - */ - readonly $options!: ComponentOptions; - - /** - * Dictionary with initialized input properties of the component - */ - readonly $props!: Dictionary; - - /** - * List of child components - */ - // @ts-ignore (ts error) - readonly $children?: Array; - - /** - * Link to the parent component - */ - // @ts-ignore (ts error) - readonly $parent?: this['Component']; - - /** - * Link to the closest non-functional parent component - */ - readonly $normalParent?: this['Component']; - - /** - * Link to the root component - */ - readonly $root!: this['Root']; - - /** - * Description object of the used rendering engine - */ - readonly $renderEngine!: RenderEngine; - - /** - * True if the component can be attached to a parent render function - */ - readonly isFlyweight?: boolean; - - /** - * Temporary unique component' string identifier - * (only for functional components) - */ - protected readonly $componentId?: string; - - /** - * Link to a component meta object - */ - protected readonly meta!: ComponentMeta; - - /** - * Value that increments on every re-rendering of the component - */ - protected renderCounter!: number; - - /** - * The last reason why the component was re-rendered. - * The `null` value is means that the component was re-rendered by using $forceUpdate. - */ - protected lastSelfReasonToRender?: Nullable; - - /** - * Timestamp of the last component rendering - */ - protected lastTimeOfRender?: DOMHighResTimeStamp; - - /** - * Cache table for virtual nodes - */ - protected readonly renderTmp!: Dictionary; - - /** - * The special symbol that is tied with async rendering - */ - protected readonly $asyncLabel!: symbol; - - /** - * Link to the parent component - * (using with async rendering) - */ - // @ts-ignore (ts error) - protected readonly $remoteParent?: this['Component']; - - /** - * Internal API for async operations - */ - protected readonly $async!: Async; - - /** - * Map of component attributes that aren't recognized as input properties - */ - protected readonly $attrs!: Dictionary; - - /** - * Map of external listeners of component events - */ - protected readonly $listeners!: Dictionary>; - - /** - * Map of references to elements that have a "ref" attribute - */ - protected readonly $refs!: Dictionary; - - /** - * Map of handlers that wait appearing of references to elements that have a "ref" attribute - */ - protected readonly $refHandlers!: Dictionary; - - /** - * Map of available render slots - */ - protected readonly $slots!: Dictionary; - - /** - * Map of available scoped render slots - */ - protected readonly $scopedSlots!: Dictionary; - - /** - * Map of component fields that can force re-rendering - */ - protected readonly $data!: Dictionary; - - /** - * The raw map of component fields that can force re-rendering - */ - protected readonly $fields!: Dictionary; - - /** - * @deprecated - * @see [[ComponentInterface.$fields]] - */ - protected readonly $$data!: Dictionary; - - /** - * The raw map of component fields that can't force re-rendering - */ - protected readonly $systemFields!: Dictionary; - - /** - * Map of fields and system fields that were modified and need to synchronize - * (only for functional components) - */ - protected readonly $modifiedFields!: Dictionary; - - /** - * Name of the active field to initialize - */ - protected readonly $activeField?: string; - - /** - * Cache for component links - */ - protected readonly $syncLinkCache!: SyncLinkCache; - - /** - * Link to a function that creates virtual nodes - */ - protected $createElement!: CreateElement; - - /** - * Promise of the component initializing - */ - protected $initializer?: Promise; - - /** - * Logs an event with the specified context - * - * @param ctxOrOpts - logging context or logging options (logLevel, context) - * @param [details] - event details - */ - log?(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void; - - /** - * Activates the component. - * The deactivated component won't load data from providers on initializing. - * - * Basically, you don't need to think about a component activation, - * because it's automatically synchronized with `keep-alive` or the special input property. - * - * @param [force] - if true, then the component will be forced to activate, even if it is already activated - */ - activate(force?: boolean): void {} - - /** - * Deactivates the component. - * The deactivated component won't load data from providers on initializing. - * - * Basically, you don't need to think about a component activation, - * because it's automatically synchronized with `keep-alive` or the special input property. - */ - deactivate(): void {} - - /** - * Forces the component to re-render - */ - $forceUpdate(): void {} - - /** - * Executes the specified function on the next render tick - * @param cb - */ - $nextTick(cb: Function | BoundFn): void; - - /** - * Returns a promise that will be resolved on the next render tick - */ - $nextTick(): Promise; - $nextTick(): CanPromise {} - - /** - * Mounts the component to a DOM element - * @param elementOrSelector - link to an element or selector to an element - */ - protected $mount(elementOrSelector?: Element | string): this { - return this; - } - - /** - * Destroys the component - */ - protected $destroy(): void {} - - /** - * Sets a new reactive value to the specified property of an object - * - * @param object - * @param key - * @param value - */ - protected $set(object: object, key: unknown, value: T): T { - return value; - } - - /** - * Deletes the specified reactive property from an object - * - * @param object - * @param key - */ - protected $delete(object: object, key: unknown): void {} - - /** - * Sets a watcher to a component/object property by the specified path - * - * @param path - * @param handler - * @param opts - */ - protected $watch( - path: WatchPath, - opts: WatchOptions, - handler: RawWatchHandler - ): Nullable; - - /** - * Sets a watcher to a component/object property by the specified path - * - * @param path - * @param handler - */ - protected $watch( - path: WatchPath, - handler: RawWatchHandler - ): Nullable; - - /** - * Sets a watcher to the specified watchable object - * - * @param obj - * @param handler - */ - protected $watch( - obj: object, - handler: RawWatchHandler - ): Nullable; - - /** - * Sets a watcher to the specified watchable object - * - * @param obj - * @param handler - * @param opts - */ - protected $watch( - obj: object, - opts: WatchOptions, - handler: RawWatchHandler - ): Nullable; - - protected $watch(): Nullable { - return null; - } - - /** - * Attaches an event listener to the specified component event - * - * @param event - * @param handler - */ - protected $on(event: CanArray, handler: ProxyCb): this { - return this; - } - - /** - * Attaches a single event listener to the specified component event - * - * @param event - * @param handler - */ - protected $once(event: string, handler: ProxyCb): this { - return this; - } - - /** - * Detaches an event listeners from the component - * - * @param [event] - * @param [handler] - */ - protected $off(event?: CanArray, handler?: Function): this { - return this; - } - - /** - * Emits a component event - * - * @param event - * @param args - */ - protected $emit(event: string, ...args: unknown[]): this { - return this; - } - - /** - * Hook handler: the component has been created - * (only for flyweight components) - */ - protected onCreatedHook(): void { - // Loopback - } - - /** - * Hook handler: the component has been bound - * (only for functional and flyweight components) - */ - protected onBindHook(): void { - // Loopback - } - - /** - * Hook handler: the component has been mounted - * (only for functional and flyweight components) - */ - protected onInsertedHook(): void { - // Loopback - } - - /** - * Hook handler: the component has been updated - * (only for functional and flyweight components) - */ - protected onUpdateHook(): void { - // Loopback - } - - /** - * Hook handler: the component has been unbound - * (only for functional and flyweight components) - */ - protected onUnbindHook(): void { - // Loopback - } -} - -/** - * A special interface to provide access to protected properties and methods outside the component. - * It's used to create the "friendly classes" feature. - */ -export interface UnsafeComponentInterface { - /** - * Type: context type - */ - readonly CTX: CTX; - - // Don't use referring from CTX for primitive types, because it breaks TS - - componentId: string; - - // @ts-ignore (access) - $componentId: CTX['$componentId']; - - componentName: string; - instance: this; - - // @ts-ignore (access) - meta: CTX['meta']; - hook: Hook; - - renderGroup: string; - renderCounter: number; - renderTmp: Dictionary; - - lastSelfReasonToRender?: Nullable; - lastTimeOfRender?: DOMHighResTimeStamp; - - $asyncLabel: symbol; - $activeField: CanUndef; - - // @ts-ignore (access) - $initializer: CTX['$initializer']; - - // @ts-ignore (access) - $renderEngine: CTX['$renderEngine']; - - // @ts-ignore (access) - $parent: CTX['$parent']; - - // @ts-ignore (access) - $remoteParent: CTX['$remoteParent']; - - // @ts-ignore (access) - $children: CTX['$children']; - - // @ts-ignore (access) - $async: CTX['$async']; - - // @ts-ignore (access) - $attrs: CTX['$attrs']; - - // @ts-ignore (access) - $listeners: CTX['$listeners']; - - // @ts-ignore (access) - $refs: CTX['$refs']; - - // @ts-ignore (access) - $refHandlers: CTX['$refHandlers']; - - // @ts-ignore (access) - $slots: CTX['$slots']; - - // @ts-ignore (access) - $scopedSlots: CTX['$scopedSlots']; - - // @ts-ignore (access) - $data: CTX['$data']; - - // @ts-ignore (access) - $fields: CTX['$fields']; - - // @ts-ignore (access) - $systemFields: CTX['$fields']; - - // @ts-ignore (access) - $modifiedFields: CTX['$modifiedFields']; - - // @ts-ignore (access) - $syncLinkCache: CTX['$syncLinkCache']; - - // @ts-ignore (access) - $createElement: CTX['$createElement']; - - // @ts-ignore (access) - $watch: CTX['$watch']; - - // @ts-ignore (access) - $on: CTX['$on']; - - // @ts-ignore (access) - $once: CTX['$once']; - - // @ts-ignore (access) - $off: CTX['$off']; - - // @ts-ignore (access) - $emit: CTX['$emit']; - - // @ts-ignore (access) - $set: CTX['$set']; - - // @ts-ignore (access) - $delete: CTX['$delete']; - - // @ts-ignore (access) - $forceUpdate: CTX['$forceUpdate']; - - // @ts-ignore (access) - $nextTick: CTX['$nextTick']; - - // @ts-ignore (access) - $destroy: CTX['$destroy']; - - // @ts-ignore (access) - log: CTX['log']; - - // @ts-ignore (access) - activate: CTX['activate']; - - // @ts-ignore (access) - deactivate: CTX['deactivate']; - - // @ts-ignore (access) - onCreatedHook: CTX['onCreatedHook']; - - // @ts-ignore (access) - onBindHook: CTX['onBindHook']; - - // @ts-ignore (access) - onInsertedHook: CTX['onInsertedHook']; - - // @ts-ignore (access) - onUpdateHook: CTX['onUpdateHook']; - - // @ts-ignore (access) - onUnbindHook: CTX['onUnbindHook']; - - // Internal render helpers - - // @ts-ignore (access) - _c: CTX['$createElement']; - - _o: Function; - _q: Function; - _s: Function; - _v: Function; - _e: Function; - _f: Function; - _n: Function; - _i: Function; - _m: Function; - _l: Function; - _g: Function; - _k: Function; - _b: Function; - _t: Function; - _u: Function; -} +export * from 'core/component/interface/component/types'; +export * from 'core/component/interface/component/component'; +export * from 'core/component/interface/component/unsafe'; diff --git a/src/core/component/interface/component/types.ts b/src/core/component/interface/component/types.ts new file mode 100644 index 0000000000..e0494e2ab5 --- /dev/null +++ b/src/core/component/interface/component/types.ts @@ -0,0 +1,33 @@ +/*! + * 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'; + +/** + * Constructor function of a component + */ +export interface ComponentConstructor { + new(): T; +} + +/** + * Root DOM element that is tied with a component + */ +export type ComponentElement = Element & { + component?: T; +}; + +/** + * Base context of a functional component + */ +export interface FunctionalCtx { + componentName: string; + meta: ComponentMeta; + instance: Dictionary; + $options: Dictionary; +} diff --git a/src/core/component/interface/component/unsafe.ts b/src/core/component/interface/component/unsafe.ts new file mode 100644 index 0000000000..76c824f01d --- /dev/null +++ b/src/core/component/interface/component/unsafe.ts @@ -0,0 +1,128 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentInterface } from 'core/component/interface/component/component'; + +/** + * A helper structure to pack the unsafe interface: + * it fixes some ambiguous TS warnings + */ +export type UnsafeGetter = + Dictionary & U['CTX'] & U & {unsafe: any}; + +/** + * A special interface to provide access to protected properties and methods outside the component. + * It's used to create the "friendly classes" feature. + */ +export interface UnsafeComponentInterface { + /** + * Type: context type + */ + readonly CTX: CTX; + + // @ts-ignore (access) + meta: CTX['meta']; + + // Don't use referring from CTX for primitive types, because it breaks TS + renderCounter: number; + + // @ts-ignore (access) + $componentId: CTX['$componentId']; + + // @ts-ignore (access) + $remoteParent: CTX['$remoteParent']; + + // @ts-ignore (access) + $fields: CTX['$fields']; + + // @ts-ignore (access) + $systemFields: CTX['$fields']; + + // @ts-ignore (access) + $modifiedFields: CTX['$modifiedFields']; + + // @ts-ignore (access) + $attrs: CTX['$attrs']; + + // @ts-ignore (access) + $listeners: CTX['$listeners']; + + // @ts-ignore (access) + $refs: CTX['$refs']; + + // @ts-ignore (access) + $refHandlers: CTX['$refHandlers']; + + // @ts-ignore (access) + $slots: CTX['$slots']; + + $activeField: CanUndef; + + // @ts-ignore (access) + $syncLinkCache: CTX['$syncLinkCache']; + + // @ts-ignore (access) + $async: CTX['$async']; + + // @ts-ignore (access) + $initializer: CTX['$initializer']; + + // @ts-ignore (access) + $watch: CTX['$watch']; + + // @ts-ignore (access) + $on: CTX['$on']; + + // @ts-ignore (access) + $once: CTX['$once']; + + // @ts-ignore (access) + $off: CTX['$off']; + + // @ts-ignore (access) + $emit: CTX['$emit']; + + // @ts-ignore (access) + $set: CTX['$set']; + + // @ts-ignore (access) + $delete: CTX['$delete']; + + // @ts-ignore (access) + $forceUpdate: CTX['$forceUpdate']; + + // @ts-ignore (access) + $nextTick: CTX['$nextTick']; + + // @ts-ignore (access) + $destroy: CTX['$destroy']; + + // @ts-ignore (access) + log: CTX['log']; + + // @ts-ignore (access) + activate: CTX['activate']; + + // @ts-ignore (access) + deactivate: CTX['deactivate']; + + // @ts-ignore (access) + onCreatedHook: CTX['onCreatedHook']; + + // @ts-ignore (access) + onBindHook: CTX['onBindHook']; + + // @ts-ignore (access) + onInsertedHook: CTX['onInsertedHook']; + + // @ts-ignore (access) + onUpdateHook: CTX['onUpdateHook']; + + // @ts-ignore (access) + onUnbindHook: CTX['onUnbindHook']; +} From 6e22a98bafcbfa5fc1e59cd3227a90a436d042a9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 14:01:24 +0300 Subject: [PATCH 0021/2313] fix: fixed TS error --- src/core/component/interface/component/component.ts | 10 +++++----- src/core/component/interface/component/unsafe.ts | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 707c85b88f..445f360060 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -136,6 +136,11 @@ export abstract class ComponentInterface { */ readonly $normalParent?: this['Component']; + /** + * Link to a parent component if the current component was dynamically created and mounted + */ + readonly $remoteParent?: this['Component']; + /** * API of the used rendering engine */ @@ -158,11 +163,6 @@ export abstract class ComponentInterface { */ protected readonly $componentId?: string; - /** - * Link to a parent component if the current component was dynamically created and mounted - */ - protected readonly $remoteParent?: this['Component']; - /** * Map of watchable component properties that can force re-rendering */ diff --git a/src/core/component/interface/component/unsafe.ts b/src/core/component/interface/component/unsafe.ts index 76c824f01d..28dddeb442 100644 --- a/src/core/component/interface/component/unsafe.ts +++ b/src/core/component/interface/component/unsafe.ts @@ -34,9 +34,6 @@ export interface UnsafeComponentInterface Date: Thu, 14 Apr 2022 14:06:06 +0300 Subject: [PATCH 0022/2313] feat: added `renderTracked` and `renderTriggered` --- src/core/component/interface/life-cycle.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/component/interface/life-cycle.ts b/src/core/component/interface/life-cycle.ts index b56a3ac63b..c292e7e225 100644 --- a/src/core/component/interface/life-cycle.ts +++ b/src/core/component/interface/life-cycle.ts @@ -6,6 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * List of supported life-cycle hooks of a component + */ export type Hook = 'beforeRuntime' | 'beforeCreate' | @@ -21,4 +24,6 @@ export type Hook = 'deactivated' | 'beforeDestroy' | 'destroyed' | + 'renderTracked' | + 'renderTriggered' | 'errorCaptured'; From 1eb3c1142ab4ed85c76b4f151fa86399274f5b9c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 14:22:34 +0300 Subject: [PATCH 0023/2313] refactor: refactoring and better doc --- src/core/component/interface/link.ts | 14 +++++++ src/core/component/interface/mod.ts | 39 +++++++++++++++-- src/core/component/interface/watch.ts | 60 ++++++++++++--------------- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/src/core/component/interface/link.ts b/src/core/component/interface/link.ts index b91824241e..94599e6e69 100644 --- a/src/core/component/interface/link.ts +++ b/src/core/component/interface/link.ts @@ -6,11 +6,25 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * Link structure + */ export interface SyncLink { + /** + * Link path + */ path: string; + + /** + * Synchronize the link value and with all tied objects + * @param [value] - new value to set + */ sync(value?: T): void; } +/** + * Map of registered links + */ export type SyncLinkCache = Map< string | object, Dictionary> diff --git a/src/core/component/interface/mod.ts b/src/core/component/interface/mod.ts index 673444ba6f..9ae026db9a 100644 --- a/src/core/component/interface/mod.ts +++ b/src/core/component/interface/mod.ts @@ -8,10 +8,43 @@ import type { PARENT } from 'core/component/const'; +/** + * Available "pure" types of a modifier value + */ export type ModVal = string | boolean | number; -export type StrictModDeclVal = CanArray; -export type ModDeclVal = StrictModDeclVal | typeof PARENT; +/** + * Available types of a modifier value. + * If a value wrapped with an array, it interprets as a value by default. + */ +export type ModDeclVal = CanArray; + +/** + * Expanded available types of a modifier value with support a feature of referring to a parent. + * If a value wrapped with an array, it interprets as a value by default. + */ +export type ExpandedModDeclVal = ModDeclVal | typeof PARENT; + +/** + * Dictionary of registered modifiers and their possible values + * + * @example + * ```typescript + * const mods: ModsDecl = { + * focused: [ + * true, + * + * // The default value + * [false] + * ], + * + * opened: [ + * true, + * false + * ] + * } + * ``` + */ export interface ModsDecl { - [name: string]: Nullable; + [name: string]: Nullable; } diff --git a/src/core/component/interface/watch.ts b/src/core/component/interface/watch.ts index 533947da66..3171ef1dc0 100644 --- a/src/core/component/interface/watch.ts +++ b/src/core/component/interface/watch.ts @@ -10,29 +10,10 @@ import type { WatchPath as RawWatchPath, WatchOptions, WatchHandlerParams } from import type { Group, Label, Join } from 'core/async'; import type { PropertyInfo } from 'core/component/reflection'; -import type { ComponentInterface } from 'core/component/interface'; +import type { ComponentInterface } from 'core/component/interface/component'; export { WatchOptions, WatchHandlerParams }; -export type WatchPath = - string | - PropertyInfo | - {ctx: object; path?: RawWatchPath}; - -export interface RawWatchHandler { - (a: A, b?: B, params?: WatchHandlerParams): AnyToIgnore; - (this: CTX, a: A, b?: B, params?: WatchHandlerParams): AnyToIgnore; -} - -export interface WatchHandler { - (a: A, b: B, params?: WatchHandlerParams): unknown; - (...args: A[]): unknown; -} - -export interface WatchWrapper { - (ctx: CTX['unsafe'], handler: WatchHandler): CanPromise | Function>; -} - export interface FieldWatcher< A = unknown, B = A @@ -49,7 +30,7 @@ export interface FieldWatcher< functional?: boolean; /** - * If false, then the handler that is invoked on watcher events does not take any arguments from an event + * If false, then the handler that is invoked on watcher events does not take any arguments from the event * @default `true` */ provideArgs?: boolean; @@ -62,19 +43,19 @@ export interface WatchObject< > extends WatchOptions { /** * Group name of a watcher - * (for Async) + * (provided to Async) */ group?: Group; /** * Label of a watcher - * (for Async) + * (provided to Async) */ label?: Label; /** * Join strategy of a watcher - * (for Async) + * (provided to Async) */ join?: Join; @@ -107,7 +88,7 @@ export interface WatchObject< args?: unknown[]; /** - * If false, then the handler that is invoked on watcher events does not take any arguments from an event + * If false, then the handler that is invoked on watcher events does not take any arguments from the event * @default `true` */ provideArgs?: boolean; @@ -143,12 +124,6 @@ export interface MethodWatcher< A = unknown, B = A > extends WatchOptions { - /** - * @deprecated - * @see [[MethodWatcher.path]] - */ - field?: string; - /** * Path to a component property to watch or event to listen */ @@ -156,7 +131,7 @@ export interface MethodWatcher< /** * Group name of the watcher - * (for Async) + * (provided to Async) */ group?: Group; @@ -184,7 +159,7 @@ export interface MethodWatcher< args?: CanArray; /** - * If false, then the handler that is invoked on watcher events does not take any arguments from an event + * If false, then the handler that is invoked on watcher events does not take any arguments from the event * @default `true` */ provideArgs?: boolean; @@ -209,3 +184,22 @@ export interface MethodWatcher< */ wrapper?: WatchWrapper; } + +export type WatchPath = + string | + PropertyInfo | + {ctx: object; path?: RawWatchPath}; + +export interface RawWatchHandler { + (a: A, b?: B, params?: WatchHandlerParams): AnyToIgnore; + (this: CTX, a: A, b?: B, params?: WatchHandlerParams): AnyToIgnore; +} + +export interface WatchHandler { + (a: A, b: B, params?: WatchHandlerParams): unknown; + (...args: A[]): unknown; +} + +export interface WatchWrapper { + (ctx: CTX['unsafe'], handler: WatchHandler): CanPromise | Function>; +} From 98431d0114208d25910f060d5e4b190b4ef32116 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 14:24:58 +0300 Subject: [PATCH 0024/2313] refactor: removed legacy --- src/core/component/interface/engine.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/interface/engine.ts b/src/core/component/interface/engine.ts index d78c1fda03..c1fa3d9511 100644 --- a/src/core/component/interface/engine.ts +++ b/src/core/component/interface/engine.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNode, RenderContext } from 'core/component/engines'; -import type { ComponentInterface } from 'core/component/interface/component'; +import type { VNode } from 'core/component/engines'; +import type { ComponentInterface } from 'core/component/interface/component/component'; export interface RenderEngineFeatures { regular: boolean; @@ -40,7 +40,7 @@ export interface RenderEngine { proxyGetters: ProxyGetters; cloneVNode(vnode: VNode): VNode; - patchVNode(vnode: VNode, component: ComponentInterface, renderCtx: RenderContext): VNode; + patchVNode(vnode: VNode, component: ComponentInterface): VNode; renderVNode(vnode: VNode, parent: ComponentInterface): Node; renderVNode(vnodes: VNode[], parent: ComponentInterface): Node[]; From e290190c6bd15e984203cca878b43348cd1e6aa2 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 16:38:27 +0300 Subject: [PATCH 0025/2313] refactor: migration to Vue@3 --- src/core/component/directives/hook/README.md | 12 ++++---- src/core/component/directives/hook/index.ts | 28 ++++++++++++------- .../component/directives/hook/interface.ts | 16 ++++++----- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/core/component/directives/hook/README.md b/src/core/component/directives/hook/README.md index e8a5991806..fcd56d1af6 100644 --- a/src/core/component/directives/hook/README.md +++ b/src/core/component/directives/hook/README.md @@ -8,10 +8,12 @@ attach the hook listeners. ``` < .&__class v-hook = { & - bind: (el, opts, vnode, oldVnode) => console.log(el, opts, vnode, oldVnode), - inserted: onInserted, - update: onUpdate, - componentUpdated: onComponentUpdated, - unbind: onUnbind + created: (el, opts, vnode, oldVnode) => console.log(el, opts, vnode, oldVnode), + beforeMount: onBeforeMount, + mounted: onMounted, + beforeUpdate: onBeforeUpdate, + updated: onUpdated, + beforeUnmount: onBeforeUnmount, + unmounted: onUnmounted } . ``` diff --git a/src/core/component/directives/hook/index.ts b/src/core/component/directives/hook/index.ts index 72f87618d1..53c14389e8 100644 --- a/src/core/component/directives/hook/index.ts +++ b/src/core/component/directives/hook/index.ts @@ -19,23 +19,31 @@ import type { DirectiveOptions } from 'core/component/directives/hook/interface' export * from 'core/component/directives/hook/interface'; ComponentEngine.directive('hook', { - bind(el: Element, opts: DirectiveOptions): void { - opts.value?.bind?.apply(opts.value, arguments); + created(el: Element, opts: DirectiveOptions): void { + opts.value?.created?.apply(opts.value, arguments); }, - inserted(el: Element, opts: DirectiveOptions): void { - opts.value?.inserted?.apply(opts.value, arguments); + beforeMount(el: Element, opts: DirectiveOptions): void { + opts.value?.beforeMount?.apply(opts.value, arguments); }, - update(el: Element, opts: DirectiveOptions): void { - opts.value?.update?.apply(opts.value, arguments); + mounted(el: Element, opts: DirectiveOptions): void { + opts.value?.mounted?.apply(opts.value, arguments); }, - componentUpdated(el: Element, opts: DirectiveOptions): void { - opts.value?.componentUpdated?.apply(opts.value, arguments); + beforeUpdate(el: Element, opts: DirectiveOptions): void { + opts.value?.beforeUpdate?.apply(opts.value, arguments); }, - unbind(el: Element, opts: DirectiveOptions): void { - opts.value?.unbind?.apply(opts.value, arguments); + updated(el: Element, opts: DirectiveOptions): void { + opts.value?.updated?.apply(opts.value, arguments); + }, + + beforeUnmount(el: Element, opts: DirectiveOptions): void { + opts.value?.beforeUnmount?.apply(opts.value, arguments); + }, + + unmounted(el: Element, opts: DirectiveOptions): void { + opts.value?.unmounted?.apply(opts.value, arguments); } }); diff --git a/src/core/component/directives/hook/interface.ts b/src/core/component/directives/hook/interface.ts index f29665a99d..3d777d2e85 100644 --- a/src/core/component/directives/hook/interface.ts +++ b/src/core/component/directives/hook/interface.ts @@ -6,14 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNodeDirective, DirectiveFunction } from 'core/component/engines'; +import type { ObjectDirective, DirectiveHook } from 'core/component/engines'; -export interface DirectiveOptions extends VNodeDirective { +export interface DirectiveOptions extends ObjectDirective { value?: { - bind?: DirectiveFunction; - inserted?: DirectiveFunction; - update?: DirectiveFunction; - componentUpdated?: DirectiveFunction; - unbind?: DirectiveFunction; + created?: DirectiveHook; + beforeMount?: DirectiveHook; + mounted?: DirectiveHook; + beforeUpdate?: DirectiveHook; + updated?: DirectiveHook; + beforeUnmount?: DirectiveHook; + unmounted?: DirectiveHook; }; } From 38b74f8685000b2f9744c0557c37b8010d48f09a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 16:38:57 +0300 Subject: [PATCH 0026/2313] refactor: used new v-hook API --- src/super/i-block/i-block.ss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 6ffc3ccceb..9f47eeb916 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -176,10 +176,10 @@ 'v-hook': "!isVirtualTpl && (isFunctional || isFlyweight) ?" + "{" + - "bind: createInternalHookListener('bind')," + - "inserted: createInternalHookListener('inserted')," + - "update: createInternalHookListener('update')," + - "unbind: createInternalHookListener('unbind')" + + "created: createInternalHookListener('bind')," + + "mounted: createInternalHookListener('inserted')," + + "updated: createInternalHookListener('update')," + + "unmounted: createInternalHookListener('unbind')" + "} :" + "null" From db03a77197eae32b8bdbc68d284545e730c323fb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 18:20:28 +0300 Subject: [PATCH 0027/2313] refactor: migration to Vue3 --- src/core/component/engines/directive.ts | 47 +++++++++++++------------ 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/core/component/engines/directive.ts b/src/core/component/engines/directive.ts index c17ea6eb09..ba3ca1685d 100644 --- a/src/core/component/engines/directive.ts +++ b/src/core/component/engines/directive.ts @@ -6,50 +6,51 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { +import { ComponentEngine, Directive, DirectiveBinding, VNode } from 'core/component/engines/engine'; - ComponentEngine, - DirectiveFunction, - DirectiveOptions, - VNode - -} from 'core/component/engines/engine'; - -const addDirective = ComponentEngine.directive.bind(ComponentEngine); +let originalDirective = ComponentEngine.directive.length > 0 ? + ComponentEngine.directive.bind(ComponentEngine) : + null; /** * A wrapped version of the `ComponentEngine.directive` function with providing of hooks for non-regular components * * @param name - * @param params + * @param [directive] */ -ComponentEngine.directive = function directive(name: string, params?: DirectiveOptions | DirectiveFunction) { - if (Object.isFunction(params)) { - return addDirective(name, params); +ComponentEngine.directive = function directive(name: string, directive?: Directive) { + if (originalDirective == null) { + const ctx = Object.getPrototypeOf(this); + originalDirective = ctx.directive.bind(ctx); + } + + if (directive == null || Object.isFunction(directive)) { + return originalDirective(name, directive); } const - originalBind = params?.bind, - originalUnbind = params?.unbind; + originalCreated = directive.created, + originalUnmounted = directive.unmounted; - if (originalUnbind == null) { - return addDirective(name, params); + if (originalUnmounted == null) { + return originalDirective(name, directive); } - return addDirective(name, { - ...params, + return originalDirective(name, { + ...directive, - bind(_el: HTMLElement, _opts: DirectiveOptions, vnode: VNode) { + created(_el: Element, _opts: DirectiveBinding, vnode: VNode) { const + // eslint-disable-next-line prefer-rest-params args = Array.from(arguments); - if (Object.isFunction(originalBind)) { - originalBind.apply(this, args); + if (Object.isFunction(originalCreated)) { + originalCreated.apply(this, args); } if (vnode.fakeContext != null) { vnode.fakeContext.unsafe.$on('component-hook:before-destroy', () => { - originalUnbind.apply(this, args); + originalUnmounted.apply(this, args); }); } } From 24d4ab0b2f312f6f4bb39d12c9aa749331bd9b4a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 14 Apr 2022 18:21:03 +0300 Subject: [PATCH 0028/2313] fix: should mount the root component at the next tick --- src/core/component/engines/vue3/lib.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index d898e6207e..b7f81b8ce1 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -10,8 +10,13 @@ import makeLazy from 'core/lazy'; import { createApp, Component } from 'vue'; const App = function App(component: Component & {el: Element}, rootProps: Nullable) { - const app = createApp(component, rootProps); - app.mount(component.el); + const + app = Object.create(createApp(component, rootProps)); + + setImmediate(() => { + app.mount(component.el); + }); + return app; }; From 1006d4237737bed169d1036910656dc010b5cbee Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 12:34:52 +0300 Subject: [PATCH 0029/2313] feat: expose new render api --- src/core/component/engines/vue3/render.ts | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/core/component/engines/vue3/render.ts diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts new file mode 100644 index 0000000000..8bd034da8a --- /dev/null +++ b/src/core/component/engines/vue3/render.ts @@ -0,0 +1,39 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export { + + Fragment, + Transition, + TransitionGroup, + + openBlock, + toDisplayString, + + renderList, + renderSlot, + + createBlock, + createElementBlock, + + createVNode, + createStaticVNode, + createElementVNode, + createTextVNode, + createCommentVNode, + + normalizeClass, + normalizeStyle, + + resolveComponent, + resolveDirective, + + withCtx, + withDirectives + +} from 'vue'; From 797076eb0b006472b07369ba0f500cb2323d951f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:05:38 +0300 Subject: [PATCH 0030/2313] refactor: using simple statical sorting --- src/core/component/field/const.ts | 9 +- src/core/component/field/helpers.ts | 77 +++++++ src/core/component/field/index.ts | 318 +++----------------------- src/core/component/field/interface.ts | 11 + 4 files changed, 124 insertions(+), 291 deletions(-) create mode 100644 src/core/component/field/helpers.ts create mode 100644 src/core/component/field/interface.ts diff --git a/src/core/component/field/const.ts b/src/core/component/field/const.ts index 6707e098df..e3320a410f 100644 --- a/src/core/component/field/const.ts +++ b/src/core/component/field/const.ts @@ -6,5 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export const - fieldQueue = new Set(); +import type { ComponentField } from 'core/component/interface'; +import type { SortedFields } from 'core/component/field/interface'; + +/** + * Cache for sorted component fields + */ +export const sortedFields = new WeakMap, SortedFields>(); diff --git a/src/core/component/field/helpers.ts b/src/core/component/field/helpers.ts new file mode 100644 index 0000000000..db83c2bf26 --- /dev/null +++ b/src/core/component/field/helpers.ts @@ -0,0 +1,77 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * 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'; + +/** + * Returns a weight of the specified field relative to other fields in a scope. + * The weight describes when a field should initialize relative to other fields. + * At start are init all fields with a zero weight. + * After will be init fields with the minimal non-zero weight, etc. + * + * @param field - field to calculate the weight + * @param fields - field scope + */ +export function getFieldWeight(field: CanUndef, fields: Dictionary): number { + if (field == null) { + return 0; + } + + const + {after} = field; + + let + weight = 0; + + if (after != null) { + weight += after.size; + + for (let o = after.values(), el = o.next(); !el.done; el = o.next()) { + const + dep = fields[el.value]; + + if (dep == null) { + throw new ReferenceError(`The specified dependency "${dep}" is not found in a scope`); + } + + weight += getFieldWeight(dep, fields); + } + } + + if (!field.atom) { + weight += 1e3; + } + + return weight; +} + +/** + * Sorts the specified fields and returns an array with ordering is ready to initialize + * @param fields + */ +export function sortFields(fields: Dictionary): SortedFields { + let + val = sortedFields.get(fields); + + if (val == null) { + val = Object.entries(Object.cast>(fields)).sort(([_1, a], [_2, b]) => { + const + aWeight = getFieldWeight(a, fields), + bWeight = getFieldWeight(b, fields); + + return aWeight - bWeight; + }); + + sortedFields.set(fields, val); + } + + return val; +} diff --git a/src/core/component/field/index.ts b/src/core/component/field/index.ts index cc77b85089..d003f671d1 100644 --- a/src/core/component/field/index.ts +++ b/src/core/component/field/index.ts @@ -11,338 +11,78 @@ * @packageDocumentation */ -import { defProp } from 'core/const/props'; -import { fieldQueue } from 'core/component/field/const'; - +import { sortFields } from 'core/component/field/helpers'; import type { ComponentInterface, ComponentField } from 'core/component/interface'; -export * from 'core/component/field/const'; +export * from 'core/component/field/interface'; /** * Initializes the specified fields to a component instance. - * The function returns an object with initialized fields. - * - * This method has some "copy-paste" chunks, but it's done for better performance because it's a very "hot" function. - * Mind that the initialization of fields is a synchronous operation. + * The function returns a dictionary with the initialized fields. * - * @param fields - component fields or system fields + * @param fields - fields scope to initialize * @param component - component instance - * @param [store] - storage object for initialized fields + * @param [store] - store for initialized fields */ -// eslint-disable-next-line complexity export function initFields( fields: Dictionary, component: ComponentInterface, store: Dictionary = {} ): Dictionary { - const - unsafe = Object.cast>(component.unsafe); + const unsafe = Object.cast>( + component + ); const - {isFlyweight} = component, - {componentName, params, instance} = unsafe.meta; + {params, instance} = unsafe.meta; const ssrMode = component.$renderEngine.supports.ssr, - isNotRegular = params.functional === true || isFlyweight; - - const - // A map of fields that we should skip, i.e., not to initialize. - // For instance, some properties don't initialize if a component is a functional. - canSkip = Object.createDict(), - - // List of atoms to initialize - atomList = >>[], - - // List of non-atoms to initialize - nonAtomList = >>[]; - - const - NULL = {}; - - const defField = { - ...defProp, - value: NULL - }; + isFunctional = params.functional === true; - // At first, we should initialize all atoms, but some atoms wait for other atoms. - // That's why we sort the source list of fields and organize a simple synchronous queue. - // All atoms that wait for other atoms are added to `atomList`. - // All non-atoms are added to `nonAtomList`. - for (let keys = Object.keys(fields).sort(), i = 0; i < keys.length; i++) { + for (let sortedFields = sortFields(fields), i = 0; i < sortedFields.length; i++) { const - key = keys[i], - field = fields[key]; + [key, field] = sortedFields[i]; - let - sourceVal = store[key], - isNull = false; - - if (isFlyweight) { - if ( - field != null && ( - field.replace !== true && (Object.isTruly(field.unique) || field.src === componentName) || - field.replace === false - ) - ) { - Object.defineProperty(store, key, defField); - sourceVal = undefined; - isNull = true; - } - } + const + sourceVal = store[key]; const dontNeedInit = field == null || sourceVal !== undefined || // Don't initialize a property for the functional component unless explicitly required - !ssrMode && isNotRegular && field.functional === false || + !ssrMode && isFunctional && field.functional === false || field.init == null && field.default === undefined && instance[key] === undefined; if (field == null || dontNeedInit) { - canSkip[key] = true; store[key] = sourceVal; continue; } - if (field.atom) { - // If true, then the field does not have any dependencies and can be initialized right now - let canInit = true; - - const - {after} = field; - - if (after && after.size > 0) { - for (let o = after.values(), val = o.next(); !val.done; val = o.next()) { - const - waitKey = val.value; - - if (canSkip[waitKey] === true) { - continue; - } - - // Check the dependency is not already initialized - if (!(waitKey in store)) { - atomList.push(key); - canInit = false; - break; - } - } - } - - if (canInit) { - if (isNull) { - store[key] = undefined; - } - - unsafe.$activeField = key; - - let - val; - - if (field.init != null) { - val = field.init(component.unsafe, store); - } - - if (val === undefined) { - if (store[key] === undefined) { - // We need to clone the default value from a constructor - // to prevent linking to the same object for a non-primitive value - val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); - store[key] = val; - } - - } else { - store[key] = val; - } - - unsafe.$activeField = undefined; - } - - } else { - nonAtomList.push(key); - } - } - - // Initialize all atoms that have some dependencies - while (atomList.length > 0) { - for (let i = 0; i < atomList.length; i++) { - const - key = nonAtomList[i], - field = key != null ? fields[key] : null; - - let - isNull = false; - - const canSkip = - field == null || - key == null || - key in store && !(isNull = store[key] === NULL); - - if (canSkip) { - continue; - } - - // If true, then the field does not have any dependencies and can be initialized right now - let canInit = true; + unsafe.$activeField = key; - const - {after} = field; - - if (after && after.size > 0) { - for (let o = after.values(), val = o.next(); !val.done; val = o.next()) { - const - waitKey = val.value, - waitFor = fields[waitKey]; - - if (canSkip[waitKey] === true) { - continue; - } - - if (!waitFor) { - throw new ReferenceError(`The field "${waitKey}" is not defined`); - } - - if (!waitFor.atom) { - throw new Error(`The atom field "${key}" can't wait the non atom field "${waitKey}"`); - } - - if (!(waitKey in store)) { - fieldQueue.add(key); - canInit = false; - break; - } - } - - if (canInit) { - atomList[i] = null; - } - } - - if (canInit) { - if (isNull) { - store[key] = undefined; - } - - unsafe.$activeField = key; - fieldQueue.delete(key); - - let - val; - - if (field.init != null) { - val = field.init(component.unsafe, store); - } - - if (val === undefined) { - if (store[key] === undefined) { - // We need to clone the default value from a constructor - // to prevent linking to the same object for a non-primitive value - val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); - store[key] = val; - } - - } else { - store[key] = val; - } - - unsafe.$activeField = undefined; - } - } + let + val; - // All atoms are initialized - if (fieldQueue.size === 0) { - break; + if (field.init != null) { + val = field.init(component.unsafe, store); } - } - - // Initialize all non-atoms - while (nonAtomList.length > 0) { - for (let i = 0; i < nonAtomList.length; i++) { - const - key = nonAtomList[i], - field = key != null ? fields[key] : null; - let - isNull = false; - - const canSkip = - field == null || - key == null || - key in store && !(isNull = store[key] === NULL); - - if (canSkip) { - continue; - } - - // If true, then the field does not have any dependencies and can be initialized right now - let canInit = true; - - const - {after} = field; - - if (after && after.size > 0) { - for (let o = after.values(), val = o.next(); !val.done; val = o.next()) { - const - waitKey = val.value, - waitFor = fields[waitKey]; - - if (canSkip[waitKey] === true) { - continue; - } - - if (!waitFor) { - throw new ReferenceError(`The field "${waitKey}" is not defined`); - } - - if (!(waitKey in store)) { - fieldQueue.add(key); - canInit = false; - break; - } - } - - if (canInit) { - nonAtomList[i] = null; - } + if (val === undefined) { + if (store[key] === undefined) { + // We need to clone the default value from a constructor + // to prevent linking to the same type component for a non-primitive value + val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); + store[key] = val; } - if (canInit) { - if (isNull) { - store[key] = undefined; - } - - unsafe.$activeField = key; - fieldQueue.delete(key); - - let - val; - - if (field.init != null) { - val = field.init(component.unsafe, store); - } - - if (val === undefined) { - if (store[key] === undefined) { - // We need to clone the default value from a constructor - // to prevent linking to the same object for a non-primitive value - val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); - store[key] = val; - } - - } else { - store[key] = val; - } - - unsafe.$activeField = undefined; - } + } else { + store[key] = val; } - // All fields are initialized - if (fieldQueue.size === 0) { - break; - } + unsafe.$activeField = undefined; } return store; diff --git a/src/core/component/field/interface.ts b/src/core/component/field/interface.ts new file mode 100644 index 0000000000..d8b501cb03 --- /dev/null +++ b/src/core/component/field/interface.ts @@ -0,0 +1,11 @@ +/*! + * 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'; + +export type SortedFields = Array<[string, CanUndef]>; From be814d28579a3805f92ca6bd847ee04cdb547528 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:11:01 +0300 Subject: [PATCH 0031/2313] :art: --- src/core/component/field/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/field/index.ts b/src/core/component/field/index.ts index d003f671d1..c1b295a834 100644 --- a/src/core/component/field/index.ts +++ b/src/core/component/field/index.ts @@ -17,7 +17,7 @@ import type { ComponentInterface, ComponentField } from 'core/component/interfac export * from 'core/component/field/interface'; /** - * Initializes the specified fields to a component instance. + * Initializes the specified fields of a component instance. * The function returns a dictionary with the initialized fields. * * @param fields - fields scope to initialize From 2acb8432c52ecfab2f799c7ad2559b9362e583dc Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:21:11 +0300 Subject: [PATCH 0032/2313] chore: updated logs --- src/core/component/field/CHANGELOG.md | 6 ++++++ src/core/component/prop/CHANGELOG.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/core/component/field/CHANGELOG.md b/src/core/component/field/CHANGELOG.md index c4d6fb5445..a62bb0c9e6 100644 --- a/src/core/component/field/CHANGELOG.md +++ b/src/core/component/field/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* The module is rewritten to use static sorting of dependencies + ## v3.0.0-rc.126 (2021-01-26) #### :bug: Bug Fix diff --git a/src/core/component/prop/CHANGELOG.md b/src/core/component/prop/CHANGELOG.md index a987ef3148..9fc470a2a0 100644 --- a/src/core/component/prop/CHANGELOG.md +++ b/src/core/component/prop/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.0.0-rc.126 (2021-01-26) #### :house: Internal From e86fae169ec74feeb309193c779f5cee67a23f53 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:21:58 +0300 Subject: [PATCH 0033/2313] refactor: removed `flyweight` support; refactoring --- src/core/component/prop/index.ts | 59 +++++++++------------------- src/core/component/prop/interface.ts | 6 +-- 2 files changed, 22 insertions(+), 43 deletions(-) diff --git a/src/core/component/prop/index.ts b/src/core/component/prop/index.ts index 40b932195a..54246448aa 100644 --- a/src/core/component/prop/index.ts +++ b/src/core/component/prop/index.ts @@ -11,7 +11,6 @@ * @packageDocumentation */ -import { defProp } from 'core/const/props'; import { defaultWrapper } from 'core/component/const'; import type { ComponentInterface } from 'core/component/interface'; @@ -20,8 +19,8 @@ import type { InitPropsObjectOptions } from 'core/component/prop/interface'; export * from 'core/component/prop/interface'; /** - * Initializes input properties of the specified component instance. - * The method returns an object with initialized properties. + * Initializes the specified input properties of a component instance. + * The method returns a dictionary with the initialized properties. * * @param component * @param [opts] - additional options @@ -32,38 +31,32 @@ export function initProps( ): Dictionary { opts.store = opts.store ?? {}; + const unsafe = Object.cast>( + component + ); + const { - unsafe, - unsafe: {meta, meta: {component: {props}}}, - isFlyweight - } = component; + meta, + meta: {component: {props}} + } = unsafe; const {store, from} = opts; const ssrMode = component.$renderEngine.supports.ssr, - isNotRegular = meta.params.functional === true || component.isFlyweight; + isFunctional = meta.params.functional === true; for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { const key = keys[i], el = props[key]; - let - needSave = Boolean(isFlyweight) || opts.saveToStore; - - if (el == null) { - continue; - } - - // Don't initialize a property for a functional component unless explicitly required - if (!ssrMode && isNotRegular && el.functional === false) { + if (el == null || !ssrMode && isFunctional && el.functional === false) { continue; } - // @ts-ignore (access) - unsafe['$activeField'] = key; + unsafe.$activeField = key; let val = (from ?? component)[key]; @@ -81,36 +74,22 @@ export function initProps( } } + let + needSaveToStore = opts.saveToStore; + if (Object.isFunction(val)) { if (opts.saveToStore || val[defaultWrapper] !== true) { val = isTypeCanBeFunc(el.type) ? val.bind(component) : val.call(component); - needSave = true; + needSaveToStore = true; } } - if (needSave) { - if (isFlyweight) { - const prop = val === undefined ? - defProp : - - { - configurable: true, - enumerable: true, - writable: true, - value: val - }; - - Object.defineProperty(store, key, prop); - component.$props[key] = val; - - } else { - store[key] = val; - } + if (needSaveToStore) { + store[key] = val; } } - // @ts-ignore (access) - unsafe['$activeField'] = undefined; + unsafe.$activeField = undefined; return store; } diff --git a/src/core/component/prop/interface.ts b/src/core/component/prop/interface.ts index 357393d147..3254a8e979 100644 --- a/src/core/component/prop/interface.ts +++ b/src/core/component/prop/interface.ts @@ -11,18 +11,18 @@ */ export interface InitPropsObjectOptions { /** - * Object where is stored raw modifiers + * Dictionary where is stored the raw modifiers */ from?: Dictionary; /** - * Storage object for initialized properties + * Store for initialized properties * @default `{}` */ store?: Dictionary; /** - * If true, then property values is written to a store object + * If true, then property values will be written to the store object * @default `false` */ saveToStore?: boolean; From dd7eece6b090b4a13f689f2c4483b89eee0d6e5e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:40:33 +0300 Subject: [PATCH 0034/2313] refactor: removed `flyweight` support --- src/core/component/method/index.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/core/component/method/index.ts b/src/core/component/method/index.ts index 8cea0a083d..de66af65b7 100644 --- a/src/core/component/method/index.ts +++ b/src/core/component/method/index.ts @@ -6,13 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentInterface } from 'core/component'; - /** * [[include:core/component/method/README.md]] * @packageDocumentation */ +import type { ComponentInterface } from 'core/component/interface'; + /** * Invokes a method from the specified component instance * @@ -45,26 +45,20 @@ export function callMethodFromComponent(component: ComponentInterface, method: s */ export function attachMethodsFromMeta(component: ComponentInterface): void { const { - unsafe: { - meta, - meta: {methods} - } - } = component; + meta, + meta: {methods} + } = component.unsafe; const ssrMode = component.$renderEngine.supports.ssr, - isNotRegular = meta.params.functional === true || component.isFlyweight; + isFunctional = meta.params.functional === true; for (let keys = Object.keys(methods), i = 0; i < keys.length; i++) { const key = keys[i], el = methods[key]; - if (!el) { - continue; - } - - if (!ssrMode && isNotRegular && el.functional === false) { + if (el == null || !ssrMode && isFunctional && el.functional === false) { continue; } From c71f0e3e464d20352d676fa9994c40135203c004 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:41:38 +0300 Subject: [PATCH 0035/2313] chore: updated logs --- src/core/component/method/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/component/method/CHANGELOG.md b/src/core/component/method/CHANGELOG.md index 76418ffae2..b2b53e62cc 100644 --- a/src/core/component/method/CHANGELOG.md +++ b/src/core/component/method/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.0.0-rc.37 (2020-07-20) #### :house: Internal From cd920c339b517aeeb90e580cca28313e295587f3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 15:56:19 +0300 Subject: [PATCH 0036/2313] :art: --- src/core/component/method/index.ts | 6 +++--- src/core/component/prop/index.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/component/method/index.ts b/src/core/component/method/index.ts index de66af65b7..16812684dc 100644 --- a/src/core/component/method/index.ts +++ b/src/core/component/method/index.ts @@ -56,12 +56,12 @@ export function attachMethodsFromMeta(component: ComponentInterface): void { for (let keys = Object.keys(methods), i = 0; i < keys.length; i++) { const key = keys[i], - el = methods[key]; + method = methods[key]; - if (el == null || !ssrMode && isFunctional && el.functional === false) { + if (method == null || !ssrMode && isFunctional && method.functional === false) { continue; } - component[key] = el.fn.bind(component); + component[key] = method.fn.bind(component); } } diff --git a/src/core/component/prop/index.ts b/src/core/component/prop/index.ts index 54246448aa..0b563e5fa6 100644 --- a/src/core/component/prop/index.ts +++ b/src/core/component/prop/index.ts @@ -50,9 +50,9 @@ export function initProps( for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { const key = keys[i], - el = props[key]; + prop = props[key]; - if (el == null || !ssrMode && isFunctional && el.functional === false) { + if (prop == null || !ssrMode && isFunctional && prop.functional === false) { continue; } @@ -62,7 +62,7 @@ export function initProps( val = (from ?? component)[key]; if (val === undefined) { - val = el.default !== undefined ? el.default : Object.fastClone(meta.instance[key]); + val = prop.default !== undefined ? prop.default : Object.fastClone(meta.instance[key]); } if (val === undefined) { @@ -79,7 +79,7 @@ export function initProps( if (Object.isFunction(val)) { if (opts.saveToStore || val[defaultWrapper] !== true) { - val = isTypeCanBeFunc(el.type) ? val.bind(component) : val.call(component); + val = isTypeCanBeFunc(prop.type) ? val.bind(component) : val.call(component); needSaveToStore = true; } } From 6d2d3af8fcda259bf9717298df12feb81e937aa1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:02:33 +0300 Subject: [PATCH 0037/2313] refactor: removed `flyweight` support --- src/core/component/accessor/index.ts | 59 +++++++--------------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/src/core/component/accessor/index.ts b/src/core/component/accessor/index.ts index 5e3bba1b5c..080ec7ba35 100644 --- a/src/core/component/accessor/index.ts +++ b/src/core/component/accessor/index.ts @@ -21,92 +21,61 @@ import { cacheStatus, ComponentInterface } from 'core/component'; export function attachAccessorsFromMeta(component: ComponentInterface): void { const { meta, - meta: {params: {deprecatedProps}}, - isFlyweight + meta: {params: {deprecatedProps}} } = component.unsafe; const ssrMode = component.$renderEngine.supports.ssr, - isNotRegular = meta.params.functional === true || isFlyweight; + isFunctional = meta.params.functional === true; for (let o = meta.accessors, keys = Object.keys(o), i = 0; i < keys.length; i++) { const key = keys[i], - el = o[key]; + accessor = o[key]; - const canSkip = Boolean( - el == null || - !ssrMode && isNotRegular && el.functional === false || - - ( - isFlyweight ? - Object.getOwnPropertyDescriptor(component, key) && el.replace === true : - component[key] - ) - ); - - if (el == null || canSkip) { + if (accessor == null || component[key] != null || !ssrMode && isFunctional && accessor.functional === false) { continue; } Object.defineProperty(component, keys[i], { configurable: true, enumerable: true, - - // eslint-disable-next-line @typescript-eslint/unbound-method - get: el.get, - - // eslint-disable-next-line @typescript-eslint/unbound-method - set: el.set + get: accessor.get, + set: accessor.set }); } for (let o = meta.computedFields, keys = Object.keys(o), i = 0; i < keys.length; i++) { const key = keys[i], - el = o[key]; + computed = o[key]; - const canSkip = Boolean( - el == null || - !ssrMode && isNotRegular && el.functional === false || - - ( - isFlyweight ? - Object.getOwnPropertyDescriptor(component, key) && el.replace === true : - component[key] - ) - ); - - if (el == null || canSkip) { + if (computed == null || component[key] != null || !ssrMode && isFunctional && computed.functional === false) { continue; } // eslint-disable-next-line func-style const get = function get(this: typeof component): unknown { - if (ssrMode || isFlyweight) { - return el.get!.call(this); + if (ssrMode) { + return computed.get!.call(this); } if (cacheStatus in get) { return get[cacheStatus]; } - return get[cacheStatus] = el.get!.call(this); + return get[cacheStatus] = computed.get!.call(this); }; Object.defineProperty(component, keys[i], { configurable: true, enumerable: true, - - // eslint-disable-next-line @typescript-eslint/unbound-method - get: el.get != null ? get : undefined, - - // eslint-disable-next-line @typescript-eslint/unbound-method - set: el.set + get: computed.get != null ? get : undefined, + set: computed.set }); } - if (deprecatedProps) { + if (deprecatedProps != null) { for (let keys = Object.keys(deprecatedProps), i = 0; i < keys.length; i++) { const key = keys[i], From dac63df49da77ae51bfed4c5d43c6011b88a293b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:02:45 +0300 Subject: [PATCH 0038/2313] chore: updated logs --- src/core/component/accessor/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/component/accessor/CHANGELOG.md b/src/core/component/accessor/CHANGELOG.md index a79ad404ff..2bc941024a 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] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.11.4 (2021-11-24) #### :bug: Bug Fix From bb86c030be66f583c9da07c3d79d5b306e8c1df5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:03:46 +0300 Subject: [PATCH 0039/2313] :art: --- src/core/component/accessor/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/component/accessor/index.ts b/src/core/component/accessor/index.ts index 080ec7ba35..8434f49262 100644 --- a/src/core/component/accessor/index.ts +++ b/src/core/component/accessor/index.ts @@ -12,7 +12,9 @@ */ import { deprecate } from 'core/functools/deprecation'; -import { cacheStatus, ComponentInterface } from 'core/component'; +import { cacheStatus } from 'core/component/watch'; + +import type { ComponentInterface } from 'core/component/interface'; /** * Attaches accessors and computed fields from a meta object to the specified component instance From e22b24f30a7f46c02ceca8cba27629992a794d35 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:12:59 +0300 Subject: [PATCH 0040/2313] chore: fixed grammar & refactoring --- src/core/component/queue-emitter/README.md | 5 +++-- src/core/component/queue-emitter/index.ts | 25 ++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/core/component/queue-emitter/README.md b/src/core/component/queue-emitter/README.md index 7bbcf02132..62fe421056 100644 --- a/src/core/component/queue-emitter/README.md +++ b/src/core/component/queue-emitter/README.md @@ -8,12 +8,13 @@ import QueueEmitter from 'core/component/queue-emitter'; const eventEmitter = new QueueEmitter(); -// These listeners is invoked only when all specified events was emitted +// These listeners will be invoked only when all specified events are fired eventEmitter.on(new Set(['foo', 'bar']), () => { console.log('Crash!'); }); -// This listener does not have any events to listen, and it will be invoked after calling the .drain method +// This listener does not have any events to listen. +// It will be invoked after calling the `drain` method. eventEmitter.on(undefined, () => { console.log('Boom!'); }); diff --git a/src/core/component/queue-emitter/index.ts b/src/core/component/queue-emitter/index.ts index dceeac48e9..8d851d0ec1 100644 --- a/src/core/component/queue-emitter/index.ts +++ b/src/core/component/queue-emitter/index.ts @@ -20,28 +20,31 @@ export * from 'core/component/queue-emitter/interface'; */ export default class QueueEmitter { /** - * Queue of event listeners that is ready to fire + * Queue of event listeners that are ready to fire */ protected queue: Function[] = []; /** - * Map of tied event listeners that isn't ready to fire + * Map of tied event listeners that aren't ready to fire */ protected listeners: Dictionary = Object.createDict(); /** * Attaches a callback for the specified set of events. - * The callback will be invoked only when all specified events was emitted. + * The callback will be invoked only when all specified events are fired. * * @param event - set of events (can be undefined) * @param cb */ on(event: Nullable>, cb: Function): void { if (event != null && event.size > 0) { - for (let v = event.values(), el = v.next(); !el.done; el = v.next()) { - const key = el.value; - this.listeners[key] = this.listeners[key] ?? []; - this.listeners[key]!.push({event, cb}); + for (let o = event.values(), el = o.next(); !el.done; el = o.next()) { + const + key = el.value, + listeners = this.listeners[key] ?? []; + + listeners.push({event, cb}); + this.listeners[key] = listeners; } return; @@ -53,7 +56,7 @@ export default class QueueEmitter { /** * Emits the specified event. * If at least one of listeners returns a promise, - * the method returns promise that is resolved after all internal promises are resolved. + * the method returns promise that will be resolved after all internal promises are resolved. * * @param event */ @@ -61,7 +64,7 @@ export default class QueueEmitter { const queue = this.listeners[event]; - if (!queue) { + if (queue == null) { return; } @@ -94,9 +97,9 @@ export default class QueueEmitter { } /** - * Drains the queue of listeners that is ready to fire. + * Drains the queue of listeners that are ready to fire. * If at least one of listeners returns a promise, - * the method returns promise that is resolved after all internal promises are resolved. + * the method returns promise that will be resolved after all internal promises are resolved. */ drain(): CanPromise { const From 2fc2bdf4e59f84beb630406a7f1acf3800d1041f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:20:46 +0300 Subject: [PATCH 0041/2313] refactor: removed `flyweight` support --- src/core/component/hook/index.ts | 34 ++++++++------------------------ 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/core/component/hook/index.ts b/src/core/component/hook/index.ts index 28c2fc5676..4f6a2f8ba5 100644 --- a/src/core/component/hook/index.ts +++ b/src/core/component/hook/index.ts @@ -27,33 +27,15 @@ const * @param args - hook arguments */ export function runHook(hook: Hook, component: ComponentInterface, ...args: unknown[]): Promise { - const {unsafe, unsafe: {meta}} = component; - unsafe.hook = hook; - - let - hooks = meta.hooks[hook]; - - if (component.isFlyweight && hooks.length > 0) { - if (hooks.length === 1 && hooks[0].functional === false) { - return resolvedPromise; - } - - const - functionalHooks = []; - - for (let i = 0; i < hooks.length; i++) { - const - el = hooks[i]; + const unsafe = Object.cast>( + component + ); - if (el.functional !== false) { - functionalHooks.push(el); - } - } + unsafe.hook = hook; - if (functionalHooks.length !== hooks.length) { - hooks = functionalHooks; - } - } + const + m = component.unsafe.meta, + hooks = m.hooks[hook]; switch (hooks.length) { case 0: @@ -106,7 +88,7 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn }); } - meta.hooks[hook] = filteredHooks; + m.hooks[hook] = filteredHooks; const tasks = emitter.drain(); From edaf1ea0e9f1d0b2b98c1acce194fb3c431937ec Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:21:16 +0300 Subject: [PATCH 0042/2313] chore: updated logs --- src/core/component/hook/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/component/hook/CHANGELOG.md b/src/core/component/hook/CHANGELOG.md index 2c3d8a530d..9c0ab065e4 100644 --- a/src/core/component/hook/CHANGELOG.md +++ b/src/core/component/hook/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.0.0-rc.205 (2021-06-24) #### :bug: Bug Fix From 2731d88d806d2efe011d73cb369e998a494374ae Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 16:22:08 +0300 Subject: [PATCH 0043/2313] refactor: removed flyweight components; removed filters --- src/core/component/filters/CHANGELOG.md | 16 -- src/core/component/filters/README.md | 3 - src/core/component/filters/index.ts | 25 -- src/core/component/flyweight/CHANGELOG.md | 83 ------- src/core/component/flyweight/README.md | 6 - src/core/component/flyweight/index.ts | 221 ------------------ .../interface/component/component.ts | 21 +- 7 files changed, 4 insertions(+), 371 deletions(-) delete mode 100644 src/core/component/filters/CHANGELOG.md delete mode 100644 src/core/component/filters/README.md delete mode 100644 src/core/component/filters/index.ts delete mode 100644 src/core/component/flyweight/CHANGELOG.md delete mode 100644 src/core/component/flyweight/README.md delete mode 100644 src/core/component/flyweight/index.ts diff --git a/src/core/component/filters/CHANGELOG.md b/src/core/component/filters/CHANGELOG.md deleted file mode 100644 index 76418ffae2..0000000000 --- a/src/core/component/filters/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.0.0-rc.37 (2020-07-20) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/core/component/filters/README.md b/src/core/component/filters/README.md deleted file mode 100644 index 2beb603580..0000000000 --- a/src/core/component/filters/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core/component/filters - -This module registers a bunch of functions as filters to a component library. diff --git a/src/core/component/filters/index.ts b/src/core/component/filters/index.ts deleted file mode 100644 index bd3c34b1d8..0000000000 --- a/src/core/component/filters/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/filters/README.md]] - * @packageDocumentation - */ - -import * as strings from 'core/helpers/string'; -import { ComponentEngine } from 'core/component/engines'; - -for (let keys = Object.keys(strings), i = 0; i < keys.length; i++) { - const - key = keys[i], - val = strings[key]; - - if (Object.isFunction(val)) { - ComponentEngine.filter(key, val); - } -} diff --git a/src/core/component/flyweight/CHANGELOG.md b/src/core/component/flyweight/CHANGELOG.md deleted file mode 100644 index 5cf72e2353..0000000000 --- a/src/core/component/flyweight/CHANGELOG.md +++ /dev/null @@ -1,83 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.0.0-rc.205 (2021-06-24) - -#### :bug: Bug Fix - -* Fixed initializing of system fields - -## v3.0.0-rc.146 (2021-02-15) - -#### :bug: Bug Fix - -* Fixed creation of meta objects - -## v3.0.0-rc.142 (2021-02-11) - -#### :bug: Bug Fix - -* Fixed creation of a context - -## v3.0.0-rc.129 (2021-01-28) - -#### :house: Internal - -* Optimized creation of flyweight components - -## v3.0.0-rc.127 (2021-01-26) - -#### :bug: Bug Fix - -* Fixed `componentStatus` with flyweight components -* Fixed creation of `$async` - -## v3.0.0-rc.126 (2021-01-26) - -#### :rocket: New Feature - -* Now flyweight components support life cycle hooks - -## v3.0.0-rc.125 (2021-01-18) - -#### :bug: Bug Fix - -* Fixed a bug with the creation of nested flyweight components - -## v3.0.0-rc.99 (2020-11-17) - -#### :bug: Bug Fix - -* [Fixed providing of attributes](https://github.com/V4Fire/Client/issues/437) - -## v3.0.0-rc.92 (2020-11-03) - -#### :house: Internal - -* Refactoring - -## v3.0.0-rc.76 (2020-10-07) - -#### :bug: Bug Fix - -* Fixed a bug when flyweight components can't have refs - -## v3.0.0-rc.66 (2020-09-22) - -#### :bug: Bug Fix - -* [Fixed mixing of directives within of a component root tag](https://github.com/V4Fire/Client/pull/337) - -## v3.0.0-rc.37 (2020-07-20) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/core/component/flyweight/README.md b/src/core/component/flyweight/README.md deleted file mode 100644 index 41c5c95253..0000000000 --- a/src/core/component/flyweight/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# core/component/flyweight - -This module provides API to create a flyweight component. The flyweight components is a component borrows a context -from its parent and injects own render function to the parent render function. -The constructed component is stateless and this mechanism is pretty similar to "functional component" API in some MVVM libraries, -but provides more flexible API. diff --git a/src/core/component/flyweight/index.ts b/src/core/component/flyweight/index.ts deleted file mode 100644 index 4316941965..0000000000 --- a/src/core/component/flyweight/index.ts +++ /dev/null @@ -1,221 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/flyweight/README.md]] - * @packageDocumentation - */ - -import { deprecate } from 'core/functools/deprecation'; -import Async from 'core/async'; - -import { components } from 'core/component/const'; -import type { CreateElement, VNode } from 'core/component/engines'; - -import { forkMeta } from 'core/component/meta'; -import { initProps } from 'core/component/prop'; -import { initFields } from 'core/component/field'; -import { destroyComponent, FlyweightVNode } from 'core/component/functional'; - -import { attachMethodsFromMeta } from 'core/component/method'; -import { implementEventAPI } from 'core/component/event'; -import { attachAccessorsFromMeta } from 'core/component/accessor'; - -import { getNormalParent } from 'core/component/traverse'; -import { getComponentDataFromVNode } from 'core/component/vnode'; -import { execRenderObject } from 'core/component/render'; - -import type { ComponentInterface } from 'core/component/interface'; - -/** - * Takes a vnode and, if it has the composite attribute, returns a new vnode that contains a flyweight component, - * otherwise returns the original vnode - * - * @param vnode - * @param createElement - function to create VNode element - * @param parentComponent - parent component instance - */ -export function parseVNodeAsFlyweight( - vnode: VNode, - createElement: CreateElement, - parentComponent: ComponentInterface -): VNode | FlyweightVNode { - const - renderEngine = parentComponent.$renderEngine, - compositeAttr = vnode.data?.attrs?.['v4-flyweight-component']; - - if (!renderEngine.supports.composite || compositeAttr == null) { - return vnode; - } - - vnode.tag = 'span'; - - let - meta = components.get(compositeAttr); - - if (!meta) { - return vnode; - } - - meta = forkMeta(meta); - delete vnode.data!.attrs!['v4-flyweight-component']; - - if (parentComponent.isFlyweight) { - parentComponent = parentComponent.$normalParent!; - } - - const - componentData = getComponentDataFromVNode(compositeAttr, vnode), - componentProto = meta.constructor.prototype, - componentTpl = TPLS[compositeAttr] ?? componentProto.render; - - // To create a flyweight component we need to create the "fake" context. - // The context is based on the specified parent context by using `Object.create`. - // Also, we need to shim some component hooks. - - const - fakeCtx = Object.create(parentComponent); - - fakeCtx.isFlyweight = true; - fakeCtx.hook = 'beforeDataCreate'; - - fakeCtx.meta = meta; - fakeCtx.componentName = meta.componentName; - fakeCtx.instance = meta.instance; - - fakeCtx.unsafe = fakeCtx; - fakeCtx.$async = new Async(fakeCtx); - fakeCtx.$renderEngine = renderEngine; - - fakeCtx.$createElement = createElement.bind(fakeCtx); - fakeCtx.$destroy = () => destroyComponent(fakeCtx); - - fakeCtx._self = fakeCtx; - fakeCtx._renderProxy = fakeCtx; - fakeCtx._c = fakeCtx.$createElement; - fakeCtx._staticTrees = []; - - Object.defineProperty(fakeCtx, '$el', { - configurable: true, - enumerable: true, - value: undefined - }); - - Object.defineProperty(fakeCtx, '$refs', { - configurable: true, - enumerable: true, - value: {} - }); - - Object.defineProperty(fakeCtx, '$refHandlers', { - configurable: true, - enumerable: true, - value: {} - }); - - Object.defineProperty(fakeCtx, '$props', { - configurable: true, - enumerable: true, - value: {} - }); - - Object.defineProperty(fakeCtx, '$attrs', { - configurable: true, - enumerable: true, - value: componentData.attrs - }); - - Object.defineProperty(fakeCtx, '$systemFields', { - configurable: true, - enumerable: true, - writable: true, - value: fakeCtx - }); - - Object.defineProperty(fakeCtx, '$data', { - configurable: true, - enumerable: true, - get(): typeof fakeCtx { - deprecate({name: '$$data', type: 'property', renamedTo: '$fields'}); - return fakeCtx; - } - }); - - Object.defineProperty(fakeCtx, '$fields', { - configurable: true, - enumerable: true, - writable: true, - value: fakeCtx - }); - - Object.defineProperty(fakeCtx, '$slots', { - configurable: true, - enumerable: true, - value: componentData.slots - }); - - Object.defineProperty(fakeCtx, '$scopedSlots', { - configurable: true, - enumerable: true, - value: componentData.scopedSlots - }); - - Object.defineProperty(fakeCtx, '$parent', { - configurable: true, - enumerable: true, - value: parentComponent - }); - - Object.defineProperty(fakeCtx, '$normalParent', { - configurable: true, - enumerable: true, - value: getNormalParent(fakeCtx) - }); - - Object.defineProperty(fakeCtx, '$children', { - configurable: true, - enumerable: true, - value: vnode.children - }); - - initProps(fakeCtx, { - from: componentData.props, - store: fakeCtx, - saveToStore: true - }); - - attachMethodsFromMeta(fakeCtx); - implementEventAPI(fakeCtx); - attachAccessorsFromMeta(fakeCtx); - - initFields(meta.systemFields, fakeCtx, fakeCtx); - initFields(meta.fields, fakeCtx, fakeCtx); - - fakeCtx.onCreatedHook(); - - const newVNode = execRenderObject( - componentTpl.index(), - fakeCtx - ); - - newVNode.fakeInstance = fakeCtx; - newVNode.data = newVNode.data ?? {}; - - renderEngine.patchVNode(newVNode, fakeCtx, { - // @ts-ignore (unsafe cast) - data: componentData - }); - - // Attach component event listeners - for (let o = componentData.on, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const key = keys[i]; - fakeCtx.$on(key, o[key]); - } - - return newVNode; -} diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 445f360060..fec5b418a0 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -92,11 +92,6 @@ export abstract class ComponentInterface { */ readonly renderGroup?: string; - /** - * True if the component is attached to the parent render function as a flyweight - */ - readonly isFlyweight?: boolean; - /** * API to invoke unsafely of internal properties of the component. * This parameter helps to avoid TS errors of using protected properties and methods outside from the component class. @@ -385,17 +380,9 @@ export abstract class ComponentInterface { return this; } - /** - * Hook handler: the component has been created - * (only for flyweight components) - */ - protected onCreatedHook(): void { - // Loopback - } - /** * Hook handler: the component has been bound - * (only for functional and flyweight components) + * (only for functional components) */ protected onBindHook(): void { // Loopback @@ -403,7 +390,7 @@ export abstract class ComponentInterface { /** * Hook handler: the component has been mounted - * (only for functional and flyweight components) + * (only for functional components) */ protected onInsertedHook(): void { // Loopback @@ -411,7 +398,7 @@ export abstract class ComponentInterface { /** * Hook handler: the component has been updated - * (only for functional and flyweight components) + * (only for functional components) */ protected onUpdateHook(): void { // Loopback @@ -419,7 +406,7 @@ export abstract class ComponentInterface { /** * Hook handler: the component has been unbound - * (only for functional and flyweight components) + * (only for functional components) */ protected onUnbindHook(): void { // Loopback From 33c6023b5af5f38e2b5d6dc47e137234c0c5eacb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 17:04:03 +0300 Subject: [PATCH 0044/2313] refactor: extract options to a separate file; migration to Vue3 --- src/core/component/meta/interface/index.ts | 37 ++- src/core/component/meta/interface/options.ts | 182 ++++++++++++ src/core/component/meta/interface/types.ts | 285 ++----------------- 3 files changed, 216 insertions(+), 288 deletions(-) create mode 100644 src/core/component/meta/interface/options.ts diff --git a/src/core/component/meta/interface/index.ts b/src/core/component/meta/interface/index.ts index f3e73751fd..705a8887e1 100644 --- a/src/core/component/meta/interface/index.ts +++ b/src/core/component/meta/interface/index.ts @@ -7,22 +7,26 @@ */ import type { PropOptions } from 'core/component/decorators'; -import type { WatchObject } from 'core/component/interface/watch'; -import type { ComponentConstructor, RenderFunction, ModsDecl } from 'core/component/interface'; +import type { RenderFunction } from 'core/component/engines'; + +import type { WatchObject, ComponentConstructor, ModsDecl } from 'core/component/interface'; +import type { ComponentOptions } from 'core/component/meta/interface/options'; import type { - ComponentOptions, ComponentProp, ComponentField, + ComponentAccessor, ComponentMethod, ComponentHooks, + ComponentDirectiveOptions, ComponentWatchDependencies } from 'core/component/meta/interface/types'; +export * from 'core/component/meta/interface/options'; export * from 'core/component/meta/interface/types'; /** @@ -30,8 +34,8 @@ export * from 'core/component/meta/interface/types'; */ export interface ComponentMeta { /** - * Full name of a component. - * If the component is smart the name can be equal to `b-foo-functional`. + * The full component name. + * If the component is smart the name can contain a `-functional` postfix. */ name: string; @@ -51,7 +55,7 @@ export interface ComponentMeta { instance: Dictionary; /** - * Map of component parameters that was provided to a @component decorator + * Map of component parameters that were provided to a `@component` decorator */ params: ComponentOptions; @@ -76,17 +80,17 @@ export interface ComponentMeta { fields: Dictionary; /** - * Map of component computed fields with support of caching + * Map of component fields that can't force re-rendering */ - computedFields: Dictionary; + systemFields: Dictionary; /** - * Map of component fields that can't force re-rendering + * Map of component computed fields with support of caching */ - systemFields: Dictionary; + computedFields: Dictionary; /** - * Map of fields that contains the "Store" postfix + * Map of component fields that contains the `Store` postfix */ tiedFields: Dictionary; @@ -121,8 +125,8 @@ export interface ComponentMeta { */ component: { /** - * Full name of a component. - * If the component is smart the name can be equal to `b-foo-functional`. + * The full component name. + * If the component is smart the name can contain a `-functional` postfix. */ name: string; @@ -157,12 +161,7 @@ export interface ComponentMeta { components?: Dictionary; /** - * List of static render functions - */ - staticRenderFns: RenderFunction[]; - - /** - * Main render function of the component + * Component render function */ render: RenderFunction; }; diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts new file mode 100644 index 0000000000..e48f4bedc5 --- /dev/null +++ b/src/core/component/meta/interface/options.ts @@ -0,0 +1,182 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * Additional options to register a component + */ +export interface ComponentOptions { + /** + * The component name. + * If the name isn't specified, it will be taken from a class name by using reflection. + * This parameter can't be inherited from a parent. + * + * @example + * ```typescript + * // name == 'bExample' + * @component({name: 'bExample'}) + * class Foo extends iBlock { + * + * } + * + * // name == 'Bar' + * @component() + * class Bar extends iBlock { + * + * } + * ``` + */ + name?: string; + + /** + * If true, then the component is registered as a root component. + * The root component is the top of components hierarchy, i.e. it contains all components in an application. + * + * All components, even the root component, have a link to the root component. + * This parameter can be inherited from a parent. + * + * @default `false` + * + * @example + * ```typescript + * @component({root: true}) + * class pRoot extends iStaticPage { + * + * } + * ``` + */ + root?: boolean; + + /** + * If false, then the component won't load an external template. + * It will use the default loopback render function. + * + * It is useful for components without templates. + * This parameter can be inherited from a parent. + * + * @default `true` + */ + tpl?: boolean; + + /** + * The functional mode: + * + * 1. If true, the component will be created as a functional component. + * 2. If a dictionary, the component can be created as a functional component or regular component, depending on + * values of the input properties: + * 1. If an empty dictionary, the component will always created as functional. + * 2. If a dictionary with values, the dictionary properties represent component input properties. + * If the component invocation takes these properties with the values that + * declared within "functional" parameters, it will be created as a functional. + * Also, you can specify multiple values of one input property by using a list of values. + * Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, + * but you can directly cast a type by using "v-func" directive. + * 3. If null, all components watchers and listeners that directly specified in a class won't + * be attached to a functional component. It is useful to create superclass behaviour depending + * on a component type. + * + * A functional component is a component can be rendered once only from input properties. + * This type of components have a state and lifecycle hooks, but mutation of the state doesn't force rendering. + * Usually, functional components lighter than regular components with the first render, + * but avoid their if you have long animations within a component or if you need to frequent re-draws some deep + * structure of nested components. + * + * This parameter can be inherited from a parent, but the `null` value isn't inherited. + * + * @default `false` + * + * @example + * ```typescript + * // `bButton` will be created as a function component + * // if its `dataProvider` property is equal to `false` or not specified + * @component({functional: {dataProvider: [undefined, false]}}) + * class bButton extends iData { + * + * } + * + * // `bLink` will always be created as a functional component + * @component({functional: true}) + * class bLink extends iData { + * + * } + * ``` + * + * ``` + * // We force `b-button` to create as a regular component + * < b-button v-func = false + * + * // Within `v-func` we can use values from the runtime + * < b-button v-func = foo !== bar + * + * // The direct invoking of a functional version of `bButton` + * < b-button-functional + * ``` + */ + functional?: Nullable | Dictionary; + + /** + * A map of deprecated props with specified alternatives. + * The map keys represent deprecated props; the values represent alternatives. + * This parameter can be inherited from a parent. + * + * @example + * ```typescript + * @component({deprecatedProps: { + * value: 'items' + * }}}}) + * + * class bList extends iData { + * @prop() + * items: string[]; + * + * // @deprecated + * @prop() + * value: string[]; + * } + * ``` + */ + deprecatedProps?: Dictionary; + + /** + * If true, then the component input properties that aren't registered as props + * will be attached to a component node as attributes. + * + * This parameter can be inherited from a parent. + * + * @default `true` + * + * @example + * ```typescript + * @component() + * class bInput extends iData { + * @prop() + * value: string = ''; + * } + * ``` + * + * ``` + * < b-input :data-title = 'hello' + * ``` + */ + inheritAttrs?: boolean; + + /** + * If true, then the component is automatically inherited base modifiers from its parent. + * This parameter can be inherited from a parent. + * + * @default `true` + */ + inheritMods?: boolean; + + /** + * If false, then all default values of the component input properties are ignored + * This parameter can be inherited from a parent. + * + * @default `true` + */ + defaultProps?: boolean; +} diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index 165dbef185..4df78e67ff 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -7,266 +7,13 @@ */ import type { WatchPath } from 'core/object/watch'; -import type { ComputedOptions, DirectiveOptions } from 'core/component/engines'; import type { Hook } from 'core/component/interface/life-cycle'; -import type { ComponentInterface } from 'core/component/interface/component'; - -import type { FieldWatcher, MethodWatcher } from 'core/component/interface/watch'; import type { PropOptions, InitFieldFn, MergeFieldFn, UniqueFieldFn } from 'core/component/decorators'; +import type { WritableComputedOptions, DirectiveBinding } from 'core/component/engines'; -/** - * Additional options to register a component - */ -export interface ComponentOptions { - /** - * The component name. - * If the name isn't specified, it will be taken from a class name by using reflection. - * This parameter can't be inherited from a parent. - * - * @example - * ```typescript - * // name == 'bExample' - * @component({name: 'bExample'}) - * class Foo extends iBlock { - * - * } - * - * // name == 'Bar' - * @component() - * class Bar extends iBlock { - * - * } - * ``` - */ - name?: string; - - /** - * If true, then the component is registered as a root component. - * - * The root component is the top of components hierarchy, i.e. it contains all components in an application, - * and the application can't exist without the root. - * - * All components, even the root component, have a link to the root component. - * This parameter can be inherited from a parent. - * - * @default `false` - * - * @example - * ```typescript - * @component({root: true}) - * class pRoot extends iStaticPage { - * - * } - * ``` - */ - root?: boolean; - - /** - * If false, then the component won't load an external template and will use the default loopback render function. - * It is useful for components without templates. - * This parameter can be inherited from a parent. - * - * @default `true` - */ - tpl?: boolean; - - /** - * The functional mode: - * - * 1. if true, the component will be created as a functional component; - * 2. if a dictionary, the component can be created as a functional component or as a regular component, depending on - * values of the input properties: - * 1. If an empty dictionary, the component will always created as functional; - * 2. If a dictionary with values, the dictionary properties represent component input properties. - * If the component invocation take these properties with the values that - * declared within "functional" parameters, it will be created as functional. - * Also, you can specify multiple values of one input property by using a list of values. - * Mind that inferring of a component type is compile-based, i.e. you can't depend on values from runtime, - * but you can directly cast the type by using "v-func" directive; - * 3. If null, all components watchers and listeners that directly specified in a class don't - * be attached to a functional component. It is useful to create superclass behaviour depending - * on a component type. - * - * The functional component is a component can be rendered only once from input properties. - * This type of components have a state and lifecycle hooks, but mutation of the state don't force re-render of a - * component. Usually, functional components lighter in 2-3 times with the first render than regular components, but - * avoid their if you have long animations within a component or if you need to frequent re-draws some deep structure - * of nested components. - * - * This parameter can be inherited from a parent, but the "null" value isn't inherited. - * - * @default `false` - * - * @example - * ```typescript - * // bButton will be created as a function component - * // if its .dataProvider property is equal to false, or not specified - * @component({functional: {dataProvider: [undefined, false]}}) - * class bButton extends iData { - * - * } - * - * // bLink will always be created as a functional component - * @component({functional: true}) - * class bLink extends iData { - * - * } - * ``` - * - * ``` - * // We force b-button to create as a regular component - * < b-button v-func = false - * - * // Within "v-func" we can use values from runtime - * < b-button v-func = foo !== bar - * - * // Direct invoking of a functional version of bButton - * < b-button-functional - * ``` - */ - functional?: Nullable | Dictionary; - - /** - * If true, then the component can be used as a flyweight component. - * The flyweight component is a special kind of stateless component borrows parent context - * to create own context. This type of components have the lightest first render initialising comparing with - * functional or regular components, but there are a lot of limitations: - * - * 1. You don't have a state; - * 2. You can't use lifecycle hooks; - * 3. You can't watch changes of component properties. - * - * Also, flyweight components inherit all limitation from functional components. Also, you still have modifier API. - * This parameter can be inherited from a parent. - * - * @default `false` - * - * @example - * ```typescript - * @component({flyweight: true}}) - * class bButton extends iData { - * - * } - * ``` - * - * ``` - * // To use a component as a flyweight you need to add @ symbol - * // before the component name within a template - * < @b-button - * ``` - */ - flyweight?: boolean; - - /** - * Parameters to use "v-model" directive with a component. - * - * If the component can provide one logical value, you can use v-model directive - * to create kind of "two-way" binding. - * - * This parameter can be inherited from a parent. - * - * @example - * ```typescript - * @component({model: {prop: 'valueProp', event: 'onValueChange'}}) - * class bInput extends iData { - * @prop() - * valueProp: string = ''; - * - * @field((ctx) => ctx.sync.link()) - * value!: string; - * - * @watch('value') - * onValueChange(): void { - * this.emit('valueChange', this.value); - * } - * } - * ``` - * - * ``` - * // The value of bInput is two-way bound to bla - * < b-input v-model = bla - * ``` - */ - model?: ComponentModel; - - /** - * Map of deprecated props with specified alternatives. - * Dictionary keys represent deprecated props; values represent alternatives. - * This parameter can be inherited from a parent. - * - * @example - * ```typescript - * @component({deprecatedProps: { - * value: 'items' - * }}}}) - * - * class bList extends iData { - * @prop() - * items: string[]; - * - * // @deprecated - * @prop() - * value: string[]; - * } - * ``` - */ - deprecatedProps?: Dictionary; - - /** - * If true, then component input properties that isn't registered as props - * will be attached to a component node as attributes. - * - * This parameter can be inherited from a parent. - * - * @default `true` - * - * @example - * ```typescript - * @component() - * class bInput extends iData { - * @prop() - * value: string = ''; - * } - * ``` - * - * ``` - * < b-input :data-title = 'hello' - * ``` - */ - inheritAttrs?: boolean; - - /** - * If true, then a component is automatically inherited base modifiers from its parent. - * This parameter can be inherited from a parent. - * - * @default `true` - */ - inheritMods?: boolean; - - /** - * If false, then all default values of component input properties are ignored - * This parameter can be inherited from a parent. - * - * @default `true` - */ - defaultProps?: boolean; -} - -/** - * Component model declaration - */ -export interface ComponentModel { - /** - * Prop name that tied with the model - */ - prop?: string; - - /** - * Event name that tied with the model - */ - event?: string; -} +import type { ComponentInterface, FieldWatcher, MethodWatcher } from 'core/component/interface'; +import type { ComponentOptions } from 'core/component/meta/interface/options'; export type ComponentInfo = ComponentOptions & { name: string; @@ -298,17 +45,27 @@ export interface ComponentField extends ComputedOptions { +export interface ComponentComputedField extends Partial> { } -export interface ComponentAccessor extends ComputedOptions { +export interface ComponentAccessor extends Partial> { src: string; replace?: boolean; functional?: boolean; watchable?: boolean; } +export interface ComponentMethod { + fn: Function; + src: string; + wrapper?: boolean; + replace?: boolean; + functional?: boolean; + watchers?: Dictionary; + hooks?: ComponentMethodHooks; +} + export interface ComponentHook { fn: Function; name?: string; @@ -329,17 +86,7 @@ export type ComponentMethodHooks = { }; }; -export interface ComponentMethod { - fn: Function; - src: string; - wrapper?: boolean; - replace?: boolean; - functional?: boolean; - watchers?: Dictionary; - hooks?: ComponentMethodHooks; -} - -export interface ComponentDirectiveOptions extends DirectiveOptions { +export interface ComponentDirectiveOptions extends DirectiveBinding { } From 3d518d18145e078c2e1fefb545bacb2f3049f755 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 18:01:55 +0300 Subject: [PATCH 0045/2313] refactor: migration to Vue3 --- src/core/component/meta/CHANGELOG.md | 6 + src/core/component/meta/create.ts | 222 +-------------------- src/core/component/meta/fill.ts | 221 ++++++++++++++++++++ src/core/component/meta/fork.ts | 2 +- src/core/component/meta/index.ts | 1 + src/core/component/meta/inherit.ts | 77 ++++--- src/core/component/meta/interface/types.ts | 3 - src/core/component/meta/method.ts | 23 ++- src/core/component/meta/tpl.ts | 7 +- 9 files changed, 286 insertions(+), 276 deletions(-) create mode 100644 src/core/component/meta/fill.ts diff --git a/src/core/component/meta/CHANGELOG.md b/src/core/component/meta/CHANGELOG.md index 01b88d1c30..74542d5b21 100644 --- a/src/core/component/meta/CHANGELOG.md +++ b/src/core/component/meta/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.0.0-rc.177 (2021-04-14) #### :house: Internal diff --git a/src/core/component/meta/create.ts b/src/core/component/meta/create.ts index 4bd131ef49..620b427d29 100644 --- a/src/core/component/meta/create.ts +++ b/src/core/component/meta/create.ts @@ -6,28 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { defaultWrapper } from 'core/component/const'; - -import { getComponentMods, isAbstractComponent } from 'core/component/reflection'; -import { isTypeCanBeFunc } from 'core/component/prop'; -import { wrapRender } from 'core/component/render-function'; - import { inheritMeta } from 'core/component/meta/inherit'; -import { addMethodsToMeta } from 'core/component/meta/method'; - -import type { - - ComponentMeta, - ComponentConstructor, - ComponentConstructorInfo, - - ComponentProp, - ComponentField, - WatchObject, - - RenderFunction +import { getComponentMods } from 'core/component/reflection'; +import { wrapRender } from 'core/component/render-function'; -} from 'core/component/interface'; +import type { RenderFunction } from 'core/component/engines'; +import type { ComponentMeta, ComponentConstructorInfo } from 'core/component/interface'; /** * Creates a meta object for the specified component and returns it @@ -72,6 +56,8 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { deactivated: [], beforeDestroy: [], destroyed: [], + renderTracked: [], + renderTriggered: [], errorCaptured: [] }, @@ -95,199 +81,3 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { return meta; } - -/** - * Fills a meta object with methods and properties from the specified component class - * - * @param meta - * @param [constructor] - component constructor - */ -export function fillMeta( - meta: ComponentMeta, - constructor: ComponentConstructor = meta.constructor -): ComponentMeta { - addMethodsToMeta(meta, constructor); - - const - {component, methods, watchers, hooks} = meta; - - const instance = Object.cast(new constructor()); - meta.instance = instance; - - if (isAbstractComponent.test(meta.componentName)) { - return meta; - } - - const - isFunctional = meta.params.functional === true; - - // Methods - - for (let o = methods, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - nm = keys[i], - method = o[nm]; - - if (!method) { - continue; - } - - component.methods[nm] = method.fn; - - if (method.watchers) { - for (let o = method.watchers, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - watcher = >o[key]; - - if (isFunctional && watcher.functional === false) { - continue; - } - - const - watcherListeners = watchers[key] ?? []; - - watchers[key] = watcherListeners; - watcherListeners.push({ - ...watcher, - method: nm, - args: Array.concat([], watcher.args), - handler: Object.cast(method.fn) - }); - } - } - - // Hooks - - if (method.hooks) { - for (let o = method.hooks, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - watcher = o[key]; - - if (isFunctional && watcher.functional === false) { - continue; - } - - hooks[key].push({...watcher, fn: method.fn}); - } - } - } - - // Props - - const - defaultProps = meta.params.defaultProps !== false; - - for (let o = meta.props, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - prop = >o[key]; - - let - def, - defWrapper, - skipDefault = true; - - if (defaultProps || prop.forceDefault) { - skipDefault = false; - def = instance[key]; - defWrapper = def; - - if (def != null && typeof def === 'object' && (!isTypeCanBeFunc(prop.type) || !Object.isFunction(def))) { - defWrapper = () => Object.fastClone(def); - defWrapper[defaultWrapper] = true; - } - } - - let - defValue; - - if (!skipDefault) { - defValue = prop.default !== undefined ? prop.default : defWrapper; - } - - component.props[key] = { - type: prop.type, - required: prop.required !== false && defaultProps && defValue === undefined, - - // eslint-disable-next-line @typescript-eslint/unbound-method - validator: prop.validator, - functional: prop.functional, - default: defValue - }; - - if (Object.size(prop.watchers) > 0) { - const watcherListeners = watchers[key] ?? []; - watchers[key] = watcherListeners; - - for (let w = prop.watchers.values(), el = w.next(); !el.done; el = w.next()) { - const - watcher = el.value; - - if (isFunctional && watcher.functional === false) { - continue; - } - - watcherListeners.push(watcher); - } - } - } - - // Fields - - for (let fields = [meta.systemFields, meta.fields], i = 0; i < fields.length; i++) { - for (let o = fields[i], keys = Object.keys(o), j = 0; j < keys.length; j++) { - const - key = keys[j], - field = >o[key]; - - if (field.watchers) { - for (let w = field.watchers.values(), el = w.next(); !el.done; el = w.next()) { - const - watcher = el.value; - - if (isFunctional && watcher.functional === false) { - continue; - } - - const - watcherListeners = watchers[key] ?? []; - - watchers[key] = watcherListeners; - watcherListeners.push(watcher); - } - } - } - } - - // Modifiers - - const - {mods} = component; - - for (let o = meta.mods, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - mod = o[key]; - - let - def; - - if (mod) { - for (let i = 0; i < mod.length; i++) { - const - el = mod[i]; - - if (Object.isArray(el)) { - def = el; - break; - } - } - - mods[key] = def !== undefined ? String(def[0]) : undefined; - } - } - - return meta; -} diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts new file mode 100644 index 0000000000..eb763c715f --- /dev/null +++ b/src/core/component/meta/fill.ts @@ -0,0 +1,221 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { defaultWrapper } from 'core/component/const'; +import { getComponentContext } from 'core/component/engines/helpers'; + +import { isTypeCanBeFunc } from 'core/component/prop'; +import { isAbstractComponent } from 'core/component/reflection'; +import { addMethodsToMeta } from 'core/component/meta/method'; + +import type { ComponentConstructor, ComponentMeta, ComponentField, WatchObject } from 'core/component/interface'; + +/** + * Fills the passed meta object with methods and properties from the specified component class + * + * @param meta + * @param [constructor] - component constructor + */ +export function fillMeta( + meta: ComponentMeta, + constructor: ComponentConstructor = meta.constructor +): ComponentMeta { + addMethodsToMeta(meta, constructor); + + const { + component, + methods, + watchers, + hooks + } = meta; + + const instance = Object.cast(new constructor()); + meta.instance = instance; + + if (isAbstractComponent.test(meta.componentName)) { + return meta; + } + + const + isFunctional = meta.params.functional === true; + + // Methods + + for (let o = methods, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + nm = keys[i], + method = o[nm]; + + if (method == null) { + continue; + } + + component.methods[nm] = function wrapper() { + return method.fn.apply(getComponentContext(this), arguments); + }; + + if (method.watchers != null) { + for (let o = method.watchers, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + watcher = >o[key]; + + if (isFunctional && watcher.functional === false) { + continue; + } + + const + watcherListeners = watchers[key] ?? []; + + watchers[key] = watcherListeners; + watcherListeners.push({ + ...watcher, + method: nm, + args: Array.concat([], watcher.args), + handler: Object.cast(method.fn) + }); + } + } + + // Hooks + + if (method.hooks) { + for (let o = method.hooks, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + hook = o[key]; + + if (isFunctional && hook.functional === false) { + continue; + } + + hooks[key].push({...hook, fn: method.fn}); + } + } + } + + // Props + + const + defaultProps = meta.params.defaultProps !== false; + + for (let o = meta.props, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + prop = o[key]; + + if (prop == null) { + continue; + } + + let + def, + defWrapper, + skipDefault = true; + + if (defaultProps || prop.forceDefault) { + skipDefault = false; + def = instance[key]; + defWrapper = def; + + if (def != null && typeof def === 'object' && (!isTypeCanBeFunc(prop.type) || !Object.isFunction(def))) { + defWrapper = () => Object.fastClone(def); + defWrapper[defaultWrapper] = true; + } + } + + let + defValue; + + if (!skipDefault) { + defValue = prop.default !== undefined ? prop.default : defWrapper; + } + + component.props[key] = { + type: prop.type, + required: prop.required !== false && defaultProps && defValue === undefined, + default: defValue, + functional: prop.functional, + // eslint-disable-next-line @typescript-eslint/unbound-method + validator: prop.validator + }; + + if (Object.size(prop.watchers) > 0) { + const watcherListeners = watchers[key] ?? []; + watchers[key] = watcherListeners; + + for (let w = prop.watchers.values(), el = w.next(); !el.done; el = w.next()) { + const + watcher = el.value; + + if (isFunctional && watcher.functional === false) { + continue; + } + + watcherListeners.push(watcher); + } + } + } + + // Fields + + for (let fields = [meta.systemFields, meta.fields], i = 0; i < fields.length; i++) { + for (let o = fields[i], keys = Object.keys(o), j = 0; j < keys.length; j++) { + const + key = keys[j], + field = >o[key]; + + if (field.watchers) { + for (let w = field.watchers.values(), el = w.next(); !el.done; el = w.next()) { + const + watcher = el.value; + + if (isFunctional && watcher.functional === false) { + continue; + } + + const + watcherListeners = watchers[key] ?? []; + + watchers[key] = watcherListeners; + watcherListeners.push(watcher); + } + } + } + } + + // Modifiers + + const + {mods} = component; + + for (let o = meta.mods, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + mod = o[key]; + + let + def; + + if (mod) { + for (let i = 0; i < mod.length; i++) { + const + el = mod[i]; + + if (Object.isArray(el)) { + def = el; + break; + } + } + + mods[key] = def !== undefined ? String(def[0]) : undefined; + } + } + + return meta; +} diff --git a/src/core/component/meta/fork.ts b/src/core/component/meta/fork.ts index 54625a2a0e..95a9ce5875 100644 --- a/src/core/component/meta/fork.ts +++ b/src/core/component/meta/fork.ts @@ -9,7 +9,7 @@ import type { ComponentMeta } from 'core/component/interface'; /** - * Creates a new meta object from the specified + * Creates a new meta object based on the specified * @param base */ export function forkMeta(base: ComponentMeta): ComponentMeta { diff --git a/src/core/component/meta/index.ts b/src/core/component/meta/index.ts index 6d66e3f927..d9e97d9fb5 100644 --- a/src/core/component/meta/index.ts +++ b/src/core/component/meta/index.ts @@ -13,6 +13,7 @@ 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'; diff --git a/src/core/component/meta/inherit.ts b/src/core/component/meta/inherit.ts index fb4b286674..ef7aac8493 100644 --- a/src/core/component/meta/inherit.ts +++ b/src/core/component/meta/inherit.ts @@ -7,7 +7,7 @@ */ import { metaPointers, PARENT } from 'core/component/const'; -import type { ComponentMeta, StrictModDeclVal } from 'core/component/interface'; +import type { ComponentMeta, ModDeclVal } from 'core/component/interface'; /** * Inherits the specified meta object from another meta object. @@ -42,7 +42,6 @@ export function inheritMeta( ...pParams, ...meta.params, name: meta.params.name, - model: (meta.params.model || pParams.model) && {...pParams.model, ...meta.params.model}, deprecatedProps: {...pParams.deprecatedProps, ...meta.params.deprecatedProps} }; @@ -69,19 +68,19 @@ export function inheritMeta( for (let i = 0; i < list.length; i++) { const - [o, parentObj] = list[i]; + [store, parentObj] = list[i]; for (let keys = Object.keys(parentObj), i = 0; i < keys.length; i++) { const key = keys[i], parent = parentObj[key]; - if (!parent) { + if (parent == null) { continue; } - if (!metaPointer || !metaPointer[key]) { - o[key] = parent; + if (metaPointer == null || !metaPointer[key]) { + store[key] = parent; continue; } @@ -89,7 +88,7 @@ export function inheritMeta( after, watchers; - if (parent.watchers) { + if (parent.watchers != null) { for (let w = parent.watchers.values(), el = w.next(); !el.done; el = w.next()) { const val = el.value; watchers ??= new Map(); @@ -97,14 +96,14 @@ export function inheritMeta( } } - if ('after' in parent && parent.after) { + if ('after' in parent && parent.after != null) { for (let a = parent.after.values(), el = a.next(); !el.done; el = a.next()) { after ??= new Set(); after.add(el.value); } } - o[key] = {...parent, after, watchers}; + store[key] = {...parent, after, watchers}; } } } @@ -123,28 +122,28 @@ export function inheritMeta( for (let i = 0; i < list.length; i++) { const - [o, parentObj] = list[i]; + [store, parentObj] = list[i]; for (let keys = Object.keys(parentObj), i = 0; i < keys.length; i++) { const key = keys[i]; - o[key] = {...parentObj[key]!}; + store[key] = {...parentObj[key]!}; } } } // Methods inheritance - for (let o = meta.methods, keys = Object.keys(pMethods), i = 0; i < keys.length; i++) { + for (let {methods} = meta, keys = Object.keys(pMethods), i = 0; i < keys.length; i++) { const key = keys[i], parent = pMethods[key]; - if (!parent) { + if (parent == null) { continue; } - if (!metaPointer || !metaPointer[key]) { - o[key] = {...parent}; + if (metaPointer == null || !metaPointer[key]) { + methods[key] = {...parent}; continue; } @@ -152,48 +151,46 @@ export function inheritMeta( watchers = {}, hooks = {}; - if (parent.watchers) { + if (parent.watchers != null) { const - o = parent.watchers, - w = Object.keys(o); + {watchers} = parent; - for (let i = 0; i < w.length; i++) { - const key = w[i]; - watchers[key] = {...o[key]}; + for (let keys = Object.keys(watchers), i = 0; i < keys.length; i++) { + const key = keys[i]; + watchers[key] = {...watchers[key]}; } } - if (parent.hooks) { + if (parent.hooks != null) { const - o = parent.hooks, - w = Object.keys(o); + {hooks} = parent; - for (let i = 0; i < w.length; i++) { + for (let keys = Object.keys(hooks), i = 0; i < keys.length; i++) { const - key = w[i], - el = o[key]; + key = keys[i], + hook = hooks[key]; hooks[key] = { - ...el, - after: Object.size(el.after) > 0 ? new Set(el.after) : undefined + ...hook, + after: Object.size(hook.after) > 0 ? new Set(hook.after) : undefined }; } } - o[key] = {...parent, watchers, hooks}; + methods[key] = {...parent, watchers, hooks}; } // Modifiers inheritance - for (let o = meta.mods, keys = Object.keys(pMods), i = 0; i < keys.length; i++) { + for (let {mods} = meta, keys = Object.keys(pMods), i = 0; i < keys.length; i++) { const key = keys[i], - current = o[key], + current = mods[key], parent = (pMods[key] ?? []).slice(); - if (current) { + if (current != null) { const - values = Object.createDict(); + values = Object.createDict(); for (let o = current.slice(), i = 0; i < o.length; i++) { const @@ -201,7 +198,7 @@ export function inheritMeta( if (el !== PARENT) { if (Object.isArray(el) || !(el in values)) { - values[String(el)] = el; + values[String(el)] = Object.cast(el); } continue; @@ -228,7 +225,7 @@ export function inheritMeta( el = parent[i]; if (!(el in values)) { - values[String(el)] = el; + values[String(el)] = Object.cast(el); } if (!parentDef && Object.isArray(el)) { @@ -241,7 +238,7 @@ export function inheritMeta( } const - valuesList = []; + valuesList: ModDeclVal[] = []; for (let keys = Object.keys(values), i = 0; i < keys.length; i++) { const @@ -252,10 +249,10 @@ export function inheritMeta( } } - o[key] = valuesList; + mods[key] = valuesList; - } else if (!(key in o)) { - o[key] = parent; + } else if (!(key in mods)) { + mods[key] = parent; } } diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index 4df78e67ff..3b811d19aa 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -31,7 +31,6 @@ export interface ComponentSystemField; - replace?: boolean; functional?: boolean; functionalWatching?: boolean; after?: Set; @@ -51,7 +50,6 @@ export interface ComponentComputedField extends Partial extends Partial> { src: string; - replace?: boolean; functional?: boolean; watchable?: boolean; } @@ -60,7 +58,6 @@ export interface ComponentMethod { fn: Function; src: string; wrapper?: boolean; - replace?: boolean; functional?: boolean; watchers?: Dictionary; hooks?: ComponentMethodHooks; diff --git a/src/core/component/meta/method.ts b/src/core/component/meta/method.ts index 634f6c61cc..77520112ef 100644 --- a/src/core/component/meta/method.ts +++ b/src/core/component/meta/method.ts @@ -10,7 +10,7 @@ import { defProp } from 'core/const/props'; import type { ComponentMeta } from 'core/component/interface'; /** - * Iterates over a prototype of a component constructor and adds methods/accessors to the specified meta object + * Iterates over a prototype of a component constructor and adds methods/accessors to the passed meta object * * @param meta * @param [constructor] @@ -18,8 +18,7 @@ import type { ComponentMeta } from 'core/component/interface'; export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = meta.constructor): void { const proto = constructor.prototype, - ownProps = Object.getOwnPropertyNames(proto), - replace = !meta.params.flyweight; + ownProps = Object.getOwnPropertyNames(proto); const { componentName: src, @@ -40,7 +39,11 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me } const - desc = Object.getOwnPropertyDescriptor(proto, key); + desc = Object.getOwnPropertyDescriptor(proto, key); + + if (desc == null) { + continue; + } // Methods if ('value' in desc) { @@ -51,7 +54,7 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me continue; } - methods[key] = Object.assign(methods[key] ?? {replace, watchers: {}, hooks: {}}, {src, fn}); + methods[key] = Object.assign(methods[key] ?? {watchers: {}, hooks: {}}, {src, fn}); // Accessors } else { @@ -89,7 +92,7 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me const obj = meta[metaKey]; - // If we already have a property by this key, like a prop or a field, + // If we already have a property by this key, like a prop or field, // we need to delete it to correct override if (field[key] != null) { Object.defineProperty(proto, key, defProp); @@ -103,7 +106,7 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me // eslint-disable-next-line @typescript-eslint/unbound-method get = desc.get ?? old?.get; - // For using "super" within a setter we also create a method with a name of form `${key}Setter` + // To use `super` within a setter we also create a method with a name `${key}Setter` if (set != null) { const k = `${key}Setter`; @@ -111,14 +114,13 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me proto[k] = set; meta.methods[k] = { src, - replace, fn: set, watchers: {}, hooks: {} }; } - // For using "super" within a getter we also create a method with a name of form `${key}Getter` + // To using `super` within a getter we also create a method with a name `${key}Getter` if (get != null) { const k = `${key}Getter`; @@ -126,14 +128,13 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me proto[k] = get; meta.methods[k] = { src, - replace, fn: get, watchers: {}, hooks: {} }; } - obj[key] = Object.assign(obj[key] ?? {replace}, { + obj[key] = Object.assign(obj[key] ?? {}, { src, // eslint-disable-next-line @typescript-eslint/unbound-method get: desc.get ?? old?.get, diff --git a/src/core/component/meta/tpl.ts b/src/core/component/meta/tpl.ts index a68596e3af..28bd3d5574 100644 --- a/src/core/component/meta/tpl.ts +++ b/src/core/component/meta/tpl.ts @@ -27,9 +27,9 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v return; } - // In this case, we don't automatically attaches a render function + // In this case, we don't automatically attach a render function if (meta.params.tpl === false) { - // Loopback render function + // A loopback render function return attachTemplatesToMeta(meta, defTpls.block); } @@ -40,9 +40,6 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v const renderObj = componentTemplates[meta.componentName] ?? tpls.index(); componentTemplates[meta.componentName] = renderObj; - meta.component.staticRenderFns = - renderObj.staticRenderFns ?? []; - methods.render = { wrapper: true, watchers: {}, From 7b17ca7a50fc96830d7fc53db2b38f4b4263cdfa Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 18:41:40 +0300 Subject: [PATCH 0046/2313] chore: some grammar fixes --- src/core/component/event/README.md | 2 +- src/core/component/event/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/event/README.md b/src/core/component/event/README.md index 1e75658f83..04c0932ea3 100644 --- a/src/core/component/event/README.md +++ b/src/core/component/event/README.md @@ -1,4 +1,4 @@ # core/component/event -This module provides an event bridge between components and other part of an application. +This module provides an event bridge between components and other part of the application. Also, this module provides a bunch of functions to add base event API to a component instance. diff --git a/src/core/component/event/index.ts b/src/core/component/event/index.ts index 860531ec1f..a0babc262e 100644 --- a/src/core/component/event/index.ts +++ b/src/core/component/event/index.ts @@ -27,7 +27,7 @@ export * from 'core/component/event/component-api'; export * from 'core/component/event/interface'; /** - * Sends a message to reset all components of an application + * Sends a message to reset all components of the application * @param [type] - reset type */ export function reset(type?: ResetType): void { From 99ead6ed7a755cfadff538cac3c84415b1289ae4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 18:56:35 +0300 Subject: [PATCH 0047/2313] refactor: renamed `reflection` to `reflect` --- .../{reflection => reflect}/CHANGELOG.md | 0 .../{reflection => reflect}/README.md | 2 +- .../{reflection => reflect}/const.ts | 0 .../{reflection => reflect}/constructor.ts | 103 +---------------- src/core/component/reflect/index.ts | 19 +++ .../{reflection => reflect}/interface.ts | 18 +-- src/core/component/reflect/mod.ts | 108 ++++++++++++++++++ .../{reflection => reflect}/property.ts | 14 ++- .../{reflection => reflect}/types.ts | 0 src/core/component/reflection/index.ts | 18 --- 10 files changed, 150 insertions(+), 132 deletions(-) rename src/core/component/{reflection => reflect}/CHANGELOG.md (100%) rename src/core/component/{reflection => reflect}/README.md (92%) rename src/core/component/{reflection => reflect}/const.ts (100%) rename src/core/component/{reflection => reflect}/constructor.ts (54%) create mode 100644 src/core/component/reflect/index.ts rename src/core/component/{reflection => reflect}/interface.ts (86%) create mode 100644 src/core/component/reflect/mod.ts rename src/core/component/{reflection => reflect}/property.ts (95%) rename src/core/component/{reflection => reflect}/types.ts (100%) delete mode 100644 src/core/component/reflection/index.ts diff --git a/src/core/component/reflection/CHANGELOG.md b/src/core/component/reflect/CHANGELOG.md similarity index 100% rename from src/core/component/reflection/CHANGELOG.md rename to src/core/component/reflect/CHANGELOG.md diff --git a/src/core/component/reflection/README.md b/src/core/component/reflect/README.md similarity index 92% rename from src/core/component/reflection/README.md rename to src/core/component/reflect/README.md index a1529627e3..8fa42d9765 100644 --- a/src/core/component/reflection/README.md +++ b/src/core/component/reflect/README.md @@ -1,4 +1,4 @@ -# core/component/reflection +# core/component/reflect This module provides a bunch of functions to reflect component classes. diff --git a/src/core/component/reflection/const.ts b/src/core/component/reflect/const.ts similarity index 100% rename from src/core/component/reflection/const.ts rename to src/core/component/reflect/const.ts diff --git a/src/core/component/reflection/constructor.ts b/src/core/component/reflect/constructor.ts similarity index 54% rename from src/core/component/reflection/constructor.ts rename to src/core/component/reflect/constructor.ts index b55b2cab86..5e0d28239a 100644 --- a/src/core/component/reflection/constructor.ts +++ b/src/core/component/reflect/constructor.ts @@ -7,11 +7,10 @@ */ import { componentParams, components } from 'core/component/const'; -import { dsComponentsMods } from 'core/component/reflection/const'; -import { isAbstractComponent, isSmartComponent } from 'core/component/reflection/types'; +import { isAbstractComponent, isSmartComponent } from 'core/component/reflect/types'; -import type { ComponentOptions, ComponentMeta, ComponentConstructor, ModsDecl } from 'core/component/interface'; -import type { ComponentConstructorInfo } from 'core/component/reflection/interface'; +import type { ComponentOptions, ComponentMeta, ComponentConstructor } from 'core/component/interface'; +import type { ComponentConstructorInfo } from 'core/component/reflect/interface'; /** * Returns a component name by the specified constructor. @@ -127,99 +126,3 @@ export function getInfoFromConstructor( } }; } - -/** - * Returns a map of component modifiers from the specified component. - * This function takes the raw declaration of modifiers, normalizes it, and mixes with the design system modifiers - * (if there are specified). - * - * @param component - information object of the component - * - * @example - * ```js - * @component() - * class bButton extends iBlock { - * static mods = { - * 'opened-window': [ - * true, - * false, - * undefined, - * [false], - * bButton.PARENT - * ] - * }; - * } - * - * // {openedWindow: ['true', ['false'], bButton.PARENT]} - * getComponentMods(getInfoFromConstructor()); - * ``` - */ -export function getComponentMods(component: ComponentConstructorInfo): ModsDecl { - const - {constructor, componentName} = component; - - const - mods = {}; - - const - modsFromDS = dsComponentsMods?.[componentName], - modsFromConstructor = {...constructor['mods']}; - - if (Object.isDictionary(modsFromDS)) { - for (let keys = Object.keys(modsFromDS), i = 0; i < keys.length; i++) { - const - key = keys[i], - dsModDecl = modsFromDS[key], - modDecl = modsFromConstructor[key]; - - modsFromConstructor[key] = modDecl != null ? modDecl.concat(dsModDecl) : dsModDecl; - } - } - - for (let o = modsFromConstructor, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - modDecl = o[key], - modValues = >[]; - - if (modDecl != null) { - let - cache: Nullable> = null, - active; - - for (let i = 0; i < modDecl.length; i++) { - cache ??= new Map(); - - const - modVal = modDecl[i]; - - if (Object.isArray(modVal)) { - if (active !== undefined) { - cache.set(active, active); - } - - active = String(modVal[0]); - cache.set(active, [active]); - - } else { - const - normalizedModVal = Object.isPlainObject(modVal) ? modVal : String(modVal); - - if (!cache.has(normalizedModVal)) { - cache.set(normalizedModVal, normalizedModVal); - } - } - } - - if (cache != null) { - for (let o = cache.values(), el = o.next(); !el.done; el = o.next()) { - modValues.push(el.value); - } - } - } - - mods[key.camelize(false)] = modValues; - } - - return mods; -} diff --git a/src/core/component/reflect/index.ts b/src/core/component/reflect/index.ts new file mode 100644 index 0000000000..6bf7b9da47 --- /dev/null +++ b/src/core/component/reflect/index.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/reflect/README.md]] + * @packageDocumentation + */ + +export * from 'core/component/reflect/const'; +export * from 'core/component/reflect/types'; +export * from 'core/component/reflect/constructor'; +export * from 'core/component/reflect/mod'; +export * from 'core/component/reflect/property'; +export * from 'core/component/reflect/interface'; diff --git a/src/core/component/reflection/interface.ts b/src/core/component/reflect/interface.ts similarity index 86% rename from src/core/component/reflection/interface.ts rename to src/core/component/reflect/interface.ts index 96a932f566..ba7799b036 100644 --- a/src/core/component/reflection/interface.ts +++ b/src/core/component/reflect/interface.ts @@ -20,8 +20,8 @@ import type { */ export interface ComponentConstructorInfo { /** - * The full name of a component. - * If the component is smart the name can be equal to `b-foo-functional`. + * The full component name. + * If the component is smart the name can contain a `-functional` postfix. */ name: string; @@ -31,7 +31,7 @@ export interface ComponentConstructorInfo { componentName: string; /** - * True if the component is abstract, i.e., has a prefix in the name + * True if the component is abstract, i.e., has an abstract `i` prefix within the name */ isAbstract: boolean; @@ -46,7 +46,7 @@ export interface ComponentConstructorInfo { constructor: ComponentConstructor; /** - * Map of component parameters that was provided to a @component decorator + * Map of component parameters that were provided to a `@component` decorator */ params: ComponentOptions; @@ -56,7 +56,7 @@ export interface ComponentConstructorInfo { parent?: Function; /** - * Map of parent component parameters that was provided to a @component decorator + * Map of parent component parameters that were provided to a `@component` decorator */ parentParams?: ComponentOptions; @@ -69,8 +69,8 @@ export interface ComponentConstructorInfo { /** * Available types of a property accessor: * - * 1. computed - the cached type - * 2. accessor - the non-cached type + * 1. computed - the cached type; + * 2. accessor - the non-cached type. */ export type AccessorType = 'computed' | @@ -131,7 +131,7 @@ export interface CommonPropertyInfo { topPath: string; /** - * Original path of the property + * Original path to the property * * @example * ```js @@ -151,7 +151,7 @@ export interface CommonPropertyInfo { originalTopPath: string; /** - * Name of an accessor that is tied with the property + * Name of accessor that is tied with the property */ accessor?: string; diff --git a/src/core/component/reflect/mod.ts b/src/core/component/reflect/mod.ts new file mode 100644 index 0000000000..30eadb8902 --- /dev/null +++ b/src/core/component/reflect/mod.ts @@ -0,0 +1,108 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { dsComponentsMods } from 'core/component/reflect/const'; + +import type { ModsDecl } from 'core/component/interface'; +import type { ComponentConstructorInfo } from 'core/component/reflect/interface'; + +/** + * Returns a map of component modifiers from the specified component. + * This function takes the raw declaration of modifiers, normalizes it, and mixes with the design system modifiers + * (if there are specified). + * + * @param component - information object of the component + * + * @example + * ```js + * @component() + * class bButton extends iBlock { + * static mods = { + * 'opened-window': [ + * true, + * false, + * undefined, + * [false], + * bButton.PARENT + * ] + * }; + * } + * + * // {openedWindow: ['true', ['false'], bButton.PARENT]} + * getComponentMods(getInfoFromConstructor()); + * ``` + */ +export function getComponentMods(component: ComponentConstructorInfo): ModsDecl { + const + {constructor, componentName} = component; + + const + mods = {}; + + const + modsFromDS = dsComponentsMods?.[componentName], + modsFromConstructor = {...constructor['mods']}; + + if (Object.isDictionary(modsFromDS)) { + for (let keys = Object.keys(modsFromDS), i = 0; i < keys.length; i++) { + const + key = keys[i], + dsModDecl = modsFromDS[key], + modDecl = modsFromConstructor[key]; + + modsFromConstructor[key] = modDecl != null ? modDecl.concat(dsModDecl) : dsModDecl; + } + } + + for (let o = modsFromConstructor, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + modDecl = o[key], + modValues: Array = []; + + if (modDecl != null) { + let + cache: Nullable> = null, + active; + + for (let i = 0; i < modDecl.length; i++) { + cache ??= new Map(); + + const + modVal = modDecl[i]; + + if (Object.isArray(modVal)) { + if (active !== undefined) { + cache.set(active, active); + } + + active = String(modVal[0]); + cache.set(active, [active]); + + } else { + const + normalizedModVal = Object.isPlainObject(modVal) ? modVal : String(modVal); + + if (!cache.has(normalizedModVal)) { + cache.set(normalizedModVal, normalizedModVal); + } + } + } + + if (cache != null) { + for (let o = cache.values(), el = o.next(); !el.done; el = o.next()) { + modValues.push(el.value); + } + } + } + + mods[key.camelize(false)] = modValues; + } + + return mods; +} diff --git a/src/core/component/reflection/property.ts b/src/core/component/reflect/property.ts similarity index 95% rename from src/core/component/reflection/property.ts rename to src/core/component/reflect/property.ts index b44fe378dc..2f1ec24c76 100644 --- a/src/core/component/reflection/property.ts +++ b/src/core/component/reflect/property.ts @@ -9,8 +9,8 @@ import { deprecate } from 'core/functools/deprecation'; import { ComponentInterface } from 'core/component/interface'; -import { propRgxp, attrRgxp, storeRgxp, hasSeparator } from 'core/component/reflection/const'; -import type { PropertyInfo } from 'core/component/reflection/interface'; +import { propRgxp, attrRgxp, storeRgxp, hasSeparator } from 'core/component/reflect/const'; +import type { PropertyInfo } from 'core/component/reflect/interface'; /** * Returns an information object of a component property by the specified path @@ -85,8 +85,14 @@ export function getPropertyInfo(path: string, component: ComponentInterface): Pr name = chunks[rootI]; } - const - {props, fields, systemFields, computedFields, accessors, params: {deprecatedProps}} = component.unsafe.meta; + const { + props, + fields, + systemFields, + computedFields, + accessors, + params: {deprecatedProps} + } = component.unsafe.meta; const alternative = deprecatedProps?.[name]; diff --git a/src/core/component/reflection/types.ts b/src/core/component/reflect/types.ts similarity index 100% rename from src/core/component/reflection/types.ts rename to src/core/component/reflect/types.ts diff --git a/src/core/component/reflection/index.ts b/src/core/component/reflection/index.ts deleted file mode 100644 index ac335b4af2..0000000000 --- a/src/core/component/reflection/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/reflection/README.md]] - * @packageDocumentation - */ - -export * from 'core/component/reflection/const'; -export * from 'core/component/reflection/types'; -export * from 'core/component/reflection/constructor'; -export * from 'core/component/reflection/property'; -export * from 'core/component/reflection/interface'; From f44f699ad3b96ba280548fab26d5b1a82ba65ba1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 18:56:59 +0300 Subject: [PATCH 0048/2313] refactor: fixed imports --- src/core/component/interface/engine.ts | 2 -- src/core/component/interface/index.ts | 2 +- src/core/component/interface/watch.ts | 2 +- src/core/component/meta/create.ts | 2 +- src/core/component/meta/fill.ts | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/core/component/interface/engine.ts b/src/core/component/interface/engine.ts index c1fa3d9511..fa65f19826 100644 --- a/src/core/component/interface/engine.ts +++ b/src/core/component/interface/engine.ts @@ -34,8 +34,6 @@ export type ProxyGetter = (ctx: T) => { export type ProxyGetters = Record>; export interface RenderEngine { - minimalCtx: object; - supports: RenderEngineFeatures; proxyGetters: ProxyGetters; diff --git a/src/core/component/interface/index.ts b/src/core/component/interface/index.ts index 11caa5e513..0b760fceec 100644 --- a/src/core/component/interface/index.ts +++ b/src/core/component/interface/index.ts @@ -8,7 +8,7 @@ export * from 'core/component/interface/component'; export * from 'core/component/meta/interface'; -export * from 'core/component/reflection/interface'; +export * from 'core/component/reflect/interface'; export * from 'core/component/interface/mod'; export * from 'core/component/interface/watch'; export * from 'core/component/interface/link'; diff --git a/src/core/component/interface/watch.ts b/src/core/component/interface/watch.ts index 3171ef1dc0..661bc1e1e8 100644 --- a/src/core/component/interface/watch.ts +++ b/src/core/component/interface/watch.ts @@ -9,7 +9,7 @@ import type { WatchPath as RawWatchPath, WatchOptions, WatchHandlerParams } from 'core/object/watch'; import type { Group, Label, Join } from 'core/async'; -import type { PropertyInfo } from 'core/component/reflection'; +import type { PropertyInfo } from 'core/component/reflect'; import type { ComponentInterface } from 'core/component/interface/component'; export { WatchOptions, WatchHandlerParams }; diff --git a/src/core/component/meta/create.ts b/src/core/component/meta/create.ts index 620b427d29..26baa0a21e 100644 --- a/src/core/component/meta/create.ts +++ b/src/core/component/meta/create.ts @@ -7,7 +7,7 @@ */ import { inheritMeta } from 'core/component/meta/inherit'; -import { getComponentMods } from 'core/component/reflection'; +import { getComponentMods } from 'core/component/reflect'; import { wrapRender } from 'core/component/render-function'; import type { RenderFunction } from 'core/component/engines'; diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index eb763c715f..522f11a2e3 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -10,7 +10,7 @@ import { defaultWrapper } from 'core/component/const'; import { getComponentContext } from 'core/component/engines/helpers'; import { isTypeCanBeFunc } from 'core/component/prop'; -import { isAbstractComponent } from 'core/component/reflection'; +import { isAbstractComponent } from 'core/component/reflect'; import { addMethodsToMeta } from 'core/component/meta/method'; import type { ComponentConstructor, ComponentMeta, ComponentField, WatchObject } from 'core/component/interface'; From 83b9a1fa487b0a13bc1d70640e2886808045e8da Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 18 Apr 2022 18:58:28 +0300 Subject: [PATCH 0049/2313] :art: --- src/core/component/reflect/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/reflect/interface.ts b/src/core/component/reflect/interface.ts index ba7799b036..aacef7e639 100644 --- a/src/core/component/reflect/interface.ts +++ b/src/core/component/reflect/interface.ts @@ -162,7 +162,7 @@ export interface CommonPropertyInfo { } /** - * Information of a regular component property: prop, field, computedField, etc. + * Information of a regular component property: `prop`, `field`, `computedField`, etc. */ export interface ComponentPropertyInfo extends CommonPropertyInfo { /** From ceeb559fd006c06c3da8eb1e88b61551c5e0e39a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 12:33:57 +0300 Subject: [PATCH 0050/2313] refactor: moved interface to a folder --- .../decorators/{interface.ts => interface/index.ts} | 6 ------ 1 file changed, 6 deletions(-) rename src/core/component/decorators/{interface.ts => interface/index.ts} (98%) diff --git a/src/core/component/decorators/interface.ts b/src/core/component/decorators/interface/index.ts similarity index 98% rename from src/core/component/decorators/interface.ts rename to src/core/component/decorators/interface/index.ts index 4a68cb49e3..a56d1e5874 100644 --- a/src/core/component/decorators/interface.ts +++ b/src/core/component/decorators/interface/index.ts @@ -248,12 +248,6 @@ export interface DecoratorField< } export interface DecoratorFunctionalOptions { - /** - * If false, the instance won't be borrowed from a parent when the owner component is a flyweight - * @default `true` - */ - replace?: boolean; - /** * If false, the instance can't be used with functional components * @default `true` From 0fef1bd0081f11be2b31e41aef4c6caf8bfaeed5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 14:08:42 +0300 Subject: [PATCH 0051/2313] refactor: split `interface` into files --- .../decorators/interface/accessor.ts | 51 +++ .../component/decorators/interface/field.ts | 105 ++++++ .../component/decorators/interface/hook.ts | 25 ++ .../component/decorators/interface/index.ts | 341 +----------------- .../component/decorators/interface/method.ts | 32 ++ .../component/decorators/interface/prop.ts | 118 ++++++ .../component/decorators/interface/types.ts | 23 ++ .../component/decorators/interface/watcher.ts | 37 ++ 8 files changed, 398 insertions(+), 334 deletions(-) create mode 100644 src/core/component/decorators/interface/accessor.ts create mode 100644 src/core/component/decorators/interface/field.ts create mode 100644 src/core/component/decorators/interface/hook.ts create mode 100644 src/core/component/decorators/interface/method.ts create mode 100644 src/core/component/decorators/interface/prop.ts create mode 100644 src/core/component/decorators/interface/types.ts create mode 100644 src/core/component/decorators/interface/watcher.ts diff --git a/src/core/component/decorators/interface/accessor.ts b/src/core/component/decorators/interface/accessor.ts new file mode 100644 index 0000000000..5f718cd49f --- /dev/null +++ b/src/core/component/decorators/interface/accessor.ts @@ -0,0 +1,51 @@ +/*! + * 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/component/interface'; +import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; + +export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { + /** + * If true, the accessor value will be cached every time it read or changed + */ + cache?: boolean; + + /** + * If true, mutations of the accessor value can be watched + */ + watchable?: boolean; + + /** + * A list of dependencies for the accessor. + * The dependencies are needed to watch for changes of the accessor or to invalidate the cache. + * + * Also, when the accessor has a logically connected prop/field + * (by using a name convention "${property} -> ${property}Prop | ${property}Store"), + * we don't need to add additional dependencies. + * + * @example + * ```typescript + * @component() + * class Foo extends iBlock { + * @field() + * blaStore: number = 0; + * + * @computed({cache: true, dependencies: ['blaStore']}) + * get bar(): number { + * return this.blaStore * 2; + * } + * + * @computed({cache: true}) + * get bla(): number { + * return blaStore * 3; + * } + * } + * ``` + */ + dependencies?: WatchPath[]; +} diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts new file mode 100644 index 0000000000..60df2a78cb --- /dev/null +++ b/src/core/component/decorators/interface/field.ts @@ -0,0 +1,105 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +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'; + +export interface DecoratorSystem< + CTX extends ComponentInterface = ComponentInterface, + A = unknown, + B = A +> extends DecoratorFunctionalOptions { + /** + * If true, then the property is unique for each component. + * Also, the parameter can take a function that returns a boolean value. + * + * @default `false` + */ + unique?: boolean | UniqueFieldFn; + + /** + * Default value of the property + */ + default?: unknown; + + /** + * Initializer of the field value + * + * @example + * ``` + * @component() + * class Foo extends iBlock { + * @field({init: () => Math.random()}) + * bla!: number; + * } + * ``` + */ + init?: InitFieldFn; + + /** + * If true, the property will be initialized before all non-atom properties + * @default `false` + */ + atom?: boolean; + + /** + * Name or a list of names after which this property should be initialized + */ + after?: CanArray; + + /** + * Watcher for changes of the property + */ + watch?: DecoratorFieldWatcher; + + /** + * If false, the property can't be watched within a functional component + * @default `true` + */ + functionalWatching?: boolean; + + /** + * If true, then if a component will restore own state from the old component + * (it occurs when you use a functional component), the actual value will be merged with the previous. + * Also, this parameter can take a function to merge. + * + * @default `false` + */ + merge?: MergeFieldFn | boolean; + + /** + * Non-standard extra information of the field + */ + meta?: Dictionary; +} + +export interface DecoratorField< + CTX extends ComponentInterface = ComponentInterface, + A = unknown, + B = A +> extends DecoratorSystem { + /** + * If false, then changes of the property don't directly force re-rendering of a template. + * Mind, the template still can be re-rendered, but only at the initiative of the engine used. + * @default `true` + */ + forceUpdate?: boolean; +} + +export interface InitFieldFn { + (ctx: CTX['unsafe'], data: Dictionary): unknown; +} + +export interface MergeFieldFn { + (ctx: CTX['unsafe'], oldCtx: CTX, field: string, link?: string): unknown; +} + +export interface UniqueFieldFn { + (ctx: CTX['unsafe'], oldCtx: CTX): AnyToBoolean; +} diff --git a/src/core/component/decorators/interface/hook.ts b/src/core/component/decorators/interface/hook.ts new file mode 100644 index 0000000000..d44b1ef566 --- /dev/null +++ b/src/core/component/decorators/interface/hook.ts @@ -0,0 +1,25 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { Hook } from 'core/component/interface'; +import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; + +export type DecoratorHook = + Hook | + Hook[] | + DecoratorHookOptions | + DecoratorHookOptions[]; + +export type DecoratorHookOptions = { + [hook in Hook]?: DecoratorFunctionalOptions & { + /** + * Method name or a list of names after which this handler should be invoked on a registered hook event + */ + after?: CanArray; + } +}; diff --git a/src/core/component/decorators/interface/index.ts b/src/core/component/decorators/interface/index.ts index a56d1e5874..f5bb1431a3 100644 --- a/src/core/component/decorators/interface/index.ts +++ b/src/core/component/decorators/interface/index.ts @@ -6,337 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { - - Hook, - ComponentInterface, - - WatchPath, - WatchOptions, - WatchHandlerParams, - MethodWatcher - -} from 'core/component/interface'; - -export type Prop = - {(): T} | - {new(...args: any[]): T & object} | - {new(...args: string[]): Function}; - -export type PropType = CanArray< - Prop ->; - -export interface PropOptions { - /** - * Constructor of a property type or a list of constructors - * - * @example - * ```typescript - * @component() - * class Foo extends iBlock { - * @prop({type: Number}) - * bla!: number; - * - * @prop({type: [Number, String]}) - * baz!: number | string; - * } - * ``` - */ - type?: PropType; - - /** - * If false, then the property isn't required - * @default `true` - * - * @example - * ```typescript - * @component() - * class Foo extends iBlock { - * @prop({required: false}) - * bla?: number; - * - * @prop() - * baz: number = 0; - * } - * ``` - */ - required?: boolean; - - /** - * Default value for the property - * - * @example - * ```typescript - * @component() - * class Foo extends iBlock { - * @prop({default: 1}) - * bla!: number; - * - * @prop() - * baz: number = 0; - * } - * ``` - */ - default?: T | null | undefined | (() => T | null | undefined); - - /** - * If false, the property can't work within functional or flyweight components - * @default `true` - */ - functional?: boolean; - - /** - * Property validator - * - * @param value - * - * @example - * ```typescript - * @component() - * class Foo extends iBlock { - * @prop({type: Number, validator: (v) => v > 0}}) - * bla!: number; - * } - * ``` - */ - validator?(value: T): boolean; -} - -export interface InitFieldFn { - (ctx: CTX['unsafe'], data: Dictionary): unknown; -} - -export interface MergeFieldFn { - (ctx: CTX['unsafe'], oldCtx: CTX, field: string, link?: string): unknown; -} - -export interface UniqueFieldFn { - (ctx: CTX['unsafe'], oldCtx: CTX): unknown; -} - -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, - A = unknown, - B = A -> extends WatchOptions { - /** - * Handler (or a name of a component method) that is invoked on watcher events - */ - handler: string | DecoratorWatchHandler; - - /** @deprecated */ - fn?: string | DecoratorWatchHandler; - - /** - * If false, then the handler that is invoked on watcher events does not take any arguments from an event - * @default `true` - */ - provideArgs?: boolean; -} - -export type DecoratorFieldWatcher = - string | - DecoratorFieldWatcherObject | - DecoratorWatchHandler | - Array | DecoratorWatchHandler>; - -export interface DecoratorProp< - CTX extends ComponentInterface = ComponentInterface, - A = unknown, - B = A -> extends PropOptions { - /** - * If true, then the property always uses own default property when it is necessary - * @default `false` - */ - forceDefault?: boolean; - - /** - * Watcher for changes of the property - */ - watch?: DecoratorFieldWatcher; - - /** - * Additional information about the property - */ - meta?: Dictionary; -} - -export interface DecoratorSystem< - CTX extends ComponentInterface = ComponentInterface -> extends DecoratorFunctionalOptions { - /** - * If true, the property will be initialized before all non-atom properties - * @default `false` - */ - atom?: boolean; - - /** - * Default value for the property - */ - default?: unknown; - - /** - * If true, then the property is unique for a component. - * Also, the parameter can take a function that returns a boolean value. - * @default `false` - */ - unique?: boolean | UniqueFieldFn; - - /** - * If false, the property can't be watched within a functional component - * @default `true` - */ - functionalWatching?: boolean; - - /** - * Name or list of names after which this property should be initialized - */ - after?: CanArray; - - /** - * Initializer (constructor) of a value. - * This property is useful for complex values. - * - * @example - * ``` - * @component() - * class Foo extends iBlock { - * @field({init: () => Math.random()}) - * bla!: number; - * } - * ``` - */ - init?: InitFieldFn; - - /** - * If true, then if a component will restore own state from an old component - * (it occurs when you use a functional component), the actual value will be merged with the previous. - * Also, this parameter can take a function to merge. - * - * @default `false` - */ - merge?: MergeFieldFn | boolean; - - /** - * Additional information about the property - */ - meta?: Dictionary; -} - -export interface DecoratorField< - CTX extends ComponentInterface = ComponentInterface, - A = unknown, - B = A -> extends DecoratorSystem { - /** - * Watcher for changes of the property - */ - watch?: DecoratorFieldWatcher; - - /** - * If false, then changes of the property don't force direct re-render - * @default `true` - */ - forceUpdate?: boolean; -} - -export interface DecoratorFunctionalOptions { - /** - * If false, the instance can't be used with functional components - * @default `true` - */ - functional?: boolean; -} - -export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { - /** - * If true, a value of the accessor can be watched - */ - watchable?: boolean; - - /** - * If true, a value of the accessor will be cached - */ - cache?: boolean; - - /** - * List of dependencies for the accessor. - * The dependencies are needed to watch for changes of the accessor or to invalidate the cache. - * - * Also, when the accessor has a logically connected prop/field - * (by using a name convention "${property} -> ${property}Prop | ${property}Store"), - * we don't need to add additional dependencies. - * - * @example - * ```typescript - * @component() - * class Foo extends iBlock { - * @field() - * blaStore: number = 0; - * - * @computed({cache: true, dependencies: ['blaStore']}) - * get bar(): number { - * return this.blaStore * 2; - * } - * - * @computed({cache: true}) - * get bla(): number { - * return blaStore * 3; - * } - * } - * ``` - */ - dependencies?: WatchPath[]; -} - -export type DecoratorHookOptions = { - [hook in Hook]?: DecoratorFunctionalOptions & { - /** - * Method name or list of method names after which the method should be invoked on a hook event - */ - after?: CanArray; - } -}; - -export type DecoratorHook = - Hook | - Hook[] | - DecoratorHookOptions | - DecoratorHookOptions[]; - -export type DecoratorMethodWatcher = - string | - MethodWatcher | - Array>; - -export interface DecoratorMethod { - /** - * Watcher for changes of some properties - */ - watch?: DecoratorMethodWatcher; - - /** - * Parameters for watcher - */ - watchParams?: MethodWatcher; - - /** - * Hook or a list of hooks after which the method should be invoked - */ - hook?: DecoratorHook; -} - -export interface ParamsFactoryTransformer { - (params: object, cluster: string): Dictionary; -} - -export interface FactoryTransformer { - (params?: T): Function; -} +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 new file mode 100644 index 0000000000..73917aceb2 --- /dev/null +++ b/src/core/component/decorators/interface/method.ts @@ -0,0 +1,32 @@ +/*! + * 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 { + /** + * Watcher for changes of some properties + */ + watch?: DecoratorMethodWatcher; + + /** + * Parameters for watcher + */ + watchParams?: MethodWatcher; + + /** + * Hook or a list of hooks after which the method should be invoked + */ + hook?: DecoratorHook; +} + +export type DecoratorMethodWatcher = + string | + MethodWatcher | + Array>; diff --git a/src/core/component/decorators/interface/prop.ts b/src/core/component/decorators/interface/prop.ts new file mode 100644 index 0000000000..25e28e3b93 --- /dev/null +++ b/src/core/component/decorators/interface/prop.ts @@ -0,0 +1,118 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentInterface } from 'core/component/interface'; +import type { DecoratorFieldWatcher } from 'core/component/decorators/interface/watcher'; + +/** + * Options of a component prop + */ +export interface PropOptions { + /** + * Property type constructor or a list of constructors (if the property can have several types) + * + * @example + * ```typescript + * @component() + * class Foo extends iBlock { + * @prop({type: Number}) + * bla!: number; + * + * @prop({type: [Number, String]}) + * baz!: number | string; + * } + * ``` + */ + type?: PropType; + + /** + * Should or not the property has always a value + * @default `true` + * + * @example + * ```typescript + * @component() + * class Foo extends iBlock { + * @prop({required: false}) + * bla?: number; + * + * @prop() + * baz: number = 0; + * } + * ``` + */ + required?: boolean; + + /** + * Default value for the property + * + * @example + * ```typescript + * @component() + * class Foo extends iBlock { + * @prop({default: 1}) + * bla!: number; + * + * @prop() + * baz: number = 0; + * } + * ``` + */ + default?: T | null | undefined | (() => T | null | undefined); + + /** + * If false, the property can't work within functional or flyweight components + * @default `true` + */ + functional?: boolean; + + /** + * Property validator + * + * @param value + * + * @example + * ```typescript + * @component() + * class Foo extends iBlock { + * @prop({type: Number, validator: (v) => v > 0}}) + * bla!: number; + * } + * ``` + */ + validator?(value: T): boolean; +} + +export interface DecoratorProp< + CTX extends ComponentInterface = ComponentInterface, + A = unknown, + B = A + > extends PropOptions { + /** + * If true, then the property always uses own default property when it is necessary + * @default `false` + */ + forceDefault?: boolean; + + /** + * Watcher for changes of the property + */ + watch?: DecoratorFieldWatcher; + + /** + * Additional information about the property + */ + meta?: Dictionary; +} + +export type Prop = + {(): T} | + {new(...args: any[]): T & object} | + {new(...args: string[]): Function}; + +export type PropType = CanArray>; diff --git a/src/core/component/decorators/interface/types.ts b/src/core/component/decorators/interface/types.ts new file mode 100644 index 0000000000..b09957dd40 --- /dev/null +++ b/src/core/component/decorators/interface/types.ts @@ -0,0 +1,23 @@ +/*! + * 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 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/interface/watcher.ts b/src/core/component/decorators/interface/watcher.ts new file mode 100644 index 0000000000..1d1697392f --- /dev/null +++ b/src/core/component/decorators/interface/watcher.ts @@ -0,0 +1,37 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentInterface, WatchOptions, WatchHandlerParams } from 'core/component/interface'; + +export interface DecoratorFieldWatcherObject< + CTX extends ComponentInterface = ComponentInterface, + A = unknown, + B = A +> extends WatchOptions { + /** + * Handler (or a name of a component method) that is invoked on watcher events + */ + handler: string | DecoratorWatchHandler; + + /** + * If false, then a handler that is invoked on the watcher event does not take any arguments from the event + * @default `true` + */ + provideArgs?: boolean; +} + +export interface DecoratorWatchHandler { + (ctx: CTX['unsafe'], a: A, b: B, params?: WatchHandlerParams): unknown; + (ctx: CTX['unsafe'], ...args: A[]): unknown; +} + +export type DecoratorFieldWatcher = + string | + DecoratorFieldWatcherObject | + DecoratorWatchHandler | + Array | DecoratorWatchHandler>; From 9bf3d44cd7c64ab0268f57cf077fb180e0f8ea01 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 15:17:43 +0300 Subject: [PATCH 0052/2313] refactor: split decorators into separated files --- src/core/component/decorators/computed.ts | 28 ++ .../decorators/{base.ts => factory.ts} | 69 +++-- src/core/component/decorators/field.ts | 36 +++ src/core/component/decorators/hook.ts | 28 ++ src/core/component/decorators/index.ts | 280 +----------------- src/core/component/decorators/p.ts | 34 +++ src/core/component/decorators/prop.ts | 42 +++ src/core/component/decorators/system.ts | 36 +++ src/core/component/decorators/watch.ts | 122 ++++++++ 9 files changed, 376 insertions(+), 299 deletions(-) create mode 100644 src/core/component/decorators/computed.ts rename src/core/component/decorators/{base.ts => factory.ts} (87%) create mode 100644 src/core/component/decorators/field.ts create mode 100644 src/core/component/decorators/hook.ts create mode 100644 src/core/component/decorators/p.ts create mode 100644 src/core/component/decorators/prop.ts create mode 100644 src/core/component/decorators/system.ts create mode 100644 src/core/component/decorators/watch.ts diff --git a/src/core/component/decorators/computed.ts b/src/core/component/decorators/computed.ts new file mode 100644 index 0000000000..0a03f346fc --- /dev/null +++ b/src/core/component/decorators/computed.ts @@ -0,0 +1,28 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { paramsFactory } from 'core/component/decorators/factory'; +import type { DecoratorComponentAccessor } from 'core/component/decorators/interface'; + +/** + * Attaches meta information to a component computed field or accessor + * + * @decorator + * + * @example + * ```typescript + * @component() + * class bExample extends iBlock { + * @computed({cache: true}) + * get foo() { + * return 42; + * } + * } + * ``` + */ +export const computed = paramsFactory(null); diff --git a/src/core/component/decorators/base.ts b/src/core/component/decorators/factory.ts similarity index 87% rename from src/core/component/decorators/base.ts rename to src/core/component/decorators/factory.ts index ee9c47d56e..4730949f17 100644 --- a/src/core/component/decorators/base.ts +++ b/src/core/component/decorators/factory.ts @@ -7,15 +7,22 @@ */ import { defProp } from 'core/const/props'; + +import { storeRgxp } from 'core/component/reflect'; import { initEmitter, metaPointers } from 'core/component/const'; import { inverseFieldMap, tiedFieldMap } from 'core/component/decorators/const'; -import { storeRgxp } from 'core/component/reflection'; import type { ComponentMeta } from 'core/component/interface'; -import type { ParamsFactoryTransformer, FactoryTransformer } from 'core/component/decorators/interface'; +import type { + + DecoratorFunctionalOptions, + ParamsFactoryTransformer, + FactoryTransformer + +} from 'core/component/decorators/interface'; /** - * Factory to create component property decorators + * Factory to create a component property decorator * * @param cluster - property cluster * @param [transformer] - transformer for parameters @@ -29,34 +36,23 @@ export function paramsFactory( metaPointers[componentName] = metaPointers[componentName] ?? Object.createDict(); const - link = metaPointers[componentName]!; + link = metaPointers[componentName]; + + if (link == null) { + return; + } link[key] = true; initEmitter.once(`constructor.${componentName}`, reg); }); function reg({meta}: {meta: ComponentMeta}): void { - const wrapOpts = (opts) => { - const - p = meta.params; - - if (opts.replace === undefined && p.flyweight) { - opts.replace = false; - } - - // eslint-disable-next-line eqeqeq - if (opts.functional === undefined && p.functional === null) { - opts.functional = false; - } - - return opts; - }; + delete meta.tiedFields[key]; let p = params; - delete meta.tiedFields[key]; - + // Decorator for a method or accessor if (desc != null) { delete meta.props[key]; delete meta.fields[key]; @@ -93,8 +89,10 @@ export function paramsFactory( const name = key; - let - {watchers, hooks} = info; + let { + watchers, + hooks + } = info; if (p.watch != null) { watchers ??= {}; @@ -160,12 +158,15 @@ export function paramsFactory( return; } + // Decorator for a prop or field + delete meta.methods[key]; delete meta.accessors[key]; delete meta.computedFields[key]; - const - accessors = meta.accessors[key] ? meta.accessors : meta.computedFields; + const accessors = meta.accessors[key] ? + meta.accessors : + meta.computedFields; if (accessors[key]) { Object.defineProperty(meta.constructor.prototype, key, defProp); @@ -197,8 +198,10 @@ export function paramsFactory( const info = metaCluster[key] ?? {src: meta.componentName}; - let - {watchers, after} = info; + let { + watchers, + after + } = info; if (p.after != null) { after = new Set([].concat(p.after)); @@ -236,6 +239,18 @@ export function paramsFactory( if (tiedFieldMap[metaKey] != null && RegExp.test(storeRgxp, key)) { meta.tiedFields[key] = key.replace(storeRgxp, ''); } + + 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.ts b/src/core/component/decorators/field.ts new file mode 100644 index 0000000000..e312566830 --- /dev/null +++ b/src/core/component/decorators/field.ts @@ -0,0 +1,36 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +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 regular components, mutations of field properties force component re-rendering. + * + * @decorator + * + * @example + * ```typescript + * @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; +}); diff --git a/src/core/component/decorators/hook.ts b/src/core/component/decorators/hook.ts new file mode 100644 index 0000000000..f9ffa58aba --- /dev/null +++ b/src/core/component/decorators/hook.ts @@ -0,0 +1,28 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { paramsFactory } from 'core/component/decorators/factory'; +import type { DecoratorHook } from 'core/component/decorators/interface'; + +/** + * Attaches a hook listener to a component method. + * It means, that when a component is switched to the specified hook/s, the method will be invoked. + * + * @decorator + * @example + * ```typescript + * @component() + * class Foo extends iBlock { + * @hook('mounted') + * onMounted() { + * + * } + * } + * ``` + */ +export const hook = paramsFactory(null, (hook) => ({hook})); diff --git a/src/core/component/decorators/index.ts b/src/core/component/decorators/index.ts index 57b6fa0652..e4c5c2a6b1 100644 --- a/src/core/component/decorators/index.ts +++ b/src/core/component/decorators/index.ts @@ -11,279 +11,15 @@ * @packageDocumentation */ -import { paramsFactory } from 'core/component/decorators/base'; +export * from 'core/component/decorators/factory'; +export * from 'core/component/decorators/p'; -import type { +export * from 'core/component/decorators/prop'; +export * from 'core/component/decorators/field'; +export * from 'core/component/decorators/system'; +export * from 'core/component/decorators/computed'; - InitFieldFn, +export * from 'core/component/decorators/hook'; +export * from 'core/component/decorators/watch'; - DecoratorProp, - DecoratorSystem, - DecoratorField, - - DecoratorComponentAccessor, - DecoratorMethod, - - DecoratorHook, - DecoratorFieldWatcher, - DecoratorMethodWatcher - -} from 'core/component/decorators/interface'; - -export * from 'core/component/decorators/base'; export * from 'core/component/decorators/interface'; - -/** - * Marks a class property as a component prop - * - * @decorator - * - * @example - * ```typescript - * @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; -}); - -/** - * Marks a class property as a component field. - * In a regular component mutation of field properties force component re-rendering. - * - * @decorator - * - * @example - * ```typescript - * @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; -}); - -/** - * Marks a class property as a system field. - * Mutations of system properties never force component re-rendering. - * - * @decorator - * - * @example - * ```typescript - * @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; -}); - -/** - * Attaches extra meta information to a component computed field or accessor - * - * @decorator - * - * @example - * ```typescript - * @component() - * class bExample extends iBlock { - * @computed({cache: true}) - * get foo() { - * return 42; - * } - * } - * ``` - */ -export const computed = paramsFactory(null); - -/** - * The universal decorator for a component property/accessor/method - * - * @decorator - * - * @example - * ```typescript - * @component() - * class bExample extends iBlock { - * @p({cache: true}) - * get foo() { - * return 42; - * } - * } - * ``` - */ -export const p = paramsFactory< - DecoratorProp | - DecoratorField | - DecoratorMethod | - DecoratorComponentAccessor ->(null); - -/** - * Attaches a hook listener to a component method. - * It means, that when a component is switched to the specified hook/s, the method will be invoked. - * - * @decorator - * @example - * ```typescript - * @component() - * class Foo extends iBlock { - * @hook('mounted') - * onMounted() { - * - * } - * } - * ``` - */ -export const hook = paramsFactory(null, (hook) => ({hook})); - -/** - * Attaches a watcher of a component property/event to a component method or property. - * - * When you watch for some property changes, the handler function can take the second argument - * that refers to the old value of a property. If the object that watching is non-primitive, - * the old value will be cloned from the original old value to avoid the problem when we have two - * links to the one object. - * - * ```typescript - * @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]); - * } - * - * // When you don't declare the second argument in a watcher, - * // the previous value isn't cloned - * @watch('list') - * onListChangeWithoutCloning(value: Dictionary[]): void { - * // true - * console.log(value === arguments[1]); - * console.log(value[0] === oldValue[0]); - * } - * - * // When you watch a property in a deep and declare the second argument - * // in a watcher, the previous value is cloned deeply - * @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 an event you need to use the special delimiter ":" within a path. - * Also, you can specify an event emitter to listen by writing a link before ":". - * For instance: - * - * 1. `':onChange'` - a component will listen own event "onChange"; - * 2. `'localEmitter:onChange'` - a component will listen an event "onChange" from "localEmitter"; - * 3. `'$parent.localEmitter:onChange'` - a component will listen an event "onChange" from "$parent.localEmitter"; - * 4. `'document:scroll'` - a component will listen an event "scroll" from "window.document". - * - * A link to the event emitter is taken from component properties or from the global object. - * The empty link '' is a link to a component itself. - * - * Also, if you listen an event, you can manage when start to listen the event by using special characters at the - * beginning of a path string: - * - * 1. `'!'` - start to listen an event on the "beforeCreate" hook, for example: `'!rootEmitter:reset'`; - * 2. `'?'` - start to listen an event on the "mounted" hook, for example: `'?$el:click'`. - * - * By default, all events start to listen on the "created" hook. - * - * @decorator - * - * @example - * ```typescript - * @component() - * class bExample extends iBlock { - * @field() - * foo: Dictionary = {bla: 0}; - * - * // Watch for changes of "foo" - * @watch('foo') - * watcher1() { - * - * } - * - * // Deep watch for changes of "foo" - * @watch({path: 'foo', deep: true}}) - * watcher2() { - * - * } - * - * // Watch for changes of "foo.bla" - * @watch('foo.bla') - * watcher3() { - * - * } - * - * // Listen "onChange" event of a component - * @watch(':onChange') - * watcher3() { - * - * } - * - * // Listen "onChange" event of a component parentEmitter - * @watch('parentEmitter:onChange') - * watcher4() { - * - * } - * } - * ``` - */ -export const watch = paramsFactory< - DecoratorFieldWatcher | - DecoratorMethodWatcher ->(null, (watch) => ({watch})); diff --git a/src/core/component/decorators/p.ts b/src/core/component/decorators/p.ts new file mode 100644 index 0000000000..66ed3fd8e2 --- /dev/null +++ b/src/core/component/decorators/p.ts @@ -0,0 +1,34 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { paramsFactory } from 'core/component/decorators/factory'; +import type { DecoratorProp, DecoratorField, DecoratorMethod, DecoratorComponentAccessor } from 'core/component/decorators/interface'; + +/** + * The universal decorator for a component property/accessor/method + * + * @decorator + * + * @example + * ```typescript + * @component() + * class bExample extends iBlock { + * @p({cache: true}) + * get foo() { + * return 42; + * } + * } + * ``` + */ +export const p = paramsFactory< + DecoratorProp | + DecoratorField | + DecoratorMethod | + DecoratorComponentAccessor +>(null); + diff --git a/src/core/component/decorators/prop.ts b/src/core/component/decorators/prop.ts new file mode 100644 index 0000000000..90d4b49e97 --- /dev/null +++ b/src/core/component/decorators/prop.ts @@ -0,0 +1,42 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { paramsFactory } from 'core/component/decorators/factory'; +import type { DecoratorProp } from 'core/component/decorators/interface'; + +/** + * Marks a class property as a component prop + * + * @decorator + * + * @example + * ```typescript + * @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; +}); diff --git a/src/core/component/decorators/system.ts b/src/core/component/decorators/system.ts new file mode 100644 index 0000000000..7b08ba2550 --- /dev/null +++ b/src/core/component/decorators/system.ts @@ -0,0 +1,36 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { paramsFactory } from 'core/component/decorators/factory'; +import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/interface'; + +/** + * Marks a class property as a system field. + * Mutations of system properties never force component re-rendering. + * + * @decorator + * + * @example + * ```typescript + * @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; +}); diff --git a/src/core/component/decorators/watch.ts b/src/core/component/decorators/watch.ts new file mode 100644 index 0000000000..144cc82bea --- /dev/null +++ b/src/core/component/decorators/watch.ts @@ -0,0 +1,122 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +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 watch for some property changes, the handler function can take the second argument + * that refers to the old value of a property. If the object that watching is non-primitive, + * the old value will be cloned from the original old value to avoid the problem when we have two + * links to the one object. + * + * ```typescript + * @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]); + * } + * + * // When you don't declare the second argument in a watcher, + * // the previous value isn't cloned + * @watch('list') + * onListChangeWithoutCloning(value: Dictionary[]): void { + * // true + * console.log(value === arguments[1]); + * console.log(value[0] === oldValue[0]); + * } + * + * // When you watch a property in deep and declare the second argument + * // in a watcher, the previous value is cloned deeply + * @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 an event you need to use the special delimiter ":" within a path. + * Also, you can specify an event emitter to listen by writing a link before ":". + * For instance: + * + * 1. `':onChange'` - a component will listen its own event `onChange`; + * 2. `'localEmitter:onChange'` - a component will listen an event `onChange` from `localEmitter`; + * 3. `'$parent.localEmitter:onChange'` - a component will listen an event `onChange` from `$parent.localEmitter`; + * 4. `'document:scroll'` - a component will listen an event `scroll` from `window.document`. + * + * A link to the event emitter is taken from component properties or from the global object. + * The empty link '' is a link to a component itself. + * + * Also, if you listen an event, you can manage when start to listen the event by using special characters at the + * beginning of a path string: + * + * 1. `'!'` - start to listen an event on the "beforeCreate" hook, for example: `'!rootEmitter:reset'`; + * 2. `'?'` - start to listen an event on the "mounted" hook, for example: `'?$el:click'`. + * + * By default, all events start to listen on the "created" hook. + * + * @decorator + * + * @example + * ```typescript + * @component() + * class bExample extends iBlock { + * @field() + * foo: Dictionary = {bla: 0}; + * + * // Watch for changes of "foo" + * @watch('foo') + * watcher1() { + * + * } + * + * // Deep watch for changes of "foo" + * @watch({path: 'foo', deep: true}}) + * watcher2() { + * + * } + * + * // Watch for changes of "foo.bla" + * @watch('foo.bla') + * watcher3() { + * + * } + * + * // Listen "onChange" event of a component + * @watch(':onChange') + * watcher3() { + * + * } + * + * // Listen "onChange" event of a component parentEmitter + * @watch('parentEmitter:onChange') + * watcher4() { + * + * } + * } + * ``` + */ +export const watch = paramsFactory< + DecoratorFieldWatcher | + DecoratorMethodWatcher +>(null, (watch) => ({watch})); From 47833ba827da00c4e5cff607e5922053506a1d63 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 15:18:03 +0300 Subject: [PATCH 0053/2313] chore: updated logs & doc --- src/core/component/decorators/CHANGELOG.md | 6 ++++++ src/core/component/decorators/README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/component/decorators/CHANGELOG.md b/src/core/component/decorators/CHANGELOG.md index 1db9884887..44656fc6a8 100644 --- a/src/core/component/decorators/CHANGELOG.md +++ b/src/core/component/decorators/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.0.0-rc.199 (2021-06-16) #### :bug: Bug Fix diff --git a/src/core/component/decorators/README.md b/src/core/component/decorators/README.md index d6d029d44d..c90ae5e4fc 100644 --- a/src/core/component/decorators/README.md +++ b/src/core/component/decorators/README.md @@ -9,7 +9,7 @@ export default class bExample { @watch('foo') onFoo() { - console.log('Foo was changed'); + console.log('`Foo` was changed'); } } ``` From 1c1e8857e5b0bb8deb85db3c0f51333a0764e19a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 16:06:41 +0300 Subject: [PATCH 0054/2313] refactor: migration to Vue3 --- .../component/directives/hook/CHANGELOG.md | 6 ++ .../component/directives/image/CHANGELOG.md | 6 ++ src/core/component/directives/image/index.ts | 6 +- .../component/directives/image/interface.ts | 7 +- .../component/directives/in-view/CHANGELOG.md | 6 ++ .../component/directives/in-view/index.ts | 4 +- .../directives/resize-observer/CHANGELOG.md | 6 ++ .../directives/resize-observer/helpers.ts | 47 +++++++++++++ .../directives/resize-observer/index.ts | 67 ++++--------------- .../directives/resize-observer/interface.ts | 7 +- .../directives/update-on/CHANGELOG.md | 6 ++ .../component/directives/update-on/README.md | 2 +- .../directives/update-on/engines/index.ts | 4 +- .../component/directives/update-on/index.ts | 6 +- .../directives/update-on/interface.ts | 26 +++---- 15 files changed, 119 insertions(+), 87 deletions(-) create mode 100644 src/core/component/directives/resize-observer/helpers.ts diff --git a/src/core/component/directives/hook/CHANGELOG.md b/src/core/component/directives/hook/CHANGELOG.md index a339ebb48e..4f347c1a34 100644 --- a/src/core/component/directives/hook/CHANGELOG.md +++ b/src/core/component/directives/hook/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Migration to Vue3 + ## v3.0.0-rc.97 (2020-11-11) #### :rocket: New Feature diff --git a/src/core/component/directives/image/CHANGELOG.md b/src/core/component/directives/image/CHANGELOG.md index e162b5bd6a..4339862257 100644 --- a/src/core/component/directives/image/CHANGELOG.md +++ b/src/core/component/directives/image/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Migration to Vue3 + ## v3.0.0-rc.63 (2020-09-10) #### :house: Internal diff --git a/src/core/component/directives/image/index.ts b/src/core/component/directives/image/index.ts index 8d6b7161d4..56164d7ddc 100644 --- a/src/core/component/directives/image/index.ts +++ b/src/core/component/directives/image/index.ts @@ -19,7 +19,7 @@ import type { DirectiveOptions } from 'core/component/directives/image/interface export * from 'core/dom/image'; ComponentEngine.directive('image', { - inserted(el: HTMLElement, {value}: DirectiveOptions, vnode: VNode): void { + mounted(el: HTMLElement, {value}: DirectiveOptions, vnode: VNode): void { if (value == null) { return; } @@ -39,11 +39,11 @@ ComponentEngine.directive('image', { ImageLoader.init(el, value); }, - update(el: HTMLElement, {value, oldValue}: DirectiveOptions): void { + updated(el: HTMLElement, {value, oldValue}: DirectiveOptions): void { ImageLoader.update(el, value, oldValue); }, - unbind(el: HTMLElement): void { + unmounted(el: HTMLElement): void { ImageLoader.clearElement(el); } }); diff --git a/src/core/component/directives/image/interface.ts b/src/core/component/directives/image/interface.ts index 1fa56fb216..868064c0c8 100644 --- a/src/core/component/directives/image/interface.ts +++ b/src/core/component/directives/image/interface.ts @@ -6,13 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNodeDirective } from 'core/component/engines'; import type { InitValue } from 'core/dom/image'; +import type { DirectiveBinding } from 'core/component/engines'; -export interface DirectiveOptions extends VNodeDirective { +export interface DirectiveOptions extends DirectiveBinding> { modifiers: { [key: string]: boolean; }; - value?: InitValue; + value: CanUndef; + oldValue: CanUndef; } diff --git a/src/core/component/directives/in-view/CHANGELOG.md b/src/core/component/directives/in-view/CHANGELOG.md index aad46e4cfa..3da96045b4 100644 --- a/src/core/component/directives/in-view/CHANGELOG.md +++ b/src/core/component/directives/in-view/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Migration to Vue3 + ## v3.0.0-rc.60 (2020-09-01) #### :house: Internal diff --git a/src/core/component/directives/in-view/index.ts b/src/core/component/directives/in-view/index.ts index 47c608c958..922b44fa4c 100644 --- a/src/core/component/directives/in-view/index.ts +++ b/src/core/component/directives/in-view/index.ts @@ -17,7 +17,7 @@ import { InView, Adaptee, InViewDirectiveOptions } from 'core/dom/in-view'; export * from 'core/dom/in-view'; ComponentEngine.directive('in-view', { - inserted(el: Element, {value}: InViewDirectiveOptions): void { + mounted(el: Element, {value}: InViewDirectiveOptions): void { if (!Adaptee || !value) { return; } @@ -25,7 +25,7 @@ ComponentEngine.directive('in-view', { InView.observe(el, value); }, - unbind(el: Element): void { + unmounted(el: Element): void { InView.remove(el); } }); diff --git a/src/core/component/directives/resize-observer/CHANGELOG.md b/src/core/component/directives/resize-observer/CHANGELOG.md index 38a4a5ff80..9d0f258d3a 100644 --- a/src/core/component/directives/resize-observer/CHANGELOG.md +++ b/src/core/component/directives/resize-observer/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Migration to Vue3 + ## v3.0.0-rc.82 (2020-10-08) #### :bug: Bug Fix diff --git a/src/core/component/directives/resize-observer/helpers.ts b/src/core/component/directives/resize-observer/helpers.ts new file mode 100644 index 0000000000..c1d71b92a1 --- /dev/null +++ b/src/core/component/directives/resize-observer/helpers.ts @@ -0,0 +1,47 @@ +/*! +* V4Fire Client Core +* https://github.com/V4Fire/Client +* +* Released under the MIT license +* https://github.com/V4Fire/Client/blob/master/LICENSE +*/ + +import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; +import { DIRECTIVE_BIND } from 'core/component/directives/resize-observer/const'; + +import type { ComponentInterface } from 'core/component/interface'; +import type { ResizeWatcherObservable, ResizeWatcherObserverOptions } from 'core/component/directives/resize-observer/interface'; + +/** + * Sets a flag which indicates that the specified observable was created via the directive + * @param observable + */ +export function setCreatedViaDirectiveFlag(observable: Nullable): void { + if (observable == null) { + return; + } + + observable[DIRECTIVE_BIND] = true; +} + +/** + * Normalizes the specified directive options + * + * @param opts + * @param ctx + */ +export function normalizeOptions( + opts: ResizeWatcherInitOptions, + ctx: CanUndef +): ResizeWatcherObserverOptions { + return Object.isFunction(opts) ? + { + callback: opts, + ctx + } : + + { + ...opts, + ctx: opts.ctx ?? ctx + }; +} diff --git a/src/core/component/directives/resize-observer/index.ts b/src/core/component/directives/resize-observer/index.ts index b1e893afa7..faf33f9932 100644 --- a/src/core/component/directives/resize-observer/index.ts +++ b/src/core/component/directives/resize-observer/index.ts @@ -6,26 +6,20 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import { ResizeWatcher, ResizeWatcherInitOptions } from 'core/dom/resize-observer'; import { ComponentEngine, VNode } from 'core/component/engines'; -import type { ComponentInterface } from 'core/component/interface'; -import { ResizeWatcher, ResizeWatcherInitOptions } from 'core/dom/resize-observer'; import { DIRECTIVE_BIND } from 'core/component/directives/resize-observer/const'; +import { normalizeOptions, setCreatedViaDirectiveFlag } from 'core/component/directives/resize-observer/helpers'; -import type { - - DirectiveOptions, - ResizeWatcherObservable, - ResizeWatcherObserverOptions +import type { DirectiveOptions, ResizeWatcherObservable } from 'core/component/directives/resize-observer/interface'; -} from 'core/component/directives/resize-observer/interface'; - -export * from 'core/component/directives/resize-observer/interface'; -export * from 'core/component/directives/resize-observer/const'; export * from 'core/dom/resize-observer'; +export * from 'core/component/directives/resize-observer/const'; +export * from 'core/component/directives/resize-observer/interface'; ComponentEngine.directive('resize-observer', { - inserted(el: HTMLElement, opts: DirectiveOptions, vnode: VNode): void { + mounted(el: HTMLElement, opts: DirectiveOptions, vnode: VNode): void { const val = opts.value; @@ -39,7 +33,7 @@ ComponentEngine.directive('resize-observer', { }); }, - update(el: HTMLElement, opts: DirectiveOptions, vnode: VNode): void { + updated(el: HTMLElement, opts: DirectiveOptions, vnode: VNode): void { const oldOptions = opts.oldValue, newOptions = opts.value; @@ -49,22 +43,22 @@ ComponentEngine.directive('resize-observer', { } if (Array.isArray(oldOptions)) { - oldOptions.forEach((options) => { - ResizeWatcher.unobserve(el, options); + (oldOptions).forEach((opts) => { + ResizeWatcher.unobserve(el, opts); }); } - Array.concat([], newOptions).forEach((options: CanUndef) => { - if (options == null) { + Array.concat([], newOptions).forEach((opts) => { + if (opts == null) { return; } - options = normalizeOptions(options, vnode.fakeContext); - setCreatedViaDirectiveFlag(ResizeWatcher.observe(el, options)); + opts = normalizeOptions(opts, vnode.fakeContext); + setCreatedViaDirectiveFlag(ResizeWatcher.observe(el, opts)); }); }, - unbind(el: HTMLElement): void { + unmounted(el: HTMLElement): void { const store = ResizeWatcher.getObservableElStore(el); @@ -80,36 +74,3 @@ ComponentEngine.directive('resize-observer', { } }); -/** - * Sets a flag which indicates that the specified observable was created via the directive - * @param observable - */ -function setCreatedViaDirectiveFlag(observable: Nullable): void { - if (observable == null) { - return; - } - - observable[DIRECTIVE_BIND] = true; -} - -/** - * Normalizes the specified directive options - * - * @param opts - * @param ctx - */ -function normalizeOptions( - opts: ResizeWatcherInitOptions, - ctx: CanUndef -): ResizeWatcherObserverOptions { - return Object.isFunction(opts) ? - { - callback: opts, - ctx - } : - - { - ...opts, - ctx: opts.ctx ?? ctx - }; -} diff --git a/src/core/component/directives/resize-observer/interface.ts b/src/core/component/directives/resize-observer/interface.ts index a2554a63bd..a45bdce2a2 100644 --- a/src/core/component/directives/resize-observer/interface.ts +++ b/src/core/component/directives/resize-observer/interface.ts @@ -6,15 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNodeDirective } from 'core/component/engines'; +import type { DirectiveBinding } from 'core/component/engines'; import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; export * from 'core/dom/resize-observer/interface'; -export interface DirectiveOptions extends VNodeDirective { +export interface DirectiveOptions extends DirectiveBinding { modifiers: { [key: string]: boolean; }; - value?: CanArray; + value: CanUndef>; + oldValue: CanUndef>; } diff --git a/src/core/component/directives/update-on/CHANGELOG.md b/src/core/component/directives/update-on/CHANGELOG.md index 9f94bca7a6..293deb5390 100644 --- a/src/core/component/directives/update-on/CHANGELOG.md +++ b/src/core/component/directives/update-on/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Migration to Vue3 + ## v3.0.0-rc.164 (2021-03-22) #### :house: Internal diff --git a/src/core/component/directives/update-on/README.md b/src/core/component/directives/update-on/README.md index d8847a47f0..d4981057e2 100644 --- a/src/core/component/directives/update-on/README.md +++ b/src/core/component/directives/update-on/README.md @@ -1,7 +1,7 @@ # core/component/directives/update-on This module provides a directive to manually update an element using various event(s) from multiple emitters. -Use this directive if you want to update some parts of your template without re-render of a whole template or with functional components. +Use this directive if you want to update some parts of your template without re-rendering of a whole template or with functional components. ## Usage diff --git a/src/core/component/directives/update-on/engines/index.ts b/src/core/component/directives/update-on/engines/index.ts index c2e7979c9f..514f71257d 100644 --- a/src/core/component/directives/update-on/engines/index.ts +++ b/src/core/component/directives/update-on/engines/index.ts @@ -38,7 +38,7 @@ export default { group = {group: id}; const - handler = (...args) => (params.listener ?? params.handler)(el, ...args), + handler = (...args) => params.handler(el, ...args), errorHandler = (err) => params.errorHandler != null ? params.errorHandler(el, err) : stderr(err); const emitter = Object.isFunction(params.emitter) ? params.emitter() : params.emitter; @@ -63,7 +63,7 @@ export default { throw new Error('An event to listen is not specified'); } - $a[params.single ?? params.once ? 'once' : 'on'](emitter, params.event, handler, { + $a[params.single ? 'once' : 'on'](emitter, params.event, handler, { options: params.options, ...group }); diff --git a/src/core/component/directives/update-on/index.ts b/src/core/component/directives/update-on/index.ts index 7d99cac063..a57fc45050 100644 --- a/src/core/component/directives/update-on/index.ts +++ b/src/core/component/directives/update-on/index.ts @@ -18,11 +18,11 @@ export * from 'core/component/directives/update-on/interface'; * Directive to manually update an element by using special events */ ComponentEngine.directive('update-on', { - inserted(el: Element, {value}: DirectiveOptions, vnode: VNode): void { + mounted(el: Element, {value}: DirectiveOptions, vnode: VNode): void { add(el, value, vnode); }, - update(el: Element, {value, oldValue}: DirectiveOptions, vnode: VNode): void { + updated(el: Element, {value, oldValue}: DirectiveOptions, vnode: VNode): void { if (Object.fastCompare(value, oldValue)) { return; } @@ -30,7 +30,7 @@ ComponentEngine.directive('update-on', { add(el, value, vnode); }, - unbind(el: Element, opts: DirectiveOptions, vnode: VNode): void { + unmounted(el: Element, opts: DirectiveOptions, vnode: VNode): void { const ctx = vnode.fakeContext; diff --git a/src/core/component/directives/update-on/interface.ts b/src/core/component/directives/update-on/interface.ts index e2139d0e1a..51dd0f1099 100644 --- a/src/core/component/directives/update-on/interface.ts +++ b/src/core/component/directives/update-on/interface.ts @@ -7,20 +7,24 @@ */ import type { EventEmitterLike } from 'core/async'; -import type { WatchOptions, VNodeDirective } from 'core/component'; +import type { DirectiveBinding } from 'core/component/engines'; +import type { WatchOptions } from 'core/component/interface'; -export interface DirectiveOptions extends VNodeDirective { +export interface DirectiveOptions extends DirectiveBinding { modifiers: { [key: string]: boolean; }; - value?: CanArray; + value: CanUndef>; } export interface DirectiveValue { /** - * Event emitter. It can be specified as a simple event emitter, a promise, or a string. - * In the string case, the string represents the name of the component property to watch. + * The event emitter. + * + * It can be specified as a simple event emitter, a promise or string. + * In a string case, the string represents a name of the component property to watch. + * * Also, the emitter can be provided as a function. In that case, it will be invoked, * and the emitter is taken from the result. */ @@ -43,12 +47,6 @@ export interface DirectiveValue { */ single?: boolean; - /** - * @deprecated - * @see [[DirectiveValue.single]] - */ - once?: boolean; - /** * Additional options for an event emitter or watcher */ @@ -63,10 +61,4 @@ export interface DirectiveValue { * Function to handle error (if the emitter is specified as a promise) */ errorHandler?: Function; - - /** - * @deprecated - * @see [[DirectiveValue.handler]] - */ - listener?: Function; } From 517a57ef05df01d22ef80cd61e6cbd7c6a91f460 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 16:10:50 +0300 Subject: [PATCH 0055/2313] refactor: temporary removed `zero` --- src/core/component/engines/engine.ts | 5 - src/core/component/engines/zero/CHANGELOG.md | 28 -- src/core/component/engines/zero/README.md | 12 - src/core/component/engines/zero/component.ts | 274 ----------- src/core/component/engines/zero/config.ts | 22 - src/core/component/engines/zero/const.ts | 38 -- .../component/engines/zero/context/const.ts | 15 - .../component/engines/zero/context/index.ts | 333 ------------- src/core/component/engines/zero/engine.ts | 302 ------------ .../component/engines/zero/helpers/const.ts | 27 -- .../component/engines/zero/helpers/index.ts | 442 ------------------ .../engines/zero/helpers/interface.ts | 19 - src/core/component/engines/zero/index.ts | 35 -- src/core/component/engines/zero/interface.ts | 22 - src/core/component/engines/zero/vnode.ts | 55 --- 15 files changed, 1629 deletions(-) delete mode 100644 src/core/component/engines/zero/CHANGELOG.md delete mode 100644 src/core/component/engines/zero/README.md delete mode 100644 src/core/component/engines/zero/component.ts delete mode 100644 src/core/component/engines/zero/config.ts delete mode 100644 src/core/component/engines/zero/const.ts delete mode 100644 src/core/component/engines/zero/context/const.ts delete mode 100644 src/core/component/engines/zero/context/index.ts delete mode 100644 src/core/component/engines/zero/engine.ts delete mode 100644 src/core/component/engines/zero/helpers/const.ts delete mode 100644 src/core/component/engines/zero/helpers/index.ts delete mode 100644 src/core/component/engines/zero/helpers/interface.ts delete mode 100644 src/core/component/engines/zero/index.ts delete mode 100644 src/core/component/engines/zero/interface.ts delete mode 100644 src/core/component/engines/zero/vnode.ts diff --git a/src/core/component/engines/engine.ts b/src/core/component/engines/engine.ts index 92071ccf7a..132b956eec 100644 --- a/src/core/component/engines/engine.ts +++ b/src/core/component/engines/engine.ts @@ -10,10 +10,5 @@ export * from 'core/component/engines/vue3'; //#endif -//#if runtime.engine = zero -// @ts-ignore (double export) -export * from 'core/component/engines/zero'; -//#endif - export * from 'core/component/engines/interface'; export { VNode } from 'core/component/engines/interface'; diff --git a/src/core/component/engines/zero/CHANGELOG.md b/src/core/component/engines/zero/CHANGELOG.md deleted file mode 100644 index 48a89a052a..0000000000 --- a/src/core/component/engines/zero/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.0.0-rc.190 (2021-05-17) - -#### :bug: Bug Fix - -* Fixed resolving of refs - -## v3.0.0-rc.183 (2021-05-12) - -#### :bug: Bug Fix - -* Fixed a bug while initializing - -## v3.0.0-rc.180 (2021-04-16) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/component/engines/zero/README.md b/src/core/component/engines/zero/README.md deleted file mode 100644 index 770031d401..0000000000 --- a/src/core/component/engines/zero/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# core/component/engines/zero - -This module provides an adaptor to render components without any MVVM libraries, like Vue.js or React. -The adaptor can be helpful for SSR or simple landings. Obviously, such features, like data-binding and automatically -re-rendering not working with this adaptor. - -```js -import ZeroEngine from 'core/component/engines/zero'; - -// {ctx: {...}, node: Node} -console.log(await ZeroEngine.render('b-input', {value: 'Hello world'})); -``` diff --git a/src/core/component/engines/zero/component.ts b/src/core/component/engines/zero/component.ts deleted file mode 100644 index 2f5a77989f..0000000000 --- a/src/core/component/engines/zero/component.ts +++ /dev/null @@ -1,274 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; -import { HAS_WINDOW } from 'core/env'; - -import type Vue from 'vue'; -import type { ComponentOptions } from 'vue'; - -import * as init from 'core/component/construct'; - -import { fillMeta } from 'core/component/meta'; -import { createFakeCtx } from 'core/component/functional'; - -import { components } from 'core/component/const'; -import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; - -import { document, supports, minimalCtx } from 'core/component/engines/zero/const'; -import { cloneVNode, patchVNode, renderVNode } from 'core/component/engines'; - -const - $$ = symbolGenerator(); - -/** - * Returns a component declaration object from the specified meta object - * @param meta - */ -export function getComponent(meta: ComponentMeta): ComponentOptions { - const - {component} = fillMeta(meta); - - const - p = meta.params, - m = p.model; - - return { - ...Object.cast(component), - inheritAttrs: p.inheritAttrs, - - model: m && { - prop: m.prop, - event: m.event?.dasherize() ?? '' - } - }; -} - -/** - * Creates a zero component by the specified parameters and returns a tuple [node, ctx] - * - * @param component - component declaration object or a component name - * @param ctx - context of the component to create - */ -export async function createComponent( - component: ComponentOptions | string, - ctx: ComponentInterface -): Promise<[T?, ComponentInterface?]> { - const - // @ts-ignore (access) - createElement = ctx.$createElement; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (createElement == null) { - return []; - } - - const - meta = components.get(Object.isString(component) ? component : String(component.name)); - - if (meta == null) { - return []; - } - - const - {component: {render}} = meta; - - if (render == null) { - return []; - } - - const baseCtx = Object.assign(Object.create(minimalCtx), { - meta, - instance: meta.instance, - componentName: meta.componentName, - $renderEngine: { - supports, - minimalCtx, - cloneVNode, - patchVNode, - renderVNode - } - }); - - const fakeCtx = createFakeCtx(createElement, Object.create(ctx), baseCtx, { - initProps: true - }); - - init.createdState(fakeCtx); - - const - node = await render.call(fakeCtx, createElement); - - // @ts-ignore (access) - // eslint-disable-next-line require-atomic-updates - fakeCtx['$el'] = node; - node.component = fakeCtx; - - return [node, fakeCtx]; -} - -/** - * Mounts a component to the specified node - * - * @param nodeOrSelector - link to the parent node to mount or a selector - * @param componentNode - link to the rendered component node - * @param ctx - context of the component to mount - */ -export function mountComponent(nodeOrSelector: string | Node, [componentNode, ctx]: [Node, ComponentInterface]): void { - if (!HAS_WINDOW) { - return; - } - - const parentNode = Object.isString(nodeOrSelector) ? - document.querySelector(nodeOrSelector) : - nodeOrSelector; - - if (parentNode == null) { - return; - } - - const { - unsafe, - unsafe: {$async: $a} - } = ctx; - - const is = (el): boolean => - el === parentNode || - el.parentNode === parentNode || - el.contains(parentNode); - - if (typeof MutationObserver === 'function') { - const observer = new MutationObserver((mutations) => { - for (let i = 0; i < mutations.length; i++) { - const - mut = mutations[i]; - - for (let o = mut.addedNodes, j = 0; j < o.length; j++) { - const - node = o[j]; - - if (!(node instanceof Element)) { - continue; - } - - if (is(node)) { - mount(); - - } else { - const - childComponentId = getChildComponentId(node); - - if (childComponentId != null) { - unsafe.$emit(`child-component-mounted:${childComponentId}`); - } - } - } - - for (let o = mut.removedNodes, j = 0; j < o.length; j++) { - const - node = o[j]; - - if (!(node instanceof Element)) { - continue; - } - - if (is(node)) { - $a.setTimeout(() => { - if (!document.body.contains(node)) { - unsafe.$destroy(); - } - - }, 0, { - label: $$.removeFromDOM - }); - - } else { - const - childComponentId = getChildComponentId(node); - - if (childComponentId != null) { - unsafe.$emit(`child-component-destroyed:${childComponentId}`); - } - } - } - } - }); - - observer.observe(parentNode, { - childList: true, - subtree: true - }); - - $a.worker(observer); - - } else { - $a.on(parentNode, 'DOMNodeInserted', ({srcElement}) => { - if (is(srcElement)) { - mount(); - - } else { - const - childComponentId = getChildComponentId(srcElement); - - if (childComponentId != null) { - unsafe.$emit(`child-component-mounted:${childComponentId}`); - } - } - }); - - $a.on(parentNode, 'DOMNodeRemoved', ({srcElement}) => { - if (is(srcElement)) { - unsafe.$destroy(); - - } else { - const - childComponentId = getChildComponentId(srcElement); - - if (childComponentId != null) { - unsafe.$emit(`child-component-destroyed:${childComponentId}`); - } - } - }); - } - - parentNode.appendChild(componentNode); - - let - mounted = false; - - function mount(): void { - if (mounted) { - return; - } - - mounted = true; - init.mountedState(ctx); - } - - function getChildComponentId(node: Element): CanUndef { - if (!node.classList.contains('i-block-helper')) { - return; - } - - const - classes = node.className.split(/\s+/); - - for (let i = 0; i < classes.length; i++) { - const - classVal = classes[i]; - - if (!classVal.startsWith('uid-')) { - continue; - } - - if (classVal !== unsafe.componentId) { - return classVal; - } - } - } -} diff --git a/src/core/component/engines/zero/config.ts b/src/core/component/engines/zero/config.ts deleted file mode 100644 index 34fdc33af8..0000000000 --- a/src/core/component/engines/zero/config.ts +++ /dev/null @@ -1,22 +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 { VueConfiguration } from 'vue/types/vue'; - -export default { - silent: true, - devtools: false, - productionTip: false, - performance: false, - optionMergeStrategies: {}, - keyCodes: {}, - ignoredElements: [], - errorHandler: console.error, - warnHandler: console.warn, - async: false -}; diff --git a/src/core/component/engines/zero/const.ts b/src/core/component/engines/zero/const.ts deleted file mode 100644 index a05d9b7acc..0000000000 --- a/src/core/component/engines/zero/const.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { IS_NODE } from 'core/env'; -import type { Options } from 'core/component/engines/zero/interface'; - -export { default as minimalCtx } from 'core/component/engines/zero/context'; - -export const supports = { - regular: false, - functional: true, - composite: true, - - ssr: true, - boundCreateElement: true -}; - -export const options: Options = { - filters: {}, - directives: {} -}; - -export const document: Document = (() => { - //#if node_js - if (IS_NODE) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const {JSDOM} = require('jsdom'); - return (new JSDOM()).window.document; - } - //#endif - - return globalThis.document; -})(); diff --git a/src/core/component/engines/zero/context/const.ts b/src/core/component/engines/zero/context/const.ts deleted file mode 100644 index bdfa267f10..0000000000 --- a/src/core/component/engines/zero/context/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 - */ - -export const reservedAttrs = Object.createDict({ - is: true, - key: true, - ref: true, - slot: true, - 'slot-scope': true -}); diff --git a/src/core/component/engines/zero/context/index.ts b/src/core/component/engines/zero/context/index.ts deleted file mode 100644 index da79085238..0000000000 --- a/src/core/component/engines/zero/context/index.ts +++ /dev/null @@ -1,333 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { identity } from 'core/functools/helpers'; -import type { VNodeData } from 'vue'; - -import config from 'core/component/engines/zero/config'; - -import { document } from 'core/component/engines/zero/const'; -import { warn } from 'core/component/engines/zero/helpers'; - -import { reservedAttrs } from 'core/component/engines/zero/context/const'; -import type { KeyCode } from 'core/component/engines/zero/context/interface'; - -export * from 'core/component/engines/zero/context/const'; - -export default { - _o: identity, - _q: Object.fastCompare.bind(Object), - - _s(value: unknown): string { - if (Object.isPrimitive(value)) { - return String(value); - } - - return Object.fastHash(value); - }, - - _v(value: string): Text { - return document.createTextNode(value); - }, - - _e(value: Nullable): Comment | Text { - if (value != null) { - return document.createComment(value); - } - - return document.createTextNode(''); - }, - - _f(id: string): Function { - return resolveAsset(this.$options, 'filters', id, true) ?? identity; - }, - - _n(value: string): number | string { - const n = parseFloat(value); - return isNaN(n) ? value : n; - }, - - _i(arr: unknown[], value: unknown): number { - for (let i = 0; i < arr.length; i++) { - if (this._q(arr[i], value) === true) { - return i; - } - } - - return -1; - }, - - _m(index: number, isInFor: boolean): Node { - const - cached = this._staticTrees ?? (this._staticTrees = []); - - let - tree = cached[index]; - - if (tree == null && !isInFor) { - return tree; - } - - tree = this.$options.staticRenderFns[index] - .call(this._renderProxy, null, this); - - cached[index] = tree; - return tree; - }, - - _l(value: unknown, render: Function): unknown[] { - let - res; - - if (Object.isArray(value) || Object.isString(value)) { - res = new Array(value.length); - - for (let i = 0, l = value.length; i < l; i++) { - res[i] = render(value[i], i); - } - - } else if (Object.isNumber(value)) { - res = new Array(value); - - for (let i = 0; i < value; i++) { - res[i] = render(i + 1, i); - } - - } else if (value != null && typeof value === 'object') { - const keys = Object.keys(value); - res = new Array(keys.length); - - for (let i = 0, l = keys.length; i < l; i++) { - const key = keys[i]; - res[i] = render(value[key], key, i); - } - } - - if (res != null) { - (res)._isVList = true; - } - - return res; - }, - - _g(data: VNodeData, value?: Dictionary>): VNodeData { - if (Object.isDictionary(value)) { - const on = data.on != null ? {...data.on} : {}; - data.on = on; - - // eslint-disable-next-line guard-for-in - for (const key in value) { - const - ours = value[key], - existing = on[key]; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - on[key] = existing != null ? Array.concat([], existing, ours) : ours ?? []; - } - - } else { - warn('v-on without argument expects an Object value', this); - } - - return data; - }, - - _k( - eventKeyCode: string, - key: KeyCode, - builtInKeyCode?: Nullable, - eventKeyName?: string, - builtInKeyName?: Nullable - ): boolean { - const - configCodes = config.keyCodes[key], - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - mappedKeyCode = configCodes ?? builtInKeyCode; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (builtInKeyName != null && eventKeyName != null && configCodes == null) { - return isKeyNotMatch(builtInKeyName, eventKeyName); - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (mappedKeyCode != null) { - return isKeyNotMatch(mappedKeyCode, eventKeyCode); - } - - if (eventKeyName != null) { - return eventKeyName.dasherize() !== key; - } - - return false; - }, - - _b(data: VNodeData, tag: string, value: unknown, asProp: boolean, isSync: boolean): VNodeData { - if (value != null && typeof value === 'object') { - const - obj = Object.isArray(value) ? Object.assign({}, ...value) : value; - - let hash; - const loop = (key) => { - if (key === 'class' || key === 'style' || reservedAttrs[key] === true) { - hash = data; - - } else { - hash = asProp ? data.domProps ?? (data.domProps = {}) : data.attrs ?? (data.attrs = {}); - } - - if (!(key in hash)) { - hash[key] = obj[key]; - - if (isSync) { - const on = data.on ?? (data.on = {}); - on[`update:${key}`] = ($event) => { - obj[key] = $event; - }; - } - } - }; - - // eslint-disable-next-line guard-for-in - for (const key in obj) { - loop(key); - } - - } else { - warn('v-bind without argument expects an Object or Array value', this); - } - - return data; - }, - - _t(name: string, fallback: CanArray, props?: Dictionary, bindObject?: Dictionary): Element[] { - const - scopedSlotFn = this.$scopedSlots[name]; - - let - nodes; - - if (scopedSlotFn != null) { - props ??= {}; - - if (bindObject) { - if (typeof bindObject !== 'object') { - warn('slot v-bind without argument expects an Object', this); - } - - props = {...bindObject, ...props}; - } - - nodes = scopedSlotFn(props) ?? fallback; - - } else { - const - slotNodes = this.$slots[name]; - - if (slotNodes != null) { - slotNodes._rendered = true; - } - - nodes = slotNodes ?? fallback; - } - - const - target = props?.slot; - - if (target != null) { - return this.$createElement('template', {slot: target}, nodes); - } - - return nodes; - }, - - _u( - fns: Array>>, - - res: CanUndef, - hasDynamicKeys: boolean, - contentHashKey?: string - ): Dictionary { - res ??= {$stable: !hasDynamicKeys}; - - for (let i = 0; i < fns.length; i++) { - const - slot = fns[i]; - - if (Array.isArray(slot)) { - this._u(slot, res, hasDynamicKeys); - - } else if (slot != null) { - if (slot.proxy) { - // @ts-ignore (access) - slot.fn.proxy = true; - } - - res[slot.key] = slot.fn; - } - } - - if (contentHashKey != null) { - res.$key = contentHashKey; - } - - return res; - } -}; - -function resolveAsset(opts: Dictionary, type: string, id: string, warnMissing: boolean): CanUndef { - if (!Object.isString(id)) { - return; - } - - const - assets = >>opts[type]; - - if (assets == null) { - return; - } - - if (Object.hasOwnProperty(assets, id)) { - return assets[id]; - } - - const - camelizedId = id.camelize(false); - - if (Object.hasOwnProperty(assets, camelizedId)) { - return assets[camelizedId]; - } - - const - PascalCaseId = id.camelize(); - - if (Object.hasOwnProperty(assets, PascalCaseId)) { - return assets[PascalCaseId]; - } - - const - res = assets[id] ?? assets[camelizedId] ?? assets[PascalCaseId]; - - if (warnMissing && res == null) { - warn(`Failed to resolve ${type.slice(0, -1)}: ${id}`, opts); - } - - return res; -} - -function isKeyNotMatch(expect: CanArray, actual: KeyCode): boolean { - if (Object.isArray(expect)) { - return !expect.includes(actual); - } - - return expect !== actual; -} diff --git a/src/core/component/engines/zero/engine.ts b/src/core/component/engines/zero/engine.ts deleted file mode 100644 index 6a9a81f397..0000000000 --- a/src/core/component/engines/zero/engine.ts +++ /dev/null @@ -1,302 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { identity } from 'core/functools/helpers'; -import type { ComponentOptions, DirectiveOptions, DirectiveFunction } from 'vue'; - -import { registerComponent } from 'core/component/register'; -import type { ComponentInterface } from 'core/component/interface'; - -import config from 'core/component/engines/zero/config'; -import * as _ from 'core/component/engines/zero/helpers'; - -import { options, document } from 'core/component/engines/zero/const'; -import { getComponent, createComponent, mountComponent } from 'core/component/engines/zero/component'; - -import type { VNodeData } from 'core/component/engines/zero/interface'; - -export class ComponentEngine { - /** - * Component options - */ - $options: Dictionary = {...options}; - - /** - * Engine configuration - */ - static config: typeof config = config; - - /** - * Renders a component with specified name and input properties - * - * @param name - * @param [props] - */ - static async render(name: string, props?: Dictionary): Promise<{ctx: ComponentEngine; node: CanUndef}> { - let - meta = registerComponent(name); - - if (meta == null) { - throw new ReferenceError(`A component with the name "${name}" is not found`); - } - - if (props != null) { - meta = Object.create(meta); - - const metaProps = {...meta!.props}; - meta!.props = metaProps; - - Object.forEach(props, (val, key) => { - const - prop = metaProps[key]; - - if (prop != null) { - metaProps[key] = {...prop, default: val}; - } - }); - } - - const - ctx = new this(), - node = await ctx.$render(getComponent(meta!)); - - return {ctx, node}; - } - - /** - * Register a component with the specified name and parameters - * - * @param name - * @param params - */ - static component(name: string, params: object): Promise> { - if (Object.isFunction(params)) { - return new Promise(params); - } - - return Promise.resolve(params); - } - - /** - * Register a directive with the specified name and parameters - * - * @param name - * @param [params] - */ - static directive(name: string, params?: DirectiveOptions | DirectiveFunction): DirectiveOptions { - const - obj = {}; - - if (Object.isFunction(params)) { - obj.bind = params; - obj.update = params; - - } else if (params) { - Object.assign(obj, params); - } - - options.directives[name] = obj; - return obj; - } - - /** - * Register a filter with the specified name - * - * @param name - * @param [value] - */ - static filter(name: string, value?: Function): Function { - return options.filters[name] = value ?? identity; - } - - /** - * @param [opts] - */ - constructor(opts?: ComponentOptions) { - if (opts == null) { - return; - } - - const - {el} = opts; - - this.$render(opts).then(() => { - if (el == null) { - return; - } - - this.$mount(el); - }).catch(stderr); - } - - /** - * Renders the current component - * @param opts - component options - */ - async $render(opts: ComponentOptions): Promise> { - const res = await createComponent(opts, Object.create(this)); - this[_.$$.renderedComponent] = res; - return res[0]; - } - - /** - * Mounts the current component to the specified node - * @param nodeOrSelector - link to the parent node to mount or a selector - */ - $mount(nodeOrSelector: string | Node): void { - const - renderedComponent = this[_.$$.renderedComponent]; - - if (renderedComponent == null) { - return; - } - - mountComponent(nodeOrSelector, renderedComponent); - } - - /** - * Creates an element or component by the specified parameters - * - * @param tag - name of the tag or component to create - * @param [tagDataOrChildren] - additional data for the tag or component - * @param [children] - list of child elements - */ - $createElement( - this: ComponentInterface, - tag: string | Node, - tagDataOrChildren?: VNodeData | Node[], - children?: Array> - ): CanPromise { - if (Object.isString(tag)) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const refs = this.$refs ?? {}; - - // @ts-ignore (access) - this.$refs = refs; - - let - tagData: VNodeData; - - if (Object.isSimpleObject(tagDataOrChildren)) { - children = Array.concat([], children); - tagData = tagDataOrChildren; - - } else { - children = Array.concat([], tagDataOrChildren); - tagData = {}; - } - - const createNode = (children: Node[]) => { - let - node; - - switch (tag) { - case 'template': - node = _.createTemplate(); - break; - - case 'svg': - node = document.createElementNS(_.SVG_NMS, tag); - break; - - default: - node = document.createElement(tag); - } - - node.data = {...tagData, slots: getSlots()}; - node[_.$$.data] = node.data; - - node.elm = node; - node.context = this; - - _.addDirectives(this, node, tagData, tagData.directives); - _.addStaticDirectives(this, tagData, tagData.directives, node); - - if (node instanceof Element) { - _.addToRefs(node, tagData, refs); - _.addClass(node, tagData); - _.attachEvents(node, tagData.on); - } - - _.addProps(node, tagData.domProps); - _.addStyles(node, tagData.style); - _.addAttrs(node, tagData.attrs); - - if (node instanceof SVGElement) { - children = _.createSVGChildren(this, children); - } - - _.appendChild(node, children); - - return node; - - function getSlots(): Dictionary { - const - res = {}; - - if (children.length === 0) { - return res; - } - - const - firstChild = >children[0]; - - if (firstChild == null) { - return res; - } - - const hasSlotAttr = - 'getAttribute' in firstChild && firstChild.getAttribute('slot') != null; - - if (hasSlotAttr) { - for (let i = 0; i < children.length; i++) { - const - slot = children[i], - key = slot.getAttribute('slot'); - - if (key == null) { - continue; - } - - res[key] = slot; - } - - return res; - } - - let - slot; - - if (children.length === 1) { - slot = firstChild; - - } else { - slot = _.createTemplate(); - _.appendChild(slot, Array.from(children)); - } - - res.default = slot; - return res; - } - }; - - if (children.length > 0) { - children = children.flat(); - - // eslint-disable-next-line @typescript-eslint/unbound-method - if (children.some(Object.isPromise)) { - return Promise.all(children).then((children) => createNode(children)); - } - } - - return createNode(children); - } - - return tag; - } -} diff --git a/src/core/component/engines/zero/helpers/const.ts b/src/core/component/engines/zero/helpers/const.ts deleted file mode 100644 index 4a9a47abd6..0000000000 --- a/src/core/component/engines/zero/helpers/const.ts +++ /dev/null @@ -1,27 +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 { Options } from 'core/component/engines/zero/interface'; - -export const options: Options = { - filters: {}, - directives: {} -}; - -export const - SVG_NMS = 'http://www.w3.org/2000/svg', - XLINK_NMS = 'http://www.w3.org/1999/xlink'; - -export const eventModifiers = Object.createDict({ - '!': 'capture', - '&': 'passive', - '~': 'once' -}); - -export const - eventModifiersRgxp = new RegExp(`^[${Object.keys(eventModifiers).join('')}]+`); diff --git a/src/core/component/engines/zero/helpers/index.ts b/src/core/component/engines/zero/helpers/index.ts deleted file mode 100644 index 991c6fef7b..0000000000 --- a/src/core/component/engines/zero/helpers/index.ts +++ /dev/null @@ -1,442 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; - -import type { - - VNode, - VNodeData, - VNodeDirective, - DirectiveOptions - -} from 'vue'; - -import config from 'core/component/engines/zero/config'; -import { document } from 'core/component/engines/zero/const'; - -import { - - eventModifiers, - eventModifiersRgxp, - - SVG_NMS, - XLINK_NMS - -} from 'core/component/engines/zero/helpers/const'; - -import type { ComponentInterface } from 'core/component/interface'; -import type { DirElement, DocumentFragmentP } from 'core/component/engines/zero/helpers/interface'; - -export * from 'core/component/engines/zero/helpers/const'; -export * from 'core/component/engines/zero/helpers/interface'; - -export const - $$ = symbolGenerator(); - -export function addToRefs(el: Element, data: Nullable, refs: Nullable): void { - if (data == null) { - return; - } - - const - {ref} = data; - - if (ref == null || ref === '' || refs == null) { - return; - } - - if (data.refInFor === true) { - const - arr = (refs[ref] ?? []); - - refs[ref] = arr; - arr.push(el); - - } else { - refs[ref] = el; - } -} - -export function createSVGChildren(ctx: ComponentInterface, children: Nullable): SVGElement[] { - if (children == null || children.length === 0) { - return []; - } - - const - res = >[]; - - for (let i = 0; i < children.length; i++) { - const - el = children[i], - node = document.createElementNS(SVG_NMS, el.tagName.toLowerCase()), - data = el[$$.data]; - - if (data != null) { - const - dirs = el[$$.directives]; - - addDirectives(ctx, node, data, dirs); - addStaticDirectives(ctx, data, dirs, node); - - const - // @ts-ignore (access) - refs = ctx.$refs; - - addToRefs(el, data, refs); - - addStyles(node, el[$$.styles]); - addAttrs(node, el[$$.attrs]); - attachEvents(node, el[$$.events]); - - if (Object.isTruly(el.className)) { - node.setAttributeNS(null, 'class', el.className); - } - - res.push(node); - - } else { - res.push(children[i]); - } - - if (Object.size(el.children) > 0) { - appendChild(node, createSVGChildren(ctx, Array.from(el.children))); - } - } - - return res; -} - -export function addProps(el: DirElement, props?: Dictionary): void { - if (!props) { - return; - } - - el[$$.props] = props; - - for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { - const key = keys[i]; - el[key] = props[key]; - } -} - -export function addAttrs(el: DirElement, attrs?: Dictionary): void { - if (!attrs) { - return; - } - - el[$$.attrs] = attrs; - - for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { - const - key = keys[i], - val = attrs[key]; - - if (val != null) { - if (el instanceof SVGElement) { - el.setAttributeNS(key.split(':', 1)[0] === 'xlink' ? XLINK_NMS : null, key, val); - - } else { - el.setAttribute(key, val); - } - } - } -} - -export function addStyles(el: DirElement, styles?: CanArray>): void { - const - normalizedStyles = Array.concat([], styles); - - if (normalizedStyles.length === 0) { - return; - } - - el[$$.styles] = normalizedStyles; - - const - strStyles = []; - - for (let i = 0; i < normalizedStyles.length; i++) { - const - styles = normalizedStyles[i]; - - if (!Object.isTruly(styles)) { - continue; - } - - if (Object.isString(styles)) { - strStyles.push(styles); - continue; - } - - let - str = ''; - - for (let keys = Object.keys(styles), i = 0; i < keys.length; i++) { - const - key = keys[i], - el = styles[key]; - - if (el != null && el !== '') { - str += `${key.dasherize()}: ${el};`; - } - } - - strStyles.push(str); - } - - el.setAttribute('style', strStyles.join(';')); -} - -export function createTemplate(): DocumentFragmentP { - const - el = document.createDocumentFragment(), - attrs = {}; - - el['getAttribute'] = (key) => attrs[key]; - el['setAttribute'] = (key, val) => attrs[key] = val; - - return Object.cast(el); -} - -export function addClass(el: Element, opts: VNodeData): void { - const className = Array - .concat([], el.getAttribute('class') ?? '', opts.staticClass ?? '', ...Array.concat([], opts.class)) - .join(' ') - .trim(); - - if (className.length > 0) { - if (el instanceof SVGElement) { - el.setAttributeNS(null, 'class', className); - - } else { - el.setAttribute('class', className); - } - } -} - -export function attachEvents(el: Node, events?: Dictionary>): void { - if (events == null) { - return; - } - - for (let keys = Object.keys(events), i = 0; i < keys.length; i++) { - const - key = keys[i], - mods = eventModifiersRgxp.exec(key), - handlers = Array.concat([], events[key]), - flags = {}; - - if (mods) { - for (let o = mods[0], i = 0; i < o.length; i++) { - flags[eventModifiers[o[i]]] = true; - } - } - - for (let i = 0; i < handlers.length; i++) { - const - fn = handlers[i]; - - if (Object.isFunction(fn)) { - const - event = key.replace(eventModifiersRgxp, ''), - cache = el[$$.events] ?? {}; - - el[$$.events] = cache; - cache[event] = {fn, flags}; - - el.addEventListener(event, fn, flags); - } - } - } -} - -export function appendChild(parent: Nullable, node: Nullable>): void { - if (parent == null || node == null) { - return; - } - - if (Object.isArray(node) || node instanceof HTMLCollection) { - for (let i = 0; i < node.length; i++) { - const - el = node[i]; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (el != null) { - appendChild(parent, el); - } - } - - } else { - parent.appendChild(node); - } -} - -export function warn(message: string, vm: object): void { - // eslint-disable-next-line @typescript-eslint/unbound-method - if (Object.isFunction(config.warnHandler)) { - config.warnHandler.call(null, message, vm); - - } else if (typeof console !== 'undefined' && Object.isFunction(console.error) && !config.silent) { - console.error(`[Vue warn]: ${message}`); - } -} - -export function addStaticDirectives( - component: ComponentInterface, - data: VNodeData, - directives?: VNodeDirective[], - node?: DirElement -): void { - if (directives == null) { - return; - } - - const - store = component.$options.directives; - - if (store == null) { - return; - } - - if (node != null) { - node[$$.directives] = directives; - } - - for (let o = directives, i = 0; i < o.length; i++) { - const - dir = o[i]; - - switch (dir.name) { - case 'show': - if (!Object.isTruly(dir.value)) { - const - rule = ';display: none;'; - - if (node != null && data.tag === 'component') { - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - node.setAttribute('style', (node.getAttribute('style') ?? '') + rule); - - } else { - data.attrs = data.attrs ?? {}; - - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - data.attrs.style = (data.attrs.style ?? '') + rule; - } - } - - break; - - case 'model': - data.domProps = data.domProps ?? {}; - data.domProps.value = dir.value; - break; - - default: - // Do nothing - } - } -} - -export function addDirectives( - component: ComponentInterface, - node: DirElement, - data: VNodeData, - directives?: VNodeDirective[] -): void { - if (directives == null) { - return; - } - - const { - unsafe: {$async: $a}, - $options: {directives: store} - } = component; - - if (store == null) { - return; - } - - node[$$.directives] = directives; - - const - root = component.$root.unsafe; - - for (let o = directives, i = 0; i < o.length; i++) { - const - dir = o[i], - dirParams = store[dir.name]; - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (dirParams == null) { - continue; - } - - const vnode = Object.create(node); - vnode.context = component; - - if (Object.isFunction(dirParams)) { - dirParams(Object.cast(node), Object.cast(dir), vnode, Object.cast(undefined)); - - } else { - if (dirParams.bind) { - dirParams.bind.call(undefined, node, dir, vnode); - } - - if (dirParams.inserted) { - const events = [ - 'on-component-hook:mounted', - `child-component-mounted:${component.componentId}` - ]; - - dispatchHookFromEvents('inserted', events, dir, dirParams, vnode); - } - - if (dirParams.unbind) { - const events = [ - 'on-component-hook:beforeDestroy', - `child-component-destroyed:${component.componentId}` - ]; - - dispatchHookFromEvents('unbind', events, dir, dirParams, vnode); - } - } - } - - function dispatchHookFromEvents( - hook: string, - events: string[], - dir: VNodeDirective, - dirParams: DirectiveOptions, - vnode: VNode - ): void { - const clear = () => { - for (let i = 0; i < events.length; i++) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - root.$off(events[i], cb); - } - }; - - const cb = () => { - clear(); - - const - fn = dirParams[hook]; - - if (Object.isFunction(fn)) { - return fn(node, dir, vnode); - } - }; - - for (let i = 0; i < events.length; i++) { - root.$once(events[i], cb); - } - - $a.worker(clear); - } -} diff --git a/src/core/component/engines/zero/helpers/interface.ts b/src/core/component/engines/zero/helpers/interface.ts deleted file mode 100644 index 6c92ff7d9b..0000000000 --- a/src/core/component/engines/zero/helpers/interface.ts +++ /dev/null @@ -1,19 +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 { ComponentElement } from 'core/component'; - -export type DocumentFragmentP = DocumentFragment & { - getAttribute(nm: string): void; - setAttribute(nm: string, val: string): void; -}; - -export type DirElement = - Element | - ComponentElement | - DocumentFragmentP; diff --git a/src/core/component/engines/zero/index.ts b/src/core/component/engines/zero/index.ts deleted file mode 100644 index 5cb8699708..0000000000 --- a/src/core/component/engines/zero/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/engines/zero/README.md]] - * @packageDocumentation - */ - -import minimalCtx from 'core/component/engines/zero/context'; -import type { VNodeData } from 'core/component/engines/zero/interface'; - -//#if VueInterfaces -export * from 'vue'; -export { VNode, ScopedSlot } from 'vue/types/vnode'; -//#endif - -export * from 'core/component/engines/zero/const'; -export * from 'core/component/engines/zero/engine'; -export * from 'core/component/engines/zero/component'; -export * from 'core/component/engines/zero/vnode'; -export * from 'core/component/engines/zero/interface'; - -export { - - ComponentEngine as default, - ComponentEngine as ComponentDriver - -} from 'core/component/engines/zero/engine'; - -export { minimalCtx, VNodeData }; diff --git a/src/core/component/engines/zero/interface.ts b/src/core/component/engines/zero/interface.ts deleted file mode 100644 index b921ca8ce0..0000000000 --- a/src/core/component/engines/zero/interface.ts +++ /dev/null @@ -1,22 +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 { DirectiveOptions, VNodeData as BaseVNodeData } from 'vue'; - -export interface VNodeData extends BaseVNodeData { - model?: { - expression: string; - value: unknown; - callback(value: unknown): void; - }; -} - -export interface Options extends Dictionary { - filters: Dictionary; - directives: Dictionary; -} diff --git a/src/core/component/engines/zero/vnode.ts b/src/core/component/engines/zero/vnode.ts deleted file mode 100644 index 1da877ea80..0000000000 --- a/src/core/component/engines/zero/vnode.ts +++ /dev/null @@ -1,55 +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 { RenderContext, VNode } from 'vue'; -import type { ComponentInterface } from 'core/component'; -import * as _ from 'core/component/engines/zero/helpers'; - -/** - * Clones the specified vnode - * @param vnode - */ -export function cloneVNode(vnode: VNode): VNode { - return Object.cast(Object.cast(vnode).cloneNode(true)); -} - -/** - * Patches the specified VNode by using provided contexts - * - * @param vnode - * @param component - component instance - * @param renderCtx - render context - */ -export function patchVNode(vnode: Element, component: ComponentInterface, renderCtx: RenderContext): void { - const - {data} = renderCtx, - {meta} = component.unsafe; - - _.addToRefs(vnode, data, component.$parent?.unsafe.$refs); - _.addClass(vnode, data); - - if (data.attrs && meta.params.inheritAttrs) { - _.addAttrs(vnode, data.attrs); - } - - _.addStaticDirectives(component, data, vnode[_.$$.directives], vnode); -} - -/** - * Renders the specified VNode/s and returns the result - * - * @param vnode - * @param parent - parent component - */ -export function renderVNode(vnode: VNode, parent: ComponentInterface): Node; -export function renderVNode(vnodes: VNode[], parent: ComponentInterface): Node[]; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental -export function renderVNode(vnode: CanArray, parent: ComponentInterface): CanArray { - return Object.cast(vnode); -} From 5b9759baaa90b606ffa830b8f5ea1ab2db3832b1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 16:19:18 +0300 Subject: [PATCH 0056/2313] refactor: removed legacy --- src/core/component/engines/helpers.ts | 54 --------------------------- 1 file changed, 54 deletions(-) diff --git a/src/core/component/engines/helpers.ts b/src/core/component/engines/helpers.ts index 4c5d521efa..d65b23570a 100644 --- a/src/core/component/engines/helpers.ts +++ b/src/core/component/engines/helpers.ts @@ -6,7 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { fakeCopyLabel } from 'core/component/watch'; import type { ComponentInterface } from 'core/component'; const @@ -31,56 +30,3 @@ export function getComponentContext(component: object): Dictionary & ComponentIn return v; } - -const - toNonFakeObject = Symbol('Link to a non fake object'); - -/** - * Returns a "fake" copy of the specified (weak)map/(weak)set object - * @param obj - */ -export function fakeMapSetCopy< - T extends Map | WeakMap | Set | WeakSet ->(obj: T): T { - obj = obj[toNonFakeObject] ?? obj; - - const - Constructor = obj.constructor, - proto = Constructor.prototype; - - // @ts-ignore (constructor) - const wrap = new Constructor(); - - for (let keys = Object.getOwnPropertyNames(proto), i = 0; i < keys.length; i++) { - const - key = keys[i], - desc = Object.getOwnPropertyDescriptor(proto, key); - - if (!desc) { - continue; - } - - if (Object.isFunction(desc.value)) { - Object.defineProperty(wrap, key, { - ...desc, - value: obj[key].bind(obj) - }); - - // eslint-disable-next-line @typescript-eslint/unbound-method - } else if (Object.isFunction(desc.get)) { - Object.defineProperty(wrap, key, { - ...desc, - get: () => obj[key], - set: desc.set && ((v) => obj[key] = v) - }); - } - } - - wrap[Symbol.iterator] = obj[Symbol.iterator]?.bind(obj); - wrap[Symbol.asyncIterator] = obj[Symbol.asyncIterator]?.bind(obj); - - wrap[toNonFakeObject] = obj; - wrap[fakeCopyLabel] = true; - - return wrap; -} From 2c4f0fa49fbddedb96e78a8d8313362e827fed60 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 16:52:25 +0300 Subject: [PATCH 0057/2313] refactor: removed `flyweight` support --- src/core/component/construct/states/before-create.ts | 2 +- src/core/component/construct/states/before-data-create.ts | 1 - src/core/component/construct/states/before-mount.ts | 6 ++---- src/core/component/construct/states/created.ts | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts index f1a87bcb45..0f68d63a19 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/construct/states/before-create.ts @@ -12,7 +12,7 @@ import { asyncLabel } from 'core/component/const'; import { getComponentContext } from 'core/component/engines/helpers'; import { forkMeta } from 'core/component/meta'; -import { getPropertyInfo, PropertyInfo } from 'core/component/reflection'; +import { getPropertyInfo, PropertyInfo } from 'core/component/reflect'; import { getNormalParent } from 'core/component/traverse'; import { initFields } from 'core/component/field'; diff --git a/src/core/component/construct/states/before-data-create.ts b/src/core/component/construct/states/before-data-create.ts index ca0419c487..e6ff19f788 100644 --- a/src/core/component/construct/states/before-data-create.ts +++ b/src/core/component/construct/states/before-data-create.ts @@ -36,7 +36,6 @@ export function beforeDataCreateState( .catch(stderr); const hasWatchAPI = - !component.isFlyweight && !component.$renderEngine.supports.ssr; if (hasWatchAPI) { diff --git a/src/core/component/construct/states/before-mount.ts b/src/core/component/construct/states/before-mount.ts index 8a3c79122b..ee02925c26 100644 --- a/src/core/component/construct/states/before-mount.ts +++ b/src/core/component/construct/states/before-mount.ts @@ -23,8 +23,6 @@ export function beforeMountState(component: ComponentInterface): void { $el.component = component; } - if (!component.isFlyweight) { - runHook('beforeMount', component).catch(stderr); - callMethodFromComponent(component, 'beforeMount'); - } + runHook('beforeMount', component).catch(stderr); + callMethodFromComponent(component, 'beforeMount'); } diff --git a/src/core/component/construct/states/created.ts b/src/core/component/construct/states/created.ts index 66153abaf2..e70496d091 100644 --- a/src/core/component/construct/states/created.ts +++ b/src/core/component/construct/states/created.ts @@ -46,8 +46,7 @@ export function createdState(component: ComponentInterface): void { $a.worker(() => p.$off('on-component-hook:before-destroy', destroy)); const isRegular = - unsafe.meta.params.functional !== true && - !unsafe.isFlyweight; + unsafe.meta.params.functional !== true; if (isRegular) { const activationHooks = Object.createDict({ From f87baff8d62acf536a0f8c7d2283171c61610ab4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 16:52:43 +0300 Subject: [PATCH 0058/2313] chore: fixed grammar --- src/core/component/const/emitters.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/const/emitters.ts b/src/core/component/const/emitters.ts index 04ca9930d0..10f8e5a149 100644 --- a/src/core/component/const/emitters.ts +++ b/src/core/component/const/emitters.ts @@ -15,8 +15,8 @@ import { componentParams } from 'core/component/const/cache'; export const initEmitter = new EventEmitter({maxListeners: 1e3, newListener: false}); -// We need wrap an original "once" function of the emitter -// to attach logic to register smart components +// We need to wrap an original `once` function of the emitter +// to attach logic of registering smart components ((initEventOnce) => { initEmitter.once = function once( event: CanArray, From 5cc460b84402cc024fbdab724ce0c139cb4c064b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 18:48:37 +0300 Subject: [PATCH 0059/2313] fix: fixed TS error --- src/core/component/const/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/const/cache.ts b/src/core/component/const/cache.ts index a6710077d6..2905868762 100644 --- a/src/core/component/const/cache.ts +++ b/src/core/component/const/cache.ts @@ -18,7 +18,7 @@ export const componentParams = new Map(); /** * Map of root components */ -export const rootComponents = Object.createDict>>(); +export const rootComponents = Object.createDict>>(); /** * Link to an instance of the global root component From e38af726b92ac3aa9710721d6aa56b067c66ede5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 18:56:43 +0300 Subject: [PATCH 0060/2313] refactor: extracted helpers to a separated module --- src/core/component/context/CHANGELOG.md | 16 ++++++++++++++++ src/core/component/context/README.md | 3 +++ src/core/component/context/const.ts | 11 +++++++++++ .../{engines/helpers.ts => context/index.ts} | 12 ++++++++---- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/core/component/context/CHANGELOG.md create mode 100644 src/core/component/context/README.md create mode 100644 src/core/component/context/const.ts rename src/core/component/{engines/helpers.ts => context/index.ts} (69%) diff --git a/src/core/component/context/CHANGELOG.md b/src/core/component/context/CHANGELOG.md new file mode 100644 index 0000000000..b18e802496 --- /dev/null +++ b/src/core/component/context/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/context/README.md b/src/core/component/context/README.md new file mode 100644 index 0000000000..66d2441c19 --- /dev/null +++ b/src/core/component/context/README.md @@ -0,0 +1,3 @@ +# core/component/context + +This module provides a bunch of helpers to work with a component context. diff --git a/src/core/component/context/const.ts b/src/core/component/context/const.ts new file mode 100644 index 0000000000..2c71f1fed4 --- /dev/null +++ b/src/core/component/context/const.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export const + toRaw = Symbol('Link to a RAW component'), + ctxMap = new WeakMap(); diff --git a/src/core/component/engines/helpers.ts b/src/core/component/context/index.ts similarity index 69% rename from src/core/component/engines/helpers.ts rename to src/core/component/context/index.ts index d65b23570a..7cede7b57e 100644 --- a/src/core/component/engines/helpers.ts +++ b/src/core/component/context/index.ts @@ -6,11 +6,15 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentInterface } from 'core/component'; +/** + * [[include:core/component/context/README.md]] + * @packageDocumentation + */ + +import { toRaw, ctxMap } from 'core/component/context/const'; +import type { ComponentInterface } from 'core/component/interface'; -const - toRaw = Symbol('Link to a RAW component'), - ctxMap = new WeakMap(); +export * from 'core/component/context/const'; /** * Returns a component context object by the specified component instance From dcb9cb2c1c3a61006e24707b95fd0add22bb621e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 19 Apr 2022 19:02:52 +0300 Subject: [PATCH 0061/2313] refactor: updated imports --- src/core/component/construct/states/before-create.ts | 2 +- src/core/component/meta/fill.ts | 6 ++++-- src/core/component/ref/index.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts index 0f68d63a19..76d30090d6 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/construct/states/before-create.ts @@ -9,7 +9,7 @@ import Async from 'core/async'; import { asyncLabel } from 'core/component/const'; -import { getComponentContext } from 'core/component/engines/helpers'; +import { getComponentContext } from 'core/component/context'; import { forkMeta } from 'core/component/meta'; import { getPropertyInfo, PropertyInfo } from 'core/component/reflect'; diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 522f11a2e3..97ed84418d 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -7,10 +7,11 @@ */ import { defaultWrapper } from 'core/component/const'; -import { getComponentContext } from 'core/component/engines/helpers'; -import { isTypeCanBeFunc } from 'core/component/prop'; +import { getComponentContext } from 'core/component/context'; import { isAbstractComponent } from 'core/component/reflect'; + +import { isTypeCanBeFunc } from 'core/component/prop'; import { addMethodsToMeta } from 'core/component/meta/method'; import type { ComponentConstructor, ComponentMeta, ComponentField, WatchObject } from 'core/component/interface'; @@ -56,6 +57,7 @@ export function fillMeta( } component.methods[nm] = function wrapper() { + // eslint-disable-next-line prefer-rest-params return method.fn.apply(getComponentContext(this), arguments); }; diff --git a/src/core/component/ref/index.ts b/src/core/component/ref/index.ts index bdc86a7d46..0225c95d63 100644 --- a/src/core/component/ref/index.ts +++ b/src/core/component/ref/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import { getComponentContext } from 'core/component/engines/helpers'; +import { getComponentContext } from 'core/component/context'; import type { ComponentElement, ComponentInterface } from 'core/component/interface'; /** From ac4e3b05a46d0931509539c3be6902c748e2c484 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 20 Apr 2022 17:22:23 +0300 Subject: [PATCH 0062/2313] refactor: updated TS types --- .../component/directives/hook/interface.ts | 22 +++++++++---------- .../component/directives/image/interface.ts | 7 +----- .../directives/resize-observer/interface.ts | 9 ++------ .../directives/update-on/interface.ts | 8 ++----- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/core/component/directives/hook/interface.ts b/src/core/component/directives/hook/interface.ts index 3d777d2e85..3452754706 100644 --- a/src/core/component/directives/hook/interface.ts +++ b/src/core/component/directives/hook/interface.ts @@ -6,16 +6,16 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ObjectDirective, DirectiveHook } from 'core/component/engines'; +import type { DirectiveBinding, DirectiveHook } from 'core/component/engines'; -export interface DirectiveOptions extends ObjectDirective { - value?: { - created?: DirectiveHook; - beforeMount?: DirectiveHook; - mounted?: DirectiveHook; - beforeUpdate?: DirectiveHook; - updated?: DirectiveHook; - beforeUnmount?: DirectiveHook; - unmounted?: DirectiveHook; - }; +export interface DirectiveOptions extends DirectiveBinding> {} + +export interface DirectiveValue { + created?: DirectiveHook; + beforeMount?: DirectiveHook; + mounted?: DirectiveHook; + beforeUpdate?: DirectiveHook; + updated?: DirectiveHook; + beforeUnmount?: DirectiveHook; + unmounted?: DirectiveHook; } diff --git a/src/core/component/directives/image/interface.ts b/src/core/component/directives/image/interface.ts index 868064c0c8..cb3e7448d1 100644 --- a/src/core/component/directives/image/interface.ts +++ b/src/core/component/directives/image/interface.ts @@ -10,10 +10,5 @@ import type { InitValue } from 'core/dom/image'; import type { DirectiveBinding } from 'core/component/engines'; export interface DirectiveOptions extends DirectiveBinding> { - modifiers: { - [key: string]: boolean; - }; - - value: CanUndef; - oldValue: CanUndef; + modifiers: Record; } diff --git a/src/core/component/directives/resize-observer/interface.ts b/src/core/component/directives/resize-observer/interface.ts index a45bdce2a2..dfecab99f9 100644 --- a/src/core/component/directives/resize-observer/interface.ts +++ b/src/core/component/directives/resize-observer/interface.ts @@ -11,11 +11,6 @@ import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; export * from 'core/dom/resize-observer/interface'; -export interface DirectiveOptions extends DirectiveBinding { - modifiers: { - [key: string]: boolean; - }; - - value: CanUndef>; - oldValue: CanUndef>; +export interface DirectiveOptions extends DirectiveBinding>> { + modifiers: Record; } diff --git a/src/core/component/directives/update-on/interface.ts b/src/core/component/directives/update-on/interface.ts index 51dd0f1099..a5a411da3d 100644 --- a/src/core/component/directives/update-on/interface.ts +++ b/src/core/component/directives/update-on/interface.ts @@ -10,12 +10,8 @@ import type { EventEmitterLike } from 'core/async'; import type { DirectiveBinding } from 'core/component/engines'; import type { WatchOptions } from 'core/component/interface'; -export interface DirectiveOptions extends DirectiveBinding { - modifiers: { - [key: string]: boolean; - }; - - value: CanUndef>; +export interface DirectiveOptions extends DirectiveBinding>> { + modifiers: Record; } export interface DirectiveValue { From 27998abd51897fa88f9557414e99f630c0686b85 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 20 Apr 2022 17:23:19 +0300 Subject: [PATCH 0063/2313] feat: added support for a new hook handler `beforeCreate` --- src/core/component/engines/vue3/interface.ts | 31 ++++++++++++++++++++ src/core/component/engines/vue3/lib.ts | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/core/component/engines/vue3/interface.ts diff --git a/src/core/component/engines/vue3/interface.ts b/src/core/component/engines/vue3/interface.ts new file mode 100644 index 0000000000..48d92dc0e6 --- /dev/null +++ b/src/core/component/engines/vue3/interface.ts @@ -0,0 +1,31 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { + + CreateAppFunction as SuperCreateAppFunction, + ObjectDirective as SuperObjectDirective, + + VNode, + DirectiveBinding, + FunctionDirective + +} from 'vue'; + +export interface ObjectDirective extends SuperObjectDirective { + beforeCreate?(binding: DirectiveBinding, vnode: VNode): void; +} + +export declare type Directive = ObjectDirective | FunctionDirective; + +export interface CreateAppFunction { + (...args: Parameters>): Overwrite, { + directive(name: string): CanUndef; + directive(name: string, directive: Directive): ReturnType>; + }>; +} diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index b7f81b8ce1..c1f4859179 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -7,9 +7,11 @@ */ import makeLazy from 'core/lazy'; + import { createApp, Component } from 'vue'; +import type { CreateAppFunction } from 'core/component/engines/vue3/interface'; -const App = function App(component: Component & {el: Element}, rootProps: Nullable) { +const App = function App(component: Component & {el: Element}, rootProps: Nullable) { const app = Object.create(createApp(component, rootProps)); From 16981ac3131d4fe4a86aef5add985c24bd8a328c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 20 Apr 2022 19:10:10 +0300 Subject: [PATCH 0064/2313] feat: added a new v-attrs directive --- .../component/directives/attrs/CHANGELOG.md | 22 +++ src/core/component/directives/attrs/README.md | 19 ++ src/core/component/directives/attrs/index.ts | 175 ++++++++++++++++++ .../component/directives/attrs/interface.ts | 11 ++ 4 files changed, 227 insertions(+) create mode 100644 src/core/component/directives/attrs/CHANGELOG.md create mode 100644 src/core/component/directives/attrs/README.md create mode 100644 src/core/component/directives/attrs/index.ts create mode 100644 src/core/component/directives/attrs/interface.ts diff --git a/src/core/component/directives/attrs/CHANGELOG.md b/src/core/component/directives/attrs/CHANGELOG.md new file mode 100644 index 0000000000..4f347c1a34 --- /dev/null +++ b/src/core/component/directives/attrs/CHANGELOG.md @@ -0,0 +1,22 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Migration to Vue3 + +## v3.0.0-rc.97 (2020-11-11) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/attrs/README.md b/src/core/component/directives/attrs/README.md new file mode 100644 index 0000000000..fcd56d1af6 --- /dev/null +++ b/src/core/component/directives/attrs/README.md @@ -0,0 +1,19 @@ +# core/component/directives/hook + +This module brings a directive to provide any directive hooks into a component. +This directive is extremely useful to combine with a flyweight component because it does not have API to +attach the hook listeners. + +## Usage + +``` +< .&__class v-hook = { & + created: (el, opts, vnode, oldVnode) => console.log(el, opts, vnode, oldVnode), + beforeMount: onBeforeMount, + mounted: onMounted, + beforeUpdate: onBeforeUpdate, + updated: onUpdated, + beforeUnmount: onBeforeUnmount, + unmounted: onUnmounted +} . +``` diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts new file mode 100644 index 0000000000..5b697a6bb0 --- /dev/null +++ b/src/core/component/directives/attrs/index.ts @@ -0,0 +1,175 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/attrs/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode } from 'core/component/engines'; +import type { DirectiveOptions } from 'core/component/directives/attrs/interface'; + +export * from 'core/component/directives/attrs/interface'; + +ComponentEngine.directive('attrs', { + beforeCreate(opts: DirectiveOptions, vnode: VNode) { + const props = vnode.props ?? {}; + vnode.props ??= props; + + console.log(vnode); + + const + attrs = opts.value; + + if (attrs == null) { + return; + } + + for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { + const + key = keys[i]; + + let + val = attrs[key]; + + if (key.startsWith('v-')) { + const + [, rawName, name, arg, rawModifiers] = /(v-(.*?))(?::(.*?))?(\..*)?$/.exec(key)!; + + let + modifiers; + + if (Object.isTruly(rawModifiers)) { + modifiers = {}; + + for (let o = rawModifiers.split('.'), i = 0; i < o.length; i++) { + modifiers[o[i]] = true; + } + } + + const + dir = {name, rawName, value: val}; + + if (Object.isTruly(arg)) { + dir.arg = arg; + } + + if (Object.isTruly(modifiers)) { + dir.modifiers = modifiers; + } + + const dirs = vnode.dirs ?? []; + vnode.dirs = dirs; + + dirs.push(); + + } else if (key.startsWith('@')) { + let + event = key.slice(1); + + const + eventChunks = event.split('.'), + flags = Object.createDict(); + + for (let i = 1; i < eventChunks.length; i++) { + flags[eventChunks[i]] = true; + } + + event = eventChunks[0]; + + if (flags.right && !event.startsWith('key')) { + event = 'onContextmenu'; + delete flags.right; + + } else if (flags.middle && event !== 'mousedown') { + event = 'onMouseup'; + + } else { + event = `on${event.capitalize()}`; + } + + if (flags.capture) { + event += 'Capture'; + delete flags.capture; + } + + if (flags.once) { + event += 'Once'; + delete flags.once; + } + + if (flags.passive) { + event += 'Passive'; + delete flags.passive; + } + + if (Object.keys(flags).length > 0) { + const + original = val; + + val = (e: MouseEvent | KeyboardEvent, ...args) => { + if ( + flags.ctrl && !e.ctrlKey || + flags.alt && !e.altKey || + flags.shift && !e.shiftKey || + flags.meta && !e.metaKey || + flags.exact && ( + !flags.ctrl && e.ctrlKey || + !flags.alt && e.altKey || + !flags.shift && e.shiftKey || + !flags.meta && e.metaKey + ) + ) { + return; + } + + if (e instanceof MouseEvent) { + if (flags.middle && e.button !== 1) { + return; + } + + } else if (e instanceof KeyboardEvent) { + if ( + flags.enter && e.key !== 'Enter' || + flags.tab && e.key !== 'Tab' || + flags.delete && (e.key !== 'Delete' && e.key !== 'Backspace') || + flags.esc && e.key !== 'Escape' || + flags.space && e.key !== ' ' || + flags.up && e.key !== 'ArrowUp' || + flags.down && e.key !== 'ArrowDown' || + flags.left && e.key !== 'ArrowLeft' || + flags.right && e.key !== 'ArrowRight' + ) { + return; + } + } + + if (flags.self && e.target !== e.currentTarget) { + return; + } + + if (flags.prevent) { + e.preventDefault(); + } + + if (flags.stop) { + e.stopPropagation(); + } + + return (original)(e, ...args); + }; + } + + props[event] = val; + + } else { + props[key] = val; + } + } + } +}); diff --git a/src/core/component/directives/attrs/interface.ts b/src/core/component/directives/attrs/interface.ts new file mode 100644 index 0000000000..5203a8cb2d --- /dev/null +++ b/src/core/component/directives/attrs/interface.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { DirectiveBinding } from 'core/component/engines'; + +export interface DirectiveOptions extends DirectiveBinding> {} From 76a922b88853b473e9354d48a4b1a908d25bbcc5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 20 Apr 2022 19:10:51 +0300 Subject: [PATCH 0065/2313] feat: new API for internal directives --- src/core/component/render/components/const.ts | 9 ++++ src/core/component/render/components/index.ts | 26 ++++++++++ .../component/render/components/interface.ts | 13 +++++ .../component/render/components/v-render.ts | 23 +++++++++ .../directives/const.ts} | 2 +- src/core/component/render/directives/index.ts | 32 +++++++++++++ .../component/render/directives/interface.ts | 13 +++++ .../component/render/directives/v-attrs.ts | 25 ++++++++++ src/core/component/render/wrappers/helpers.ts | 11 +++++ src/core/component/render/wrappers/index.ts | 47 +++++++++++++++++++ 10 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/core/component/render/components/const.ts create mode 100644 src/core/component/render/components/index.ts create mode 100644 src/core/component/render/components/interface.ts create mode 100644 src/core/component/render/components/v-render.ts rename src/core/component/{engines/zero/context/interface.ts => render/directives/const.ts} (79%) create mode 100644 src/core/component/render/directives/index.ts create mode 100644 src/core/component/render/directives/interface.ts create mode 100644 src/core/component/render/directives/v-attrs.ts create mode 100644 src/core/component/render/wrappers/helpers.ts create mode 100644 src/core/component/render/wrappers/index.ts diff --git a/src/core/component/render/components/const.ts b/src/core/component/render/components/const.ts new file mode 100644 index 0000000000..ade318dd3c --- /dev/null +++ b/src/core/component/render/components/const.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 const components: Dictionary = Object.createDict(); diff --git a/src/core/component/render/components/index.ts b/src/core/component/render/components/index.ts new file mode 100644 index 0000000000..46ac826cde --- /dev/null +++ b/src/core/component/render/components/index.ts @@ -0,0 +1,26 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import 'core/component/render/components/v-render'; + +import type { VNode } from 'core/component/engines'; + +import { components } from 'core/component/render/components/const'; +import type { CreateVNode } from 'core/component/render/components/interface'; + +/** + * Creates a new VNode by the specified parameters with applying internal component directives, like `v-render`. + * The function takes an original function to create VNodes and list of arguments and returns a new VNode. + * + * @param createVNode - original function to create a VNode + * @param type - VNode type + * @param args - operation arguments + */ +export function createVNodeWithDirectives(createVNode: CreateVNode, type: string, ...args: any[]): VNode { + return components[type]?.(createVNode, type, ...args) ?? createVNode(...args); +} diff --git a/src/core/component/render/components/interface.ts b/src/core/component/render/components/interface.ts new file mode 100644 index 0000000000..a9b36db8c4 --- /dev/null +++ b/src/core/component/render/components/interface.ts @@ -0,0 +1,13 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode } from 'core/component/engines'; + +export interface CreateVNode { + (...args: any[]): VNode; +} diff --git a/src/core/component/render/components/v-render.ts b/src/core/component/render/components/v-render.ts new file mode 100644 index 0000000000..a0e8bbc2a6 --- /dev/null +++ b/src/core/component/render/components/v-render.ts @@ -0,0 +1,23 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode, VNodeTypes, VNodeProps } from 'core/component/engines'; + +import { components } from 'core/component/render/components/const'; +import type { CreateVNode } from 'core/component/render/components/interface'; + +export default vRender; +components['v-render'] = vRender; + +function vRender(createVNode: CreateVNode, type: VNodeTypes, props: Dictionary & VNodeProps): CanUndef { + if (type !== 'v-render' || props.from == null) { + return undefined; + } + + return Object.cast(props.from); +} diff --git a/src/core/component/engines/zero/context/interface.ts b/src/core/component/render/directives/const.ts similarity index 79% rename from src/core/component/engines/zero/context/interface.ts rename to src/core/component/render/directives/const.ts index fda725d022..f4165ba552 100644 --- a/src/core/component/engines/zero/context/interface.ts +++ b/src/core/component/render/directives/const.ts @@ -6,4 +6,4 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export type KeyCode = string | number; +export const directives: Function[] = []; diff --git a/src/core/component/render/directives/index.ts b/src/core/component/render/directives/index.ts new file mode 100644 index 0000000000..611ad421dc --- /dev/null +++ b/src/core/component/render/directives/index.ts @@ -0,0 +1,32 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode } from 'core/component/engines'; + +import { directives } from 'core/component/render/directives/const'; +import type { CreateVNode } from 'core/component/render/directives/interface'; + +/** + * Creates a new element VNode by the specified parameters with applying internal directives, like `v-attrs`. + * The function takes an original function to create VNodes and list of arguments and returns a new VNode. + * + * @param createVNode - original function to create a VNode + * @param args - operation arguments + */ +export function createVNodeWithDirectives(createVNode: CreateVNode, ...args: any[]): VNode { + for (let i = 0; i < directives.length; i++) { + const + res = directives[i](createVNode, ...args); + + if (res != null) { + return res; + } + } + + return createVNode(...args); +} diff --git a/src/core/component/render/directives/interface.ts b/src/core/component/render/directives/interface.ts new file mode 100644 index 0000000000..a9b36db8c4 --- /dev/null +++ b/src/core/component/render/directives/interface.ts @@ -0,0 +1,13 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode } from 'core/component/engines'; + +export interface CreateVNode { + (...args: any[]): VNode; +} diff --git a/src/core/component/render/directives/v-attrs.ts b/src/core/component/render/directives/v-attrs.ts new file mode 100644 index 0000000000..f4fc73aaaa --- /dev/null +++ b/src/core/component/render/directives/v-attrs.ts @@ -0,0 +1,25 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode, VNodeTypes, VNodeProps } from 'core/component/engines'; + +import { directives } from 'core/component/render/directives/const'; +import type { CreateVNode } from 'core/component/render/directives/interface'; + +export default vAttrs; +directives.push(vAttrs); + +function vAttrs(createVNode: CreateVNode, type: VNodeTypes, props: Dictionary & VNodeProps): CanUndef { + console.log(props); + + if (props['v-attrs'] == null) { + return undefined; + } + + return Object.cast(props.from); +} diff --git a/src/core/component/render/wrappers/helpers.ts b/src/core/component/render/wrappers/helpers.ts new file mode 100644 index 0000000000..cbc86de871 --- /dev/null +++ b/src/core/component/render/wrappers/helpers.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export function isSpecialComponent(component: string): boolean { + return component === 'v-render' || component.endsWith('-functional'); +} diff --git a/src/core/component/render/wrappers/index.ts b/src/core/component/render/wrappers/index.ts new file mode 100644 index 0000000000..38ffb06958 --- /dev/null +++ b/src/core/component/render/wrappers/index.ts @@ -0,0 +1,47 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { createVNode, createElementVNode, resolveComponent, withDirectives } from 'vue'; + +import * as dir from 'core/component/render/directives'; +import * as comp from 'core/component/render/components'; + +import { isSpecialComponent } from 'core/component/render/wrappers/helpers'; + +export function wrapCreateVNode(original: T): T { + return Object.cast((type, ...args) => comp.createVNodeWithDirectives(original, type, ...args)); +} + +export function wrapCreateElementVNode(original: T): T { + return Object.cast((...args) => dir.createVNodeWithDirectives(original, ...args)); +} + +export function wrapResolveComponent(original: T): T { + return Object.cast((name, ...args) => { + if (isSpecialComponent(name)) { + return name; + } + + return original(name, ...args); + }); +} + +export function wrapWithDirectives(original: T): T { + return Object.cast((vnode, dirs) => { + for (let i = 0; i < dirs.length; i++) { + const + [dir, value, arg, modifiers] = dirs[i]; + + if (dir.beforeCreate != null) { + dir.beforeCreate({value, arg, modifiers, dir}, vnode); + } + } + + return original(vnode, dirs); + }); +} From e64a64c057f974825eddcc017c6191eb25926f51 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 12:22:53 +0300 Subject: [PATCH 0066/2313] feat: provide contexts to `beforeCreate` handlers --- src/core/component/engines/directive.ts | 11 ++++++++++- src/core/component/engines/vue3/interface.ts | 20 ++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/core/component/engines/directive.ts b/src/core/component/engines/directive.ts index ba3ca1685d..17a3d9a44e 100644 --- a/src/core/component/engines/directive.ts +++ b/src/core/component/engines/directive.ts @@ -24,10 +24,19 @@ ComponentEngine.directive = function directive(name: string, directive?: Directi originalDirective = ctx.directive.bind(ctx); } - if (directive == null || Object.isFunction(directive)) { + if (directive == null) { + return originalDirective(name); + } + + if (Object.isFunction(directive)) { return originalDirective(name, directive); } + if (directive.beforeCreate != null) { + const directiveCtx = Object.assign(Object.create(directive), {directive: originalDirective}); + directive.beforeCreate = directive.beforeCreate.bind(directiveCtx); + } + const originalCreated = directive.created, originalUnmounted = directive.unmounted; diff --git a/src/core/component/engines/vue3/interface.ts b/src/core/component/engines/vue3/interface.ts index 48d92dc0e6..5922c608c2 100644 --- a/src/core/component/engines/vue3/interface.ts +++ b/src/core/component/engines/vue3/interface.ts @@ -17,15 +17,23 @@ import type { } from 'vue'; +export interface ResolveDirective { + directive(name: string): CanUndef; + directive(name: string, directive: Directive): ReturnType>; +} + export interface ObjectDirective extends SuperObjectDirective { - beforeCreate?(binding: DirectiveBinding, vnode: VNode): void; + beforeCreate?( + this: ResolveDirective & ObjectDirective, + binding: DirectiveBinding, + vnode: VNode + ): void; } -export declare type Directive = ObjectDirective | FunctionDirective; +export declare type Directive = + ObjectDirective | + FunctionDirective; export interface CreateAppFunction { - (...args: Parameters>): Overwrite, { - directive(name: string): CanUndef; - directive(name: string, directive: Directive): ReturnType>; - }>; + (...args: Parameters>): Overwrite, ResolveDirective>; } From 02d2634b989ffb48987a5aae557ac08b68b38c18 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 19:20:34 +0300 Subject: [PATCH 0067/2313] feat: improved `v-attrs` support of directives --- src/core/component/directives/attrs/const.ts | 14 ++ src/core/component/directives/attrs/index.ts | 191 ++++++++++++++++--- 2 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 src/core/component/directives/attrs/const.ts diff --git a/src/core/component/directives/attrs/const.ts b/src/core/component/directives/attrs/const.ts new file mode 100644 index 0000000000..dcd7c700b7 --- /dev/null +++ b/src/core/component/directives/attrs/const.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 + */ + +export const + modRgxp = /\S\./, + directiveRgxp = /v-(.*?)(?::(.*?))?(\..*)?$/; + +export const + handlers = new WeakMap>(); diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index 5b697a6bb0..f7f912e18c 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -11,66 +11,152 @@ * @packageDocumentation */ -import { ComponentEngine, VNode } from 'core/component/engines'; +import { ComponentEngine, DirectiveBinding, VNode } from 'core/component/engines'; +import type { ComponentInterface } from 'core/component/interface'; + +import { modRgxp, directiveRgxp, handlers } from 'core/component/directives/attrs/const'; import type { DirectiveOptions } from 'core/component/directives/attrs/interface'; +export * from 'core/component/directives/attrs/const'; export * from 'core/component/directives/attrs/interface'; ComponentEngine.directive('attrs', { beforeCreate(opts: DirectiveOptions, vnode: VNode) { - const props = vnode.props ?? {}; - vnode.props ??= props; - - console.log(vnode); - - const + let + handlerStore, attrs = opts.value; if (attrs == null) { return; } - for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { - const - key = keys[i]; + const + ctx = Object.cast(opts.instance), + props = vnode.props ?? {}; + + attrs = {...attrs}; + vnode.props ??= props; + for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { let - val = attrs[key]; + attrName = keys[i], + attrVal = attrs[attrName]; - if (key.startsWith('v-')) { + if (attrName.startsWith('v-')) { const - [, rawName, name, arg, rawModifiers] = /(v-(.*?))(?::(.*?))?(\..*)?$/.exec(key)!; + [, name, arg = '', rawModifiers = ''] = directiveRgxp.exec(attrName)!; let - modifiers; + dir; + + switch (name) { + case 'show': { + const + {r} = ctx.$renderEngine; + + dir = r.vShow; + break; + } + + case 'on': { + if (Object.isDictionary(attrVal)) { + for (let events = Object.keys(attrVal), i = 0; i < events.length; i++) { + const + key = events[i], + event = `@${key}`; + + attrs[event] = attrVal[key]; + keys.push(event); + } + } + + continue; + } + + case 'bind': { + if (Object.isDictionary(attrVal)) { + for (let events = Object.keys(attrVal), i = 0; i < events.length; i++) { + const key = events[i]; + attrs[key] = attrVal[key]; + keys.push(key); + } + } + + continue; + } + + case 'model': { + const + {r} = ctx.$renderEngine; + + switch (vnode.type) { + case 'input': + dir = r[`vModel${(props.type ?? '').capitalize()}`] ?? r.vModelText; + break; + + case 'select': + dir = r.vModelSelect; + break; + + default: + dir = r.vModelDynamic; + } + + const + cache = getHandlerStore(), + modelProp = String(attrVal), + handlerKey = `onUpdate:modelValue:${modelProp}`; + + let + handler = cache.get(handlerKey); + + if (handler == null) { + handler = (newVal) => ctx[modelProp] = newVal; + cache.set(handlerKey, handler); + } + + props['onUpdate:modelValue'] = handler; + attrVal = ctx[modelProp]; + + break; + } + + default: + dir = this.directive(name); + } + + if (dir == null) { + throw new ReferenceError(`The specified directive "${name}" is not registered`); + } - if (Object.isTruly(rawModifiers)) { + const modifiers = {}; + if (rawModifiers.length > 0) { for (let o = rawModifiers.split('.'), i = 0; i < o.length; i++) { modifiers[o[i]] = true; } } - const - dir = {name, rawName, value: val}; + const dirDecl: DirectiveBinding = { + arg, + modifiers, - if (Object.isTruly(arg)) { - dir.arg = arg; - } + value: attrVal, + oldValue: null, - if (Object.isTruly(modifiers)) { - dir.modifiers = modifiers; - } + instance: opts.instance, + dir: Object.isFunction(dir) ? {created: dir} : dir + }; const dirs = vnode.dirs ?? []; vnode.dirs = dirs; - dirs.push(); + dirs.push(dirDecl); - } else if (key.startsWith('@')) { + } else if (attrName.startsWith('@')) { let - event = key.slice(1); + event = attrName.slice(1); const eventChunks = event.split('.'), @@ -110,9 +196,9 @@ ComponentEngine.directive('attrs', { if (Object.keys(flags).length > 0) { const - original = val; + originalHandler = attrVal; - val = (e: MouseEvent | KeyboardEvent, ...args) => { + attrVal = (e: MouseEvent | KeyboardEvent, ...args) => { if ( flags.ctrl && !e.ctrlKey || flags.alt && !e.altKey || @@ -161,15 +247,58 @@ ComponentEngine.directive('attrs', { e.stopPropagation(); } - return (original)(e, ...args); + return (originalHandler)(e, ...args); }; } - props[event] = val; + props[event] = attrVal; } else { - props[key] = val; + attrName = attrName.startsWith(':') ? attrName.slice(1) : attrName; + + if (modRgxp.test(attrName)) { + const attrChunks = attrName.split('.'); + attrName = attrName.startsWith('.') ? `.${attrChunks[1]}` : attrChunks[0]; + + if (attrChunks.includes('camel')) { + attrName = attrName.camelize(false); + } + + if (attrChunks.includes('prop') && !attrName.startsWith('.')) { + if (attrName.startsWith('^')) { + throw new SyntaxError('Invalid v-bind modifiers'); + } + + attrName = `.${attrName}`; + } + + if (attrChunks.includes('attr') && !attrName.startsWith('^')) { + if (attrName.startsWith('.')) { + throw new SyntaxError('Invalid v-bind modifiers'); + } + + attrName = `^${attrName}`; + } + } + + console.log(attrName, attrVal); + props[attrName] = attrVal; + } + } + + function getHandlerStore() { + if (handlerStore != null) { + return handlerStore; } + + handlerStore = handlers.get(ctx); + + if (handlerStore == null) { + handlerStore = new Map(); + handlers.set(ctx, handlerStore); + } + + return handlerStore; } } }); From 8f1a1d57da59d9d5e5b085ee7d738d08410d6f63 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 19:20:51 +0300 Subject: [PATCH 0068/2313] fix: `v-attrs` is enable by default --- src/core/component/directives/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index ca12cad9c1..27591bab11 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -23,3 +23,4 @@ import 'core/component/directives/update-on'; //#endif import 'core/component/directives/hook'; +import 'core/component/directives/attrs'; From ba98b08298d6a8120d3ce396378d534474b0c136 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 19:21:11 +0300 Subject: [PATCH 0069/2313] refactor: removed legacy --- .../component/render-function/CHANGELOG.md | 86 ----- src/core/component/render-function/README.md | 3 - src/core/component/render-function/const.ts | 10 - .../render-function/create-element.ts | 308 ------------------ src/core/component/render-function/index.ts | 14 - src/core/component/render-function/render.ts | 277 ---------------- src/core/component/render-function/v-attrs.ts | 191 ----------- 7 files changed, 889 deletions(-) delete mode 100644 src/core/component/render-function/CHANGELOG.md delete mode 100644 src/core/component/render-function/README.md delete mode 100644 src/core/component/render-function/const.ts delete mode 100644 src/core/component/render-function/create-element.ts delete mode 100644 src/core/component/render-function/index.ts delete mode 100644 src/core/component/render-function/render.ts delete mode 100644 src/core/component/render-function/v-attrs.ts diff --git a/src/core/component/render-function/CHANGELOG.md b/src/core/component/render-function/CHANGELOG.md deleted file mode 100644 index 1c8c43aa5d..0000000000 --- a/src/core/component/render-function/CHANGELOG.md +++ /dev/null @@ -1,86 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.12.1 (2021-11-26) - -#### :bug: Bug Fix - -* Fixed using `asyncRender` within nested flyweight components - -## v3.0.0-rc.188 (2021-05-14) - -#### :bug: Bug Fix - -* Don't skip a context of `createElement` - -## v3.0.0-rc.186 (2021-05-13) - -#### :bug: Bug Fix - -* Fixed context providing to functional components - -## v3.0.0-rc.183 (2021-05-12) - -#### :bug: Bug Fix - -* Fixed a bug with functional components after adding Zero - -## v3.0.0-rc.179 (2021-04-15) - -#### :bug: Bug Fix - -* Fixed resolving refs within functional components - -## v3.0.0-rc.112 (2020-12-18) - -#### :bug: Bug Fix - -* Fixed providing of render groups - -## v3.0.0-rc.105 (2020-12-09) - -#### :bug: Bug Fix - -* Fixed a bug with redundant `v-for` invokes - -## v3.0.0-rc.99 (2020-11-17) - -#### :bug: Bug Fix - -* [Fixed dynamic creation of flyweight components](https://github.com/V4Fire/Client/issues/434) - -## v3.0.0-rc.96 (2020-11-10) - -#### :rocket: New Feature - -* Added support of creation flyweight components via `$createElement` - -## v3.0.0-rc.92 (2020-11-03) - -#### :bug: Bug Fix - -* Fixed providing of styles - -#### :house: Internal - -* Refactoring - -## v3.0.0-rc.55 (2020-08-05) - -#### :bug: Bug Fix - -* Fixed an issue with `unsafe` after refactoring - -## v3.0.0-rc.48 (2020-08-02) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/core/component/render-function/README.md b/src/core/component/render-function/README.md deleted file mode 100644 index 0844e096bf..0000000000 --- a/src/core/component/render-function/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core/component/render-function - -This module provides API to wrap render functions. diff --git a/src/core/component/render-function/const.ts b/src/core/component/render-function/const.ts deleted file mode 100644 index 833defaf0c..0000000000 --- a/src/core/component/render-function/const.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export const - vAttrsRgxp = /(v-(.*?))(?::(.*?))?(\..*)?$/; diff --git a/src/core/component/render-function/create-element.ts b/src/core/component/render-function/create-element.ts deleted file mode 100644 index 82098e7d74..0000000000 --- a/src/core/component/render-function/create-element.ts +++ /dev/null @@ -1,308 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; -import * as c from 'core/component/const'; - -import { attachTemplatesToMeta } from 'core/component/meta'; -import { getComponentRenderCtxFromVNode } from 'core/component/vnode'; -import { execRenderObject } from 'core/component/render'; - -import { parseVNodeAsFlyweight } from 'core/component/flyweight'; -import { createFakeCtx, initComponentVNode, FlyweightVNode } from 'core/component/functional'; - -import { applyDynamicAttrs } from 'core/component/render-function/v-attrs'; -import { registerComponent } from 'core/component/register'; - -import type { CreateElement, VNode, VNodeData } from 'core/component/engines'; -import type { FunctionalCtx, ComponentInterface, UnsafeComponentInterface } from 'core/component/interface'; - -export const - $$ = symbolGenerator(); - -/** - * Wraps the specified createElement function and returns a pair: - * the wrapped function, and a list of registered render tasks. - * - * This method adds V4Fire specific logic (v-attrs, composites, etc.) to a simple createElement function. - * - * @param nativeCreateElement - original createElement function - * @param baseCtx - base component context - */ -export function wrapCreateElement( - nativeCreateElement: CreateElement, - baseCtx: ComponentInterface -): [CreateElement, Function[]] { - const tasks = baseCtx[$$.tasks] ?? []; - baseCtx[$$.tasks] = tasks; - - const - engine = baseCtx.$renderEngine, - {supports} = engine; - - const wrappedCreateElement = function wrappedCreateElement( - this: Nullable, - tag: CanUndef, - tagData?: VNodeData, - children?: VNode[] - ): CanPromise { - // eslint-disable-next-line - 'use strict'; - - const - ctx = this ?? baseCtx, - unsafe = Object.cast(ctx), - attrs = Object.isPlainObject(tagData) ? tagData.attrs : undefined; - - const createElement = function createElement(this: unknown, ...args: unknown[]) { - if (supports.boundCreateElement) { - const dontProvideBoundContext = nativeCreateElement[$$.wrappedCreateElement] === true; - return nativeCreateElement.apply(dontProvideBoundContext ? this : unsafe, args); - } - - return nativeCreateElement.apply(this, args); - }; - - let - tagName = tag, - flyweightComponent; - - if (attrs == null) { - if (tag === 'v-render') { - return createElement(); - } - - } else { - if (tag === 'v-render') { - return attrs.from ?? createElement(); - } - - if (tagName?.[0] === '@') { - flyweightComponent = tagName.slice(1); - tagName = 'span'; - - } else { - flyweightComponent = attrs['v4-flyweight-component']; - } - - if (tagName != null && flyweightComponent != null) { - tagName = tagName === 'span' ? flyweightComponent : tagName.dasherize(); - attrs['v4-flyweight-component'] = tagName; - } - } - - const - component = registerComponent(tagName); - - if (Object.isPlainObject(tagData)) { - applyDynamicAttrs(tagData, component); - } - - let - renderKey = ''; - - if (attrs != null) { - if (attrs['render-key'] != null) { - renderKey = `${tagName}:${attrs['global-name']}:${attrs['render-key']}`; - } - - if (renderKey !== '' && component == null) { - attrs['data-render-key'] = renderKey; - delete attrs['render-key']; - } - } - - let - vnode = >unsafe.renderTmp[renderKey], - needLinkToEl = Boolean(flyweightComponent); - - const needCreateFunctionalComponent = - !supports.regular || - - vnode == null && - flyweightComponent == null && - supports.functional && - component?.params.functional === true; - - if (component && needCreateFunctionalComponent) { - needLinkToEl = true; - - const - {componentName} = component; - - let - renderObj = c.componentTemplates[componentName]; - - if (renderObj == null) { - attachTemplatesToMeta(component, TPLS[componentName]); - renderObj = c.componentTemplates[componentName]; - } - - if (renderObj == null) { - return createElement(); - } - - const - node = createElement('span', {...tagData, tag: undefined}, children), - renderCtx = getComponentRenderCtxFromVNode(component, node, ctx); - - let - baseCtx = >c.renderCtxCache[componentName]; - - if (baseCtx == null) { - baseCtx = Object.create(engine.minimalCtx); - - // @ts-ignore (access) - baseCtx.componentName = componentName; - - // @ts-ignore (access) - baseCtx.meta = component; - - // @ts-ignore (access) - component.params.functional = true; - - // @ts-ignore (access) - baseCtx.instance = component.instance; - - // @ts-ignore (access) - baseCtx.$options = {}; - } - - c.renderCtxCache[componentName] = baseCtx; - - // @ts-ignore (access) - baseCtx._l = ctx._l; - - // @ts-ignore (access) - baseCtx._u = ctx._u; - - const fakeCtx = createFakeCtx(Object.cast(wrappedCreateElement), renderCtx, baseCtx!, { - initProps: true - }); - - const createComponentVNode = () => { - const - vnode = execRenderObject(renderObj!, fakeCtx); - - if (Object.isPromise(vnode)) { - return vnode.then((vnode) => initComponentVNode(Object.cast(vnode), fakeCtx, renderCtx)); - } - - return initComponentVNode(vnode, fakeCtx, renderCtx); - }; - - if (supports.ssr && Object.isPromise(fakeCtx.unsafe.$initializer)) { - return fakeCtx.unsafe.$initializer.then(async () => patchVNode(await createComponentVNode())); - } - - vnode = createComponentVNode(); - } - - if (vnode == null) { - // eslint-disable-next-line prefer-rest-params - vnode = createElement.apply(unsafe, arguments); - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (vnode == null) { - return createElement(); - } - - if (flyweightComponent != null) { - vnode = parseVNodeAsFlyweight(vnode, wrappedCreateElement, ctx); - } - } - - return patchVNode(vnode); - - function patchVNode(vnode: T): T { - const - vData = vnode.data, - ref = vData != null && (vData[$$.ref] ?? vData.ref); - - if (renderKey !== '') { - unsafe.renderTmp[renderKey] = engine.cloneVNode(vnode); - } - - // Add $refs link if it does not exist - if (vData != null && ref != null && ctx !== baseCtx) { - vData[$$.ref] = ref; - vData.ref = `${ref}:${ctx.componentId}`; - - Object.defineProperty(unsafe.$refs, ref, { - configurable: true, - enumerable: true, - get: () => { - const - r = baseCtx.unsafe.$refs, - l = r[`${ref}:${unsafe.$componentId}`] ?? r[`${ref}:${ctx.componentId}`]; - - if (l != null) { - return l; - } - - return 'fakeInstance' in vnode ? vnode.fakeInstance : vnode.elm; - } - }); - } - - // Add $el link if it does not exist - if (needLinkToEl && 'fakeInstance' in vnode) { - Object.defineProperty(vnode.fakeInstance, '$el', { - enumerable: true, - configurable: true, - - set(): void { - // Loopback - }, - - get(): CanUndef { - return vnode.elm; - } - }); - } - - if (tasks.length > 0) { - for (let i = 0; i < tasks.length; i++) { - tasks[i](vnode); - } - - tasks.splice(0); - } - - vnode.fakeContext = ctx; - return vnode; - } - }; - - wrappedCreateElement[$$.wrappedCreateElement] = true; - - if (supports.ssr) { - const wrappedAsyncCreateElement = function wrappedAsyncCreateElement( - this: Nullable, - tag: CanUndef, - opts?: VNodeData, - children?: VNode[] - ): CanPromise { - if (children != null && children.length > 0) { - children = children.flat(); - - // eslint-disable-next-line @typescript-eslint/unbound-method - if (children.some(Object.isPromise)) { - return Promise.all(children).then((children) => wrappedCreateElement.call(this, tag, opts, children)); - } - } - - // eslint-disable-next-line prefer-rest-params - return wrappedCreateElement.apply(this, arguments); - }; - - return [wrappedAsyncCreateElement, tasks]; - } - - return [wrappedCreateElement, tasks]; -} diff --git a/src/core/component/render-function/index.ts b/src/core/component/render-function/index.ts deleted file mode 100644 index b11bd3b322..0000000000 --- a/src/core/component/render-function/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/render-function/README.md]] - * @packageDocumentation - */ - -export * from 'core/component/render-function/render'; diff --git a/src/core/component/render-function/render.ts b/src/core/component/render-function/render.ts deleted file mode 100644 index b68565df33..0000000000 --- a/src/core/component/render-function/render.ts +++ /dev/null @@ -1,277 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { beforeMountHooks, mountedHooks } from 'core/component/const'; - -import { resolveRefs } from 'core/component/ref'; -import { wrapCreateElement } from 'core/component/render-function/create-element'; - -import type { CreateElement, RenderContext, VNode } from 'core/component/engines'; -import type { ComponentInterface, ComponentMeta, RenderFunction } from 'core/component/interface'; - -/** - * Wraps the specified render function and returns a new function. - * This method adds V4Fire specific logic (v-attrs, composites, etc.) to a simple render function. - * - * @param meta - component meta object - */ -export function wrapRender(meta: ComponentMeta): RenderFunction { - return function render( - this: ComponentInterface, - nativeCreateElement: CreateElement, - baseCtx: RenderContext - ): VNode { - const - {unsafe} = this; - - const - renderCounter = ++unsafe.renderCounter, - now = Date.now(); - - if (!IS_PROD) { - const - {lastSelfReasonToRender, lastTimeOfRender} = unsafe; - - let - diff; - - if (lastTimeOfRender != null && (diff = now - lastTimeOfRender) < 100) { - const printableReason = lastSelfReasonToRender != null ? - { - ...lastSelfReasonToRender, - path: lastSelfReasonToRender.path.join('.') - } : - - 'forceUpdate'; - - console.warn( - `There is too frequent redrawing of the component "${this.componentName}" (# ${renderCounter}; ${diff}ms).`, - printableReason - ); - } - } - - unsafe.lastTimeOfRender = now; - - const - {methods: {render: originalRender}} = meta; - - if (originalRender) { - const - asyncLabel = unsafe.$asyncLabel; - - const - [createElement, tasks] = wrapCreateElement(nativeCreateElement, this); - - unsafe.$createElement = createElement; - unsafe._c = createElement; - - // Wrap slot directive to support async rendering - unsafe._u = (fns, res) => { - res ??= {}; - - for (let i = 0; i < fns.length; i++) { - const - el = fns[i]; - - if (el == null) { - continue; - } - - if (Array.isArray(el)) { - unsafe._u(el, res); - - } else { - res[el.key] = function execAsyncTasks(this: unknown, ...args: unknown[]): VNode[] { - const - children = fns[i].fn.apply(this, args); - - if (tasks.length > 0) { - for (let i = 0; i < tasks.length; i++) { - tasks[i](children); - } - - tasks.splice(0); - } - - return children; - }; - } - } - - return res; - }; - - const forEach = (unsafe._originalL ?? unsafe._l); - unsafe._originalL = forEach; - - // Wrap v-for directive to support async loop rendering - unsafe._l = (iterable, forEachCb) => { - const - res = forEach(iterable, forEachCb); - - if (iterable?.[asyncLabel] != null) { - tasks.push((vnodes?: CanArray) => { - if (vnodes == null) { - return; - } - - const - isTemplateParent = Object.isArray(vnodes); - - let - vnode: VNode; - - if (isTemplateParent) { - while (Object.isArray(vnodes)) { - let - newVNode = vnodes[0]; - - for (let i = 0; i < vnodes.length; i++) { - const - el = vnodes[i]; - - if (!Object.isArray(el) && el.context) { - newVNode = el; - } - } - - vnodes = newVNode; - } - - if (vnodes == null) { - return; - } - - vnode = vnodes; - - } else { - vnode = >vnodes; - } - - if (vnode.context == null || !('$async' in vnode.context)) { - return; - } - - const - ctx = vnode.context; - - if (!isTemplateParent) { - vnode['fakeInstance'] = ctx; - } - - // Function that render a chunk of VNodes - const fn = () => { - iterable[asyncLabel]((iterable, desc, returnEls) => { - desc.async.setImmediate(syncFn, { - group: desc.renderGroup - }); - - function syncFn(): void { - const - els = [], - renderNodes = >>[], - nodes = []; - - const - parent = isTemplateParent ? vnode.elm?.parentNode : vnode.elm; - - if (parent == null) { - return returnEls([]); - } - - // @ts-ignore (readonly) - ctx['renderGroup'] = desc?.renderGroup; - - for (let o = forEach(iterable, forEachCb), i = 0; i < o.length; i++) { - const - el = o[i]; - - if (el == null) { - continue; - } - - if (Object.isArray(el)) { - for (let o = el, i = 0; i < o.length; i++) { - const - el = >o[i]; - - if (el == null) { - continue; - } - - if (el.elm) { - el.elm[asyncLabel] = true; - renderNodes.push(el.elm); - - } else { - nodes.push(el); - renderNodes.push(null); - } - } - - } else if (el.elm != null) { - el.elm[asyncLabel] = true; - renderNodes.push(el.elm); - - } else { - nodes.push(el); - renderNodes.push(null); - } - } - - const - renderedVNodes = unsafe.$renderEngine.renderVNode(nodes, ctx); - - for (let i = 0, j = 0; i < renderNodes.length; i++) { - const - el = >>(renderNodes[i] ?? renderedVNodes[j++]); - - if (Object.isArray(el)) { - for (let i = 0; i < el.length; i++) { - const - node = el[i]; - - if (node != null) { - els.push(parent.appendChild(node)); - } - } - - } else if (el != null) { - els.push(parent.appendChild(el)); - } - } - - // @ts-ignore (readonly) - ctx['renderGroup'] = undefined; - resolveRefs(ctx); - - return returnEls(els); - } - }); - }; - - if (mountedHooks[ctx.hook] != null) { - ctx.$nextTick(fn); - - } else { - const hooks = ctx.meta.hooks[beforeMountHooks[ctx.hook] != null ? 'mounted' : 'beforeUpdated']; - hooks.push({fn, once: true}); - } - }); - } - - return res; - }; - - return originalRender.fn.call(this, createElement, baseCtx); - } - - return nativeCreateElement(); - }; -} diff --git a/src/core/component/render-function/v-attrs.ts b/src/core/component/render-function/v-attrs.ts deleted file mode 100644 index f3b443c063..0000000000 --- a/src/core/component/render-function/v-attrs.ts +++ /dev/null @@ -1,191 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { parseStyle } from 'core/component/vnode'; -import type { VNodeData } from 'core/component/engines'; - -import { vAttrsRgxp } from 'core/component/render-function/const'; -import type { ComponentMeta } from 'core/component/interface'; - -/** - * Applies dynamic attributes from v-attrs to the specified vnode - * - * @param vData - vnode data object - * @param [component] - component meta object that is tied to the vnode - */ -export function applyDynamicAttrs(vData: VNodeData, component?: ComponentMeta): void { - const - attrs = vData.attrs ?? {}, - attrsSpreadObj = attrs['v-attrs'], - slotsSpreadObj = attrs['v-slots']; - - vData.attrs = attrs; - delete attrs['v-attrs']; - delete attrs['v-slots']; - - if (Object.isPlainObject(slotsSpreadObj)) { - const - slotOpts = vData.scopedSlots ?? {}; - - for (let keys = Object.keys(slotsSpreadObj), i = 0; i < keys.length; i++) { - const - key = keys[i]; - - let nm = `@${key}`; - nm = slotOpts[nm] ? nm : '@'; - - if (slotOpts[nm]) { - const - fn = slotOpts[nm]; - - slotOpts[key] = (obj) => { - obj.slotContent = slotsSpreadObj[key]; - return (fn)(obj); - }; - - if (nm === '@') { - delete slotOpts[nm]; - } - } - } - - delete slotOpts['@']; - } - - if (Object.isPlainObject(attrsSpreadObj)) { - const - eventOpts = vData.on ?? {}, - nativeEventOpts = vData.nativeOn ?? {}, - directiveOpts = vData.directives ?? []; - - vData.on = eventOpts; - vData.nativeOn = nativeEventOpts; - vData.directives = directiveOpts; - - for (let keys = Object.keys(attrsSpreadObj), i = 0; i < keys.length; i++) { - let - key = keys[i], - val = attrsSpreadObj[key]; - - if (component) { - const - propKey = `${key}Prop`; - - if (!component.props[key] && component.props[propKey]) { - key = propKey; - } - } - - if (key.startsWith('@')) { - let - event = key.slice(1); - - if (component) { - const - eventChunks = event.split('.'), - flags = >{}; - - for (let i = 1; i < eventChunks.length; i++) { - flags[eventChunks[i]] = true; - } - - event = eventChunks[0].dasherize(); - - if (flags.native) { - if (flags.right) { - event = 'contextmenu'; - } - - if (flags.capture) { - event = `!${event}`; - } - - if (flags.once) { - event = `~${event}`; - } - - if (flags.passive) { - event = `&${event}`; - } - - if (flags.self || flags.prevent || flags.stop) { - const - originalFn = val; - - val = (e: Event | MouseEvent) => { - if (flags.prevent) { - e.preventDefault(); - } - - if (flags.self && e.target !== e.currentTarget) { - return null; - } - - if (flags.stop) { - e.stopPropagation(); - } - - return (originalFn)(e); - }; - } - - if (!(event in nativeEventOpts)) { - nativeEventOpts[event] = val; - } - - } else if (!(event in eventOpts)) { - eventOpts[event] = val; - } - - } else if (!(event in eventOpts)) { - eventOpts[event] = val; - } - - } else if (key.startsWith('v-')) { - const - [, rawName, name, arg, rawModifiers] = vAttrsRgxp.exec(key)!; - - let - modifiers; - - if (Object.isTruly(rawModifiers)) { - modifiers = {}; - - for (let o = rawModifiers.split('.'), i = 0; i < o.length; i++) { - modifiers[o[i]] = true; - } - } - - const - dir = {name, rawName, value: val}; - - if (Object.isTruly(arg)) { - dir.arg = arg; - } - - if (Object.isTruly(modifiers)) { - dir.modifiers = modifiers; - } - - directiveOpts.push(Object.cast(dir)); - - } else if (key === 'staticClass') { - vData.staticClass = Array.concat([], vData.staticClass, val).join(' '); - - } else if (key === 'class') { - vData.class = Array.concat([], vData.class, val); - - } else if (key === 'style') { - vData.style = parseStyle(vData.style, parseStyle(val)); - - } else if (attrs[key] == null) { - attrs[key] = val; - } - } - } -} From 148429f326713d13fdec59f8e28ada66697d3855 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 19:21:30 +0300 Subject: [PATCH 0070/2313] feat: improved vue3 engine --- src/core/component/engines/engine.ts | 5 +- src/core/component/engines/index.ts | 1 - src/core/component/engines/interface.ts | 11 +++- src/core/component/engines/vue3/component.ts | 15 +++--- src/core/component/engines/vue3/config.ts | 5 +- src/core/component/engines/vue3/const.ts | 21 +------- src/core/component/engines/vue3/index.ts | 14 ++--- src/core/component/engines/vue3/render.ts | 41 ++++++++++++-- src/core/component/engines/vue3/vnode.ts | 57 -------------------- 9 files changed, 62 insertions(+), 108 deletions(-) delete mode 100644 src/core/component/engines/vue3/vnode.ts diff --git a/src/core/component/engines/engine.ts b/src/core/component/engines/engine.ts index 132b956eec..236458f426 100644 --- a/src/core/component/engines/engine.ts +++ b/src/core/component/engines/engine.ts @@ -6,9 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -//#if runtime.engine = vue3 export * from 'core/component/engines/vue3'; -//#endif - export * from 'core/component/engines/interface'; + export { VNode } from 'core/component/engines/interface'; +export { CreateAppFunction, Directive, ObjectDirective } from 'core/component/engines/vue3'; diff --git a/src/core/component/engines/index.ts b/src/core/component/engines/index.ts index 19e6c8a3b2..e96e4eb865 100644 --- a/src/core/component/engines/index.ts +++ b/src/core/component/engines/index.ts @@ -13,5 +13,4 @@ import 'core/component/engines/directive'; -export * from 'core/component/engines/helpers'; export * from 'core/component/engines/engine'; diff --git a/src/core/component/engines/interface.ts b/src/core/component/engines/interface.ts index 32c511a1c1..692d15e825 100644 --- a/src/core/component/engines/interface.ts +++ b/src/core/component/engines/interface.ts @@ -7,8 +7,17 @@ */ import type { VNode as SuperVNode } from 'vue'; +import type { RendererElement, RendererNode } from 'core/component/engines'; import type { ComponentInterface } from 'core/component/interface'; -export interface VNode extends SuperVNode { +//#if @ignore +export * from '@vue/runtime-dom'; +//#endif + +export interface VNode< + HostNode = RendererNode, + HostElement = RendererElement, + ExtraProps = {[key: string]: any} +> extends SuperVNode { fakeContext?: ComponentInterface; } diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index bc06efd7a9..b07363a0c0 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -13,12 +13,14 @@ import { beforeRenderHooks } from 'core/component/const'; import { fillMeta } from 'core/component/meta'; import { implementComponentForceUpdateAPI } from 'core/component/render'; +import { getComponentContext } from 'core/component/context'; -import { getComponentContext, ComponentEngine, ComponentOptions } from 'core/component/engines'; +import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; import type { ComponentMeta } from 'core/component/interface'; -import { supports, minimalCtx, proxyGetters } from 'core/component/engines/vue3/const'; -import { cloneVNode, patchVNode, renderVNode } from 'core/component/engines/vue3/vnode'; +import { supports, proxyGetters } from 'core/component/engines/vue3/const'; + +import * as r from 'core/component/engines/vue3/render'; /** * Returns a component declaration object from the specified component meta object @@ -83,12 +85,9 @@ export function getComponent(meta: ComponentMeta): ComponentOptions { - logger.error('errorHandler', err, info, getComponentInfoLog(vm)); + console.log(err); + //logger.error('errorHandler', err, info, getComponentInfoLog(vm)); }; Vue.config.warnHandler = (msg, vm, trace) => { - logger.warn('warnHandler', msg, trace, getComponentInfoLog(vm)); + //logger.warn('warnHandler', msg, trace, getComponentInfoLog(vm)); }; const diff --git a/src/core/component/engines/vue3/const.ts b/src/core/component/engines/vue3/const.ts index 954e890c09..6e7a2b5f28 100644 --- a/src/core/component/engines/vue3/const.ts +++ b/src/core/component/engines/vue3/const.ts @@ -6,31 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import Vue from 'core/component/engines/vue3/lib'; - export const supports = { regular: true, functional: true, - composite: true, - - ssr: false, - boundCreateElement: true + ssr: false }; -export const minimalCtx = (() => { - const - obj = Vue.prototype, - ctx = {}; - - for (const key in obj) { - if (key.length === 2) { - ctx[key] = obj[key]; - } - } - - return ctx; -})(); - export const proxyGetters = Object.createDict({ prop: (ctx) => ({ key: '_props', diff --git a/src/core/component/engines/vue3/index.ts b/src/core/component/engines/vue3/index.ts index e9927e83fe..335b7c3fe0 100644 --- a/src/core/component/engines/vue3/index.ts +++ b/src/core/component/engines/vue3/index.ts @@ -14,17 +14,9 @@ import Vue from 'core/component/engines/vue3/lib'; import 'core/component/engines/vue3/config'; -export * from 'vue'; - -/** @deprecated */ -export { Vue as ComponentDriver }; -export { Vue as ComponentEngine }; -export { Vue as default }; - export * from 'core/component/engines/vue3/const'; -export * from 'core/component/engines/vue3/vnode'; export * from 'core/component/engines/vue3/component'; +export * from 'core/component/engines/vue3/interface'; -//#if VueInterfaces -export { VNode } from 'vue'; -//#endif +export { Vue as ComponentEngine }; +export { Vue as default }; diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 8bd034da8a..f556dbd278 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -6,34 +6,65 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import { + + resolveComponent as resolveComponentSuper, + createVNode as createVNodeSuper, + createElementVNode as createElementVNodeSuper, + withDirectives as withDirectivesSuper + +} from 'vue'; + +import { + + wrapResolveComponent, + wrapCreateVNode, + wrapCreateElementVNode, + wrapWithDirectives + +} from 'core/component/render/wrappers'; + export { Fragment, Transition, TransitionGroup, - openBlock, + toHandlers, toDisplayString, renderList, renderSlot, + openBlock, createBlock, createElementBlock, - createVNode, createStaticVNode, - createElementVNode, createTextVNode, createCommentVNode, normalizeClass, normalizeStyle, + mergeProps, - resolveComponent, resolveDirective, withCtx, - withDirectives + withKeys, + withModifiers, + + vShow, + vModelText, + vModelSelect, + vModelCheckbox, + vModelRadio, + vModelDynamic } from 'vue'; + +export const + resolveComponent = wrapResolveComponent(resolveComponentSuper), + createVNode = wrapCreateVNode(createVNodeSuper), + createElementVNode = wrapCreateElementVNode(createElementVNodeSuper), + withDirectives = wrapWithDirectives(withDirectivesSuper); diff --git a/src/core/component/engines/vue3/vnode.ts b/src/core/component/engines/vue3/vnode.ts deleted file mode 100644 index d27f09bd87..0000000000 --- a/src/core/component/engines/vue3/vnode.ts +++ /dev/null @@ -1,57 +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 { VNode } from 'vue'; -import Vue from 'core/component/engines/vue3/lib'; - -import { patchComponentVData } from 'core/component/vnode'; -import type { ComponentInterface } from 'core/component/interface'; - -/** - * Clones the specified vnode - * @param vnode - */ -export function cloneVNode(vnode: VNode): VNode { - return vnode; -} - -/** - * Patches the specified VNode by using provided contexts - * - * @param vnode - * @param component - component instance - * @param renderCtx - render context - */ -export function patchVNode(vnode: VNode, component: ComponentInterface, renderCtx: RenderContext): void { - patchComponentVData(vnode.data, renderCtx.data, { - patchAttrs: Boolean(component.unsafe.meta.params.inheritAttrs) - }); -} - -/** - * Renders the specified VNode/s and returns the result - * - * @param vnode - * @param parent - parent component - */ -export function renderVNode(vnode: VNode, parent: ComponentInterface): Node; -export function renderVNode(vnodes: VNode[], parent: ComponentInterface): Node[]; -export function renderVNode(vnode: CanArray, parent: ComponentInterface): CanArray { - const vue = new Vue({ - render: (c) => Object.isArray(vnode) ? c('div', vnode) : vnode - }); - - Object.set(vue, '$root', Object.create(parent.$root)); - Object.set(vue, '$root.$remoteParent', parent); - Object.set(vue, '$root.unsafe', vue.$root); - - const el = document.createElement('div'); - vue.$mount(el); - - return Object.isArray(vnode) ? Array.from(vue.$el.childNodes) : vue.$el; -} From 907a06dd33900741cf599db525709199c32b0a26 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 19:22:02 +0300 Subject: [PATCH 0071/2313] feat: added render API --- src/core/component/interface/engine.ts | 105 +++++++++++++++++++++---- 1 file changed, 89 insertions(+), 16 deletions(-) diff --git a/src/core/component/interface/engine.ts b/src/core/component/interface/engine.ts index fa65f19826..c432ac192b 100644 --- a/src/core/component/interface/engine.ts +++ b/src/core/component/interface/engine.ts @@ -6,16 +6,100 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNode } from 'core/component/engines'; -import type { ComponentInterface } from 'core/component/interface/component/component'; +import type { + + Fragment, + Transition, + TransitionGroup, + + toHandlers, + toDisplayString, + + renderList, + renderSlot, + + openBlock, + createBlock, + createElementBlock, + + createVNode, + createStaticVNode, + createElementVNode, + createTextVNode, + createCommentVNode, + + normalizeClass, + normalizeStyle, + mergeProps, + + resolveComponent, + resolveDirective, + + withCtx, + withKeys, + withModifiers, + withDirectives, + + vShow, + vModelText, + vModelSelect, + vModelCheckbox, + vModelRadio, + vModelDynamic + +} from 'vue'; export interface RenderEngineFeatures { regular: boolean; functional: boolean; - composite: boolean; - ssr: boolean; - boundCreateElement: boolean; +} + +export interface RenderEngine { + supports: RenderEngineFeatures; + proxyGetters: ProxyGetters; + r: RenderAPI; +} + +export interface RenderAPI { + Fragment: typeof Fragment; + Transition: typeof Transition; + TransitionGroup: typeof TransitionGroup; + + toHandlers: typeof toHandlers; + toDisplayString: typeof toDisplayString; + + renderList: typeof renderList; + renderSlot: typeof renderSlot; + + openBlock: typeof openBlock; + createBlock: typeof createBlock; + createElementBlock: typeof createElementBlock; + + createVNode: typeof createVNode; + createStaticVNode: typeof createStaticVNode; + createElementVNode: typeof createElementVNode; + createTextVNode: typeof createTextVNode; + createCommentVNode: typeof createCommentVNode; + + normalizeClass: typeof normalizeClass; + normalizeStyle: typeof normalizeStyle; + mergeProps: typeof mergeProps; + + resolveComponent: typeof resolveComponent; + resolveDirective: typeof resolveDirective; + + withCtx: typeof withCtx; + withKeys: typeof withKeys; + withModifiers: typeof withModifiers; + withDirectives: typeof withDirectives; + + vShow: typeof vShow; + vModelText: typeof vModelText; + vModelSelect: typeof vModelSelect; + vModelCheckbox: typeof vModelCheckbox; + vModelRadio: typeof vModelRadio; + vModelDynamic: typeof vModelDynamic; } export type ProxyGetterType = @@ -32,14 +116,3 @@ export type ProxyGetter = (ctx: T) => { }; export type ProxyGetters = Record>; - -export interface RenderEngine { - supports: RenderEngineFeatures; - proxyGetters: ProxyGetters; - - cloneVNode(vnode: VNode): VNode; - patchVNode(vnode: VNode, component: ComponentInterface): VNode; - - renderVNode(vnode: VNode, parent: ComponentInterface): Node; - renderVNode(vnodes: VNode[], parent: ComponentInterface): Node[]; -} From 65a06ca54a67b856fffee9aec6c82a8e2a95a41d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 21 Apr 2022 19:22:31 +0300 Subject: [PATCH 0072/2313] fix: added support for new template api --- src/core/component/register/decorator.ts | 6 +----- src/core/component/register/helpers.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/component/register/decorator.ts b/src/core/component/register/decorator.ts index 44ad555725..df62e0cf8c 100644 --- a/src/core/component/register/decorator.ts +++ b/src/core/component/register/decorator.ts @@ -11,7 +11,7 @@ import { identity } from 'core/functools'; import * as c from 'core/component/const'; import { createMeta, fillMeta, attachTemplatesToMeta } from 'core/component/meta'; -import { getInfoFromConstructor } from 'core/component/reflection'; +import { getInfoFromConstructor } from 'core/component/reflect'; import { getComponent, ComponentEngine } from 'core/component/engines'; import { registerParentComponents } from 'core/component/register/helpers'; @@ -125,10 +125,6 @@ export function component(opts?: ComponentOptions): Function { function attachTemplatesAndResolve(tpls?: Dictionary) { attachTemplatesToMeta(meta, tpls); - - // @ts-ignore (access) - component.staticRenderFns = meta.component.staticRenderFns; - return resolve(component); } } diff --git a/src/core/component/register/helpers.ts b/src/core/component/register/helpers.ts index 73f7ca0d09..9dd0915c36 100644 --- a/src/core/component/register/helpers.ts +++ b/src/core/component/register/helpers.ts @@ -9,7 +9,7 @@ import { isComponent, componentInitializers, componentParams, components } from 'core/component/const'; import type { ComponentMeta } from 'core/component/interface'; -import type { ComponentConstructorInfo } from 'core/component/reflection'; +import type { ComponentConstructorInfo } from 'core/component/reflect'; /** * Registers a parent component of the specified component to a component library (vue, react, etc.). From 2e0edba951263acd878ba5950450d2781d44a2da Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 18:23:57 +0300 Subject: [PATCH 0073/2313] feat: improved vue features support --- src/core/component/directives/attrs/const.ts | 38 +++++ src/core/component/directives/attrs/index.ts | 158 ++++++++++--------- 2 files changed, 119 insertions(+), 77 deletions(-) diff --git a/src/core/component/directives/attrs/const.ts b/src/core/component/directives/attrs/const.ts index dcd7c700b7..6a58a732c9 100644 --- a/src/core/component/directives/attrs/const.ts +++ b/src/core/component/directives/attrs/const.ts @@ -12,3 +12,41 @@ export const export const handlers = new WeakMap>(); + +export const keyModifiers = Object.createDict({ + enter: true, + tab: true, + delete: true, + esc: true, + space: true, + up: true, + down: true, + left: true, + right: true +}); + +export const modifiers = Object.createDict({ + left: true, + middle: true, + right: true, + ctrl: true, + alt: true, + shift: true, + meta: true, + exact: true, + self: true, + prevent: true, + stop: true +}); + +export const classAttrs = Object.createDict({ + class: 'class', + '^class': 'class', + '.className': '.className' +}); + +export const styleAttrs = Object.createDict({ + style: 'style', + '^style': 'style', + '.style': '.style' +}); diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index f7f912e18c..9fbaea9ec5 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -14,13 +14,28 @@ import { ComponentEngine, DirectiveBinding, VNode } from 'core/component/engines'; import type { ComponentInterface } from 'core/component/interface'; -import { modRgxp, directiveRgxp, handlers } from 'core/component/directives/attrs/const'; +import { + + modRgxp, + directiveRgxp, + + handlers, + + modifiers, + keyModifiers, + + classAttrs, + styleAttrs + +} from 'core/component/directives/attrs/const'; + import type { DirectiveOptions } from 'core/component/directives/attrs/interface'; export * from 'core/component/directives/attrs/const'; export * from 'core/component/directives/attrs/interface'; ComponentEngine.directive('attrs', { + // eslint-disable-next-line complexity beforeCreate(opts: DirectiveOptions, vnode: VNode) { let handlerStore, @@ -37,11 +52,15 @@ ComponentEngine.directive('attrs', { attrs = {...attrs}; vnode.props ??= props; + const + {r} = ctx.$renderEngine; + for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { let attrName = keys[i], attrVal = attrs[attrName]; + // Directive if (attrName.startsWith('v-')) { const [, name, arg = '', rawModifiers = ''] = directiveRgxp.exec(attrName)!; @@ -51,9 +70,6 @@ ComponentEngine.directive('attrs', { switch (name) { case 'show': { - const - {r} = ctx.$renderEngine; - dir = r.vShow; break; } @@ -86,9 +102,6 @@ ComponentEngine.directive('attrs', { } case 'model': { - const - {r} = ctx.$renderEngine; - switch (vnode.type) { case 'input': dir = r[`vModel${(props.type ?? '').capitalize()}`] ?? r.vModelText; @@ -153,8 +166,11 @@ ComponentEngine.directive('attrs', { vnode.dirs = dirs; dirs.push(dirDecl); + continue; + } - } else if (attrName.startsWith('@')) { + // Event listener + if (attrName.startsWith('@')) { let event = attrName.slice(1); @@ -196,92 +212,80 @@ ComponentEngine.directive('attrs', { if (Object.keys(flags).length > 0) { const - originalHandler = attrVal; - - attrVal = (e: MouseEvent | KeyboardEvent, ...args) => { - if ( - flags.ctrl && !e.ctrlKey || - flags.alt && !e.altKey || - flags.shift && !e.shiftKey || - flags.meta && !e.metaKey || - flags.exact && ( - !flags.ctrl && e.ctrlKey || - !flags.alt && e.altKey || - !flags.shift && e.shiftKey || - !flags.meta && e.metaKey - ) - ) { - return; - } + registeredModifiers = Object.keys(Object.select(flags, modifiers)), + registeredKeyModifiers = Object.keys(Object.select(flags, keyModifiers)); - if (e instanceof MouseEvent) { - if (flags.middle && e.button !== 1) { - return; - } + if (registeredModifiers.length > 0) { + attrVal = r.withModifiers.call(ctx, attrVal, registeredKeyModifiers); + } - } else if (e instanceof KeyboardEvent) { - if ( - flags.enter && e.key !== 'Enter' || - flags.tab && e.key !== 'Tab' || - flags.delete && (e.key !== 'Delete' && e.key !== 'Backspace') || - flags.esc && e.key !== 'Escape' || - flags.space && e.key !== ' ' || - flags.up && e.key !== 'ArrowUp' || - flags.down && e.key !== 'ArrowDown' || - flags.left && e.key !== 'ArrowLeft' || - flags.right && e.key !== 'ArrowRight' - ) { - return; - } - } + if (registeredKeyModifiers.length > 0) { + attrVal = r.withKeys.call(ctx, attrVal, registeredKeyModifiers); + } + } - if (flags.self && e.target !== e.currentTarget) { - return; - } + props[event] = attrVal; + continue; + } - if (flags.prevent) { - e.preventDefault(); - } + // Simple property + attrName = attrName.startsWith(':') ? + attrName.slice(1) : + attrName; - if (flags.stop) { - e.stopPropagation(); - } + if (modRgxp.test(attrName)) { + const attrChunks = attrName.split('.'); + attrName = attrName.startsWith('.') ? `.${attrChunks[1]}` : attrChunks[0]; - return (originalHandler)(e, ...args); - }; + if (attrChunks.includes('camel')) { + attrName = attrName.camelize(false); } - props[event] = attrVal; - - } else { - attrName = attrName.startsWith(':') ? attrName.slice(1) : attrName; + if (attrChunks.includes('prop') && !attrName.startsWith('.')) { + if (attrName.startsWith('^')) { + throw new SyntaxError('Invalid v-bind modifiers'); + } - if (modRgxp.test(attrName)) { - const attrChunks = attrName.split('.'); - attrName = attrName.startsWith('.') ? `.${attrChunks[1]}` : attrChunks[0]; + attrName = `.${attrName}`; + } - if (attrChunks.includes('camel')) { - attrName = attrName.camelize(false); + if (attrChunks.includes('attr') && !attrName.startsWith('^')) { + if (attrName.startsWith('.')) { + throw new SyntaxError('Invalid v-bind modifiers'); } - if (attrChunks.includes('prop') && !attrName.startsWith('.')) { - if (attrName.startsWith('^')) { - throw new SyntaxError('Invalid v-bind modifiers'); - } + attrName = `^${attrName}`; + } + } - attrName = `.${attrName}`; - } + if (classAttrs[attrName] != null) { + attrName = classAttrs[attrName]; + attrVal = r.normalizeClass.call(ctx, attrVal); - if (attrChunks.includes('attr') && !attrName.startsWith('^')) { - if (attrName.startsWith('.')) { - throw new SyntaxError('Invalid v-bind modifiers'); - } + if (vnode.patchFlag < 6) { + vnode.patchFlag = 6; + } - attrName = `^${attrName}`; - } + } else if (styleAttrs[attrName] != null) { + attrVal = r.normalizeStyle.call(ctx, attrVal); + + if (vnode.patchFlag < 6) { + vnode.patchFlag = 6; } - console.log(attrName, attrVal); + } else { + if (vnode.patchFlag < 14) { + vnode.patchFlag = 14; + } + + const dynamicProps = vnode['dynamicProps'] ?? []; + vnode['dynamicProps'] = Array.union(dynamicProps, attrName); + } + + if (props[attrName] != null) { + Object.assign(props, r.mergeProps({[attrName]: props[attrName]}, {[attrName]: attrVal})); + + } else { props[attrName] = attrVal; } } From b919d5091d348eb5c57b5630687a224c12ed56d7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 18:34:13 +0300 Subject: [PATCH 0074/2313] docs: added doc --- src/core/component/directives/attrs/README.md | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/core/component/directives/attrs/README.md b/src/core/component/directives/attrs/README.md index fcd56d1af6..1363f21c0d 100644 --- a/src/core/component/directives/attrs/README.md +++ b/src/core/component/directives/attrs/README.md @@ -1,19 +1,27 @@ -# core/component/directives/hook +# core/component/directives/attrs -This module brings a directive to provide any directive hooks into a component. -This directive is extremely useful to combine with a flyweight component because it does not have API to -attach the hook listeners. +This module brings a directive to set any component or tag attributes from the passed dictionary. ## Usage ``` -< .&__class v-hook = { & - created: (el, opts, vnode, oldVnode) => console.log(el, opts, vnode, oldVnode), - beforeMount: onBeforeMount, - mounted: onMounted, - beforeUpdate: onBeforeUpdate, - updated: onUpdated, - beforeUnmount: onBeforeUnmount, - unmounted: onUnmounted +< div v-attrs = { & + /// We can pass any available directive except `v-if` + 'v-show': showCondition, + + /// We can pass any event listeners with support of Vue modifiers + '@click.capture.self': clickHandler, + + /// Or just regular props or attributes + ':style': styles, + + /// `:` is optional + class: extraClasses + + /// Attribute modifiers are also supported + '.some-field.camel': someFieldValue } . + +/// To use `v-model`, provide a model store name as a string +< input v-attrs = {'v-model': 'textStore'} ``` From 4a4de44ddf1d1f10e08ce266af2438970edd5225 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 18:34:24 +0300 Subject: [PATCH 0075/2313] chore: updated log --- src/core/component/directives/attrs/CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/core/component/directives/attrs/CHANGELOG.md b/src/core/component/directives/attrs/CHANGELOG.md index 4f347c1a34..b18e802496 100644 --- a/src/core/component/directives/attrs/CHANGELOG.md +++ b/src/core/component/directives/attrs/CHANGELOG.md @@ -11,12 +11,6 @@ Changelog ## v3.??.?? (2022-??-??) -#### :house: Internal - -* Migration to Vue3 - -## v3.0.0-rc.97 (2020-11-11) - #### :rocket: New Feature * Initial release From c5ffb6fc94563ff6d071f923f9ace0eb6240de22 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 18:38:52 +0300 Subject: [PATCH 0076/2313] chore: updated examples and interfaces --- src/core/component/directives/hook/README.md | 2 +- src/core/component/directives/image/README.md | 4 ++-- src/core/component/directives/image/index.ts | 2 +- src/core/component/directives/image/interface.ts | 4 +--- src/core/component/directives/in-view/README.md | 2 +- .../component/directives/resize-observer/README.md | 2 +- .../directives/resize-observer/interface.ts | 3 ++- src/core/component/directives/update-on/README.md | 14 +++++++------- .../component/directives/update-on/interface.ts | 5 ++--- 9 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/core/component/directives/hook/README.md b/src/core/component/directives/hook/README.md index fcd56d1af6..88340474cd 100644 --- a/src/core/component/directives/hook/README.md +++ b/src/core/component/directives/hook/README.md @@ -7,7 +7,7 @@ attach the hook listeners. ## Usage ``` -< .&__class v-hook = { & +< div v-hook = { & created: (el, opts, vnode, oldVnode) => console.log(el, opts, vnode, oldVnode), beforeMount: onBeforeMount, mounted: onMounted, diff --git a/src/core/component/directives/image/README.md b/src/core/component/directives/image/README.md index f66ab5e574..a3f28d182c 100644 --- a/src/core/component/directives/image/README.md +++ b/src/core/component/directives/image/README.md @@ -5,11 +5,11 @@ This module provides a directive to load images by using `background-image` or ` ## Usage ``` -< .&__not-img v-image = { & +< .not-img v-image = { & src: 'https://fakeimg.com' } . -< img.&__img v-image = { & +< img.img v-image = { & src: 'https://fakeimg.com' } . ``` diff --git a/src/core/component/directives/image/index.ts b/src/core/component/directives/image/index.ts index 56164d7ddc..aae491fbe9 100644 --- a/src/core/component/directives/image/index.ts +++ b/src/core/component/directives/image/index.ts @@ -40,7 +40,7 @@ ComponentEngine.directive('image', { }, updated(el: HTMLElement, {value, oldValue}: DirectiveOptions): void { - ImageLoader.update(el, value, oldValue); + ImageLoader.update(el, value, oldValue ?? undefined); }, unmounted(el: HTMLElement): void { diff --git a/src/core/component/directives/image/interface.ts b/src/core/component/directives/image/interface.ts index cb3e7448d1..1a34d1ea97 100644 --- a/src/core/component/directives/image/interface.ts +++ b/src/core/component/directives/image/interface.ts @@ -9,6 +9,4 @@ import type { InitValue } from 'core/dom/image'; import type { DirectiveBinding } from 'core/component/engines'; -export interface DirectiveOptions extends DirectiveBinding> { - modifiers: Record; -} +export interface DirectiveOptions extends DirectiveBinding> {} diff --git a/src/core/component/directives/in-view/README.md b/src/core/component/directives/in-view/README.md index e0ba109df9..c51911c52b 100644 --- a/src/core/component/directives/in-view/README.md +++ b/src/core/component/directives/in-view/README.md @@ -5,7 +5,7 @@ This module provides a directive to track elements entering or leaving the viewp ## Usage ``` -< .&__class v-in-view = [{ & +< div v-in-view = [{ & threshold: 0.7, delay: 2000, callback: () => emit('elementInViewport'), diff --git a/src/core/component/directives/resize-observer/README.md b/src/core/component/directives/resize-observer/README.md index 2585b8bdbd..ea9c1c53d5 100644 --- a/src/core/component/directives/resize-observer/README.md +++ b/src/core/component/directives/resize-observer/README.md @@ -5,7 +5,7 @@ This module provides a directive to track changes in the size of DOM elements us ## Usage ``` -< .&__class v-resize-observer = { & +< div v-resize-observer = { & callback: () => emit('elementResized') } . ``` diff --git a/src/core/component/directives/resize-observer/interface.ts b/src/core/component/directives/resize-observer/interface.ts index dfecab99f9..c026e41a98 100644 --- a/src/core/component/directives/resize-observer/interface.ts +++ b/src/core/component/directives/resize-observer/interface.ts @@ -6,9 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { DirectiveBinding } from 'core/component/engines'; import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; +import type { DirectiveBinding } from 'core/component/engines'; + export * from 'core/dom/resize-observer/interface'; export interface DirectiveOptions extends DirectiveBinding>> { diff --git a/src/core/component/directives/update-on/README.md b/src/core/component/directives/update-on/README.md index d4981057e2..78ed4d151d 100644 --- a/src/core/component/directives/update-on/README.md +++ b/src/core/component/directives/update-on/README.md @@ -8,7 +8,7 @@ Use this directive if you want to update some parts of your template without re- 1. Simple event listener. ``` -< .&__example v-update-on = { & +< div v-update-on = { & emitter: parentEmitter, event: 'foo', handler: (el, event) => myHandler(el, event) @@ -18,7 +18,7 @@ Use this directive if you want to update some parts of your template without re- 2. Listening to a one-time event. ``` -< .&__example v-update-on = { & +< div v-update-on = { & emitter: parentEmitter, single: true, event: 'foo', @@ -29,7 +29,7 @@ Use this directive if you want to update some parts of your template without re- 3. Providing extra options to the emitter. ``` -< .&__example v-update-on = { & +< div v-update-on = { & emitter: document, event: 'touchmove', handler: myHandler, @@ -42,7 +42,7 @@ Use this directive if you want to update some parts of your template without re- 4. Multiple event listeners. ``` -< .&__example v-update-on = [ & +< div v-update-on = [ & { emitter: parentEmitter, event: ['foo', 'baz'], @@ -61,7 +61,7 @@ Use this directive if you want to update some parts of your template without re- 4. Handling of a promise. ``` -< .&__example v-update-on = { & +< div v-update-on = { & emitter: somePromiseValue, handler: myHandler, errorHandler: myErrorHandler @@ -71,7 +71,7 @@ Use this directive if you want to update some parts of your template without re- 4. Watching for a component property. ``` -< .&__example v-update-on = { & +< div v-update-on = { & emitter: 'bla.bar', handler: myHandler, options: {deep: true} @@ -81,7 +81,7 @@ Use this directive if you want to update some parts of your template without re- 5. Providing an async group prefix. ``` -< .&__example v-update-on = { & +< div v-update-on = { & emitter: 'bla.bar', handler: myHandler, options: {deep: true}, diff --git a/src/core/component/directives/update-on/interface.ts b/src/core/component/directives/update-on/interface.ts index a5a411da3d..a013a333d0 100644 --- a/src/core/component/directives/update-on/interface.ts +++ b/src/core/component/directives/update-on/interface.ts @@ -7,12 +7,11 @@ */ import type { EventEmitterLike } from 'core/async'; + import type { DirectiveBinding } from 'core/component/engines'; import type { WatchOptions } from 'core/component/interface'; -export interface DirectiveOptions extends DirectiveBinding>> { - modifiers: Record; -} +export interface DirectiveOptions extends DirectiveBinding>> {} export interface DirectiveValue { /** From 30f59caa21a7c5c609267e4c01682542f2e3803c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 18:42:04 +0300 Subject: [PATCH 0077/2313] :art: --- src/core/component/directives/in-view/index.ts | 2 +- src/core/component/directives/resize-observer/helpers.ts | 4 ++-- src/core/component/directives/resize-observer/interface.ts | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/component/directives/in-view/index.ts b/src/core/component/directives/in-view/index.ts index 922b44fa4c..0038400db1 100644 --- a/src/core/component/directives/in-view/index.ts +++ b/src/core/component/directives/in-view/index.ts @@ -11,8 +11,8 @@ * @packageDocumentation */ -import { ComponentEngine } from 'core/component/engines'; import { InView, Adaptee, InViewDirectiveOptions } from 'core/dom/in-view'; +import { ComponentEngine } from 'core/component/engines'; export * from 'core/dom/in-view'; diff --git a/src/core/component/directives/resize-observer/helpers.ts b/src/core/component/directives/resize-observer/helpers.ts index c1d71b92a1..d9e38e37ed 100644 --- a/src/core/component/directives/resize-observer/helpers.ts +++ b/src/core/component/directives/resize-observer/helpers.ts @@ -7,9 +7,9 @@ */ import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; -import { DIRECTIVE_BIND } from 'core/component/directives/resize-observer/const'; - import type { ComponentInterface } from 'core/component/interface'; + +import { DIRECTIVE_BIND } from 'core/component/directives/resize-observer/const'; import type { ResizeWatcherObservable, ResizeWatcherObserverOptions } from 'core/component/directives/resize-observer/interface'; /** diff --git a/src/core/component/directives/resize-observer/interface.ts b/src/core/component/directives/resize-observer/interface.ts index c026e41a98..9356abcb4e 100644 --- a/src/core/component/directives/resize-observer/interface.ts +++ b/src/core/component/directives/resize-observer/interface.ts @@ -7,7 +7,6 @@ */ import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; - import type { DirectiveBinding } from 'core/component/engines'; export * from 'core/dom/resize-observer/interface'; From 71ef6f181373d8a585af1f464ad96bcafa53e100 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 18:49:32 +0300 Subject: [PATCH 0078/2313] refactor: removed redundant directive logic --- src/core/component/engines/vue3/render.ts | 4 +-- src/core/component/render/directives/const.ts | 9 ------ src/core/component/render/directives/index.ts | 32 ------------------- .../component/render/directives/interface.ts | 13 -------- .../component/render/directives/v-attrs.ts | 25 --------------- src/core/component/render/wrappers/index.ts | 11 ++----- 6 files changed, 4 insertions(+), 90 deletions(-) delete mode 100644 src/core/component/render/directives/const.ts delete mode 100644 src/core/component/render/directives/index.ts delete mode 100644 src/core/component/render/directives/interface.ts delete mode 100644 src/core/component/render/directives/v-attrs.ts diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index f556dbd278..2097933efc 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -10,7 +10,6 @@ import { resolveComponent as resolveComponentSuper, createVNode as createVNodeSuper, - createElementVNode as createElementVNodeSuper, withDirectives as withDirectivesSuper } from 'vue'; @@ -19,7 +18,6 @@ import { wrapResolveComponent, wrapCreateVNode, - wrapCreateElementVNode, wrapWithDirectives } from 'core/component/render/wrappers'; @@ -40,6 +38,7 @@ export { createBlock, createElementBlock, + createElementVNode, createStaticVNode, createTextVNode, createCommentVNode, @@ -66,5 +65,4 @@ export { export const resolveComponent = wrapResolveComponent(resolveComponentSuper), createVNode = wrapCreateVNode(createVNodeSuper), - createElementVNode = wrapCreateElementVNode(createElementVNodeSuper), withDirectives = wrapWithDirectives(withDirectivesSuper); diff --git a/src/core/component/render/directives/const.ts b/src/core/component/render/directives/const.ts deleted file mode 100644 index f4165ba552..0000000000 --- a/src/core/component/render/directives/const.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export const directives: Function[] = []; diff --git a/src/core/component/render/directives/index.ts b/src/core/component/render/directives/index.ts deleted file mode 100644 index 611ad421dc..0000000000 --- a/src/core/component/render/directives/index.ts +++ /dev/null @@ -1,32 +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 { VNode } from 'core/component/engines'; - -import { directives } from 'core/component/render/directives/const'; -import type { CreateVNode } from 'core/component/render/directives/interface'; - -/** - * Creates a new element VNode by the specified parameters with applying internal directives, like `v-attrs`. - * The function takes an original function to create VNodes and list of arguments and returns a new VNode. - * - * @param createVNode - original function to create a VNode - * @param args - operation arguments - */ -export function createVNodeWithDirectives(createVNode: CreateVNode, ...args: any[]): VNode { - for (let i = 0; i < directives.length; i++) { - const - res = directives[i](createVNode, ...args); - - if (res != null) { - return res; - } - } - - return createVNode(...args); -} diff --git a/src/core/component/render/directives/interface.ts b/src/core/component/render/directives/interface.ts deleted file mode 100644 index a9b36db8c4..0000000000 --- a/src/core/component/render/directives/interface.ts +++ /dev/null @@ -1,13 +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 { VNode } from 'core/component/engines'; - -export interface CreateVNode { - (...args: any[]): VNode; -} diff --git a/src/core/component/render/directives/v-attrs.ts b/src/core/component/render/directives/v-attrs.ts deleted file mode 100644 index f4fc73aaaa..0000000000 --- a/src/core/component/render/directives/v-attrs.ts +++ /dev/null @@ -1,25 +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 { VNode, VNodeTypes, VNodeProps } from 'core/component/engines'; - -import { directives } from 'core/component/render/directives/const'; -import type { CreateVNode } from 'core/component/render/directives/interface'; - -export default vAttrs; -directives.push(vAttrs); - -function vAttrs(createVNode: CreateVNode, type: VNodeTypes, props: Dictionary & VNodeProps): CanUndef { - console.log(props); - - if (props['v-attrs'] == null) { - return undefined; - } - - return Object.cast(props.from); -} diff --git a/src/core/component/render/wrappers/index.ts b/src/core/component/render/wrappers/index.ts index 38ffb06958..7766df407a 100644 --- a/src/core/component/render/wrappers/index.ts +++ b/src/core/component/render/wrappers/index.ts @@ -6,9 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { createVNode, createElementVNode, resolveComponent, withDirectives } from 'vue'; +import type { createVNode, resolveComponent, withDirectives } from 'vue'; -import * as dir from 'core/component/render/directives'; import * as comp from 'core/component/render/components'; import { isSpecialComponent } from 'core/component/render/wrappers/helpers'; @@ -17,10 +16,6 @@ export function wrapCreateVNode(original: T): T { return Object.cast((type, ...args) => comp.createVNodeWithDirectives(original, type, ...args)); } -export function wrapCreateElementVNode(original: T): T { - return Object.cast((...args) => dir.createVNodeWithDirectives(original, ...args)); -} - export function wrapResolveComponent(original: T): T { return Object.cast((name, ...args) => { if (isSpecialComponent(name)) { @@ -32,13 +27,13 @@ export function wrapResolveComponent(original } export function wrapWithDirectives(original: T): T { - return Object.cast((vnode, dirs) => { + return Object.cast(function withDirectives(vnode, dirs) { for (let i = 0; i < dirs.length; i++) { const [dir, value, arg, modifiers] = dirs[i]; if (dir.beforeCreate != null) { - dir.beforeCreate({value, arg, modifiers, dir}, vnode); + dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); } } From b4cccced1c52e44c0dd4a5585e5fc3c602107a39 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:02:05 +0300 Subject: [PATCH 0079/2313] fix: fixed a type of `DirectiveArguments` --- src/core/component/engines/engine.ts | 11 ++++++++++- src/core/component/engines/vue3/interface.ts | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/core/component/engines/engine.ts b/src/core/component/engines/engine.ts index 236458f426..f730925e96 100644 --- a/src/core/component/engines/engine.ts +++ b/src/core/component/engines/engine.ts @@ -10,4 +10,13 @@ export * from 'core/component/engines/vue3'; export * from 'core/component/engines/interface'; export { VNode } from 'core/component/engines/interface'; -export { CreateAppFunction, Directive, ObjectDirective } from 'core/component/engines/vue3'; + +export { + + CreateAppFunction, + + Directive, + DirectiveArguments, + ObjectDirective + +} from 'core/component/engines/vue3'; diff --git a/src/core/component/engines/vue3/interface.ts b/src/core/component/engines/vue3/interface.ts index 5922c608c2..b23633334e 100644 --- a/src/core/component/engines/vue3/interface.ts +++ b/src/core/component/engines/vue3/interface.ts @@ -34,6 +34,13 @@ export declare type Directive = ObjectDirective | FunctionDirective; +export declare type DirectiveArguments = Array< + [Directive] | + [Directive, any] | + [Directive, any, string] | + [Directive, any, string, Record] +>; + export interface CreateAppFunction { (...args: Parameters>): Overwrite, ResolveDirective>; } From 9151dd4cbaccf6c651bb9b137e0e16a19a8e085a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:29:21 +0300 Subject: [PATCH 0080/2313] fix: safety work with a context --- src/core/component/directives/attrs/index.ts | 27 ++++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index 9fbaea9ec5..ac066bb3a0 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -52,8 +52,13 @@ ComponentEngine.directive('attrs', { attrs = {...attrs}; vnode.props ??= props; - const - {r} = ctx.$renderEngine; + let + r: CanUndef; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (ctx != null) { + r = ctx.$renderEngine.r; + } for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { let @@ -70,7 +75,7 @@ ComponentEngine.directive('attrs', { switch (name) { case 'show': { - dir = r.vShow; + dir = r?.vShow; break; } @@ -104,15 +109,15 @@ ComponentEngine.directive('attrs', { case 'model': { switch (vnode.type) { case 'input': - dir = r[`vModel${(props.type ?? '').capitalize()}`] ?? r.vModelText; + dir = r?.[`vModel${(props.type ?? '').capitalize()}`] ?? r?.vModelText; break; case 'select': - dir = r.vModelSelect; + dir = r?.vModelSelect; break; default: - dir = r.vModelDynamic; + dir = r?.vModelDynamic; } const @@ -216,11 +221,11 @@ ComponentEngine.directive('attrs', { registeredKeyModifiers = Object.keys(Object.select(flags, keyModifiers)); if (registeredModifiers.length > 0) { - attrVal = r.withModifiers.call(ctx, attrVal, registeredKeyModifiers); + attrVal = r?.withModifiers.call(ctx, attrVal, registeredKeyModifiers); } if (registeredKeyModifiers.length > 0) { - attrVal = r.withKeys.call(ctx, attrVal, registeredKeyModifiers); + attrVal = r?.withKeys.call(ctx, attrVal, registeredKeyModifiers); } } @@ -260,14 +265,14 @@ ComponentEngine.directive('attrs', { if (classAttrs[attrName] != null) { attrName = classAttrs[attrName]; - attrVal = r.normalizeClass.call(ctx, attrVal); + attrVal = r?.normalizeClass.call(ctx, attrVal); if (vnode.patchFlag < 6) { vnode.patchFlag = 6; } } else if (styleAttrs[attrName] != null) { - attrVal = r.normalizeStyle.call(ctx, attrVal); + attrVal = r?.normalizeStyle.call(ctx, attrVal); if (vnode.patchFlag < 6) { vnode.patchFlag = 6; @@ -283,7 +288,7 @@ ComponentEngine.directive('attrs', { } if (props[attrName] != null) { - Object.assign(props, r.mergeProps({[attrName]: props[attrName]}, {[attrName]: attrVal})); + Object.assign(props, r?.mergeProps({[attrName]: props[attrName]}, {[attrName]: attrVal})); } else { props[attrName] = attrVal; From f2f3a854bdc8077bda93f9ca5e539c05eeb9a517 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:31:31 +0300 Subject: [PATCH 0081/2313] feat: added a new directive `tag` --- .../component/directives/tag/CHANGELOG.md | 16 +++++++++++++ src/core/component/directives/tag/README.md | 9 ++++++++ src/core/component/directives/tag/index.ts | 23 +++++++++++++++++++ .../component/directives/tag/interface.ts | 11 +++++++++ 4 files changed, 59 insertions(+) create mode 100644 src/core/component/directives/tag/CHANGELOG.md create mode 100644 src/core/component/directives/tag/README.md create mode 100644 src/core/component/directives/tag/index.ts create mode 100644 src/core/component/directives/tag/interface.ts diff --git a/src/core/component/directives/tag/CHANGELOG.md b/src/core/component/directives/tag/CHANGELOG.md new file mode 100644 index 0000000000..b18e802496 --- /dev/null +++ b/src/core/component/directives/tag/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/tag/README.md b/src/core/component/directives/tag/README.md new file mode 100644 index 0000000000..8e2d5a534f --- /dev/null +++ b/src/core/component/directives/tag/README.md @@ -0,0 +1,9 @@ +# core/component/directives/tag + +This module brings a directive to specify dynamically a tag name to create. + +## Usage + +``` +< div v-tag = 'span' +``` diff --git a/src/core/component/directives/tag/index.ts b/src/core/component/directives/tag/index.ts new file mode 100644 index 0000000000..6c210a8740 --- /dev/null +++ b/src/core/component/directives/tag/index.ts @@ -0,0 +1,23 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/hook/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode } from 'core/component/engines'; +import type { DirectiveOptions } from 'core/component/directives/tag/interface'; + +export * from 'core/component/directives/tag/interface'; + +ComponentEngine.directive('tag', { + beforeCreate(opts: DirectiveOptions, vnode: VNode) { + vnode.type = opts.value; + } +}); diff --git a/src/core/component/directives/tag/interface.ts b/src/core/component/directives/tag/interface.ts new file mode 100644 index 0000000000..090dc70806 --- /dev/null +++ b/src/core/component/directives/tag/interface.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { DirectiveBinding } from 'core/component/engines'; + +export interface DirectiveOptions extends DirectiveBinding {} From 33dda045c1745db68c782cb10f97bfbbd8340985 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:31:57 +0300 Subject: [PATCH 0082/2313] feat: import `tag` by default --- src/core/component/directives/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 27591bab11..4400add122 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -22,5 +22,6 @@ import 'core/component/directives/image'; import 'core/component/directives/update-on'; //#endif +import 'core/component/directives/tag'; import 'core/component/directives/hook'; import 'core/component/directives/attrs'; From 21b8b526f63bbc6982ad58c29ec39a3f27862341 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:40:30 +0300 Subject: [PATCH 0083/2313] fix: added default fallback --- src/core/component/directives/tag/index.ts | 2 +- src/core/component/directives/tag/interface.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/tag/index.ts b/src/core/component/directives/tag/index.ts index 6c210a8740..4641ccbd4a 100644 --- a/src/core/component/directives/tag/index.ts +++ b/src/core/component/directives/tag/index.ts @@ -18,6 +18,6 @@ export * from 'core/component/directives/tag/interface'; ComponentEngine.directive('tag', { beforeCreate(opts: DirectiveOptions, vnode: VNode) { - vnode.type = opts.value; + vnode.type = opts.value ?? vnode.type; } }); diff --git a/src/core/component/directives/tag/interface.ts b/src/core/component/directives/tag/interface.ts index 090dc70806..042018514a 100644 --- a/src/core/component/directives/tag/interface.ts +++ b/src/core/component/directives/tag/interface.ts @@ -8,4 +8,4 @@ import type { DirectiveBinding } from 'core/component/engines'; -export interface DirectiveOptions extends DirectiveBinding {} +export interface DirectiveOptions extends DirectiveBinding> {} From 76e7520718fe61c573234a00beb92b8852f361e1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:40:59 +0300 Subject: [PATCH 0084/2313] fix: use `v-tag` instead of `tag :is` --- build/snakeskin/filters/tag-name.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/snakeskin/filters/tag-name.js b/build/snakeskin/filters/tag-name.js index 5acc3c8a42..3ea9c606b1 100644 --- a/build/snakeskin/filters/tag-name.js +++ b/build/snakeskin/filters/tag-name.js @@ -38,8 +38,8 @@ module.exports = [ return rootTag; } - attrs[':is'] = ["rootTag || 'div'"]; - return 'tag'; + attrs['v-tag'] = ["rootTag || 'div'"]; + return 'div'; } return tag; From 430423dce34286a2163c0ea5c73a4321aa6d35ef Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:44:43 +0300 Subject: [PATCH 0085/2313] feat: added support for data- shorthands --- src/core/component/directives/attrs/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index ac066bb3a0..685e64d3e0 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -283,6 +283,10 @@ ComponentEngine.directive('attrs', { vnode.patchFlag = 14; } + if (attrName.startsWith('-')) { + attrName = `data${attrName}`; + } + const dynamicProps = vnode['dynamicProps'] ?? []; vnode['dynamicProps'] = Array.union(dynamicProps, attrName); } From 9958e5967b362561ecae29d6111f8c9f3444a367 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 22 Apr 2022 19:46:12 +0300 Subject: [PATCH 0086/2313] refactor: use `v-attrs` instead of `:v-attrs` --- src/base/b-image/b-image.ss | 4 ++-- src/base/b-list/b-list.ss | 6 +++--- src/base/b-slider/b-slider.ss | 2 +- src/base/b-tree/README.md | 2 +- src/base/b-tree/b-tree.ss | 6 +++--- src/form/b-button/b-button.ss | 2 +- src/form/b-select/b-select.ss | 4 ++-- src/super/i-block/i-block.ss | 2 +- src/super/i-input/README.md | 2 +- src/super/i-input/i-input.ss | 2 +- src/traits/i-control-list/README.md | 4 ++-- src/traits/i-control-list/i-control-list.ss | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/base/b-image/b-image.ss b/src/base/b-image/b-image.ss index cd0047a399..2f65bfb9fb 100644 --- a/src/base/b-image/b-image.ss +++ b/src/base/b-image/b-image.ss @@ -26,7 +26,7 @@ < b-image.&__overlay-img & v-else-if = Object.isPlainObject(overlayImg) | - :v-attrs = overlayImg + v-attrs = overlayImg . - block broken @@ -39,7 +39,7 @@ < b-image.&__broken-img & v-else-if = Object.isPlainObject(brokenImg) | - :v-attrs = brokenImg + v-attrs = brokenImg . - block image diff --git a/src/base/b-list/b-list.ss b/src/base/b-list/b-list.ss index b1b663dd6c..76bf4eb88d 100644 --- a/src/base/b-list/b-list.ss +++ b/src/base/b-list/b-list.ss @@ -48,7 +48,7 @@ } })) | - :v-attrs = el.attrs + v-attrs = el.attrs . - block preIcon < span.&__cell.&__link-icon.&__link-pre-icon v-if = el.preIcon || vdom.getSlot('preIcon') @@ -68,7 +68,7 @@ < template v-if = item < component & :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) + v-attrs = getItemProps(el, i) . < template v-else @@ -101,6 +101,6 @@ < tag.&__wrapper & :is = listTag | - :v-attrs = attrs + v-attrs = attrs . += self.list('items') diff --git a/src/base/b-slider/b-slider.ss b/src/base/b-slider/b-slider.ss index 98a162658d..9affd3d9d0 100644 --- a/src/base/b-slider/b-slider.ss +++ b/src/base/b-slider/b-slider.ss @@ -34,7 +34,7 @@ . < component.&__option.&__item & :is = getItemComponentName(el, i) | - :v-attrs = getItemAttrs(el, i) + v-attrs = getItemAttrs(el, i) . /* diff --git a/src/base/b-tree/README.md b/src/base/b-tree/README.md index e7d5947246..7809c8d5e0 100644 --- a/src/base/b-tree/README.md +++ b/src/base/b-tree/README.md @@ -187,7 +187,7 @@ The component supports a bunch of slots to provide: ``` < b-tree :item = 'b-checkbox' | :items = listOfItems < template #fold = o - < .&__fold :v-attrs = o.params + < .&__fold v-attrs = o.params ➕ ``` diff --git a/src/base/b-tree/b-tree.ss b/src/base/b-tree/b-tree.ss index 4849e3a49f..a575d1fd76 100644 --- a/src/base/b-tree/b-tree.ss +++ b/src/base/b-tree/b-tree.ss @@ -31,14 +31,14 @@ - block fold < template v-if = Object.size(field.get('children.length', el)) > 0 += self.slot('fold', {':params': 'getFoldProps(el)'}) - < .&__fold :v-attrs = getFoldProps(el) + < .&__fold v-attrs = getFoldProps(el) - block item += self.slot('default', {':item': 'getItemProps(el, i)'}) < component.&__item & v-if = item | :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) + v-attrs = getItemProps(el, i) . - block children @@ -47,7 +47,7 @@ :items = el.children | :folded = getFoldedPropValue(el) | :item = item | - :v-attrs = nestedTreeProps + v-attrs = nestedTreeProps . < template & #default = o | diff --git a/src/form/b-button/b-button.ss b/src/form/b-button/b-button.ss index 27480985c3..45d028f5a4 100644 --- a/src/form/b-button/b-button.ss +++ b/src/form/b-button/b-button.ss @@ -30,7 +30,7 @@ @focus = focus | @blur = blur | - :v-attrs = attrs | + v-attrs = attrs | ${attrs|!html} . diff --git a/src/form/b-select/b-select.ss b/src/form/b-select/b-select.ss index b89db45cd3..385c2af831 100644 --- a/src/form/b-select/b-select.ss +++ b/src/form/b-select/b-select.ss @@ -43,14 +43,14 @@ } })) | - :v-attrs = el.attrs | + v-attrs = el.attrs | ${itemAttrs} . += self.slot('default', {':item': 'el'}) < template v-if = item < component & :is = Object.isFunction(item) ? item(el, i) : item | - :v-attrs = getItemProps(el, i) + v-attrs = getItemProps(el, i) . < template v-else diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 9f47eeb916..8246f60313 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -198,7 +198,7 @@ - block root < ?.${self.name()} - < _ :v-attrs = rootAttrs | ${rootAttrs|!html} + < _ v-attrs = rootAttrs | ${rootAttrs|!html} /** * Generates an icon layout diff --git a/src/super/i-input/README.md b/src/super/i-input/README.md index 4719bb593f..b875aeb39a 100644 --- a/src/super/i-input/README.md +++ b/src/super/i-input/README.md @@ -484,7 +484,7 @@ You can also manage a type of the created tag and other options by using the pre @focus = ${@focusHandler || 'onFocus'} | @blur = ${@blurHandler || 'onBlur'} | - :v-attrs = tmp.attrs | + v-attrs = tmp.attrs | ${Object.assign({}, attrs, @attrs)|!html} . diff --git a/src/super/i-input/i-input.ss b/src/super/i-input/i-input.ss index 5fb3be3649..b792cdf427 100644 --- a/src/super/i-input/i-input.ss +++ b/src/super/i-input/i-input.ss @@ -63,7 +63,7 @@ @focus = ${@focusHandler || 'onFocus'} | @blur = ${@blurHandler || 'onBlur'} | - :v-attrs = tmp.attrs | + v-attrs = tmp.attrs | ${Object.assign({}, attrs, @attrs)|!html} . += content diff --git a/src/traits/i-control-list/README.md b/src/traits/i-control-list/README.md index 722dbe2f9d..6aa3e1c83e 100644 --- a/src/traits/i-control-list/README.md +++ b/src/traits/i-control-list/README.md @@ -71,7 +71,7 @@ __b-dummy-control-list.ss__ < .&__primary-control < component & v-func = false | - :v-attrs = {...controls[0].attrs} | + v-attrs = {...controls[0].attrs} | :is = controls[0].component || 'b-button' | :instanceOf = bButton | @[getControlEvent(controls[0])] = callControlAction(controls[0], ...arguments) @@ -88,7 +88,7 @@ Calls an event handler for the specified control. < .&__primary-control < component & v-func = false | - :v-attrs = {...controls[0].attrs} | + v-attrs = {...controls[0].attrs} | :is = controls[0].component || 'b-button' | :instanceOf = bButton | @[getControlEvent(controls[0])] = callControlAction(controls[0], ...arguments) diff --git a/src/traits/i-control-list/i-control-list.ss b/src/traits/i-control-list/i-control-list.ss index ea735cdc0c..d7b71afa0d 100644 --- a/src/traits/i-control-list/i-control-list.ss +++ b/src/traits/i-control-list/i-control-list.ss @@ -52,7 +52,7 @@ :is = el.component || 'b-button-functional' | :instanceOf = bButton | :class = ${componentName} ? provide.elClasses(${componentName}, ${elClassesJSON}) : provide.elClasses(${elClassesJSON}) | - :v-attrs = el.attrs | + v-attrs = el.attrs | @[getControlEvent(el)] = callControlAction(el, ...arguments) . - if content From 3cde1882f95678e8ed5153e60c2992147cd095df Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 14:03:41 +0300 Subject: [PATCH 0087/2313] fix: fixed pathFlag for styles --- src/core/component/directives/attrs/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index 685e64d3e0..534019f33c 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -274,8 +274,8 @@ ComponentEngine.directive('attrs', { } else if (styleAttrs[attrName] != null) { attrVal = r?.normalizeStyle.call(ctx, attrVal); - if (vnode.patchFlag < 6) { - vnode.patchFlag = 6; + if (vnode.patchFlag < 4) { + vnode.patchFlag = 4; } } else { From 40e60a4a2c6408378133980540478933e15649c7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 19:22:35 +0300 Subject: [PATCH 0088/2313] refactor: review the module & added new helpers --- src/core/component/render/api.ts | 25 +++ src/core/component/render/components/index.ts | 17 -- .../component/render/components/interface.ts | 13 -- .../component/render/components/v-render.ts | 4 +- .../render/{components => }/const.ts | 0 src/core/component/render/helpers.ts | 159 ++++++++++++++++++ src/core/component/render/index.ts | 46 +---- src/core/component/render/interface.ts | 13 +- src/core/component/render/vnode.ts | 77 +++++++++ src/core/component/render/wrappers.ts | 58 +++++++ src/core/component/render/wrappers/helpers.ts | 11 -- src/core/component/render/wrappers/index.ts | 42 ----- 12 files changed, 329 insertions(+), 136 deletions(-) create mode 100644 src/core/component/render/api.ts delete mode 100644 src/core/component/render/components/interface.ts rename src/core/component/render/{components => }/const.ts (100%) create mode 100644 src/core/component/render/helpers.ts create mode 100644 src/core/component/render/vnode.ts create mode 100644 src/core/component/render/wrappers.ts delete mode 100644 src/core/component/render/wrappers/helpers.ts delete mode 100644 src/core/component/render/wrappers/index.ts diff --git a/src/core/component/render/api.ts b/src/core/component/render/api.ts new file mode 100644 index 0000000000..621e775412 --- /dev/null +++ b/src/core/component/render/api.ts @@ -0,0 +1,25 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Implements the base component force update API to a component instance + * + * @param component + * @param forceUpdate - native function to update a component + */ +export function implementComponentForceUpdateAPI(component: ComponentInterface, forceUpdate: Function): void { + component.$forceUpdate = () => { + if (!('renderCounter' in component)) { + return; + } + + forceUpdate.call(component); + }; +} diff --git a/src/core/component/render/components/index.ts b/src/core/component/render/components/index.ts index 46ac826cde..afe7eee6e6 100644 --- a/src/core/component/render/components/index.ts +++ b/src/core/component/render/components/index.ts @@ -7,20 +7,3 @@ */ import 'core/component/render/components/v-render'; - -import type { VNode } from 'core/component/engines'; - -import { components } from 'core/component/render/components/const'; -import type { CreateVNode } from 'core/component/render/components/interface'; - -/** - * Creates a new VNode by the specified parameters with applying internal component directives, like `v-render`. - * The function takes an original function to create VNodes and list of arguments and returns a new VNode. - * - * @param createVNode - original function to create a VNode - * @param type - VNode type - * @param args - operation arguments - */ -export function createVNodeWithDirectives(createVNode: CreateVNode, type: string, ...args: any[]): VNode { - return components[type]?.(createVNode, type, ...args) ?? createVNode(...args); -} diff --git a/src/core/component/render/components/interface.ts b/src/core/component/render/components/interface.ts deleted file mode 100644 index a9b36db8c4..0000000000 --- a/src/core/component/render/components/interface.ts +++ /dev/null @@ -1,13 +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 { VNode } from 'core/component/engines'; - -export interface CreateVNode { - (...args: any[]): VNode; -} diff --git a/src/core/component/render/components/v-render.ts b/src/core/component/render/components/v-render.ts index a0e8bbc2a6..f7ea6904be 100644 --- a/src/core/component/render/components/v-render.ts +++ b/src/core/component/render/components/v-render.ts @@ -8,8 +8,8 @@ import type { VNode, VNodeTypes, VNodeProps } from 'core/component/engines'; -import { components } from 'core/component/render/components/const'; -import type { CreateVNode } from 'core/component/render/components/interface'; +import { components } from 'core/component/render/const'; +import type { CreateVNode } from 'core/component/render/interface'; export default vRender; components['v-render'] = vRender; diff --git a/src/core/component/render/components/const.ts b/src/core/component/render/const.ts similarity index 100% rename from src/core/component/render/components/const.ts rename to src/core/component/render/const.ts diff --git a/src/core/component/render/helpers.ts b/src/core/component/render/helpers.ts new file mode 100644 index 0000000000..af08404b80 --- /dev/null +++ b/src/core/component/render/helpers.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 + */ + +/** + * Returns true if a component by the passed name is special (v-render, etc.) or functional + * @param componentName + */ +export function isSpecialComponent(componentName: string): boolean { + return componentName === 'v-render' || componentName.endsWith('-functional'); +} + +const + isHandler = /^on[^a-z]/; + +/** + * Merges the specified props into one and returns it + * @param args + */ +export function mergeProps(...args: Dictionary[]): Dictionary { + const + res: Dictionary = {}; + + for (let i = 0; i < args.length; i++) { + const + toMerge = args[i]; + + for (const key in toMerge) { + if (key === 'class') { + if (res.class !== toMerge.class) { + res.class = normalizeClass(Object.cast([res.class, toMerge.class])); + } + + } else if (key === 'style') { + res.style = normalizeStyle(Object.cast([res.style, toMerge.style])); + + } else if (isHandler.test(key)) { + const + existing = res[key], + incoming = toMerge[key]; + + if ( + existing !== incoming && + !(Object.isArray(existing) && existing.includes(incoming)) + ) { + res[key] = Object.isTruly(existing) ? ([]).concat(existing, incoming) : incoming; + } + + } else if (key !== '') { + res[key] = toMerge[key]; + } + } + } + + return res; +} + +/** + * Normalizes the passed class attribute and returns the result + * @param classValue + */ +export function normalizeClass(classValue: CanArray): string { + let + res = ''; + + if (Object.isString(classValue)) { + res = classValue; + + } else if (Object.isArray(classValue)) { + for (let i = 0; i < classValue.length; i++) { + const + normalizedClass = normalizeClass(classValue[i]); + + if (normalizedClass !== '') { + res += `${normalizedClass} `; + } + } + + } else if (Object.isDictionary(classValue)) { + for (let keys = Object.keys(classValue), i = 0; i < keys.length; i++) { + const + key = keys[i]; + + if (Object.isTruly(classValue[key])) { + res += `${key} `; + } + } + } + + return res.trim(); +} + +/** + * Normalizes the passed CSS style value and returns the result + * @param style + */ +export function normalizeStyle(style: CanArray>): string | Dictionary { + if (Object.isArray(style)) { + const + res = {}; + + for (let i = 0; i < style.length; i++) { + const + el = style[i], + normalizedStyle = Object.isString(el) ? parseStringStyle(el) : normalizeStyle(el); + + if (Object.size(normalizedStyle) > 0) { + for (let keys = Object.keys(normalizedStyle), i = keys.length; i < keys.length; i++) { + const key = keys[i]; + res[key] = normalizedStyle[key]; + } + } + } + + return res; + } + + if (Object.isString(style)) { + return style.trim(); + } + + if (Object.isDictionary(style)) { + return style; + } + + return ''; +} + +const + listDelimiterRE = /;(?![^(]*\))/g, + propertyDelimiterRE = /:(.+)/; + +/** + * Parses the specified CSS style string and returns a dictionary with the parsed rules + * @param style + */ +export function parseStringStyle(style: string): Dictionary { + const + res = {}; + + style.split(listDelimiterRE).forEach((singleStyle) => { + singleStyle = singleStyle.trim(); + + if (singleStyle !== '') { + const + chunks = singleStyle.split(propertyDelimiterRE); + + if (chunks.length > 1) { + res[chunks[0].trim()] = chunks[1].trim(); + } + } + }); + + return res; +} diff --git a/src/core/component/render/index.ts b/src/core/component/render/index.ts index f6d22c9e55..7df99d8894 100644 --- a/src/core/component/render/index.ts +++ b/src/core/component/render/index.ts @@ -11,46 +11,10 @@ * @packageDocumentation */ -import type { VNode } from 'core/component/engines'; -import type { ComponentInterface } from 'core/component/interface'; -import type { RenderObject } from 'core/component/render/interface'; +import 'core/component/render/components'; +export * from 'core/component/render/api'; +export * from 'core/component/render/vnode'; +export * from 'core/component/render/wrappers'; +export * from 'core/component/render/helpers'; export * from 'core/component/render/interface'; - -/** - * Executes the specified render object - * - * @param renderObject - * @param ctx - component context - */ -export function execRenderObject(renderObject: RenderObject, ctx: object): VNode { - const - fns = renderObject.staticRenderFns; - - if (fns) { - const staticTrees: VNode[] = Object.cast(ctx['_staticTrees'] ?? []); - ctx['_staticTrees'] = staticTrees; - - for (let i = 0; i < fns.length; i++) { - staticTrees.push(fns[i].call(ctx)); - } - } - - return renderObject.render.call(ctx); -} - -/** - * Implements the base component force update API to a component instance - * - * @param component - * @param forceUpdate - native function to update a component - */ -export function implementComponentForceUpdateAPI(component: ComponentInterface, forceUpdate: Function): void { - component.$forceUpdate = () => { - if (!('renderCounter' in component)) { - return; - } - - forceUpdate.call(component); - }; -} diff --git a/src/core/component/render/interface.ts b/src/core/component/render/interface.ts index 6f3fe71248..a9b36db8c4 100644 --- a/src/core/component/render/interface.ts +++ b/src/core/component/render/interface.ts @@ -6,15 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { FunctionalCtx } from 'core/component/interface'; -import type { CreateElement, VNode, RenderContext as SuperRenderContext } from 'core/component/engines'; +import type { VNode } from 'core/component/engines'; -export interface RenderContext extends SuperRenderContext { - $root?: FunctionalCtx; - $options?: Dictionary; -} - -export interface RenderObject { - staticRenderFns?: Function[]; - render(el: CreateElement, ctx?: RenderContext): VNode; +export interface CreateVNode { + (...args: any[]): VNode; } diff --git a/src/core/component/render/vnode.ts b/src/core/component/render/vnode.ts new file mode 100644 index 0000000000..83aa7b5aea --- /dev/null +++ b/src/core/component/render/vnode.ts @@ -0,0 +1,77 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode } from 'core/component/engines'; + +import { components } from 'core/component/render/const'; +import { mergeProps } from 'core/component/render/helpers'; + +import type { CreateVNode } from 'core/component/render/interface'; +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Creates a new VNode by the specified parameters with applying internal component directives, like `v-render`. + * The function takes an original function to create VNodes and list of arguments and returns a new VNode. + * + * @param createVNode - original function to create a VNode + * @param type - VNode type + * @param args - operation arguments + */ +export function createVNodeWithDirectives(createVNode: CreateVNode, type: string, ...args: any[]): VNode { + return components[type]?.(createVNode, type, ...args) ?? createVNode(...args); +} + +export function resolveStaticalAttrs(this: ComponentInterface, vnode: T): T { + const + props = >>(Object.isString(vnode.type) ? vnode.props : vnode); + + if (props == null) { + return vnode; + } + + for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { + const + key = keys[i]; + + switch (key) { + case 'data-cached-dynamic-class': { + // eslint-disable-next-line no-new-func + const classes = Function('self', `return ${props[key]}`)(this); + + Object.assign(props, mergeProps({class: props.class}, {class: classes})); + delete props[key]; + + break; + } + + case 'data-cached-dynamic-style': { + // eslint-disable-next-line no-new-func + const style = Function('self', `return ${props[key]}`)(this); + + Object.assign(props, mergeProps({style: props.style}, {style})); + delete props[key]; + + break; + } + + default: + // Do nothing + } + } + + const + {children} = vnode; + + if (Object.isArray(children)) { + for (let i = 0; i < children.length; i++) { + resolveStaticalAttrs.call(this, children[i]); + } + } + + return vnode; +} diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts new file mode 100644 index 0000000000..ac6ce413d8 --- /dev/null +++ b/src/core/component/render/wrappers.ts @@ -0,0 +1,58 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { + + resolveComponent, + resolveDynamicComponent, + + createVNode, + withDirectives, + + VNode, + DirectiveArguments + +} from 'core/component/engines'; + +import { registerComponent } from 'core/component/register'; +import { createVNodeWithDirectives } from 'core/component/render/vnode'; +import { isSpecialComponent } from 'core/component/render/helpers'; + +import type { ComponentInterface } from 'core/component/interface'; + +export function wrapCreateVNode(original: T): T { + return Object.cast((type, ...args) => createVNodeWithDirectives(original, type, ...args)); +} + +export function wrapResolveComponent( + original: T +): T { + return Object.cast((name, ...args) => { + if (isSpecialComponent(name)) { + return name; + } + + registerComponent(name); + return original(name, ...args); + }); +} + +export function wrapWithDirectives(original: T): T { + return Object.cast(function withDirectives(this: ComponentInterface, vnode: VNode, dirs: DirectiveArguments) { + for (let i = 0; i < dirs.length; i++) { + const + [dir, value, arg, modifiers] = dirs[i]; + + if (Object.isDictionary(dir) && Object.isFunction(dir.beforeCreate)) { + dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); + } + } + + return original(vnode, dirs); + }); +} diff --git a/src/core/component/render/wrappers/helpers.ts b/src/core/component/render/wrappers/helpers.ts deleted file mode 100644 index cbc86de871..0000000000 --- a/src/core/component/render/wrappers/helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export function isSpecialComponent(component: string): boolean { - return component === 'v-render' || component.endsWith('-functional'); -} diff --git a/src/core/component/render/wrappers/index.ts b/src/core/component/render/wrappers/index.ts deleted file mode 100644 index 7766df407a..0000000000 --- a/src/core/component/render/wrappers/index.ts +++ /dev/null @@ -1,42 +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 { createVNode, resolveComponent, withDirectives } from 'vue'; - -import * as comp from 'core/component/render/components'; - -import { isSpecialComponent } from 'core/component/render/wrappers/helpers'; - -export function wrapCreateVNode(original: T): T { - return Object.cast((type, ...args) => comp.createVNodeWithDirectives(original, type, ...args)); -} - -export function wrapResolveComponent(original: T): T { - return Object.cast((name, ...args) => { - if (isSpecialComponent(name)) { - return name; - } - - return original(name, ...args); - }); -} - -export function wrapWithDirectives(original: T): T { - return Object.cast(function withDirectives(vnode, dirs) { - for (let i = 0; i < dirs.length; i++) { - const - [dir, value, arg, modifiers] = dirs[i]; - - if (dir.beforeCreate != null) { - dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); - } - } - - return original(vnode, dirs); - }); -} From cd549624368f7e2c1da7326ff5f216ac7541b227 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 19:22:58 +0300 Subject: [PATCH 0089/2313] refactor: migration to static attrs --- build/snakeskin/filters/bem.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/build/snakeskin/filters/bem.js b/build/snakeskin/filters/bem.js index a8a444b051..0c2e98a007 100644 --- a/build/snakeskin/filters/bem.js +++ b/build/snakeskin/filters/bem.js @@ -9,7 +9,6 @@ */ const - $C = require('collection.js'), {wrapAttrArray} = include('build/snakeskin/filters/helpers'); const @@ -26,23 +25,15 @@ module.exports = [ * @returns {string} */ function bem2Component(block, attrs, rootTag, value) { - attrs[':class'] = attrs[':class'] || []; - const - elName = value.replace(elSeparatorRgxp, ''), - classes = attrs[':class'], - styles = attrs[':style']; - - const newClasses = classes.concat( - $C(classes).includes('componentId') ? [] : 'componentId', - `classes && classes['${elName}']` - ); + elName = value.replace(elSeparatorRgxp, ''); - attrs[':class'] = wrapAttrArray(newClasses); + attrs['data-cached-dynamic-class'] = wrapAttrArray([ + 'self.componentId', + `self.classes && self.classes['${elName}']` + ]); - if (!styles || !styles.length) { - attrs[':style'] = [`styles && styles['${elName}']`]; - } + attrs['data-cached-dynamic-style'] = wrapAttrArray([`self.styles && self.styles['${elName}']`]); return block + value; } From 983a2788859240d1d19579ae0516ae7ebbca224b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 19:23:17 +0300 Subject: [PATCH 0090/2313] refactor: use static helpers --- src/core/component/directives/attrs/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index 534019f33c..326665d928 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -12,7 +12,7 @@ */ import { ComponentEngine, DirectiveBinding, VNode } from 'core/component/engines'; -import type { ComponentInterface } from 'core/component/interface'; +import { mergeProps, normalizeStyle, normalizeClass } from 'core/component/render'; import { @@ -29,6 +29,7 @@ import { } from 'core/component/directives/attrs/const'; +import type { ComponentInterface } from 'core/component/interface'; import type { DirectiveOptions } from 'core/component/directives/attrs/interface'; export * from 'core/component/directives/attrs/const'; @@ -265,14 +266,14 @@ ComponentEngine.directive('attrs', { if (classAttrs[attrName] != null) { attrName = classAttrs[attrName]; - attrVal = r?.normalizeClass.call(ctx, attrVal); + attrVal = normalizeClass(Object.cast(attrVal)); if (vnode.patchFlag < 6) { vnode.patchFlag = 6; } } else if (styleAttrs[attrName] != null) { - attrVal = r?.normalizeStyle.call(ctx, attrVal); + attrVal = normalizeStyle(Object.cast(attrVal)); if (vnode.patchFlag < 4) { vnode.patchFlag = 4; @@ -292,7 +293,7 @@ ComponentEngine.directive('attrs', { } if (props[attrName] != null) { - Object.assign(props, r?.mergeProps({[attrName]: props[attrName]}, {[attrName]: attrVal})); + Object.assign(props, mergeProps({[attrName]: props[attrName]}, {[attrName]: attrVal})); } else { props[attrName] = attrVal; From 913f26f135fc0271ef5e3bec4483a5aa04729d53 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 19:23:32 +0300 Subject: [PATCH 0091/2313] fix: re-export `resolveStaticalAttrs` --- src/core/component/engines/vue3/render.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 2097933efc..ecd71037e1 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -8,19 +8,23 @@ import { - resolveComponent as resolveComponentSuper, - createVNode as createVNodeSuper, - withDirectives as withDirectivesSuper + resolveComponent as superResolveComponent, + resolveDynamicComponent as superResolveDynamicComponent, + + createVNode as superCreateVNode, + withDirectives as superWithDirectives } from 'vue'; import { + resolveStaticalAttrs, + wrapResolveComponent, wrapCreateVNode, wrapWithDirectives -} from 'core/component/render/wrappers'; +} from 'core/component/render'; export { @@ -48,6 +52,7 @@ export { mergeProps, resolveDirective, + resolveTransitionHooks, withCtx, withKeys, @@ -62,7 +67,10 @@ export { } from 'vue'; +export { resolveStaticalAttrs }; + export const - resolveComponent = wrapResolveComponent(resolveComponentSuper), - createVNode = wrapCreateVNode(createVNodeSuper), - withDirectives = wrapWithDirectives(withDirectivesSuper); + resolveComponent = wrapResolveComponent(superResolveComponent), + resolveDynamicComponent = wrapResolveComponent(superResolveDynamicComponent), + createVNode = wrapCreateVNode(superCreateVNode), + withDirectives = wrapWithDirectives(superWithDirectives); From ef7fad2ffc91a1d12d62202b0b0994f703364637 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 19:24:06 +0300 Subject: [PATCH 0092/2313] refactor: addded new props `styles` & `classes` --- .../interface/component/component.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index fec5b418a0..03e6c7db5b 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -69,6 +69,43 @@ export abstract class ComponentInterface { */ readonly instance!: this; + /** + * Additional classes for the component elements. + * It can be useful if you need to attach some extra classes to internal component elements. + * Be sure you know what you are doing because this mechanism is tied to an internal component markup. + * + * @example + * ```js + * // Key names are tied with component elements, + * // and values contain a CSS class or list of classes we want to add + * + * { + * foo: 'bla', + * bar: ['bla', 'baz'] + * } + * ``` + */ + readonly classes?: Dictionary>; + + /** + * Additional styles for the component elements. + * It can be useful if you need to attach some extra styles to internal component elements. + * Be sure you know what you are doing because this mechanism is tied to an internal component markup. + * + * @example + * ```js + * // Key names are tied with component elements, + * // and values contains a CSS style string, a style object or list of style strings + * + * { + * foo: 'color: red', + * bar: {color: 'blue'}, + * baz: ['color: red', 'background: green'] + * } + * ``` + */ + readonly styles?: Dictionary | Dictionary>; + /** * Name of the active component hook */ From aae30bb4a44100ef184812dbc142d85f5b12ee94 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 25 Apr 2022 19:24:24 +0300 Subject: [PATCH 0093/2313] fix: fixed TS errors --- src/core/component/interface/engine.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/component/interface/engine.ts b/src/core/component/interface/engine.ts index c432ac192b..916a44888e 100644 --- a/src/core/component/interface/engine.ts +++ b/src/core/component/interface/engine.ts @@ -33,6 +33,8 @@ import type { mergeProps, resolveComponent, + resolveDynamicComponent, + resolveTransitionHooks, resolveDirective, withCtx, @@ -87,6 +89,8 @@ export interface RenderAPI { mergeProps: typeof mergeProps; resolveComponent: typeof resolveComponent; + resolveDynamicComponent: typeof resolveDynamicComponent; + resolveTransitionHooks: typeof resolveTransitionHooks; resolveDirective: typeof resolveDirective; withCtx: typeof withCtx; From 57f6d163dafea5b0a0974d71d3b0976bb2d87f1b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 09:26:22 +0300 Subject: [PATCH 0094/2313] refacor: added caching of dynamic functions & added doc --- src/core/component/render/vnode.ts | 45 ++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/core/component/render/vnode.ts b/src/core/component/render/vnode.ts index 83aa7b5aea..20b5b663cf 100644 --- a/src/core/component/render/vnode.ts +++ b/src/core/component/render/vnode.ts @@ -26,7 +26,28 @@ export function createVNodeWithDirectives(createVNode: CreateVNode, type: string return components[type]?.(createVNode, type, ...args) ?? createVNode(...args); } -export function resolveStaticalAttrs(this: ComponentInterface, vnode: T): T { +const + staticAttrsCache: Dictionary = Object.createDict(); + +/** + * Interpolates values from some static attributes of the passed VNode + * + * @param vnode + * @example + * ```js + * // `.componentId = 'id-1'` + * // `.componentName = 'b-example'` + * // `.color = 'red'` + * const ctx = this; + * + * // {class: 'id-1 b-example', style: {color: 'red'}} + * interpolateStaticAttrs.call(ctx, { + * 'data-cached-dynamic-class': '[self.componentId, self.componentName]', + * 'data-cached-dynamic-style': '{color: self.color}' + * }) + * ``` + */ +export function interpolateStaticAttrs(this: ComponentInterface, vnode: T): T { const props = >>(Object.isString(vnode.type) ? vnode.props : vnode); @@ -36,12 +57,13 @@ export function resolveStaticalAttrs(this: Compone for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { const - key = keys[i]; + key = keys[i], + fnBody = String(props[key]); switch (key) { case 'data-cached-dynamic-class': { // eslint-disable-next-line no-new-func - const classes = Function('self', `return ${props[key]}`)(this); + const classes = compileFn(fnBody)(this); Object.assign(props, mergeProps({class: props.class}, {class: classes})); delete props[key]; @@ -51,7 +73,7 @@ export function resolveStaticalAttrs(this: Compone case 'data-cached-dynamic-style': { // eslint-disable-next-line no-new-func - const style = Function('self', `return ${props[key]}`)(this); + const style = compileFn(fnBody)(this); Object.assign(props, mergeProps({style: props.style}, {style})); delete props[key]; @@ -69,9 +91,22 @@ export function resolveStaticalAttrs(this: Compone if (Object.isArray(children)) { for (let i = 0; i < children.length; i++) { - resolveStaticalAttrs.call(this, children[i]); + interpolateStaticAttrs.call(this, children[i]); } } return vnode; + + function compileFn(fnBody: string): Function { + let + fn = staticAttrsCache[fnBody]; + + if (fn == null) { + // eslint-disable-next-line no-new-func + fn = Function('self', `return ${fnBody}`); + staticAttrsCache[fnBody] = fn; + } + + return fn; + } } From bdc6eaca6705ccea9807fc4f7c0f35d747cfba3c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 09:33:52 +0300 Subject: [PATCH 0095/2313] refactor: filter redundant directives --- src/core/component/render/wrappers.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index ac6ce413d8..b258ec3e30 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -44,15 +44,33 @@ export function wrapResolveComponent(original: T): T { return Object.cast(function withDirectives(this: ComponentInterface, vnode: VNode, dirs: DirectiveArguments) { + const + resolvedDirs: DirectiveArguments = []; + for (let i = 0; i < dirs.length; i++) { + const + decl = dirs[i]; + const [dir, value, arg, modifiers] = dirs[i]; - if (Object.isDictionary(dir) && Object.isFunction(dir.beforeCreate)) { - dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); + if (Object.isDictionary(dir)) { + if (Object.isFunction(dir.beforeCreate)) { + dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); + + if (Object.keys(dir).length > 1 && value != null) { + resolvedDirs.push(decl); + } + + } else if (Object.keys(dir).length > 0 && value != null) { + resolvedDirs.push(decl); + } + + } else if (value != null) { + resolvedDirs.push(decl); } } - return original(vnode, dirs); + return original(vnode, resolvedDirs); }); } From 1f3d00f5bdf8b86f8d460ef05cce8122b1d0924b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 09:35:30 +0300 Subject: [PATCH 0096/2313] refactor: don't force render if it really need --- src/core/component/engines/vue3/component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index b07363a0c0..7babd10e43 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -72,7 +72,7 @@ export function getComponent(meta: ComponentMeta): ComponentOptions Date: Tue, 26 Apr 2022 09:35:56 +0300 Subject: [PATCH 0097/2313] refactor: resolveStaticalAttrs -> interpolateStaticAttrs --- src/core/component/engines/vue3/render.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index ecd71037e1..66bdf61f45 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -18,7 +18,7 @@ import { import { - resolveStaticalAttrs, + interpolateStaticAttrs, wrapResolveComponent, wrapCreateVNode, @@ -67,7 +67,7 @@ export { } from 'vue'; -export { resolveStaticalAttrs }; +export { interpolateStaticAttrs }; export const resolveComponent = wrapResolveComponent(superResolveComponent), From dc7eba157d5fe14d82ac3271b2d31afb4b83c92b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 11:05:35 +0300 Subject: [PATCH 0098/2313] fix: fixed TS type --- src/core/component/engines/vue3/interface.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/component/engines/vue3/interface.ts b/src/core/component/engines/vue3/interface.ts index b23633334e..5ff74f9dcd 100644 --- a/src/core/component/engines/vue3/interface.ts +++ b/src/core/component/engines/vue3/interface.ts @@ -42,5 +42,8 @@ export declare type DirectiveArguments = Array< >; export interface CreateAppFunction { - (...args: Parameters>): Overwrite, ResolveDirective>; + (...args: Parameters>): Overwrite< + ReturnType>, + ResolveDirective + >; } From a83a84d3e4fad8dce9b50fac1c30b7938505d531 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 16:32:37 +0300 Subject: [PATCH 0099/2313] refactor: optimized class interpolation --- build/snakeskin/filters/bem.js | 12 +---- src/core/component/render/vnode.ts | 79 +++++++++++++++++------------- src/super/i-block/i-block.ss | 15 ++---- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/build/snakeskin/filters/bem.js b/build/snakeskin/filters/bem.js index 0c2e98a007..bdd6a99ffd 100644 --- a/build/snakeskin/filters/bem.js +++ b/build/snakeskin/filters/bem.js @@ -25,16 +25,8 @@ module.exports = [ * @returns {string} */ function bem2Component(block, attrs, rootTag, value) { - const - elName = value.replace(elSeparatorRgxp, ''); - - attrs['data-cached-dynamic-class'] = wrapAttrArray([ - 'self.componentId', - `self.classes && self.classes['${elName}']` - ]); - - attrs['data-cached-dynamic-style'] = wrapAttrArray([`self.styles && self.styles['${elName}']`]); - + attrs['data-cached-class-component-id'] = wrapAttrArray([true]); + attrs['data-cached-class-provided-classes-styles'] = wrapAttrArray([value.replace(elSeparatorRgxp, '')]); return block + value; } ]; diff --git a/src/core/component/render/vnode.ts b/src/core/component/render/vnode.ts index 20b5b663cf..305ca55acb 100644 --- a/src/core/component/render/vnode.ts +++ b/src/core/component/render/vnode.ts @@ -37,13 +37,14 @@ const * ```js * // `.componentId = 'id-1'` * // `.componentName = 'b-example'` - * // `.color = 'red'` + * // `.classes = {'elem-name': 'alias'}` * const ctx = this; * - * // {class: 'id-1 b-example', style: {color: 'red'}} + * // {class: 'id-1 b-example alias'} * interpolateStaticAttrs.call(ctx, { - * 'data-cached-dynamic-class': '[self.componentId, self.componentName]', - * 'data-cached-dynamic-style': '{color: self.color}' + * 'data-cached-class-component-id': '' + * 'data-cached-class-provided-classes-styles': 'elem-name' + * 'data-cached-dynamic-class': '[self.componentName]' * }) * ``` */ @@ -55,34 +56,46 @@ export function interpolateStaticAttrs(this: Compo return vnode; } - for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { + { const - key = keys[i], - fnBody = String(props[key]); + key = 'data-cached-class-component-id', + val = props[key]; - switch (key) { - case 'data-cached-dynamic-class': { - // eslint-disable-next-line no-new-func - const classes = compileFn(fnBody)(this); + if (val != null) { + Object.assign(props, mergeProps({class: props.class}, {class: this.componentId})); + delete props[key]; + } + } - Object.assign(props, mergeProps({class: props.class}, {class: classes})); - delete props[key]; + { + const + key = 'data-cached-class-provided-classes-styles', + name = props[key]; - break; + if (name != null) { + if (this.classes?.[name] != null) { + Object.assign(props, mergeProps({class: props.class}, {class: this.classes[name]})); } - case 'data-cached-dynamic-style': { - // eslint-disable-next-line no-new-func - const style = compileFn(fnBody)(this); + if (this.styles?.[name] != null) { + Object.assign(props, mergeProps({style: props.style}, {style: this.styles[name]})); + } - Object.assign(props, mergeProps({style: props.style}, {style})); - delete props[key]; + delete props[key]; + } + } - break; - } + { + const + key = 'data-cached-dynamic-class', + fnBody = props[key]; - default: - // Do nothing + if (fnBody != null) { + // eslint-disable-next-line no-new-func + const classVal = compileFn(fnBody)(this); + + Object.assign(props, mergeProps({class: props.class}, {class: classVal})); + delete props[key]; } } @@ -96,17 +109,17 @@ export function interpolateStaticAttrs(this: Compo } return vnode; +} - function compileFn(fnBody: string): Function { - let - fn = staticAttrsCache[fnBody]; +function compileFn(fnBody: string): Function { + let + fn = staticAttrsCache[fnBody]; - if (fn == null) { - // eslint-disable-next-line no-new-func - fn = Function('self', `return ${fnBody}`); - staticAttrsCache[fnBody] = fn; - } - - return fn; + if (fn == null) { + // eslint-disable-next-line no-new-func + fn = Function('self', `return ${fnBody}`); + staticAttrsCache[fnBody] = fn; } + + return fn; } diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 8246f60313..bc7b35e932 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -169,12 +169,12 @@ rootAttrs[':class'] = value - rootAttrs = { & - ':class': '[...provide.componentClasses("' + self.name() + '", mods), "i-block-helper", componentId]', + 'class': 'i-block-helper', - ':-render-group': 'renderGroup', - ':-render-counter': 'renderCounter', + 'data-cached-class-component-id': '', + 'data-cached-dynamic-class': 'self.provide.componentClasses("' + self.name() + '", self.mods)', - 'v-hook': "!isVirtualTpl && (isFunctional || isFlyweight) ?" + + 'v-hook': "!isVirtualTpl && isFunctional ?" + "{" + "created: createInternalHookListener('bind')," + "mounted: createInternalHookListener('inserted')," + @@ -242,12 +242,7 @@ ? content = attrs ? attrs = {} - < template v-if = $scopedSlots['${name}'] - < slot name = ${name} | ${Object.assign({}, slotAttrs, attrs)|!html} - - < template v-else - < slot name = ${name} - += content + < slot name = ${name} | ${Object.assign({}, slotAttrs, attrs)|!html} - block headHelpers From 0e602a4e65cac7a7be212c203823ea83d499cae9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 16:58:26 +0300 Subject: [PATCH 0100/2313] refactor: improved vue3 support --- src/core/component/engines/vue3/lib.ts | 78 ++++++++++++++++++----- src/core/component/engines/vue3/render.ts | 23 +++++-- src/core/component/render/wrappers.ts | 33 +++++++++- 3 files changed, 110 insertions(+), 24 deletions(-) diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index c1f4859179..9e8b51a9a8 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -1,3 +1,5 @@ +/* eslint-disable prefer-spread */ + /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -22,29 +24,71 @@ const App = function App(component: Component & {el: Element} return app; }; -const Vue = makeLazy(App, { - use: Function, +const Vue = makeLazy( + App, + + { + use: Function, + + component: Function, + directive: Function, + + mixin: Function, + provide: Function, + version: '', + + mount: Function, + unmount: Function, + + config: { + performance: false, + + errorHandler: Function, + warnHandler: Function, + + compilerOptions: {}, + globalProperties: {}, + optionMergeStrategies: {} + } + }, + + { + call: { + component: (contexts, ...args) => { + if (args.length === 1) { + contexts.forEach((ctx) => { + ctx.component.apply(ctx, args); + }); - component: Function, - directive: Function, + return; + } - mixin: Function, - provide: Function, - version: '', + return contexts.at(-1)?.component.apply(ctx, args); + }, - mount: Function, - unmount: Function, + directive: (contexts, ...args: any[]) => { + if (args.length === 1) { + contexts.forEach((ctx) => { + ctx.directive.apply(ctx, args); + }); + } - config: { - performance: false, + return contexts.at(-1)?.component.apply(ctx, args); + }, - errorHandler: Function, - warnHandler: Function, + mixin: (contexts, ...args) => { + contexts.forEach((ctx) => { + ctx.mixin.apply(ctx, args); + }); + }, - compilerOptions: {}, - globalProperties: {}, - optionMergeStrategies: {} + provide: (contexts, ...args) => { + contexts.forEach((ctx) => { + ctx.provide.apply(ctx, args); + }); + } + } } -}); +); export default Vue; diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 66bdf61f45..130f5df3b8 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -12,6 +12,11 @@ import { resolveDynamicComponent as superResolveDynamicComponent, createVNode as superCreateVNode, + createElementVNode as superCreateElementVNode, + + createBlock as superCreateBlock, + createElementBlock as superCreateElementBlock, + withDirectives as superWithDirectives } from 'vue'; @@ -21,8 +26,10 @@ import { interpolateStaticAttrs, wrapResolveComponent, + wrapWithDirectives, + wrapCreateVNode, - wrapWithDirectives + wrapCreateElementVNode, wrapCreateBlock, wrapCreateElementBlock } from 'core/component/render'; @@ -39,10 +46,7 @@ export { renderSlot, openBlock, - createBlock, - createElementBlock, - createElementVNode, createStaticVNode, createTextVNode, createCommentVNode, @@ -71,6 +75,15 @@ export { interpolateStaticAttrs }; export const resolveComponent = wrapResolveComponent(superResolveComponent), - resolveDynamicComponent = wrapResolveComponent(superResolveDynamicComponent), + resolveDynamicComponent = wrapResolveComponent(superResolveDynamicComponent); + +export const createVNode = wrapCreateVNode(superCreateVNode), + createElementVNode = wrapCreateElementVNode(superCreateElementVNode); + +export const + createBlock = wrapCreateBlock(superCreateBlock), + createElementBlock = wrapCreateElementBlock(superCreateElementBlock); + +export const withDirectives = wrapWithDirectives(superWithDirectives); diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index b258ec3e30..99cfebcf40 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -12,6 +12,11 @@ import type { resolveDynamicComponent, createVNode, + createElementVNode, + + createBlock, + createElementBlock, + withDirectives, VNode, @@ -20,13 +25,37 @@ import type { } from 'core/component/engines'; import { registerComponent } from 'core/component/register'; -import { createVNodeWithDirectives } from 'core/component/render/vnode'; +import { createVNodeWithDirectives, interpolateStaticAttrs } from 'core/component/render/vnode'; import { isSpecialComponent } from 'core/component/render/helpers'; import type { ComponentInterface } from 'core/component/interface'; export function wrapCreateVNode(original: T): T { - return Object.cast((type, ...args) => createVNodeWithDirectives(original, type, ...args)); + return Object.cast(function createVNode(this: ComponentInterface, type: string, ...args: unknown[]) { + const vnode = createVNodeWithDirectives(original, type, ...args); + return interpolateStaticAttrs.call(this, vnode); + }); +} + +export function wrapCreateElementVNode(original: T): T { + return Object.cast(function createElementVNode(this: ComponentInterface) { + // eslint-disable-next-line prefer-spread + return interpolateStaticAttrs.call(this, original.apply(null, arguments)); + }); +} + +export function wrapCreateBlock(original: T): T { + return Object.cast(function wrapCreateBlock(this: ComponentInterface) { + // eslint-disable-next-line prefer-spread + return interpolateStaticAttrs.call(this, original.apply(null, arguments)); + }); +} + +export function wrapCreateElementBlock(original: T): T { + return Object.cast(function createElementBlock(this: ComponentInterface) { + // eslint-disable-next-line prefer-spread + return interpolateStaticAttrs.call(this, original.apply(null, arguments)); + }); } export function wrapResolveComponent( From 282965fecbdd6ff9abe146d0669792ee1d989d7b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 16:58:44 +0300 Subject: [PATCH 0101/2313] :up: --- package-lock.json | 59 ++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1672086bc8..3b1515ca22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "devDependencies": { "@playwright/test": "1.20.0", "@types/jasmine": "3.10.3", - "@v4fire/core": "3.84.0", + "@v4fire/core": "3.86.1", "@v4fire/linters": "1.9.0", "husky": "7.0.4", "nyc": "15.1.0", @@ -3996,9 +3996,9 @@ } }, "node_modules/@v4fire/core": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.84.0.tgz", - "integrity": "sha512-aXsmhB0ipIWBdaJ6UfaN0q+170YcjsR4AR3IkjhjO/Ttve8K+ZzpKAIJHybxjfeY+SBDlPH2180tA9RJExEagw==", + "version": "3.86.1", + "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.86.1.tgz", + "integrity": "sha512-LycZBIbCtxX4EJXAylj0kDLACStpSLLkk8BAZjibo6DhfX7LfL4X/yb9oHvnQ2RDofIc8Vrn9E9sFkoGa5P/Sg==", "dev": true, "dependencies": { "@swc/core": "1.2.153", @@ -4064,7 +4064,7 @@ "tsc-alias": "1.6.1", "tsconfig": "7.0.0", "typedoc": "0.22.12", - "typescript": "4.4.4", + "typescript": "4.6.2", "upath": "2.0.1" } }, @@ -4251,9 +4251,9 @@ } }, "node_modules/@v4fire/core/node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", "dev": true, "optional": true, "bin": { @@ -17704,9 +17704,9 @@ } }, "node_modules/marked": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", - "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.14.tgz", + "integrity": "sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ==", "dev": true, "optional": true, "bin": { @@ -23340,7 +23340,7 @@ }, "node_modules/ss2vue3": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/snakeskintpl/ss2vue3.git#118a05375297fe3ce59fc5695e16edc3ab129192", + "resolved": "git+ssh://git@github.com/snakeskintpl/ss2vue3.git#fb09671c61a800d223a81e3bfd5ba0ab95f5dc33", "license": "MIT", "optional": true, "engines": { @@ -26167,9 +26167,9 @@ } }, "node_modules/typedoc": { - "version": "0.22.14", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.14.tgz", - "integrity": "sha512-tlf9wIcsrnQSjetStrnRutuy2RjZkG5PK2umwveZLTkuC2K9VywOZTdu2G19BdOPzGrhZjf9WK7pthXqnFQejg==", + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", + "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", "dev": true, "optional": true, "dependencies": { @@ -26186,7 +26186,7 @@ "node": ">= 12.10.0" }, "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x" + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x" } }, "node_modules/typescript": { @@ -30546,9 +30546,9 @@ } }, "@v4fire/core": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.84.0.tgz", - "integrity": "sha512-aXsmhB0ipIWBdaJ6UfaN0q+170YcjsR4AR3IkjhjO/Ttve8K+ZzpKAIJHybxjfeY+SBDlPH2180tA9RJExEagw==", + "version": "3.86.1", + "resolved": "https://registry.npmjs.org/@v4fire/core/-/core-3.86.1.tgz", + "integrity": "sha512-LycZBIbCtxX4EJXAylj0kDLACStpSLLkk8BAZjibo6DhfX7LfL4X/yb9oHvnQ2RDofIc8Vrn9E9sFkoGa5P/Sg==", "dev": true, "requires": { "@babel/core": "7.17.5", @@ -30608,7 +30608,7 @@ "tsconfig-paths": "3.13.0", "tslib": "2.3.1", "typedoc": "0.22.12", - "typescript": "4.4.4", + "typescript": "4.6.2", "upath": "2.0.1", "w3c-xmlserializer": "2.0.0" }, @@ -30754,9 +30754,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", "dev": true, "optional": true }, @@ -41401,9 +41401,9 @@ } }, "marked": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", - "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.14.tgz", + "integrity": "sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ==", "dev": true, "optional": true }, @@ -45803,7 +45803,7 @@ } }, "ss2vue3": { - "version": "git+ssh://git@github.com/snakeskintpl/ss2vue3.git#118a05375297fe3ce59fc5695e16edc3ab129192", + "version": "git+ssh://git@github.com/snakeskintpl/ss2vue3.git#fb09671c61a800d223a81e3bfd5ba0ab95f5dc33", "from": "ss2vue3@github:snakeskintpl/ss2vue3", "optional": true, "requires": {} @@ -48011,8 +48011,9 @@ } }, "typedoc": { - "version": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.14.tgz", - "integrity": "sha512-tlf9wIcsrnQSjetStrnRutuy2RjZkG5PK2umwveZLTkuC2K9VywOZTdu2G19BdOPzGrhZjf9WK7pthXqnFQejg==", + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", + "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", "dev": true, "optional": true, "requires": { diff --git a/package.json b/package.json index be0203f87a..bc50ae7ef2 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ }, "devDependencies": { "@types/jasmine": "3.10.3", - "@v4fire/core": "3.84.0", + "@v4fire/core": "3.86.1", "@v4fire/linters": "1.9.0", "husky": "7.0.4", "nyc": "15.1.0", From 8cc82fd2b143f7d287d17cdace7c3b2bf247c51b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 17:14:36 +0300 Subject: [PATCH 0102/2313] fix: fixed context providing --- src/core/component/engines/vue3/lib.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index 9e8b51a9a8..cded607247 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -63,7 +63,8 @@ const Vue = makeLazy( return; } - return contexts.at(-1)?.component.apply(ctx, args); + const ctx = contexts.at(-1); + return ctx?.component.apply(ctx, args); }, directive: (contexts, ...args: any[]) => { @@ -73,7 +74,8 @@ const Vue = makeLazy( }); } - return contexts.at(-1)?.component.apply(ctx, args); + const ctx = contexts.at(-1); + return ctx?.directive.apply(ctx, args); }, mixin: (contexts, ...args) => { From 05cfcd6b75559c6a78c6b8f257152e792c36ddb9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 17:18:15 +0300 Subject: [PATCH 0103/2313] fix: added `return` --- src/core/component/engines/vue3/lib.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index cded607247..edf7dd6133 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -72,6 +72,8 @@ const Vue = makeLazy( contexts.forEach((ctx) => { ctx.directive.apply(ctx, args); }); + + return; } const ctx = contexts.at(-1); From 661d7358c30fcdc7ab53b01940d5dd5e8c58838c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 17:44:21 +0300 Subject: [PATCH 0104/2313] refactor: removed freezing of static literals --- build/snakeskin/filters/const.js | 12 ------------ build/snakeskin/filters/tag.js | 21 +-------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/build/snakeskin/filters/const.js b/build/snakeskin/filters/const.js index dba0b6aa8a..2c7f9bbb32 100644 --- a/build/snakeskin/filters/const.js +++ b/build/snakeskin/filters/const.js @@ -33,18 +33,6 @@ exports.tagRgxp = /<[^>]+>/; */ exports.componentElRgxp = new RegExp(`\\b${validators.baseBlockName}__[a-z0-9][a-z0-9-_]*\\b`); -/** - * RegExp to match declaration of an object literal - * - * @type {!RegExp} - * @example - * ``` - * [1, 2] - * {a: 1} - * ``` - */ -exports.isObjLiteral = /^\s*[[{]/; - /** * RegExp to match requiring of svg images * diff --git a/build/snakeskin/filters/tag.js b/build/snakeskin/filters/tag.js index 3efbc634b9..1719eaab98 100644 --- a/build/snakeskin/filters/tag.js +++ b/build/snakeskin/filters/tag.js @@ -12,7 +12,7 @@ const $C = require('collection.js'); const - {isObjLiteral, isSvgRequire, isV4Prop, isStaticV4Prop} = include('build/snakeskin/filters/const'); + {isSvgRequire, isV4Prop, isStaticV4Prop} = include('build/snakeskin/filters/const'); module.exports = [ /** @@ -44,14 +44,6 @@ module.exports = [ return; } - el = $C(el).map((el) => { - if (Object.isString(el) && isObjLiteral.test(el) && isStaticLiteral(el)) { - return `opt.memoizeLiteral(${el})`; - } - - return el; - }); - const dataAttrBind = ':-'; @@ -71,14 +63,3 @@ module.exports = [ }); } ]; - -function isStaticLiteral(v) { - try { - // eslint-disable-next-line no-new-func - Function(`return ${v}`)(); - return true; - - } catch { - return false; - } -} From e74e89544cc766a959754f8dc5b79f1d27ec8803 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 26 Apr 2022 17:45:45 +0300 Subject: [PATCH 0105/2313] refactor: removed flyweight support --- build/snakeskin/filters/tag-name.js | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/build/snakeskin/filters/tag-name.js b/build/snakeskin/filters/tag-name.js index 3ea9c606b1..7939e8e4a4 100644 --- a/build/snakeskin/filters/tag-name.js +++ b/build/snakeskin/filters/tag-name.js @@ -130,32 +130,6 @@ module.exports = [ return 'button'; } - return tag; - }, - - /** - * Expands the `@component` snippet as a `` tag - * - * @param {string} tag - * @param {!Object} attrs - * @returns {string} - * - * @example - * ``` - * /// - * < @b-button - * ``` - */ - function expandFlyweightComponent(tag, attrs) { - const - flyweightPrfx = '@'; - - if (tag.startsWith(flyweightPrfx)) { - attrs['v4-flyweight-component'] = [tag.slice(flyweightPrfx.length)]; - attrs[':instance-of'] = attrs['v4-flyweight-component']; - return 'span'; - } - return tag; } ]; From 7874424481aad87dae62a9a9d53dfdf4f7b7a438 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 14:37:23 +0300 Subject: [PATCH 0106/2313] refactor: simple refactoring --- src/core/component/watch/component-api.ts | 292 +++++++++++----------- 1 file changed, 152 insertions(+), 140 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index 2a5b162626..db18657c1b 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -13,11 +13,13 @@ import watch, { mute, watchHandlers, - MultipleWatchHandler + MultipleWatchHandler, + + Watcher } from 'core/object/watch'; -import { getPropertyInfo, bindingRgxp } from 'core/component/reflection'; +import { getPropertyInfo, bindingRgxp } from 'core/component/reflect'; import { @@ -39,7 +41,7 @@ import type { ComponentInterface, RawWatchHandler } from 'core/component/interfa import type { ImplementComponentWatchAPIOptions } from 'core/component/watch/interface'; /** - * Implements the base component watch API to a component instance + * Implements watch API to the passed component instance * * @param component * @param [opts] - additional options @@ -55,102 +57,12 @@ export function implementComponentWatchAPI( } = component; const - isNotRegular = Boolean(component.isFlyweight) || params.functional === true, + isFunctional = params.functional === true, usedHandlers = new Set(); let timerId; - // The handler to invalidate the cache of computed fields - // eslint-disable-next-line @typescript-eslint/typedef - const invalidateComputedCache = () => function invalidateComputedCache(val, oldVal, info) { - if (info == null) { - return; - } - - const - {path} = info, - rootKey = String(path[0]); - - // If was changed there properties that can affect cached computed fields, - // then we need to invalidate these caches - if (computedFields[rootKey]?.get != null) { - delete Object.getOwnPropertyDescriptor(component, rootKey)?.get?.[cacheStatus]; - } - - // We need to provide this mutation to other listeners. - // This behavior fixes the bug when we have some accessor that depends on a property from another component. - - const - ctx = invalidateComputedCache[tiedWatchers] != null ? component : info.root[toComponentObject] ?? component, - currentDynamicHandlers = immediateDynamicHandlers.get(ctx)?.[rootKey]; - - if (currentDynamicHandlers) { - for (let o = currentDynamicHandlers.values(), el = o.next(); !el.done; el = o.next()) { - el.value(val, oldVal, info); - } - } - }; - - // The handler to broadcast events of accessors - // eslint-disable-next-line @typescript-eslint/typedef - const emitAccessorEvents = () => function emitAccessorEvents(mutations, ...args) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (args.length > 0) { - mutations = [Object.cast([mutations, ...args])]; - } - - for (let i = 0; i < mutations.length; i++) { - const - eventArgs = mutations[i], - info = eventArgs[2]; - - const - {path} = info; - - if (path[path.length - 1] === '__proto__') { - continue; - } - - if (info.parent != null) { - const - {path: parentPath} = info.parent.info; - - if (parentPath[parentPath.length - 1] === '__proto__') { - continue; - } - } - - const - rootKey = String(path[0]), - ctx = emitAccessorEvents[tiedWatchers] != null ? component : info.root[toComponentObject] ?? component, - currentDynamicHandlers = dynamicHandlers.get(ctx)?.[rootKey]; - - if (currentDynamicHandlers) { - for (let o = currentDynamicHandlers.values(), el = o.next(); !el.done; el = o.next()) { - const - handler = el.value; - - // 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(...eventArgs); - usedHandlers.add(handler); - - if (timerId == null) { - timerId = setImmediate(() => { - timerId = undefined; - usedHandlers.clear(); - }); - } - } - } - } - }; - const fieldsInfo = proxyGetters.field(component), systemFieldsInfo = proxyGetters.system(component); @@ -164,7 +76,7 @@ export function implementComponentWatchAPI( }; // We need to manage situations when we have accessors with dependencies from external components, - // that why we iterate over all dependencies list, + // that's why we iterate over the all dependencies list, // find external dependencies and attach watchers that directly update state if (watchDependencies.size > 0) { const @@ -192,7 +104,8 @@ export function implementComponentWatchAPI( for (let j = 0; j < deps.length; j++) { const dep = deps[j], - watchInfo = getPropertyInfo(Array.concat([], dep).join('.'), component); + depPath = Object.isString(dep) ? dep : dep.join('.'), + watchInfo = getPropertyInfo(depPath, component); newDeps[j] = dep; @@ -267,58 +180,19 @@ export function implementComponentWatchAPI( let fieldWatchOpts; - if (!isNotRegular && opts?.tieFields) { + if (!isFunctional && opts?.tieFields) { fieldWatchOpts = {...watchOpts, tiedWith: component}; } else { fieldWatchOpts = watchOpts; } - // Initializes the specified watcher on a component instance - const initWatcher = (name, watcher) => { - mute(watcher.proxy); - - watcher.proxy[toComponentObject] = component; - Object.defineProperty(component, name, { - enumerable: true, - configurable: true, - value: watcher.proxy - }); - - if (isNotRegular) { - // We need to track all modified fields of a function instance - // to restore state if a parent has re-created the component - const w = watch(watcher.proxy, {deep: true, collapse: true, immediate: true}, (v, o, i) => { - unsafe.$modifiedFields[String(i.path[0])] = true; - }); - - $a.worker(() => w.unwatch()); - } - }; - // Watcher of fields let fieldsWatcher; - const initFieldsWatcher = () => { - const immediateFieldWatchOpts = { - ...fieldWatchOpts, - immediate: true - }; - - fieldsWatcher = watch(fieldsInfo.value, immediateFieldWatchOpts, invalidateComputedCache()); - $a.worker(() => fieldsWatcher.unwatch()); - - { - const w = watch(fieldsWatcher.proxy, fieldWatchOpts, emitAccessorEvents()); - $a.worker(() => w.unwatch()); - } - - initWatcher(fieldsInfo.key, fieldsWatcher); - }; - - if (isNotRegular) { + if (isFunctional) { // Don't force watching of fields until it becomes necessary fieldsInfo.value[watcherInitializer] = () => { delete fieldsInfo.value[watcherInitializer]; @@ -378,8 +252,8 @@ export function implementComponentWatchAPI( }); // Watching of component props. - // The root component and functional/flyweight components can't watch props. - if (!isNotRegular && !params.root) { + // The root component and functional components can't watch their props. + if (!isFunctional && !params.root) { const props = proxyGetters.prop(component), propsStore = props.value; @@ -397,7 +271,7 @@ export function implementComponentWatchAPI( if (!('watch' in props)) { const propsWatcher = watch(propsStore, propWatchOpts); $a.worker(() => propsWatcher.unwatch()); - initWatcher((props).key, propsWatcher); + initWatcher(props.key, propsWatcher); } // We need to attach default watchers for all props that can affect component computed fields @@ -456,4 +330,142 @@ export function implementComponentWatchAPI( } } } + + // Initializes the specified watcher on a component instance + function initWatcher(name: Nullable, watcher: Watcher) { + if (name == null) { + return; + } + + mute(watcher.proxy); + + watcher.proxy[toComponentObject] = component; + Object.defineProperty(component, name, { + enumerable: true, + configurable: true, + value: watcher.proxy + }); + + if (isFunctional) { + // We need to track all modified fields of a function instance + // to restore state if a parent has re-created the component + const w = watch(watcher.proxy, {deep: true, collapse: true, immediate: true}, (v, o, i) => { + unsafe.$modifiedFields[String(i.path[0])] = true; + }); + + $a.worker(() => w.unwatch()); + } + } + + // Initializes the field watcher on a component instance + function initFieldsWatcher() { + const immediateFieldWatchOpts = { + ...fieldWatchOpts, + immediate: true + }; + + fieldsWatcher = watch(fieldsInfo.value, immediateFieldWatchOpts, invalidateComputedCache()); + $a.worker(() => fieldsWatcher.unwatch()); + + { + const w = watch(fieldsWatcher.proxy, fieldWatchOpts, emitAccessorEvents()); + $a.worker(() => w.unwatch()); + } + + initWatcher(fieldsInfo.key, fieldsWatcher); + } + + // The handler to invalidate cache of computed fields + function invalidateComputedCache(): RawWatchHandler { + // eslint-disable-next-line @typescript-eslint/typedef + return function invalidateComputedCache(val, oldVal, info) { + if (info == null) { + return; + } + + const + {path} = info, + rootKey = String(path[0]); + + // If there has been changed properties that can affect memoized computed fields, + // then we need to invalidate these caches + if (computedFields[rootKey]?.get != null) { + delete Object.getOwnPropertyDescriptor(component, rootKey)?.get?.[cacheStatus]; + } + + // We need to provide this mutation to other listeners. + // This behavior fixes a bug when we have some accessor that depends on a property from another component. + + const + ctx = invalidateComputedCache[tiedWatchers] != null ? component : info.root[toComponentObject] ?? component, + currentDynamicHandlers = immediateDynamicHandlers.get(ctx)?.[rootKey]; + + if (currentDynamicHandlers) { + for (let o = currentDynamicHandlers.values(), el = o.next(); !el.done; el = o.next()) { + el.value(val, oldVal, info); + } + } + }; + } + + // The handler to broadcast events of accessors + function emitAccessorEvents(): MultipleWatchHandler { + // eslint-disable-next-line @typescript-eslint/typedef + return function emitAccessorEvents(mutations, ...args) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (args.length > 0) { + mutations = [Object.cast([mutations, ...args])]; + } + + for (let i = 0; i < mutations.length; i++) { + const + eventArgs = mutations[i], + info = eventArgs[2]; + + const + {path} = info; + + if (path[path.length - 1] === '__proto__') { + continue; + } + + if (info.parent != null) { + const + {path: parentPath} = info.parent.info; + + if (parentPath[parentPath.length - 1] === '__proto__') { + continue; + } + } + + const + rootKey = String(path[0]), + ctx = emitAccessorEvents[tiedWatchers] != null ? component : info.root[toComponentObject] ?? component, + currentDynamicHandlers = dynamicHandlers.get(ctx)?.[rootKey]; + + if (currentDynamicHandlers) { + for (let o = currentDynamicHandlers.values(), el = o.next(); !el.done; el = o.next()) { + const + handler = el.value; + + // 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(...eventArgs); + usedHandlers.add(handler); + + if (timerId == null) { + timerId = setImmediate(() => { + timerId = undefined; + usedHandlers.clear(); + }); + } + } + } + } + }; + } } From 447899d9ac1e504d3024c98b062efbc6f38261d8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 14:54:17 +0300 Subject: [PATCH 0107/2313] refactor: simple refactoring --- src/core/component/watch/create.ts | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index 3bf2d0a39b..a481a2f145 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -7,16 +7,16 @@ */ import watch, { mute, unmute, unwrap, getProxyType, isProxy, WatchHandlerParams } from 'core/object/watch'; +import { getPropertyInfo, PropertyInfo } from 'core/component/reflect'; -import { getPropertyInfo, PropertyInfo } from 'core/component/reflection'; -import type { ComponentInterface, WatchOptions, RawWatchHandler } from 'core/component/interface'; - -import { tiedWatchers, watcherInitializer, fakeCopyLabel } from 'core/component/watch/const'; +import { tiedWatchers, watcherInitializer } from 'core/component/watch/const'; import { cloneWatchValue } from 'core/component/watch/clone'; import { attachDynamicWatcher } from 'core/component/watch/helpers'; +import type { ComponentInterface, WatchOptions, RawWatchHandler } from 'core/component/interface'; + /** - * Creates a function to watch changes from the specified component instance and returns it + * Creates a function to watch property changes from the specified component instance and returns it * @param component */ // eslint-disable-next-line max-lines-per-function @@ -26,10 +26,6 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface // eslint-disable-next-line @typescript-eslint/typedef,max-lines-per-function return function watchFn(this: unknown, path, optsOrHandler, rawHandler?) { - if (component.isFlyweight) { - return null; - } - let handler: RawWatchHandler, opts: WatchOptions; @@ -51,8 +47,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface } else { if (isProxy(path)) { - // @ts-ignore (lazy binding) - info = {ctx: path}; + info = Object.cast({ctx: path}); } else { info = path; @@ -114,7 +109,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface isDefinedPath = Object.size(info.path) > 0, watchInfo = isAccessor ? null : component.$renderEngine.proxyGetters[info.type]?.(info.ctx); - const normalizedOpts = { + const normalizedOpts: WatchOptions = { collapse: true, ...opts, ...watchInfo?.opts @@ -266,8 +261,8 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface unmute(proxy); Object.defineProperty(propCtx, info.name, { - enumerable: true, configurable: true, + enumerable: true, get: () => proxy[info.name], @@ -328,10 +323,6 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface // eslint-disable-next-line @typescript-eslint/no-use-before-define attachDeepProxy(); - if (value?.[fakeCopyLabel] === true) { - return; - } - let valueByPath = Object.get(value, slicedPathChunks); valueByPath = unwrap(valueByPath) ?? valueByPath; From a84a907a93c221dccce7f4de6143ae05b0e3dada Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 14:54:53 +0300 Subject: [PATCH 0108/2313] refactor: removed `fakeCopyLabel` --- src/core/component/watch/const.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/component/watch/const.ts b/src/core/component/watch/const.ts index d8b59bef2e..460cc77421 100644 --- a/src/core/component/watch/const.ts +++ b/src/core/component/watch/const.ts @@ -14,12 +14,11 @@ export const export const tiedWatchers = Symbol('List of tied watchers'), - watcherInitializer = Symbol('Watcher initializer'), - toComponentObject = Symbol('Link to a component object'); + watcherInitializer = Symbol('Watcher initializer'); export const cacheStatus = Symbol('Cache status'), - fakeCopyLabel = Symbol('Fake copy label'); + toComponentObject = Symbol('Link to a component object'); export const customWatcherRgxp = /^([!?]?)([^!?:]*):(.*)/; From 45a5c1a1f510117641a13f3d92d72d7ce600ccd8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 14:56:45 +0300 Subject: [PATCH 0109/2313] refactor: better doc --- src/core/component/watch/clone.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/watch/clone.ts b/src/core/component/watch/clone.ts index d39fe4a374..643bc59f75 100644 --- a/src/core/component/watch/clone.ts +++ b/src/core/component/watch/clone.ts @@ -12,15 +12,15 @@ import type { WatchOptions } from 'core/object/watch'; * Clones the specified watcher value * * @param value - * @param [opts] + * @param [watchOpts] - watch options */ -export function cloneWatchValue(value: T, opts?: WatchOptions): T { +export function cloneWatchValue(value: T, watchOpts?: WatchOptions): T { if (value == null || typeof value !== 'object' || Object.isFrozen(value)) { return value; } const - isDeep = opts?.deep; + isDeep = watchOpts?.deep; let needClone = false; From 04c34b8d91ffd847a7b69d5a6fe05763e161d83e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 15:25:26 +0300 Subject: [PATCH 0110/2313] refactor: better doc --- src/core/component/watch/helpers.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/core/component/watch/helpers.ts b/src/core/component/watch/helpers.ts index a6674c8544..077709edbb 100644 --- a/src/core/component/watch/helpers.ts +++ b/src/core/component/watch/helpers.ts @@ -8,26 +8,26 @@ import watch, { WatchOptions, MultipleWatchHandler } from 'core/object/watch'; -import type { PropertyInfo } from 'core/component/reflection'; -import type { ComponentInterface } from 'core/component/interface'; - +import type { PropertyInfo } from 'core/component/reflect'; import { dynamicHandlers } from 'core/component/watch/const'; + +import type { ComponentInterface } from 'core/component/interface'; import type { DynamicHandlers } from 'core/component/watch/interface'; /** * Attaches a dynamic watcher to the specified property. - * This function is used to manage the situation when we are watching some accessor. + * This function is used to manage a situation when we are watching some accessor. * * @param component - component that is watched * @param prop - property to watch - * @param opts - options for watching - * @param handler + * @param watchOpts - options of watching + * @param handler - handler of mutations * @param [store] - store with dynamic handlers */ export function attachDynamicWatcher( component: ComponentInterface, prop: PropertyInfo, - opts: WatchOptions, + watchOpts: WatchOptions, handler: Function, store: DynamicHandlers = dynamicHandlers ): Function { @@ -50,13 +50,13 @@ export function attachDynamicWatcher( if ( // We don't watch deep mutations - !opts.deep && info.path.length > (Object.isDictionary(info.obj) ? 1 : 2) || + !watchOpts.deep && info.path.length > (Object.isDictionary(info.obj) ? 1 : 2) || // We don't watch prototype mutations - !opts.withProto && info.fromProto || + !watchOpts.withProto && info.fromProto || - // The mutation was already fired - opts.eventFilter && !Object.isTruly(opts.eventFilter(value, oldValue, info)) + // The mutation has been already fired + watchOpts.eventFilter && !Object.isTruly(watchOpts.eventFilter(value, oldValue, info)) ) { continue; } @@ -82,10 +82,10 @@ export function attachDynamicWatcher( watcher; if (Object.size(prop.path) > 0) { - watcher = watch(prop.ctx, prop.path, opts, wrapper); + watcher = watch(prop.ctx, prop.path, watchOpts, wrapper); } else { - watcher = watch(prop.ctx, opts, wrapper); + watcher = watch(prop.ctx, watchOpts, wrapper); } destructor = () => { From 9ac21b28470ede8c2b7c112282c53259c015875f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 15:26:06 +0300 Subject: [PATCH 0111/2313] :art: --- src/core/component/watch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/watch/README.md b/src/core/component/watch/README.md index 18fb11a312..4c0034ef18 100644 --- a/src/core/component/watch/README.md +++ b/src/core/component/watch/README.md @@ -1,3 +1,3 @@ # core/component/watch -This module provides API to add the feature of object watching to components. +This module provides API to add a feature of object watching to components. From d344a5abb999f09ea0af9d3068f7e6581e15577f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:43:30 +0300 Subject: [PATCH 0112/2313] :art: --- src/core/component/directives/attrs/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index 326665d928..a50e00dcb8 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable complexity */ + /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -36,7 +38,6 @@ export * from 'core/component/directives/attrs/const'; export * from 'core/component/directives/attrs/interface'; ComponentEngine.directive('attrs', { - // eslint-disable-next-line complexity beforeCreate(opts: DirectiveOptions, vnode: VNode) { let handlerStore, From 78729d9341f79d46b67b9b337ba90ee1f05515f0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:44:11 +0300 Subject: [PATCH 0113/2313] feat: added a new property to story only prop dependencies --- src/core/component/meta/fill.ts | 79 ++++++++++++++++------ src/core/component/meta/interface/index.ts | 12 +++- src/core/component/meta/interface/types.ts | 1 + 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 97ed84418d..ef82390e7e 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -1,3 +1,5 @@ +/* eslint-disable complexity */ + /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -9,7 +11,7 @@ import { defaultWrapper } from 'core/component/const'; import { getComponentContext } from 'core/component/context'; -import { isAbstractComponent } from 'core/component/reflect'; +import { isAbstractComponent, bindingRgxp } from 'core/component/reflect'; import { isTypeCanBeFunc } from 'core/component/prop'; import { addMethodsToMeta } from 'core/component/meta/method'; @@ -17,7 +19,7 @@ import { addMethodsToMeta } from 'core/component/meta/method'; import type { ComponentConstructor, ComponentMeta, ComponentField, WatchObject } from 'core/component/interface'; /** - * Fills the passed meta object with methods and properties from the specified component class + * Fills the passed meta object with methods and properties from the specified component class constructor * * @param meta * @param [constructor] - component constructor @@ -30,9 +32,17 @@ export function fillMeta( const { component, + params, + methods, + accessors, + computedFields, + watchers, - hooks + hooks, + + watchDependencies, + watchPropDependencies } = meta; const instance = Object.cast(new constructor()); @@ -43,7 +53,8 @@ export function fillMeta( } const - isFunctional = meta.params.functional === true; + isRoot = params.root === true, + isFunctional = params.functional === true; // Methods @@ -104,7 +115,7 @@ export function fillMeta( // Props const - defaultProps = meta.params.defaultProps !== false; + defaultProps = params.defaultProps !== false; for (let o = meta.props, keys = Object.keys(o), i = 0; i < keys.length; i++) { const @@ -138,28 +149,52 @@ export function fillMeta( defValue = prop.default !== undefined ? prop.default : defWrapper; } - component.props[key] = { - type: prop.type, - required: prop.required !== false && defaultProps && defValue === undefined, - default: defValue, - functional: prop.functional, - // eslint-disable-next-line @typescript-eslint/unbound-method - validator: prop.validator - }; + if (!isRoot || defValue !== undefined) { + component.props[key] = { + type: prop.type, + required: prop.required !== false && defaultProps && defValue === undefined, - if (Object.size(prop.watchers) > 0) { - const watcherListeners = watchers[key] ?? []; - watchers[key] = watcherListeners; + default: defValue, + functional: prop.functional, - for (let w = prop.watchers.values(), el = w.next(); !el.done; el = w.next()) { - const - watcher = el.value; + // eslint-disable-next-line @typescript-eslint/unbound-method + validator: prop.validator + }; + } - if (isFunctional && watcher.functional === false) { - continue; + if (!isRoot && !isFunctional) { + if (Object.size(prop.watchers) > 0) { + const watcherListeners = watchers[key] ?? []; + watchers[key] = watcherListeners; + + for (let w = prop.watchers.values(), el = w.next(); !el.done; el = w.next()) { + watcherListeners.push(el.value); } + } + + const + normalizedKey = key.replace(bindingRgxp, ''); + + if ((computedFields[normalizedKey] ?? accessors[normalizedKey]) != null) { + const props = watchPropDependencies.get(normalizedKey) ?? new Set(); + watchPropDependencies.set(normalizedKey, props); - watcherListeners.push(watcher); + } else if (watchDependencies.size > 0) { + for (let o = watchDependencies.entries(), el = o.next(); !el.done; el = o.next()) { + const + [key, deps] = el.value; + + for (let j = 0; j < deps.length; j++) { + const + dep = deps[j]; + + if ((Object.isArray(dep) ? dep : dep.split('.'))[0] === prop) { + const props = watchPropDependencies.get(key) ?? new Set(); + watchPropDependencies.set(key, props); + break; + } + } + } } } } diff --git a/src/core/component/meta/interface/index.ts b/src/core/component/meta/interface/index.ts index 705a8887e1..18c0045897 100644 --- a/src/core/component/meta/interface/index.ts +++ b/src/core/component/meta/interface/index.ts @@ -17,12 +17,13 @@ import type { ComponentProp, ComponentField, - ComponentAccessor, ComponentMethod, + ComponentAccessor, ComponentHooks, ComponentDirectiveOptions, - ComponentWatchDependencies + ComponentWatchDependencies, + ComponentWatchPropDependencies } from 'core/component/meta/interface/types'; @@ -110,10 +111,15 @@ export interface ComponentMeta { watchers: Dictionary; /** - * Map of dependencies to watch (to invalidate the cache of computed fields) + * Map of dependencies to watch (to invalidate cache of computed fields) */ watchDependencies: ComponentWatchDependencies; + /** + * Map of prop dependencies to watch (to invalidate cache of computed fields) + */ + watchPropDependencies: ComponentWatchPropDependencies; + /** * Map of component hook listeners */ diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index 3b811d19aa..5e27b46af7 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -88,3 +88,4 @@ export interface ComponentDirectiveOptions extends DirectiveBinding { } export type ComponentWatchDependencies = Map; +export type ComponentWatchPropDependencies = Map>; From d5ac9938359b537378709637aab4ba4583d49619 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:45:48 +0300 Subject: [PATCH 0114/2313] refactor: more optimal alghoritm to init prop watchers --- src/core/component/watch/component-api.ts | 64 ++++++----------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index db18657c1b..34e63dbb5b 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -19,7 +19,7 @@ import watch, { } from 'core/object/watch'; -import { getPropertyInfo, bindingRgxp } from 'core/component/reflect'; +import { getPropertyInfo } from 'core/component/reflect'; import { @@ -52,7 +52,7 @@ export function implementComponentWatchAPI( ): void { const { unsafe, - unsafe: {$async: $a, meta: {watchDependencies, computedFields, accessors, params}}, + unsafe: {$async: $a, meta: {computedFields, watchDependencies, watchPropDependencies, params}}, $renderEngine: {proxyGetters} } = component; @@ -266,8 +266,8 @@ export function implementComponentWatchAPI( postfixes: ['Prop'] }; - // If a component engine does not have the own mechanism of watching - // we need to wrap a prop object + // If a component engine does not have the own mechanism of watching, + // we need to wrap a prop object by myself if (!('watch' in props)) { const propsWatcher = watch(propsStore, propWatchOpts); $a.worker(() => propsWatcher.unwatch()); @@ -275,54 +275,22 @@ export function implementComponentWatchAPI( } // We need to attach default watchers for all props that can affect component computed fields - if (Object.size(computedFields) > 0 || Object.size(accessors) > 0) { - for (let keys = Object.keys(propsStore), i = 0; i < keys.length; i++) { + if (watchPropDependencies.size > 0) { + for (let o = watchPropDependencies.entries(), el = o.next(); !el.done; el = o.next()) { const - prop = keys[i], - - // Remove from the prop name "Store" and "Prop" postfixes - normalizedKey = prop.replace(bindingRgxp, ''); - - let - tiedLinks, - needWatch = Boolean(computedFields[normalizedKey] ?? accessors[normalizedKey]); - - // We have some accessor that tied with this prop - if (needWatch) { - tiedLinks = [[normalizedKey]]; - - // We don't have the direct connection between the prop and any accessor, - // but we have a set of dependencies, so we need to check it - } else if (watchDependencies.size > 0) { - tiedLinks = []; - - for (let o = watchDependencies.entries(), el = o.next(); !el.done; el = o.next()) { - const - [key, deps] = el.value; - - for (let j = 0; j < deps.length; j++) { - const - dep = deps[j]; - - if ((Object.isArray(dep) ? dep[0] : dep) === prop) { - needWatch = true; - tiedLinks.push([key]); - break; - } - } - } - } + [computed, props] = el.value, + tiedLinks = [computed]; - // Skip redundant watchers - if (needWatch) { - const - immediateHandler = invalidateComputedCache(), - handler = emitAccessorEvents(); + const + immediateHandler = invalidateComputedCache(), + handler = emitAccessorEvents(); - // Provide the list of connections to handlers - invalidateComputedCache[tiedWatchers] = tiedLinks; - emitAccessorEvents[tiedWatchers] = tiedLinks; + // Provide a list of connections to the handlers + immediateHandler[tiedWatchers] = tiedLinks; + handler[tiedWatchers] = tiedLinks; + for (let o = props.values(), el = o.next(); !el.done; el = o.next()) { + const prop = el.value; unsafe.$watch(prop, {...propWatchOpts, immediate: true}, immediateHandler); unsafe.$watch(prop, propWatchOpts, handler); } From dbffe26577d18cf5dedadbbe58630944d71eeb52 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:46:02 +0300 Subject: [PATCH 0115/2313] :art: --- src/core/component/reflect/const.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/reflect/const.ts b/src/core/component/reflect/const.ts index bef7aa09f3..169c320ba7 100644 --- a/src/core/component/reflect/const.ts +++ b/src/core/component/reflect/const.ts @@ -7,10 +7,10 @@ */ export const - bindingRgxp = /(?:Prop|Store)$/, propRgxp = /Prop$|^\$props/, - attrRgxp = /^\$attrs/, storeRgxp = /Store$/, + attrRgxp = /^\$attrs/, + bindingRgxp = /(?:Prop|Store)$/, hasSeparator = /\./; export const From 427b33825ef973dd97a1748177980265b47300be Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:46:37 +0300 Subject: [PATCH 0116/2313] refactor: removed redundant iterator --- src/core/component/meta/inherit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/meta/inherit.ts b/src/core/component/meta/inherit.ts index ef7aac8493..4f09a74f1f 100644 --- a/src/core/component/meta/inherit.ts +++ b/src/core/component/meta/inherit.ts @@ -54,7 +54,7 @@ export function inheritMeta( } } else { - meta.watchDependencies = new Map(pWatchDependencies.entries()); + meta.watchDependencies = new Map(pWatchDependencies); } // Props/fields inheritance From be23c168a17bcfa8bba2a356b24ddae5ab88cb38 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:57:02 +0300 Subject: [PATCH 0117/2313] refactor: fixed TS errors --- src/core/component/meta/create.ts | 36 ++++++++++++++++------ src/core/component/meta/interface/types.ts | 2 +- src/core/component/meta/tpl.ts | 8 ++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/core/component/meta/create.ts b/src/core/component/meta/create.ts index 26baa0a21e..2f18799afb 100644 --- a/src/core/component/meta/create.ts +++ b/src/core/component/meta/create.ts @@ -6,11 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { inheritMeta } from 'core/component/meta/inherit'; import { getComponentMods } from 'core/component/reflect'; -import { wrapRender } from 'core/component/render-function'; +import { getComponentContext } from 'core/component/context'; +import { inheritMeta } from 'core/component/meta/inherit'; -import type { RenderFunction } from 'core/component/engines'; import type { ComponentMeta, ComponentConstructorInfo } from 'core/component/interface'; /** @@ -18,12 +17,13 @@ import type { ComponentMeta, ComponentConstructorInfo } from 'core/component/int * @param component - component constructor info */ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { - const meta = { + const meta: ComponentMeta = { name: component.name, componentName: component.componentName, parentMeta: component.parentMeta, constructor: component.constructor, + instance: {}, params: component.params, @@ -32,14 +32,15 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { fields: {}, tiedFields: {}, - computedFields: {}, systemFields: {}, - tiedSystemFields: {}, + computedFields: {}, - accessors: {}, methods: {}, + accessors: {}, watchers: {}, + watchDependencies: new Map(), + watchPropDependencies: new Map(), hooks: { beforeRuntime: [], @@ -66,14 +67,29 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { mods: {}, props: {}, methods: {}, - staticRenderFns: [], - render: (() => { + render: (() => { throw new ReferenceError(`A render function for the component "${component.componentName}" is not specified`); }) } }; - meta.component.render = wrapRender(meta); + const + map = new WeakMap(); + + meta.component.render = Object.cast((ctx, cache) => { + const + unsafe = getComponentContext(ctx); + + if (map.has(ctx)) { + return map.get(ctx)(); + } + + const + fn = meta.methods.render!.fn(unsafe, cache); + + map.set(ctx, fn); + return fn(); + }); if (component.parentMeta) { inheritMeta(meta, component.parentMeta); diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index 5e27b46af7..4ca340f9d2 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -56,7 +56,7 @@ export interface ComponentAccessor extends Partial; diff --git a/src/core/component/meta/tpl.ts b/src/core/component/meta/tpl.ts index 28bd3d5574..5005dc3a31 100644 --- a/src/core/component/meta/tpl.ts +++ b/src/core/component/meta/tpl.ts @@ -10,7 +10,7 @@ import * as defTpls from 'core/block.ss'; import { componentTemplates } from 'core/component/const'; -import type { ComponentMeta, ComponentMethod } from 'core/component/interface'; +import type { ComponentMeta } from 'core/component/interface'; /** * Attaches templates to the specified meta object @@ -23,7 +23,7 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v {methods, methods: {render}} = meta; // We have a custom render function - if (render && !render.wrapper) { + if (render != null && !render.wrapper) { return; } @@ -40,10 +40,10 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v const renderObj = componentTemplates[meta.componentName] ?? tpls.index(); componentTemplates[meta.componentName] = renderObj; - methods.render = { + methods.render = { wrapper: true, watchers: {}, hooks: {}, - fn: renderObj.render + fn: renderObj }; } From d79bab1db5922cc3f0d5864301bea9c20b4e44fb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 17:57:48 +0300 Subject: [PATCH 0118/2313] :pen: --- src/core/component/meta/interface/options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts index e48f4bedc5..1dc9e31d8e 100644 --- a/src/core/component/meta/interface/options.ts +++ b/src/core/component/meta/interface/options.ts @@ -173,7 +173,7 @@ export interface ComponentOptions { inheritMods?: boolean; /** - * If false, then all default values of the component input properties are ignored + * If false, then all default values of the component input properties are ignored. * This parameter can be inherited from a parent. * * @default `true` From ab8bcf01ed40e9089032cd103694907657124ade Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 18:01:12 +0300 Subject: [PATCH 0119/2313] refactor: simple refactoring --- src/core/component/hook/index.ts | 2 +- src/core/component/ref/index.ts | 2 +- src/core/component/watch/README.md | 2 +- src/core/component/watch/bind.ts | 2 +- src/core/component/watch/component-api.ts | 4 ++-- src/core/component/watch/create.ts | 2 +- src/core/component/watch/helpers.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/component/hook/index.ts b/src/core/component/hook/index.ts index 4f6a2f8ba5..eb56970272 100644 --- a/src/core/component/hook/index.ts +++ b/src/core/component/hook/index.ts @@ -60,7 +60,7 @@ export function runHook(hook: Hook, component: ComponentInterface, ...args: unkn default: { const emitter = new QueueEmitter(), - filteredHooks = []; + filteredHooks: ComponentHook[] = []; for (let i = 0; i < hooks.length; i++) { const diff --git a/src/core/component/ref/index.ts b/src/core/component/ref/index.ts index 0225c95d63..07451b0a40 100644 --- a/src/core/component/ref/index.ts +++ b/src/core/component/ref/index.ts @@ -42,7 +42,7 @@ export function resolveRefs(component: ComponentInterface): void { if (Object.isArray(ref)) { const - refList = []; + refList: unknown[] = []; let needRewrite = false; diff --git a/src/core/component/watch/README.md b/src/core/component/watch/README.md index 4c0034ef18..716aea7bf9 100644 --- a/src/core/component/watch/README.md +++ b/src/core/component/watch/README.md @@ -1,3 +1,3 @@ # core/component/watch -This module provides API to add a feature of object watching to components. +This module provides API to add a feature of object watching to a component. diff --git a/src/core/component/watch/bind.ts b/src/core/component/watch/bind.ts index dff02086a6..fb083d283e 100644 --- a/src/core/component/watch/bind.ts +++ b/src/core/component/watch/bind.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { getPropertyInfo } from 'core/component/reflection'; +import { getPropertyInfo } from 'core/component/reflect'; import { wrapWithSuspending } from 'core/async'; import { beforeHooks } from 'core/component/const'; diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index 34e63dbb5b..c0b1ca61bd 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -96,7 +96,7 @@ export function implementComponentWatchAPI( [key, deps] = el.value; const - newDeps = []; + newDeps: typeof deps = []; let needForkDeps = false; @@ -143,7 +143,7 @@ export function implementComponentWatchAPI( } const - modifiedMutations = []; + modifiedMutations: any[] = []; for (let i = 0; i < mutations.length; i++) { const diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index a481a2f145..7d7e0dffb3 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -312,7 +312,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface slicedPathChunks = pathChunks.slice(1); const - destructors = []; + destructors: Function[] = []; const watchHandler = (value, oldValue, info) => { for (let i = destructors.length; --i > 0;) { diff --git a/src/core/component/watch/helpers.ts b/src/core/component/watch/helpers.ts index 077709edbb..8d2857d5d0 100644 --- a/src/core/component/watch/helpers.ts +++ b/src/core/component/watch/helpers.ts @@ -42,7 +42,7 @@ export function attachDynamicWatcher( } const - filteredMutations = []; + filteredMutations: unknown[] = []; for (let i = 0; i < mutations.length; i++) { const From f98dd0f51afe6f78b54780e80c36d9cdd497f98c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 18:02:16 +0300 Subject: [PATCH 0120/2313] chore: better doc --- src/core/component/watch/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/watch/interface.ts b/src/core/component/watch/interface.ts index 2f13ea1a69..725e6d077d 100644 --- a/src/core/component/watch/interface.ts +++ b/src/core/component/watch/interface.ts @@ -26,7 +26,7 @@ export interface BindRemoteWatchersParams watchers?: Dictionary; /** - * Information object about a property to watch + * Information object of a property to watch */ info?: PropertyInfo; } From 38c1c7989fdcd5231e37f5153613a67fbf8685b1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 18:03:48 +0300 Subject: [PATCH 0121/2313] refactor: removed `any` --- src/core/component/watch/component-api.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index c0b1ca61bd..337613c249 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -15,7 +15,8 @@ import watch, { watchHandlers, MultipleWatchHandler, - Watcher + Watcher, + WatchHandlerParams } from 'core/object/watch'; @@ -143,7 +144,7 @@ export function implementComponentWatchAPI( } const - modifiedMutations: any[] = []; + modifiedMutations: Array<[unknown, unknown, WatchHandlerParams]> = []; for (let i = 0; i < mutations.length; i++) { const From 8ca42a843c9219d72a4a8cdcb531b444e7a02521 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 19:26:37 +0300 Subject: [PATCH 0122/2313] chore: better doc --- src/core/component/watch/bind.ts | 44 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/core/component/watch/bind.ts b/src/core/component/watch/bind.ts index fb083d283e..998c3cd11f 100644 --- a/src/core/component/watch/bind.ts +++ b/src/core/component/watch/bind.ts @@ -49,7 +49,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR const watchersMap = p.watchers ?? meta.watchers, - // True if the method was invoked with passing custom async instance as a property + // True if the method has been invoked with passing custom async instance as a property customAsync = $a !== unsafe.$async; // Iterate over all registered watchers and listeners and initialize their @@ -60,7 +60,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR const watchers = watchersMap[watchPath]; - if (!watchers) { + if (watchers == null) { continue; } @@ -76,7 +76,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR watcherNeedMounted = false; // Custom watchers looks like ':foo', 'bla:foo', '?bla:foo' - // and uses to listen to some events instead listen of changing of component fields + // and uses to listen to custom events instead property mutations const customWatcher = customWatcherRgxp.exec(watchPath); if (customWatcher) { @@ -85,7 +85,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR watcherNeedMounted = m === '?'; } - const exec = () => { + 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'` @@ -146,20 +146,21 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR let handler: AnyFunction; - // Right now, we need to create a wrapper for our "raw" handler - // because there are some conditions for the watcher: - // 1. It can provide or not provide arguments from an event that it listens; - // 2. The handler can be specified as a function or as a method name from a component. - + // Right now, we need to create a wrapper for our handler because there are some conditions for the watcher: + // + // 1. It can provide or not provide arguments from an event that it listens. + // 2. The handler can be specified as a function or as a component method name. + // // Also, we have two different cases: + // // 1. We listen to a custom event, OR we watch for some component property, - // but we don't need to analyze the old value of the property; - // 2. We watch for some component property, and we need to analyze the old value of the property. - + // but we don't need to analyze the old property value. + // 2. We watch for some component property, and we need to analyze the old property value. + // // These cases are based on one problem: if we watch for some property that isn't primitive, - // like a hash table or a list, and we add a new item to this structure but don't change the original object, + // like a hash table or list, and we add a new item to this structure but don't change the original object, // the new and old values will be equal. - + // // class bButton { // @field() // foo = {baz: 0}; @@ -173,8 +174,8 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR // this.foo.baz++; // } // } - - // To fix this problem, we can check a handler if it requires the second argument by using a length property, + // + // To fix this problem, we can check a handler if it requires the second argument by using a `length` property, // and if the argument is needed, we can clone the old value and keep it within a closure. // The situation when we need to keep the old value (a property watcher with handler length more than one), @@ -231,8 +232,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR } // Apply a watcher wrapper if specified. - // Mind that the wrapper must return a function as a result, - // but it can be packed to a promise. + // Mind, the wrapper must return a function as the result, but it can be packed to a promise. if (watchInfo.wrapper) { handler = watchInfo.wrapper(component.unsafe, handler); } @@ -242,7 +242,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR if (Object.isPromise(handler)) { $a.promise(handler, asyncParams).then((handler) => { if (!Object.isFunction(handler)) { - throw new TypeError('A handler to watch is not a function'); + throw new TypeError('The handler to watch is not a function'); } if (customWatcher) { @@ -344,17 +344,17 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR // Add listener to a component `created` hook if the component isn't created yet if (watcherNeedCreated && isBeforeCreate) { - hooks.created.unshift({fn: exec}); + hooks.created.unshift({fn: attachWatcher}); continue; } // Add listener to a component `mounted/activate`d hook if the component isn't mounted/activates yet // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (watcherNeedMounted && (isBeforeCreate || component.$el == null)) { - hooks[isDeactivated ? 'activated' : 'mounted'].unshift({fn: exec}); + hooks[isDeactivated ? 'activated' : 'mounted'].unshift({fn: attachWatcher}); continue; } - exec(); + attachWatcher(); } } From 2fae8c756213526a1bfd2a963df502abbb6e9d6d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 27 Apr 2022 19:57:20 +0300 Subject: [PATCH 0123/2313] fix: fixed filling of prop dependencies --- src/core/component/meta/fill.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index ef82390e7e..3c3db0b7d8 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -119,8 +119,8 @@ export function fillMeta( for (let o = meta.props, keys = Object.keys(o), i = 0; i < keys.length; i++) { const - key = keys[i], - prop = o[key]; + propName = keys[i], + prop = o[propName]; if (prop == null) { continue; @@ -133,7 +133,7 @@ export function fillMeta( if (defaultProps || prop.forceDefault) { skipDefault = false; - def = instance[key]; + def = instance[propName]; defWrapper = def; if (def != null && typeof def === 'object' && (!isTypeCanBeFunc(prop.type) || !Object.isFunction(def))) { @@ -150,7 +150,7 @@ export function fillMeta( } if (!isRoot || defValue !== undefined) { - component.props[key] = { + component.props[propName] = { type: prop.type, required: prop.required !== false && defaultProps && defValue === undefined, @@ -164,8 +164,8 @@ export function fillMeta( if (!isRoot && !isFunctional) { if (Object.size(prop.watchers) > 0) { - const watcherListeners = watchers[key] ?? []; - watchers[key] = watcherListeners; + const watcherListeners = watchers[propName] ?? []; + watchers[propName] = watcherListeners; for (let w = prop.watchers.values(), el = w.next(); !el.done; el = w.next()) { watcherListeners.push(el.value); @@ -173,10 +173,13 @@ export function fillMeta( } const - normalizedKey = key.replace(bindingRgxp, ''); + normalizedKey = propName.replace(bindingRgxp, ''); if ((computedFields[normalizedKey] ?? accessors[normalizedKey]) != null) { - const props = watchPropDependencies.get(normalizedKey) ?? new Set(); + const + props = watchPropDependencies.get(normalizedKey) ?? new Set(); + + props.add(normalizedKey); watchPropDependencies.set(normalizedKey, props); } else if (watchDependencies.size > 0) { @@ -188,8 +191,11 @@ export function fillMeta( const dep = deps[j]; - if ((Object.isArray(dep) ? dep : dep.split('.'))[0] === prop) { - const props = watchPropDependencies.get(key) ?? new Set(); + if ((Object.isArray(dep) ? dep : dep.split('.'))[0] === propName) { + const + props = watchPropDependencies.get(key) ?? new Set(); + + props.add(propName); watchPropDependencies.set(key, props); break; } From ec80d4ca6f7990a9d1d0fb6af3addc23e0ad976f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 11:32:07 +0300 Subject: [PATCH 0124/2313] feat: added a new option `flush` --- src/core/component/interface/watch.ts | 17 +++- src/core/component/watch/create.ts | 141 ++++++++++++++++---------- 2 files changed, 102 insertions(+), 56 deletions(-) diff --git a/src/core/component/interface/watch.ts b/src/core/component/interface/watch.ts index 661bc1e1e8..b25e9ee841 100644 --- a/src/core/component/interface/watch.ts +++ b/src/core/component/interface/watch.ts @@ -6,13 +6,26 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { WatchPath as RawWatchPath, WatchOptions, WatchHandlerParams } from 'core/object/watch'; +import type { + + WatchPath as RawWatchPath, + WatchOptions as RawWatchOptions, + WatchHandlerParams + +} from 'core/object/watch'; + import type { Group, Label, Join } from 'core/async'; import type { PropertyInfo } from 'core/component/reflect'; import type { ComponentInterface } from 'core/component/interface/component'; -export { WatchOptions, WatchHandlerParams }; +export { WatchHandlerParams }; + +export type Flush = 'post' | 'pre' | 'sync'; + +export interface WatchOptions extends RawWatchOptions { + flush?: Flush; +} export interface FieldWatcher< A = unknown, diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index 7d7e0dffb3..d20faf5b45 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -13,6 +13,7 @@ import { tiedWatchers, watcherInitializer } from 'core/component/watch/const'; import { cloneWatchValue } from 'core/component/watch/clone'; import { attachDynamicWatcher } from 'core/component/watch/helpers'; +import type { ComponentMeta } from 'core/component/meta'; import type { ComponentInterface, WatchOptions, RawWatchHandler } from 'core/component/interface'; /** @@ -27,8 +28,9 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface // eslint-disable-next-line @typescript-eslint/typedef,max-lines-per-function return function watchFn(this: unknown, path, optsOrHandler, rawHandler?) { let - handler: RawWatchHandler, - opts: WatchOptions; + info: PropertyInfo, + opts: WatchOptions, + handler: RawWatchHandler; if (Object.isFunction(optsOrHandler)) { handler = optsOrHandler; @@ -39,8 +41,8 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface opts = optsOrHandler ?? {}; } - let - info: PropertyInfo; + const + originalHandler = handler; if (Object.isString(path)) { info = getPropertyInfo(path, component); @@ -60,10 +62,13 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface } } + const + isDefinedPath = Object.size(info.path) > 0; + let - meta, isRoot = false, - isFunctional = false; + isFunctional = false, + meta: CanUndef = undefined; if (info.type !== 'mounted') { const @@ -85,11 +90,11 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface switch (info.type) { case 'system': - f = meta.systemFields[info.name]; + f = meta?.systemFields[info.name]; break; case 'field': - f = meta.fields[info.name]; + f = meta?.fields[info.name]; break; default: @@ -105,9 +110,9 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface isAccessor = Boolean(info.type === 'accessor' || info.type === 'computed' || info.accessor), isMountedWatcher = info.type === 'mounted'; - const - isDefinedPath = Object.size(info.path) > 0, - watchInfo = isAccessor ? null : component.$renderEngine.proxyGetters[info.type]?.(info.ctx); + const watchInfo = !isAccessor ? + component.$renderEngine.proxyGetters[info.type]?.(info.ctx) : + null; const normalizedOpts: WatchOptions = { collapse: true, @@ -124,8 +129,12 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface return null; } - const - originalHandler = handler; + const {flush} = normalizedOpts; + delete normalizedOpts.flush; + + if (flush === 'sync') { + normalizedOpts.immediate = true; + } let oldVal; @@ -150,25 +159,33 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface } handler = (val, _, i) => { - if (!isDefinedPath && Object.isArray(val) && val.length > 0) { - i = (<[unknown, unknown, PropertyInfo]>val[val.length - 1])[2]; - } + const h = () => { + if (!isDefinedPath && Object.isArray(val) && val.length > 0) { + i = (<[unknown, unknown, PropertyInfo]>val[val.length - 1])[2]; + } - if (isMountedWatcher) { - val = info.ctx; - patchPath(i); + if (isMountedWatcher) { + val = info.ctx; + patchPath(i); - } else if (isAccessor) { - val = Object.get(info.ctx, info.accessor ?? info.name); - } + } else if (isAccessor) { + val = Object.get(info.ctx, info.accessor ?? info.name); + } - const - res = originalHandler.call(this, val, oldVal, i); + const + res = originalHandler.call(this, val, oldVal, i); - oldVal = cloneWatchValue(isDefinedPath ? val : getVal(), normalizedOpts); - Object.set(watchCache, cacheKey, oldVal); + oldVal = cloneWatchValue(isDefinedPath ? val : getVal(), normalizedOpts); + Object.set(watchCache, cacheKey, oldVal); + + return res; + }; + + if (flush === 'post') { + return component.$nextTick().then(h); + } - return res; + return h(); }; handler[tiedWatchers] = originalHandler[tiedWatchers]; @@ -182,46 +199,62 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface } else { if (isMountedWatcher) { handler = (val, ...args) => { - let - oldVal = args[0], - handlerParams = args[1]; + const h = () => { + let + oldVal = args[0], + handlerParams = args[1]; + + if (!isDefinedPath && needCollapse && Object.isArray(val) && val.length > 0) { + handlerParams = (<[unknown, unknown, PropertyInfo]>val[val.length - 1])[2]; + + } else if (args.length === 0) { + return originalHandler.call(this, val.map(([val, oldVal, i]) => { + patchPath(i); + return [val, oldVal, i]; + })); + } - if (!isDefinedPath && needCollapse && Object.isArray(val) && val.length > 0) { - handlerParams = (<[unknown, unknown, PropertyInfo]>val[val.length - 1])[2]; + if (needCollapse) { + val = info.ctx; + oldVal = val; + } - } else if (args.length === 0) { - return originalHandler.call(this, val.map(([val, oldVal, i]) => { - patchPath(i); - return [val, oldVal, i]; - })); - } + patchPath(handlerParams); + return originalHandler.call(this, val, oldVal, handlerParams); + }; - if (needCollapse) { - val = info.ctx; - oldVal = val; + if (flush === 'post') { + return component.$nextTick().then(h); } - patchPath(handlerParams); - return originalHandler.call(this, val, oldVal, handlerParams); + return h(); }; } else if (isAccessor) { handler = (val, _, i) => { - if (needCollapse) { - val = Object.get(info.ctx, info.accessor ?? info.name); + const h = () => { + if (needCollapse) { + val = Object.get(info.ctx, info.accessor ?? info.name); - } else { - val = Object.get(component, info.originalPath); - } + } else { + val = Object.get(component, info.originalPath); + } - if (!isDefinedPath && Object.isArray(i?.path)) { - oldVal = Object.get(oldVal, [i.path[0]]); - } + if (!isDefinedPath && Object.isArray(i?.path)) { + oldVal = Object.get(oldVal, [i.path[0]]); + } - const res = originalHandler.call(this, val, oldVal, i); - oldVal = isDefinedPath ? val : getVal(); + const res = originalHandler.call(this, val, oldVal, i); + oldVal = isDefinedPath ? val : getVal(); - return res; + return res; + }; + + if (flush === 'post') { + return component.$nextTick().then(h); + } + + return h(); }; } From 0474f9bed9ab225736888f5ff1920612ac631a5c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 11:41:00 +0300 Subject: [PATCH 0125/2313] refactor: now the `immediate` watcher is similar to Vue3 --- src/core/component/watch/create.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index d20faf5b45..64463fd66b 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -129,12 +129,11 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface return null; } - const {flush} = normalizedOpts; - delete normalizedOpts.flush; + const + {flush} = normalizedOpts; - if (flush === 'sync') { - normalizedOpts.immediate = true; - } + delete normalizedOpts.flush; + normalizedOpts.immediate = flush === 'sync'; let oldVal; From d673fce6d247649529e6bbb6d109819b680e0921 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 13:21:24 +0300 Subject: [PATCH 0126/2313] fix: fixed prop dependencies --- src/core/component/meta/fill.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 3c3db0b7d8..6652b9be3a 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -179,7 +179,7 @@ export function fillMeta( const props = watchPropDependencies.get(normalizedKey) ?? new Set(); - props.add(normalizedKey); + props.add(propName); watchPropDependencies.set(normalizedKey, props); } else if (watchDependencies.size > 0) { From 3ce43cc295ab7edd76ac5c15d0fc86059e9411de Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 13:27:11 +0300 Subject: [PATCH 0127/2313] fix: fixed a link to props --- src/core/component/engines/vue3/const.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/engines/vue3/const.ts b/src/core/component/engines/vue3/const.ts index 6e7a2b5f28..2cb91a884e 100644 --- a/src/core/component/engines/vue3/const.ts +++ b/src/core/component/engines/vue3/const.ts @@ -14,10 +14,10 @@ export const supports = { export const proxyGetters = Object.createDict({ prop: (ctx) => ({ - key: '_props', + key: '$props', - get value(): typeof ctx._props { - return ctx._props; + get value(): typeof ctx.$props { + return ctx.$props; }, watch: (path, handler) => ctx.$vueWatch(path, (val, oldVal) => { From 841a140f0a7e5275ada794f9796a9819426bc3fb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 16:10:18 +0300 Subject: [PATCH 0128/2313] feat: added a new cache type for accessors `auto` --- src/core/component/accessor/index.ts | 49 ++++++++++++------- src/core/component/decorators/factory.ts | 16 ++++-- .../decorators/interface/accessor.ts | 9 ++-- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/core/component/accessor/index.ts b/src/core/component/accessor/index.ts index 8434f49262..975592da8b 100644 --- a/src/core/component/accessor/index.ts +++ b/src/core/component/accessor/index.ts @@ -30,29 +30,18 @@ export function attachAccessorsFromMeta(component: ComponentInterface): void { ssrMode = component.$renderEngine.supports.ssr, isFunctional = meta.params.functional === true; - for (let o = meta.accessors, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - accessor = o[key]; - - if (accessor == null || component[key] != null || !ssrMode && isFunctional && accessor.functional === false) { - continue; - } - - Object.defineProperty(component, keys[i], { - configurable: true, - enumerable: true, - get: accessor.get, - set: accessor.set - }); - } - for (let o = meta.computedFields, keys = Object.keys(o), i = 0; i < keys.length; i++) { const key = keys[i], computed = o[key]; - if (computed == null || component[key] != null || !ssrMode && isFunctional && computed.functional === false) { + const canSkip = + computed == null || + computed.cache === 'auto' || + component[key] != null || + !ssrMode && isFunctional && computed.functional === false; + + if (canSkip) { continue; } @@ -69,7 +58,7 @@ export function attachAccessorsFromMeta(component: ComponentInterface): void { return get[cacheStatus] = computed.get!.call(this); }; - Object.defineProperty(component, keys[i], { + Object.defineProperty(component, key, { configurable: true, enumerable: true, get: computed.get != null ? get : undefined, @@ -77,6 +66,28 @@ export function attachAccessorsFromMeta(component: ComponentInterface): void { }); } + for (let o = meta.accessors, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + accessor = o[key]; + + const canSkip = + accessor == null || + component[key] != null || + !ssrMode && isFunctional && accessor.functional === false; + + if (canSkip) { + continue; + } + + Object.defineProperty(component, key, { + configurable: true, + enumerable: true, + get: accessor.get, + set: accessor.set + }); + } + if (deprecatedProps != null) { for (let keys = Object.keys(deprecatedProps), i = 0; i < keys.length; i++) { const diff --git a/src/core/component/decorators/factory.ts b/src/core/component/decorators/factory.ts index 4730949f17..a5cc390ad0 100644 --- a/src/core/component/decorators/factory.ts +++ b/src/core/component/decorators/factory.ts @@ -69,6 +69,7 @@ export function paramsFactory( } else if ( p.cache === true || + p.cache === 'auto' || p.cache !== false && (Object.isArray(p.dependencies) || key in meta.computedFields) ) { metaKey = 'computedFields'; @@ -140,15 +141,20 @@ export function paramsFactory( return; } - const hasCache = 'cache' in p; - delete p.cache; + const needOverrideComputed = metaKey === 'accessors' ? + key in meta.computedFields : + !('cache' in p) && key in meta.accessors; - if (metaKey === 'accessors' ? key in meta.computedFields : !hasCache && key in meta.accessors) { - metaCluster[key] = wrapOpts({...meta.computedFields[key], ...p}); + if (needOverrideComputed) { + metaCluster[key] = wrapOpts({...meta.computedFields[key], ...p, cache: false}); delete meta.computedFields[key]; } else { - metaCluster[key] = wrapOpts({...info, ...p}); + metaCluster[key] = wrapOpts({ + ...info, + ...p, + cache: metaKey === 'computedFields' ? p.cache ?? true : false + }); } if (p.dependencies != null) { diff --git a/src/core/component/decorators/interface/accessor.ts b/src/core/component/decorators/interface/accessor.ts index 5f718cd49f..7d101e6194 100644 --- a/src/core/component/decorators/interface/accessor.ts +++ b/src/core/component/decorators/interface/accessor.ts @@ -6,14 +6,17 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { WatchPath } from 'core/component/interface'; +import type { WatchPath, ComponentAccessorCacheType } from 'core/component/interface'; import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { /** - * If true, the accessor value will be cached every time it read or changed + * If true, the accessor value will be cached every time it read or changed. + * The option is set to true by default if also provided `dependencies` or the bound accessor matches by a name with + * another prop or field. If the options value is passed as `auto`, caching will be delegated to the used render + * engine. */ - cache?: boolean; + cache?: ComponentAccessorCacheType; /** * If true, mutations of the accessor value can be watched From 2d95ea404f7826575dd644639e96c98bfb50d233 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 16:10:30 +0300 Subject: [PATCH 0129/2313] :up: --- src/core/component/accessor/CHANGELOG.md | 4 ++++ src/core/component/decorators/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/core/component/accessor/CHANGELOG.md b/src/core/component/accessor/CHANGELOG.md index 2bc941024a..66d7b0b6ee 100644 --- a/src/core/component/accessor/CHANGELOG.md +++ b/src/core/component/accessor/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :rocket: New Feature + +* Added a new cache type for accessors `auto` + #### :house: Internal * Refactoring diff --git a/src/core/component/decorators/CHANGELOG.md b/src/core/component/decorators/CHANGELOG.md index 44656fc6a8..7480398149 100644 --- a/src/core/component/decorators/CHANGELOG.md +++ b/src/core/component/decorators/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :rocket: New Feature + +* Added a new cache type for accessors `auto` + #### :house: Internal * Refactoring From eeca1012ab40ec469f3ba5f8f8e70d786c98dc24 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 16:10:53 +0300 Subject: [PATCH 0130/2313] fix: fixed prop watching --- src/core/component/watch/component-api.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index 337613c249..cdd79d2912 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -280,7 +280,7 @@ export function implementComponentWatchAPI( for (let o = watchPropDependencies.entries(), el = o.next(); !el.done; el = o.next()) { const [computed, props] = el.value, - tiedLinks = [computed]; + tiedLinks = [[computed]]; const immediateHandler = invalidateComputedCache(), @@ -292,7 +292,7 @@ export function implementComponentWatchAPI( for (let o = props.values(), el = o.next(); !el.done; el = o.next()) { const prop = el.value; - unsafe.$watch(prop, {...propWatchOpts, immediate: true}, immediateHandler); + unsafe.$watch(prop, {...propWatchOpts, flush: 'sync'}, immediateHandler); unsafe.$watch(prop, propWatchOpts, handler); } } @@ -347,14 +347,17 @@ export function implementComponentWatchAPI( // The handler to invalidate cache of computed fields function invalidateComputedCache(): RawWatchHandler { // eslint-disable-next-line @typescript-eslint/typedef - return function invalidateComputedCache(val, oldVal, info) { + return function invalidateComputedCache(val, ...args) { + const + oldVal = args[0], + info = args[1]; + if (info == null) { return; } const - {path} = info, - rootKey = String(path[0]); + rootKey = String(info.path[0]); // If there has been changed properties that can affect memoized computed fields, // then we need to invalidate these caches @@ -388,10 +391,7 @@ export function implementComponentWatchAPI( for (let i = 0; i < mutations.length; i++) { const - eventArgs = mutations[i], - info = eventArgs[2]; - - const + [val, oldVal, info] = mutations[i], {path} = info; if (path[path.length - 1] === '__proto__') { @@ -423,7 +423,7 @@ export function implementComponentWatchAPI( continue; } - handler(...eventArgs); + handler(val, oldVal, info); usedHandlers.add(handler); if (timerId == null) { From de04bf13f49813e2a43190b3cb62fa2be215b7c6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 16:13:30 +0300 Subject: [PATCH 0131/2313] feat: added support for cache delegation of computed fields --- src/core/component/meta/create.ts | 1 + src/core/component/meta/fill.ts | 129 ++++++++++++--------- src/core/component/meta/interface/index.ts | 7 +- src/core/component/meta/interface/types.ts | 5 + 4 files changed, 85 insertions(+), 57 deletions(-) diff --git a/src/core/component/meta/create.ts b/src/core/component/meta/create.ts index 2f18799afb..ce3865f41b 100644 --- a/src/core/component/meta/create.ts +++ b/src/core/component/meta/create.ts @@ -66,6 +66,7 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { name: component.name, mods: {}, props: {}, + computed: {}, methods: {}, render: (() => { throw new ReferenceError(`A render function for the component "${component.componentName}" is not specified`); diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 6652b9be3a..5099dd9acf 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -56,62 +56,6 @@ export function fillMeta( isRoot = params.root === true, isFunctional = params.functional === true; - // Methods - - for (let o = methods, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - nm = keys[i], - method = o[nm]; - - if (method == null) { - continue; - } - - component.methods[nm] = function wrapper() { - // eslint-disable-next-line prefer-rest-params - return method.fn.apply(getComponentContext(this), arguments); - }; - - if (method.watchers != null) { - for (let o = method.watchers, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - watcher = >o[key]; - - if (isFunctional && watcher.functional === false) { - continue; - } - - const - watcherListeners = watchers[key] ?? []; - - watchers[key] = watcherListeners; - watcherListeners.push({ - ...watcher, - method: nm, - args: Array.concat([], watcher.args), - handler: Object.cast(method.fn) - }); - } - } - - // Hooks - - if (method.hooks) { - for (let o = method.hooks, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - hook = o[key]; - - if (isFunctional && hook.functional === false) { - continue; - } - - hooks[key].push({...hook, fn: method.fn}); - } - } - } - // Props const @@ -232,6 +176,79 @@ export function fillMeta( } } + // Computed fields + + for (let o = computedFields, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + nm = keys[i], + computed = o[nm]; + + if (computed == null || computed.cache !== 'auto') { + continue; + } + + component.computed[nm] = { + get: computed.get, + set: computed.set + }; + } + + // Methods + + for (let o = methods, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + nm = keys[i], + method = o[nm]; + + if (method == null) { + continue; + } + + component.methods[nm] = function wrapper() { + // eslint-disable-next-line prefer-rest-params + return method.fn.apply(getComponentContext(this), arguments); + }; + + if (method.watchers != null) { + for (let o = method.watchers, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + watcher = >o[key]; + + if (isFunctional && watcher.functional === false) { + continue; + } + + const + watcherListeners = watchers[key] ?? []; + + watchers[key] = watcherListeners; + watcherListeners.push({ + ...watcher, + method: nm, + args: Array.concat([], watcher.args), + handler: Object.cast(method.fn) + }); + } + } + + // Method hooks + + if (method.hooks) { + for (let o = method.hooks, keys = Object.keys(o), i = 0; i < keys.length; i++) { + const + key = keys[i], + hook = o[key]; + + if (isFunctional && hook.functional === false) { + continue; + } + + hooks[key].push({...hook, fn: method.fn}); + } + } + } + // Modifiers const diff --git a/src/core/component/meta/interface/index.ts b/src/core/component/meta/interface/index.ts index 18c0045897..12bce05c61 100644 --- a/src/core/component/meta/interface/index.ts +++ b/src/core/component/meta/interface/index.ts @@ -7,7 +7,7 @@ */ import type { PropOptions } from 'core/component/decorators'; -import type { RenderFunction } from 'core/component/engines'; +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'; @@ -146,6 +146,11 @@ export interface ComponentMeta { */ props: Dictionary; + /** + * Map of component computed fields + */ + computed: Dictionary>>; + /** * Map of component methods */ diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index 4ca340f9d2..ad3937824e 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -48,8 +48,13 @@ export interface ComponentComputedField extends Partial extends Partial> { src: string; + cache: ComponentAccessorCacheType; functional?: boolean; watchable?: boolean; } From 811087c3cf5c3464955c771d21caf07659c04999 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 16:13:43 +0300 Subject: [PATCH 0132/2313] :up: --- src/core/component/meta/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/component/meta/CHANGELOG.md b/src/core/component/meta/CHANGELOG.md index 74542d5b21..2e3d8ae542 100644 --- a/src/core/component/meta/CHANGELOG.md +++ b/src/core/component/meta/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :rocket: New Feature + +* Added support for cache delegation of computed fields + #### :house: Internal * Refactoring From 06680cb8fbaad96aaf07aef015531397d4c59613 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 16:14:32 +0300 Subject: [PATCH 0133/2313] :up: --- src/core/component/watch/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/component/watch/CHANGELOG.md b/src/core/component/watch/CHANGELOG.md index c632a7ae7e..d52dd3e986 100644 --- a/src/core/component/watch/CHANGELOG.md +++ b/src/core/component/watch/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Added a new watch option `flush` + +#### :house: Internal + +* Refactoring + ## v3.15.2 (2021-12-28) #### :bug: Bug Fix From a50128796ae94d86e56aa99ae4ec383760c7738b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 17:04:30 +0300 Subject: [PATCH 0134/2313] refactor: some optimizations --- src/core/component/watch/component-api.ts | 131 ++++++++++++++-------- 1 file changed, 82 insertions(+), 49 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index cdd79d2912..6fa9335ecb 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -81,11 +81,11 @@ export function implementComponentWatchAPI( // find external dependencies and attach watchers that directly update state if (watchDependencies.size > 0) { const - immediateHandler = invalidateComputedCache(), - handler = emitAccessorEvents(); + invalidateComputedCache = createComputedCacheInvalidator(), + broadcastAccessorMutations = createAccessorMutationEmitter(); - handler[tiedWatchers] = []; - immediateHandler[tiedWatchers] = handler[tiedWatchers]; + broadcastAccessorMutations[tiedWatchers] = []; + invalidateComputedCache[tiedWatchers] = broadcastAccessorMutations[tiedWatchers]; const watchOpts = { deep: true, @@ -94,7 +94,11 @@ export function implementComponentWatchAPI( for (let o = watchDependencies.entries(), el = o.next(); !el.done; el = o.next()) { const - [key, deps] = el.value; + [path, deps] = el.value; + + const + rootProp = (Object.isArray(path) ? path : path.split('.'))[0], + canCached = computedFields[rootProp] != null && computedFields[rootProp]!.cache !== 'auto'; const newDeps: typeof deps = []; @@ -103,42 +107,44 @@ export function implementComponentWatchAPI( needForkDeps = false; for (let j = 0; j < deps.length; j++) { + const dep = deps[j]; + newDeps[j] = dep; + const - dep = deps[j], depPath = Object.isString(dep) ? dep : dep.join('.'), watchInfo = getPropertyInfo(depPath, component); - newDeps[j] = dep; - if (watchInfo.ctx === component && !watchDependencies.has(dep)) { needForkDeps = true; newDeps[j] = watchInfo.path; continue; } - const invalidateCache = (value, oldValue, info) => { - info = Object.assign(Object.create(info), { - path: [key], - parent: {value, oldValue, info} - }); + if (canCached) { + const invalidateCache = (value, oldValue, info) => { + info = Object.assign(Object.create(info), { + path: Array.concat([], path), + parent: {value, oldValue, info} + }); - immediateHandler(value, oldValue, info); - }; + invalidateComputedCache(value, oldValue, info); + }; - attachDynamicWatcher( - component, - watchInfo, + attachDynamicWatcher( + component, + watchInfo, - { - ...watchOpts, - immediate: true - }, + { + ...watchOpts, + immediate: true + }, - invalidateCache, - immediateDynamicHandlers - ); + invalidateCache, + immediateDynamicHandlers + ); + } - const broadcastEvents = (mutations, ...args) => { + const broadcastMutations = (mutations, ...args) => { if (args.length > 0) { mutations = [Object.cast([mutations, ...args])]; } @@ -155,7 +161,7 @@ export function implementComponentWatchAPI( oldValue, Object.assign(Object.create(info), { - path: [key], + path: Array.concat([], path), originalPath: watchInfo.type === 'mounted' ? [watchInfo.name, ...info.originalPath] : @@ -166,14 +172,14 @@ export function implementComponentWatchAPI( ]); } - handler(modifiedMutations); + broadcastAccessorMutations(modifiedMutations); }; - attachDynamicWatcher(component, watchInfo, watchOpts, broadcastEvents, dynamicHandlers); + attachDynamicWatcher(component, watchInfo, watchOpts, broadcastMutations, dynamicHandlers); } if (needForkDeps) { - watchDependencies.set(key, newDeps); + watchDependencies.set(path, newDeps); } } } @@ -213,11 +219,21 @@ export function implementComponentWatchAPI( immediate: true }; - const systemFieldsWatcher = watch(systemFieldsInfo.value, immediateSystemWatchOpts, invalidateComputedCache()); + const systemFieldsWatcher = watch( + systemFieldsInfo.value, + immediateSystemWatchOpts, + createComputedCacheInvalidator() + ); + $a.worker(() => systemFieldsWatcher.unwatch()); { - const w = watch(systemFieldsWatcher.proxy, watchOpts, emitAccessorEvents()); + const w = watch( + systemFieldsWatcher.proxy, + watchOpts, + createAccessorMutationEmitter() + ); + $a.worker(() => w.unwatch()); } @@ -279,28 +295,48 @@ export function implementComponentWatchAPI( if (watchPropDependencies.size > 0) { for (let o = watchPropDependencies.entries(), el = o.next(); !el.done; el = o.next()) { const - [computed, props] = el.value, - tiedLinks = [[computed]]; + [path, props] = el.value; const - immediateHandler = invalidateComputedCache(), - handler = emitAccessorEvents(); + invalidateComputedCache = createComputedCacheInvalidator(), + broadcastAccessorMutations = createAccessorMutationEmitter(); + + let + rootProp, + tiedLinks; + + if (Object.isArray(path)) { + rootProp = path; + tiedLinks = [path]; + + } else { + rootProp = path.split('.')[0]; + tiedLinks = [[path]]; + } // Provide a list of connections to the handlers - immediateHandler[tiedWatchers] = tiedLinks; - handler[tiedWatchers] = tiedLinks; + invalidateComputedCache[tiedWatchers] = tiedLinks; + broadcastAccessorMutations[tiedWatchers] = tiedLinks; + + const canCached = + computedFields[rootProp] != null && + computedFields[rootProp]!.cache !== 'auto'; for (let o = props.values(), el = o.next(); !el.done; el = o.next()) { - const prop = el.value; - unsafe.$watch(prop, {...propWatchOpts, flush: 'sync'}, immediateHandler); - unsafe.$watch(prop, propWatchOpts, handler); + const + prop = el.value; + + if (canCached) { + unsafe.$watch(prop, {...propWatchOpts, flush: 'sync'}, invalidateComputedCache); + } + + unsafe.$watch(prop, propWatchOpts, broadcastAccessorMutations); } } } } } - // Initializes the specified watcher on a component instance function initWatcher(name: Nullable, watcher: Watcher) { if (name == null) { return; @@ -326,26 +362,24 @@ export function implementComponentWatchAPI( } } - // Initializes the field watcher on a component instance function initFieldsWatcher() { const immediateFieldWatchOpts = { ...fieldWatchOpts, immediate: true }; - fieldsWatcher = watch(fieldsInfo.value, immediateFieldWatchOpts, invalidateComputedCache()); + fieldsWatcher = watch(fieldsInfo.value, immediateFieldWatchOpts, createComputedCacheInvalidator()); $a.worker(() => fieldsWatcher.unwatch()); { - const w = watch(fieldsWatcher.proxy, fieldWatchOpts, emitAccessorEvents()); + const w = watch(fieldsWatcher.proxy, fieldWatchOpts, createAccessorMutationEmitter()); $a.worker(() => w.unwatch()); } initWatcher(fieldsInfo.key, fieldsWatcher); } - // The handler to invalidate cache of computed fields - function invalidateComputedCache(): RawWatchHandler { + function createComputedCacheInvalidator(): RawWatchHandler { // eslint-disable-next-line @typescript-eslint/typedef return function invalidateComputedCache(val, ...args) { const @@ -380,8 +414,7 @@ export function implementComponentWatchAPI( }; } - // The handler to broadcast events of accessors - function emitAccessorEvents(): MultipleWatchHandler { + function createAccessorMutationEmitter(): MultipleWatchHandler { // eslint-disable-next-line @typescript-eslint/typedef return function emitAccessorEvents(mutations, ...args) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition From 1d44165d833a90e7cfa79529462eeab622c76c7f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 17:11:53 +0300 Subject: [PATCH 0135/2313] refactor: :art: --- src/core/component/watch/component-api.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index 6fa9335ecb..66b07e5728 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -1,3 +1,5 @@ +/* eslint-disable max-lines-per-function */ + /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -343,8 +345,8 @@ export function implementComponentWatchAPI( } mute(watcher.proxy); - watcher.proxy[toComponentObject] = component; + Object.defineProperty(component, name, { enumerable: true, configurable: true, @@ -368,11 +370,21 @@ export function implementComponentWatchAPI( immediate: true }; - fieldsWatcher = watch(fieldsInfo.value, immediateFieldWatchOpts, createComputedCacheInvalidator()); + fieldsWatcher = watch( + fieldsInfo.value, + immediateFieldWatchOpts, + createComputedCacheInvalidator() + ); + $a.worker(() => fieldsWatcher.unwatch()); { - const w = watch(fieldsWatcher.proxy, fieldWatchOpts, createAccessorMutationEmitter()); + const w = watch( + fieldsWatcher.proxy, + fieldWatchOpts, + createAccessorMutationEmitter() + ); + $a.worker(() => w.unwatch()); } From dc05cd3943cb5175285447c06b7ad7ad086e496b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 28 Apr 2022 17:26:00 +0300 Subject: [PATCH 0136/2313] fix: removed invalid optimization --- src/core/component/watch/component-api.ts | 63 +++++++---------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index 66b07e5728..408f8608d3 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -98,10 +98,6 @@ export function implementComponentWatchAPI( const [path, deps] = el.value; - const - rootProp = (Object.isArray(path) ? path : path.split('.'))[0], - canCached = computedFields[rootProp] != null && computedFields[rootProp]!.cache !== 'auto'; - const newDeps: typeof deps = []; @@ -122,29 +118,27 @@ export function implementComponentWatchAPI( continue; } - if (canCached) { - const invalidateCache = (value, oldValue, info) => { - info = Object.assign(Object.create(info), { - path: Array.concat([], path), - parent: {value, oldValue, info} - }); + const invalidateCache = (value, oldValue, info) => { + info = Object.assign(Object.create(info), { + path: Array.concat([], path), + parent: {value, oldValue, info} + }); - invalidateComputedCache(value, oldValue, info); - }; + invalidateComputedCache(value, oldValue, info); + }; - attachDynamicWatcher( - component, - watchInfo, + attachDynamicWatcher( + component, + watchInfo, - { - ...watchOpts, - immediate: true - }, + { + ...watchOpts, + immediate: true + }, - invalidateCache, - immediateDynamicHandlers - ); - } + invalidateCache, + immediateDynamicHandlers + ); const broadcastMutations = (mutations, ...args) => { if (args.length > 0) { @@ -303,35 +297,18 @@ export function implementComponentWatchAPI( invalidateComputedCache = createComputedCacheInvalidator(), broadcastAccessorMutations = createAccessorMutationEmitter(); - let - rootProp, - tiedLinks; - - if (Object.isArray(path)) { - rootProp = path; - tiedLinks = [path]; - - } else { - rootProp = path.split('.')[0]; - tiedLinks = [[path]]; - } + const + tiedLinks = Object.isArray(path) ? [path] : [[path]]; // Provide a list of connections to the handlers invalidateComputedCache[tiedWatchers] = tiedLinks; broadcastAccessorMutations[tiedWatchers] = tiedLinks; - const canCached = - computedFields[rootProp] != null && - computedFields[rootProp]!.cache !== 'auto'; - for (let o = props.values(), el = o.next(); !el.done; el = o.next()) { const prop = el.value; - if (canCached) { - unsafe.$watch(prop, {...propWatchOpts, flush: 'sync'}, invalidateComputedCache); - } - + unsafe.$watch(prop, {...propWatchOpts, flush: 'sync'}, invalidateComputedCache); unsafe.$watch(prop, propWatchOpts, broadcastAccessorMutations); } } From a4074c2fd5c74818a06c777cb6d1ad227e12f6c1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 16:36:01 +0300 Subject: [PATCH 0137/2313] feat: added a new directive `v-async-target` --- .../directives/async/target/CHANGELOG.md | 16 +++++++++++++ .../directives/async/target/README.md | 2 ++ .../directives/async/target/index.ts | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/core/component/directives/async/target/CHANGELOG.md create mode 100644 src/core/component/directives/async/target/README.md create mode 100644 src/core/component/directives/async/target/index.ts diff --git a/src/core/component/directives/async/target/CHANGELOG.md b/src/core/component/directives/async/target/CHANGELOG.md new file mode 100644 index 0000000000..b18e802496 --- /dev/null +++ b/src/core/component/directives/async/target/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/async/target/README.md b/src/core/component/directives/async/target/README.md new file mode 100644 index 0000000000..b9e6cff111 --- /dev/null +++ b/src/core/component/directives/async/target/README.md @@ -0,0 +1,2 @@ +# core/component/directives/async/target + diff --git a/src/core/component/directives/async/target/index.ts b/src/core/component/directives/async/target/index.ts new file mode 100644 index 0000000000..c22431f13b --- /dev/null +++ b/src/core/component/directives/async/target/index.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/async/target/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode, DirectiveBinding } from 'core/component/engines'; +import type { ComponentInterface } from 'core/component/interface'; + +export * from 'core/component/directives/async/async/interface'; + +ComponentEngine.directive('async-target', { + beforeCreate(opts: DirectiveBinding, vnode: VNode): void { + const ctx = Object.cast(opts.instance); + ctx.$emit('[[V-ASYNC-TARGET]]', vnode); + } +}); From 8ae9e98862f2140469bbd0e78da6227eadf70357 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 16:36:31 +0300 Subject: [PATCH 0138/2313] feat: added a boilerplate for new async directives --- .../directives/async/async/CHANGELOG.md | 16 +++++++++++++ .../directives/async/async/README.md | 2 ++ .../component/directives/async/async/index.ts | 24 +++++++++++++++++++ .../directives/async/async/interface.ts | 15 ++++++++++++ src/core/component/directives/async/index.ts | 10 ++++++++ src/core/component/directives/index.ts | 1 + 6 files changed, 68 insertions(+) create mode 100644 src/core/component/directives/async/async/CHANGELOG.md create mode 100644 src/core/component/directives/async/async/README.md create mode 100644 src/core/component/directives/async/async/index.ts create mode 100644 src/core/component/directives/async/async/interface.ts create mode 100644 src/core/component/directives/async/index.ts diff --git a/src/core/component/directives/async/async/CHANGELOG.md b/src/core/component/directives/async/async/CHANGELOG.md new file mode 100644 index 0000000000..b18e802496 --- /dev/null +++ b/src/core/component/directives/async/async/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/async/async/README.md b/src/core/component/directives/async/async/README.md new file mode 100644 index 0000000000..5230072f3e --- /dev/null +++ b/src/core/component/directives/async/async/README.md @@ -0,0 +1,2 @@ +# core/component/directives/async/async + diff --git a/src/core/component/directives/async/async/index.ts b/src/core/component/directives/async/async/index.ts new file mode 100644 index 0000000000..f7a492349d --- /dev/null +++ b/src/core/component/directives/async/async/index.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/async/async/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine } from 'core/component/engines'; + +import type { DirectiveOptions } from 'core/component/directives/async/async/interface'; + +export * from 'core/component/directives/async/async/interface'; + +ComponentEngine.directive('async', { + created(node: Element, opts: DirectiveOptions): void { + + } +}); diff --git a/src/core/component/directives/async/async/interface.ts b/src/core/component/directives/async/async/interface.ts new file mode 100644 index 0000000000..525e416fb6 --- /dev/null +++ b/src/core/component/directives/async/async/interface.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 + */ + +import type { DirectiveBinding } from 'core/component/engines'; + +export interface DirectiveOptions extends DirectiveBinding> {} + +export interface DirectiveValue { + +} diff --git a/src/core/component/directives/async/index.ts b/src/core/component/directives/async/index.ts new file mode 100644 index 0000000000..0d87420af3 --- /dev/null +++ b/src/core/component/directives/async/index.ts @@ -0,0 +1,10 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import 'core/component/directives/async/target'; +import 'core/component/directives/async/async'; diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 4400add122..89abd05f83 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -25,3 +25,4 @@ import 'core/component/directives/update-on'; import 'core/component/directives/tag'; import 'core/component/directives/hook'; import 'core/component/directives/attrs'; +import 'core/component/directives/async'; From 9e26a6373511a8e4444a4064da91a1ce7f2207a9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 16:37:17 +0300 Subject: [PATCH 0139/2313] feat: added a new wrapper for `v-for`; fixed some bugs --- src/core/component/engines/vue3/render.ts | 14 +++++++++----- src/core/component/render/wrappers.ts | 23 ++++++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 130f5df3b8..50e553c916 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -17,6 +17,7 @@ import { createBlock as superCreateBlock, createElementBlock as superCreateElementBlock, + renderList as superRenderList, withDirectives as superWithDirectives } from 'vue'; @@ -24,12 +25,16 @@ import { import { interpolateStaticAttrs, - wrapResolveComponent, - wrapWithDirectives, wrapCreateVNode, - wrapCreateElementVNode, wrapCreateBlock, wrapCreateElementBlock + wrapCreateElementVNode, + + wrapCreateBlock, + wrapCreateElementBlock, + + wrapRenderList, + wrapWithDirectives } from 'core/component/render'; @@ -42,9 +47,7 @@ export { toHandlers, toDisplayString, - renderList, renderSlot, - openBlock, createStaticVNode, @@ -86,4 +89,5 @@ export const createElementBlock = wrapCreateElementBlock(superCreateElementBlock); export const + renderList = wrapRenderList(superRenderList), withDirectives = wrapWithDirectives(superWithDirectives); diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index 99cfebcf40..ab89cfedca 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -17,6 +17,7 @@ import type { createBlock, createElementBlock, + renderList, withDirectives, VNode, @@ -71,6 +72,17 @@ export function wrapResolveComponent(original: T): T { + return Object.cast(function renderList( + this: ComponentInterface, + src: Iterable | Dictionary, + cb: AnyFunction + ) { + this.$emit('[[V-FOR-CB]]', cb); + return original(src, cb); + }); +} + export function wrapWithDirectives(original: T): T { return Object.cast(function withDirectives(this: ComponentInterface, vnode: VNode, dirs: DirectiveArguments) { const @@ -78,24 +90,25 @@ export function wrapWithDirectives(original: T) for (let i = 0; i < dirs.length; i++) { const - decl = dirs[i]; + decl = dirs[i], + [dir, value, arg, modifiers] = decl; const - [dir, value, arg, modifiers] = dirs[i]; + cantIgnoreDir = value != null || decl.length !== 2; if (Object.isDictionary(dir)) { if (Object.isFunction(dir.beforeCreate)) { dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); - if (Object.keys(dir).length > 1 && value != null) { + if (Object.keys(dir).length > 1 && cantIgnoreDir) { resolvedDirs.push(decl); } - } else if (Object.keys(dir).length > 0 && value != null) { + } else if (Object.keys(dir).length > 0 && cantIgnoreDir) { resolvedDirs.push(decl); } - } else if (value != null) { + } else if (cantIgnoreDir) { resolvedDirs.push(decl); } } From a428d582a08059d9055fddafe6b606efdf1bfeb8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 16:37:44 +0300 Subject: [PATCH 0140/2313] refactor: fixed imports --- src/core/component/index.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/core/component/index.ts b/src/core/component/index.ts index f63ba40e76..96e29547ae 100644 --- a/src/core/component/index.ts +++ b/src/core/component/index.ts @@ -6,12 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import 'core/component/filters'; import 'core/component/directives'; export * from 'core/component/const'; export * from 'core/component/functional'; -export * from 'core/component/flyweight'; export * from 'core/component/hook'; export * from 'core/component/field'; @@ -19,7 +17,7 @@ export * from 'core/component/ref'; export * from 'core/component/watch'; export * from 'core/component/register'; -export * from 'core/component/reflection'; +export * from 'core/component/reflect'; export * from 'core/component/method'; export * from 'core/component/event'; @@ -37,16 +35,9 @@ export { export { cloneVNode, - renderVNode, - patchVNode, ComponentEngine as default, - - VNode, - VNodeDirective, - - CreateElement, - ScopedSlot + VNode } from 'core/component/engines'; From c7dd950985054feff74218de925a8d9728b4b6b5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 17:15:54 +0300 Subject: [PATCH 0141/2313] feat: added a feature of dynamic rendering --- src/core/component/engines/vue3/lib.ts | 10 +++--- src/core/component/engines/vue3/render.ts | 42 ++++++++++++++++++++++- src/core/component/interface/engine.ts | 9 +++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index edf7dd6133..1a3b04682d 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -13,13 +13,15 @@ import makeLazy from 'core/lazy'; import { createApp, Component } from 'vue'; import type { CreateAppFunction } from 'core/component/engines/vue3/interface'; -const App = function App(component: Component & {el: Element}, rootProps: Nullable) { +const App = function App(component: Component & {el?: Element}, rootProps: Nullable) { const app = Object.create(createApp(component, rootProps)); - setImmediate(() => { - app.mount(component.el); - }); + if (component.el != null) { + setImmediate(() => { + app.mount(component.el); + }); + } return app; }; diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 50e553c916..b6a5d7b97b 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -18,10 +18,14 @@ import { createElementBlock as superCreateElementBlock, renderList as superRenderList, - withDirectives as superWithDirectives + withDirectives as superWithDirectives, + + VNode } from 'vue'; +import Vue from 'core/component/engines/vue3/lib'; + import { interpolateStaticAttrs, @@ -38,6 +42,8 @@ import { } from 'core/component/render'; +import type { ComponentInterface } from 'core/component/interface'; + export { Fragment, @@ -53,6 +59,7 @@ export { createStaticVNode, createTextVNode, createCommentVNode, + cloneVNode, normalizeClass, normalizeStyle, @@ -91,3 +98,36 @@ export const export const renderList = wrapRenderList(superRenderList), withDirectives = wrapWithDirectives(superWithDirectives); + +/** + * Renders the specified VNode and returns the result + * + * @param vnode + * @param [parent] - parent component + */ +export function render(vnode: VNode, parent?: ComponentInterface): Node; + +/** + * Renders the specified list of VNode-s and returns the result + * + * @param vnodes + * @param [parent] - parent component + */ +export function render(vnodes: VNode[], parent?: ComponentInterface): Node[]; +export function render(vnode: CanArray, parent?: ComponentInterface): CanArray { + const vue = new Vue({ + render: () => vnode + }); + + if (parent != null) { + Object.set(vue, '$root', Object.create(parent.$root)); + Object.set(vue, '$root.$remoteParent', parent); + Object.set(vue, '$root.unsafe', vue['$root']); + } + + const + el = document.createElement('div'), + root = vue.mount(el); + + return Object.isArray(vnode) ? Array.from(el.childNodes) : root.$el; +} diff --git a/src/core/component/interface/engine.ts b/src/core/component/interface/engine.ts index 916a44888e..465c47939c 100644 --- a/src/core/component/interface/engine.ts +++ b/src/core/component/interface/engine.ts @@ -8,7 +8,9 @@ import type { + VNode, Fragment, + Transition, TransitionGroup, @@ -27,6 +29,7 @@ import type { createElementVNode, createTextVNode, createCommentVNode, + cloneVNode, normalizeClass, normalizeStyle, @@ -51,6 +54,8 @@ import type { } from 'vue'; +import type { ComponentInterface } from 'core/component/interface'; + export interface RenderEngineFeatures { regular: boolean; functional: boolean; @@ -64,6 +69,9 @@ export interface RenderEngine { } export interface RenderAPI { + render(vnode: VNode, parent?: ComponentInterface): Node; + render(vnode: VNode[], parent?: ComponentInterface): Node[]; + Fragment: typeof Fragment; Transition: typeof Transition; TransitionGroup: typeof TransitionGroup; @@ -83,6 +91,7 @@ export interface RenderAPI { createElementVNode: typeof createElementVNode; createTextVNode: typeof createTextVNode; createCommentVNode: typeof createCommentVNode; + cloneVNode: typeof cloneVNode; normalizeClass: typeof normalizeClass; normalizeStyle: typeof normalizeStyle; From 722b6709a2cc359d7e3803830e1d013e0f251134 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 17:17:34 +0300 Subject: [PATCH 0142/2313] refactor: removed getSlot --- src/super/i-block/modules/vdom/index.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/super/i-block/modules/vdom/index.ts b/src/super/i-block/modules/vdom/index.ts index 9c6adad348..0c3ff239ad 100644 --- a/src/super/i-block/modules/vdom/index.ts +++ b/src/super/i-block/modules/vdom/index.ts @@ -264,17 +264,4 @@ export default class VDOM extends Friend { return search(vnode); } - - /** - * Returns a slot by the specified name - * - * @param name - * @param [ctx] - component context to get the slot - */ - getSlot( - name: string, - ctx: iBlock = this.component - ): CanUndef { - return Object.get(ctx, `$slots.${name}`) ?? Object.get(ctx, `$scopedSlots.${name}`); - } } From 73061252c5f74ff650167ccddb156137f766216e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 18 May 2022 17:44:45 +0300 Subject: [PATCH 0143/2313] fix: fixed render function --- src/super/i-block/modules/vdom/index.ts | 47 ++++++++++++++----------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/super/i-block/modules/vdom/index.ts b/src/super/i-block/modules/vdom/index.ts index 0c3ff239ad..c4236bb034 100644 --- a/src/super/i-block/modules/vdom/index.ts +++ b/src/super/i-block/modules/vdom/index.ts @@ -11,15 +11,7 @@ * @packageDocumentation */ -import { - - execRenderObject, - - RenderObject, - VNode, - ScopedSlot - -} from 'core/component'; +import type { VNode } from 'core/component'; import type iBlock from 'super/i-block/i-block'; import Friend from 'super/i-block/modules/friend'; @@ -38,22 +30,37 @@ export * from 'super/i-block/modules/vdom/interface'; */ export default class VDOM extends Friend { /** - * Renders the specified data - * @param data + * Renders the specified VNode and returns the result + * @param vnode * * @example * ```js - * this.render(this.$createElement('b-example', { - * attrs: { - * prop1: 'foo' - * } - * })); + * const div = this.render(Vue.h('div', {class: 'foo'})); + * + * console.log(div.tagName); // DIV + * console.log(div.classList.contains('foo')); // true + * ``` + */ + render(vnode: VNode): Node; + + /** + * Renders the specified list of VNode-s and returns the result + * @param vnodes + * + * @example + * ```js + * const divs = this.render([ + * Vue.h('div', {class: 'foo'}), + * Vue.h('div', {class: 'bar'}), + * ]); + * + * console.log(div[0].tagName); // DIV + * console.log(div[1].classList.contains('bar')); // true * ``` */ - render(data: VNode): Node; - render(data: VNode[]): Node[]; - render(data: CanArray): CanArray { - return this.ctx.$renderEngine.renderVNode(Object.cast(data), this.ctx); + render(vnodes: VNode[]): Node[]; + render(vnode: CanArray): CanArray { + return this.ctx.$renderEngine.r.render(Object.cast(vnode), this.ctx); } /** From fbb07828a18386a4863debb9677638dac141797c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 12:07:17 +0300 Subject: [PATCH 0144/2313] fix: removed invalid context binding --- src/core/component/engines/directive.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/core/component/engines/directive.ts b/src/core/component/engines/directive.ts index 17a3d9a44e..1ef488f114 100644 --- a/src/core/component/engines/directive.ts +++ b/src/core/component/engines/directive.ts @@ -8,9 +8,8 @@ import { ComponentEngine, Directive, DirectiveBinding, VNode } from 'core/component/engines/engine'; -let originalDirective = ComponentEngine.directive.length > 0 ? - ComponentEngine.directive.bind(ComponentEngine) : - null; +// eslint-disable-next-line @typescript-eslint/unbound-method +const staticDirective = ComponentEngine.directive.length > 0 ? ComponentEngine.directive : null; /** * A wrapped version of the `ComponentEngine.directive` function with providing of hooks for non-regular components @@ -19,17 +18,20 @@ let originalDirective = ComponentEngine.directive.length > 0 ? * @param [directive] */ ComponentEngine.directive = function directive(name: string, directive?: Directive) { + const + ctx = Object.getPrototypeOf(this), + originalDirective = staticDirective ?? ctx.directive; + if (originalDirective == null) { - const ctx = Object.getPrototypeOf(this); - originalDirective = ctx.directive.bind(ctx); + throw new Error("A function to register directives isn't found"); } if (directive == null) { - return originalDirective(name); + return originalDirective.call(ctx, name); } if (Object.isFunction(directive)) { - return originalDirective(name, directive); + return originalDirective.call(ctx, name, directive); } if (directive.beforeCreate != null) { @@ -42,10 +44,10 @@ ComponentEngine.directive = function directive(name: string, directive?: Directi originalUnmounted = directive.unmounted; if (originalUnmounted == null) { - return originalDirective(name, directive); + return originalDirective.call(ctx, name, directive); } - return originalDirective(name, { + return originalDirective.call(ctx, name, { ...directive, created(_el: Element, _opts: DirectiveBinding, vnode: VNode) { From dc009c26105c8d20dee75c4b475010e3f7f1d6ff Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 12:14:52 +0300 Subject: [PATCH 0145/2313] refactor: renamed `render` to `render-daemon` --- src/core/{render => render-daemon}/CHANGELOG.md | 0 src/core/{render => render-daemon}/README.md | 0 src/core/{render => render-daemon}/const.ts | 2 +- src/core/{render => render-daemon}/index.ts | 6 +++--- src/core/{render => render-daemon}/interface.ts | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/core/{render => render-daemon}/CHANGELOG.md (100%) rename src/core/{render => render-daemon}/README.md (100%) rename src/core/{render => render-daemon}/const.ts (90%) rename src/core/{render => render-daemon}/index.ts (95%) rename src/core/{render => render-daemon}/interface.ts (100%) diff --git a/src/core/render/CHANGELOG.md b/src/core/render-daemon/CHANGELOG.md similarity index 100% rename from src/core/render/CHANGELOG.md rename to src/core/render-daemon/CHANGELOG.md diff --git a/src/core/render/README.md b/src/core/render-daemon/README.md similarity index 100% rename from src/core/render/README.md rename to src/core/render-daemon/README.md diff --git a/src/core/render/const.ts b/src/core/render-daemon/const.ts similarity index 90% rename from src/core/render/const.ts rename to src/core/render-daemon/const.ts index 28441d6e1e..8d4621094a 100644 --- a/src/core/render/const.ts +++ b/src/core/render-daemon/const.ts @@ -7,7 +7,7 @@ */ import Async from 'core/async'; -import type { Task } from 'core/render/interface'; +import type { Task } from 'core/render-daemon/interface'; /** * The maximum number of tasks per one render iteration diff --git a/src/core/render/index.ts b/src/core/render-daemon/index.ts similarity index 95% rename from src/core/render/index.ts rename to src/core/render-daemon/index.ts index bd2abaad0a..eb6b91096c 100644 --- a/src/core/render/index.ts +++ b/src/core/render-daemon/index.ts @@ -20,10 +20,10 @@ import { TASKS_PER_TICK, DELAY -} from 'core/render/const'; +} from 'core/render-daemon/const'; -export * from 'core/render/const'; -export * from 'core/render/interface'; +export * from 'core/render-daemon/const'; +export * from 'core/render-daemon/interface'; let inProgress = false, diff --git a/src/core/render/interface.ts b/src/core/render-daemon/interface.ts similarity index 100% rename from src/core/render/interface.ts rename to src/core/render-daemon/interface.ts From f116f492154758fe93b0e05af32e44698cebede6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 12:15:54 +0300 Subject: [PATCH 0146/2313] refactor: moved `render-daemon` to `component` --- src/core/{ => component}/render-daemon/CHANGELOG.md | 0 src/core/{ => component}/render-daemon/README.md | 0 src/core/{ => component}/render-daemon/const.ts | 2 +- src/core/{ => component}/render-daemon/index.ts | 6 +++--- src/core/{ => component}/render-daemon/interface.ts | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/core/{ => component}/render-daemon/CHANGELOG.md (100%) rename src/core/{ => component}/render-daemon/README.md (100%) rename src/core/{ => component}/render-daemon/const.ts (89%) rename src/core/{ => component}/render-daemon/index.ts (93%) rename src/core/{ => component}/render-daemon/interface.ts (100%) diff --git a/src/core/render-daemon/CHANGELOG.md b/src/core/component/render-daemon/CHANGELOG.md similarity index 100% rename from src/core/render-daemon/CHANGELOG.md rename to src/core/component/render-daemon/CHANGELOG.md diff --git a/src/core/render-daemon/README.md b/src/core/component/render-daemon/README.md similarity index 100% rename from src/core/render-daemon/README.md rename to src/core/component/render-daemon/README.md diff --git a/src/core/render-daemon/const.ts b/src/core/component/render-daemon/const.ts similarity index 89% rename from src/core/render-daemon/const.ts rename to src/core/component/render-daemon/const.ts index 8d4621094a..13f2362ebb 100644 --- a/src/core/render-daemon/const.ts +++ b/src/core/component/render-daemon/const.ts @@ -7,7 +7,7 @@ */ import Async from 'core/async'; -import type { Task } from 'core/render-daemon/interface'; +import type { Task } from 'core/component/render-daemon/interface'; /** * The maximum number of tasks per one render iteration diff --git a/src/core/render-daemon/index.ts b/src/core/component/render-daemon/index.ts similarity index 93% rename from src/core/render-daemon/index.ts rename to src/core/component/render-daemon/index.ts index eb6b91096c..c31b449810 100644 --- a/src/core/render-daemon/index.ts +++ b/src/core/component/render-daemon/index.ts @@ -20,10 +20,10 @@ import { TASKS_PER_TICK, DELAY -} from 'core/render-daemon/const'; +} from 'core/component/render-daemon/const'; -export * from 'core/render-daemon/const'; -export * from 'core/render-daemon/interface'; +export * from 'core/component/render-daemon/const'; +export * from 'core/component/render-daemon/interface'; let inProgress = false, diff --git a/src/core/render-daemon/interface.ts b/src/core/component/render-daemon/interface.ts similarity index 100% rename from src/core/render-daemon/interface.ts rename to src/core/component/render-daemon/interface.ts From 77c3a424c34db18014e21d421303e634d4bf2c97 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 12:25:40 +0300 Subject: [PATCH 0147/2313] :art: --- src/core/component/render-daemon/CHANGELOG.md | 6 ++++++ src/core/component/render-daemon/README.md | 4 ++-- src/core/component/render-daemon/index.ts | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/component/render-daemon/CHANGELOG.md b/src/core/component/render-daemon/CHANGELOG.md index bfa0268bc4..55ee7e45df 100644 --- a/src/core/component/render-daemon/CHANGELOG.md +++ b/src/core/component/render-daemon/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Renamed the module to `component/render-daemon` + ## v3.0.0-rc.131 (2021-01-29) #### :house: Internal diff --git a/src/core/component/render-daemon/README.md b/src/core/component/render-daemon/README.md index 86b49ac1c5..40dd19518d 100644 --- a/src/core/component/render-daemon/README.md +++ b/src/core/component/render-daemon/README.md @@ -1,3 +1,3 @@ -# core/render +# core/component/render-daemon -This module provides API to render async components. +This module provides API to register and manage tasks of async rendering. diff --git a/src/core/component/render-daemon/index.ts b/src/core/component/render-daemon/index.ts index c31b449810..bbad4475c1 100644 --- a/src/core/component/render-daemon/index.ts +++ b/src/core/component/render-daemon/index.ts @@ -7,13 +7,14 @@ */ /** - * [[include:core/render/README.md]] + * [[include:core/component/render-daemon/README.md]] * @packageDocumentation */ import { daemon, + queue, add as addToQueue, From fab72ae2f42767d9d7721d2971a2dcf61f9e14bb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 12:30:18 +0300 Subject: [PATCH 0148/2313] refactor: moved the module to `core/render/daemon` --- .../{render-daemon => render/daemon}/CHANGELOG.md | 2 +- .../component/{render-daemon => render/daemon}/README.md | 2 +- .../component/{render-daemon => render/daemon}/const.ts | 2 +- .../component/{render-daemon => render/daemon}/index.ts | 8 ++++---- .../{render-daemon => render/daemon}/interface.ts | 0 5 files changed, 7 insertions(+), 7 deletions(-) rename src/core/component/{render-daemon => render/daemon}/CHANGELOG.md (91%) rename src/core/component/{render-daemon => render/daemon}/README.md (70%) rename src/core/component/{render-daemon => render/daemon}/const.ts (90%) rename src/core/component/{render-daemon => render/daemon}/index.ts (91%) rename src/core/component/{render-daemon => render/daemon}/interface.ts (100%) diff --git a/src/core/component/render-daemon/CHANGELOG.md b/src/core/component/render/daemon/CHANGELOG.md similarity index 91% rename from src/core/component/render-daemon/CHANGELOG.md rename to src/core/component/render/daemon/CHANGELOG.md index 55ee7e45df..7077a6f51c 100644 --- a/src/core/component/render-daemon/CHANGELOG.md +++ b/src/core/component/render/daemon/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :boom: Breaking Change -* Renamed the module to `component/render-daemon` +* Renamed the module to `component/render/daemon` ## v3.0.0-rc.131 (2021-01-29) diff --git a/src/core/component/render-daemon/README.md b/src/core/component/render/daemon/README.md similarity index 70% rename from src/core/component/render-daemon/README.md rename to src/core/component/render/daemon/README.md index 40dd19518d..bfd5dd2f09 100644 --- a/src/core/component/render-daemon/README.md +++ b/src/core/component/render/daemon/README.md @@ -1,3 +1,3 @@ -# core/component/render-daemon +# core/component/render/daemon This module provides API to register and manage tasks of async rendering. diff --git a/src/core/component/render-daemon/const.ts b/src/core/component/render/daemon/const.ts similarity index 90% rename from src/core/component/render-daemon/const.ts rename to src/core/component/render/daemon/const.ts index 13f2362ebb..3c8bee5fdc 100644 --- a/src/core/component/render-daemon/const.ts +++ b/src/core/component/render/daemon/const.ts @@ -7,7 +7,7 @@ */ import Async from 'core/async'; -import type { Task } from 'core/component/render-daemon/interface'; +import type { Task } from 'core/component/render/daemon/interface'; /** * The maximum number of tasks per one render iteration diff --git a/src/core/component/render-daemon/index.ts b/src/core/component/render/daemon/index.ts similarity index 91% rename from src/core/component/render-daemon/index.ts rename to src/core/component/render/daemon/index.ts index bbad4475c1..0661ef6a17 100644 --- a/src/core/component/render-daemon/index.ts +++ b/src/core/component/render/daemon/index.ts @@ -7,7 +7,7 @@ */ /** - * [[include:core/component/render-daemon/README.md]] + * [[include:core/component/render/daemon/README.md]] * @packageDocumentation */ @@ -21,10 +21,10 @@ import { TASKS_PER_TICK, DELAY -} from 'core/component/render-daemon/const'; +} from 'core/component/render/daemon/const'; -export * from 'core/component/render-daemon/const'; -export * from 'core/component/render-daemon/interface'; +export * from 'core/component/render/daemon/const'; +export * from 'core/component/render/daemon/interface'; let inProgress = false, diff --git a/src/core/component/render-daemon/interface.ts b/src/core/component/render/daemon/interface.ts similarity index 100% rename from src/core/component/render-daemon/interface.ts rename to src/core/component/render/daemon/interface.ts From f4af8392887935d003ade4abab953fe3fbd520d6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 12:51:41 +0300 Subject: [PATCH 0149/2313] refactor: split the module into separated files --- .../i-block/modules/async-render/index.ts | 593 +----------------- .../modules/async-render/modules/base.ts | 184 ++++++ .../modules/async-render/modules/iter.ts | 475 ++++++++++++++ 3 files changed, 662 insertions(+), 590 deletions(-) create mode 100644 src/super/i-block/modules/async-render/modules/base.ts create mode 100644 src/super/i-block/modules/async-render/modules/iter.ts diff --git a/src/super/i-block/modules/async-render/index.ts b/src/super/i-block/modules/async-render/index.ts index bd7a71a363..4f83b992d9 100644 --- a/src/super/i-block/modules/async-render/index.ts +++ b/src/super/i-block/modules/async-render/index.ts @@ -11,599 +11,12 @@ * @packageDocumentation */ -import Range from 'core/range'; -import SyncPromise from 'core/promise/sync'; - -//#if runtime has component/async-render -import { queue, restart, deferRestart } from 'core/render'; -//#endif - -import type iBlock from 'super/i-block/i-block'; -import type { ComponentElement } from 'super/i-block/i-block'; - -import Friend from 'super/i-block/modules/friend'; -import type { TaskParams, TaskDesc } from 'super/i-block/modules/async-render/interface'; +import Super from 'super/i-block/modules/async-render/modules/iter'; +export * from 'super/i-block/modules/async-render/modules/iter'; export * from 'super/i-block/modules/async-render/interface'; /** * Class provides API to render chunks of a component template asynchronously */ -export default class AsyncRender extends Friend { - //#if runtime has component/async-render - - constructor(component: iBlock) { - super(component); - - this.meta.hooks.beforeUpdate.push({ - fn: () => this.async.clearAll({ - group: 'asyncComponents' - }) - }); - } - - /** - * Restarts the `asyncRender` daemon to force rendering - */ - forceRender(): void { - restart(); - this.localEmitter.emit('forceRender'); - } - - /** - * Restarts the `asyncRender` daemon to force rendering - * (runs on the next tick) - */ - deferForceRender(): void { - deferRestart(); - this.localEmitter.emit('forceRender'); - } - - /** - * Returns a function that returns a promise that will be resolved after firing the `forceRender` event. - * The method can take an element name as the first parameter. This element will be dropped before resolving. - * - * Notice, the initial rendering of a component is mean the same as `forceRender`. - * The method is useful to re-render a non-regular component (functional or flyweight) - * without touching the parent state. - * - * @param elementToDrop - element to drop before resolving the promise - * (if it passed as a function, it would be executed) - * - * @example - * ``` - * < button @click = asyncRender.forceRender() - * Re-render the component - * - * < .&__wrapper - * < template v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')}) - * < .&__content - * {{ Math.random() }} - * ``` - */ - waitForceRender( - elementToDrop?: string | ((ctx: this['component']) => CanPromise>) - ): () => CanPromise { - return () => { - const - canImmediateRender = this.lfc.isBeforeCreate() || this.hook === 'beforeMount'; - - if (canImmediateRender) { - return true; - } - - return this.localEmitter.promisifyOnce('forceRender').then(async () => { - if (elementToDrop != null) { - let - el; - - if (Object.isFunction(elementToDrop)) { - el = await elementToDrop(this.ctx); - - } else { - el = elementToDrop; - } - - if (Object.isString(el)) { - this.block?.element(el)?.remove(); - - } else { - el?.remove(); - } - } - - return true; - }); - }; - } - - /** - * Creates an asynchronous render stream from the specified value. - * This method helps to optimize the rendering of a component by splitting big render tasks into little. - * - * @param value - * @param [sliceOrOpts] - elements per chunk, `[start position, elements per chunk]` or additional options - * @param [opts] - additional options - * - * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & TaskDesc)` - * @emits `localEmitter.asyncRenderComplete(e: TaskParams & TaskDesc)` - * - * @example - * ``` - * /// Asynchronous rendering of components, only five elements per chunk - * < template v-for = el in asyncRender.iterate(largeList, 5) - * < my-component :data = el - * ``` - */ - iterate( - value: unknown, - sliceOrOpts: number | [number?, number?] | TaskParams = 1, - opts: TaskParams = {} - ): unknown[] { - if (value == null) { - return []; - } - - if (Object.isPlainObject(sliceOrOpts)) { - opts = sliceOrOpts; - sliceOrOpts = []; - } - - const - {filter} = opts; - - let - iterable = this.getIterable(value, filter != null); - - let - startPos, - perChunk; - - if (Object.isArray(sliceOrOpts)) { - startPos = sliceOrOpts[0]; - perChunk = sliceOrOpts[1]; - - } else { - perChunk = sliceOrOpts; - } - - startPos ??= 0; - perChunk ??= 1; - - const - firstRender = [], - untreatedEls = [], - srcIsPromise = Object.isPromise(iterable); - - let - iterator: Iterator, - lastSyncEl: IteratorResult; - - let - syncI = 0, - syncTotal = 0; - - if (!srcIsPromise) { - iterator = iterable[Symbol.iterator](); - - // eslint-disable-next-line no-multi-assign - for (let o = iterator, el = lastSyncEl = o.next(); !el.done; el = o.next(), syncI++) { - if (startPos > 0) { - startPos--; - continue; - } - - const - val = el.value; - - let - valIsPromise = Object.isPromise(val), - canRender = !valIsPromise; - - if (canRender && filter != null) { - canRender = filter.call(this.component, val, syncI, { - iterable, - i: syncI, - total: syncTotal - }); - - if (Object.isPromise(canRender)) { - valIsPromise = true; - canRender = false; - - } else if (!Object.isTruly(canRender)) { - canRender = false; - } - } - - if (canRender) { - syncTotal++; - firstRender.push(val); - - } else if (valIsPromise) { - untreatedEls.push(val); - } - - if (syncTotal >= perChunk || valIsPromise) { - break; - } - } - } - - const - BREAK = {}; - - firstRender[this.asyncLabel] = async (cb) => { - const { - async: $a, - localEmitter - } = this; - - const - weight = opts.weight ?? 1, - newIterator = createIterator(); - - let - i = 0, - total = syncTotal, - chunkTotal = 0, - chunkI = 0, - awaiting = 0; - - let - group = 'asyncComponents', - renderBuffer = []; - - let - lastTask, - lastEvent; - - for (let o = newIterator, el = o.next(); !el.done; el = o.next()) { - if (opts.group != null) { - group = `asyncComponents:${opts.group}:${chunkI}`; - } - - let - val = el.value; - - const - valIsPromise = Object.isPromise(val); - - if (valIsPromise) { - try { - // eslint-disable-next-line require-atomic-updates - val = await $a.promise(>val, {group}); - - if (val === BREAK) { - break; - } - - } catch (err) { - if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { - break; - } - - stderr(err); - continue; - } - } - - const resolveTask = (filter?: boolean) => { - if (filter === false) { - return; - } - - total++; - chunkTotal++; - renderBuffer.push(val); - - lastTask = () => { - lastTask = null; - awaiting++; - - const task = () => { - const desc: TaskDesc = { - async: $a, - renderGroup: group - }; - - cb(renderBuffer, desc, (els: Node[]) => { - chunkI++; - chunkTotal = 0; - renderBuffer = []; - - awaiting--; - lastEvent = {...opts, ...desc}; - localEmitter.emit('asyncRenderChunkComplete', lastEvent); - - $a.worker(() => { - const destroyEl = (el: CanUndef) => { - if (el == null) { - return; - } - - if (el[this.asyncLabel] != null) { - delete el[this.asyncLabel]; - $a.worker(() => destroyEl(el), {group}); - - } else { - const - els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; - - if (opts.destructor?.(el, els) !== true) { - this.destroy(el, els); - } - } - }; - - for (let i = 0; i < els.length; i++) { - destroyEl(els[i]); - } - }, {group}); - }); - }; - - return this.createTask(task, {group, weight}); - }; - - if (!valIsPromise && chunkTotal < perChunk) { - return; - } - - return lastTask(); - }; - - try { - if (filter != null) { - const needRender = filter.call(this.ctx, val, i, { - iterable, - i: syncI + i + 1, - chunk: chunkI, - total - }); - - if (Object.isPromise(needRender)) { - await $a.promise(needRender, {group}) - .then((res) => resolveTask(res === undefined || Object.isTruly(res))); - - } else { - const - res = resolveTask(Object.isTruly(needRender)); - - if (res != null) { - await res; - } - } - - } else { - const - res = resolveTask(); - - if (res != null) { - await res; - } - } - - } catch (err) { - if (err?.type === 'clearAsync' && err.link.group === group) { - break; - } - - stderr(err); - continue; - } - - i++; - } - - if (lastTask != null) { - awaiting++; - - const - res = lastTask(); - - if (res != null) { - await res; - } - } - - if (awaiting <= 0) { - localEmitter.emit('asyncRenderComplete', lastEvent); - - } else { - const id = localEmitter.on('asyncRenderChunkComplete', () => { - if (awaiting <= 0) { - localEmitter.emit('asyncRenderComplete', lastEvent); - localEmitter.off(id); - } - }); - } - - function createIterator() { - if (srcIsPromise) { - const next = () => { - if (Object.isPromise(iterable)) { - return { - done: false, - value: iterable - .then((v) => { - iterable = v; - iterator = v[Symbol.iterator](); - - const - el = iterator.next(); - - if (el.done) { - return BREAK; - } - - return el.value; - }) - - .catch((err) => { - stderr(err); - return BREAK; - }) - }; - } - - return iterator.next(); - }; - - return {next}; - } - - let - i = 0; - - const next = () => { - if (untreatedEls.length === 0 && lastSyncEl.done) { - return lastSyncEl; - } - - if (i < untreatedEls.length) { - return { - value: untreatedEls[i++], - done: false - }; - } - - return iterator.next(); - }; - - return {next}; - } - }; - - return firstRender; - } - - /** - * Returns an iterable object based on the passed value - * - * @param obj - * @param [hasFilter] - true if the passed object will be filtered - */ - protected getIterable(obj: unknown, hasFilter?: boolean): CanPromise> { - if (obj == null) { - return []; - } - - if (obj === true) { - if (hasFilter) { - return new Range(0, Infinity); - } - - return []; - } - - if (obj === false) { - if (hasFilter) { - return new Range(0, -Infinity); - } - - return []; - } - - if (Object.isNumber(obj)) { - return new Range(0, [obj]); - } - - if (Object.isArray(obj)) { - return obj; - } - - if (Object.isString(obj)) { - return obj.letters(); - } - - if (Object.isPromise(obj)) { - return obj.then(this.getIterable.bind(this)); - } - - if (typeof obj === 'object') { - if (Object.isIterable(obj)) { - return obj; - } - - return Object.entries(obj); - } - - return [obj]; - } - - /** - * Removes the given element from the DOM tree and destroys all tied components - * - * @param el - * @param [childComponentEls] - list of child component nodes - */ - protected destroy(el: Node, childComponentEls: Element[] = []): void { - el.parentNode?.removeChild(el); - - for (let i = 0; i < childComponentEls.length; i++) { - const - el = childComponentEls[i]; - - try { - (>el).component?.unsafe.$destroy(); - - } catch (err) { - stderr(err); - } - } - - try { - (>el).component?.unsafe.$destroy(); - - } catch (err) { - stderr(err); - } - } - - /** - * Creates a render task by the specified parameters - * - * @param taskFn - * @param [params] - */ - protected createTask(taskFn: AnyFunction, params: TaskParams = {}): Promise { - const - {async: $a} = this; - - const - group = params.group ?? 'asyncComponents'; - - return new SyncPromise((resolve, reject) => { - const task = { - weight: params.weight, - - fn: $a.proxy(() => { - const cb = () => { - taskFn(); - resolve(); - return true; - }; - - if (params.useRAF) { - return $a.animationFrame({group}).then(cb); - } - - return cb(); - - }, { - group, - single: false, - onClear: (err) => { - queue.delete(task); - reject(err); - } - }) - }; - - queue.add(task); - }); - } - - //#endif -} +export default class AsyncRender extends Super {} diff --git a/src/super/i-block/modules/async-render/modules/base.ts b/src/super/i-block/modules/async-render/modules/base.ts new file mode 100644 index 0000000000..6a3a4a9ae2 --- /dev/null +++ b/src/super/i-block/modules/async-render/modules/base.ts @@ -0,0 +1,184 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import SyncPromise from 'core/promise/sync'; + +//#if runtime has component/async-render +import { queue, restart, deferRestart } from 'core/component/render/daemon'; +//#endif + +import type iBlock from 'super/i-block/i-block'; +import type { ComponentElement } from 'super/i-block/i-block'; + +import Friend from 'super/i-block/modules/friend'; +import type { TaskParams } from 'super/i-block/modules/async-render/interface'; + +export * from 'super/i-block/modules/async-render/interface'; + +export default class AsyncRender extends Friend { + //#if runtime has component/async-render + + constructor(component: iBlock) { + super(component); + + this.meta.hooks.beforeUpdate.push({ + fn: () => this.async.clearAll({ + group: 'asyncComponents' + }) + }); + } + + /** + * Restarts the `asyncRender` daemon to force rendering + */ + forceRender(): void { + restart(); + this.localEmitter.emit('forceRender'); + } + + /** + * Restarts the `asyncRender` daemon to force rendering + * (runs on the next tick) + */ + deferForceRender(): void { + deferRestart(); + this.localEmitter.emit('forceRender'); + } + + /** + * Returns a function that returns a promise that will be resolved after firing the `forceRender` event. + * The method can take an element name as the first parameter. This element will be dropped before resolving. + * + * Notice, the initial component rendering is mean the same as `forceRender`. + * The method is useful to re-render a function component without touching the parent state. + * + * @param elementToDrop - element to drop before resolving the promise + * (if the element is passed as a function, it would be invoked) + * + * @example + * ``` + * < button @click = asyncRender.forceRender() + * Re-render the component + * + * < .&__wrapper + * < template v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')}) + * < .&__content + * {{ Math.random() }} + * ``` + */ + waitForceRender( + elementToDrop?: string | ((ctx: this['component']) => CanPromise>) + ): () => CanPromise { + return () => { + const + canImmediateRender = this.lfc.isBeforeCreate() || this.hook === 'beforeMount'; + + if (canImmediateRender) { + return true; + } + + return this.localEmitter.promisifyOnce('forceRender').then(async () => { + if (elementToDrop != null) { + let + el; + + if (Object.isFunction(elementToDrop)) { + el = await elementToDrop(this.ctx); + + } else { + el = elementToDrop; + } + + if (Object.isString(el)) { + this.block?.element(el)?.remove(); + + } else { + el?.remove(); + } + } + + return true; + }); + }; + } + + /** + * Removes the given element from the DOM tree and destroys all tied components + * + * @param el + * @param [childComponentEls] - list of child component nodes + */ + protected destroy(el: Node, childComponentEls: Element[] = []): void { + el.parentNode?.removeChild(el); + + for (let i = 0; i < childComponentEls.length; i++) { + const + el = childComponentEls[i]; + + try { + (>el).component?.unsafe.$destroy(); + + } catch (err) { + stderr(err); + } + } + + try { + (>el).component?.unsafe.$destroy(); + + } catch (err) { + stderr(err); + } + } + + /** + * Creates a render task by the specified parameters + * + * @param taskFn + * @param [params] + */ + protected createTask(taskFn: AnyFunction, params: TaskParams = {}): Promise { + const + {async: $a} = this; + + const + group = params.group ?? 'asyncComponents'; + + return new SyncPromise((resolve, reject) => { + const task = { + weight: params.weight, + + fn: $a.proxy(() => { + const cb = () => { + taskFn(); + resolve(); + return true; + }; + + if (params.useRAF) { + return $a.animationFrame({group}).then(cb); + } + + return cb(); + + }, { + group, + single: false, + onClear: (err) => { + queue.delete(task); + reject(err); + } + }) + }; + + queue.add(task); + }); + } + + //#endif +} diff --git a/src/super/i-block/modules/async-render/modules/iter.ts b/src/super/i-block/modules/async-render/modules/iter.ts new file mode 100644 index 0000000000..ea92c8cd74 --- /dev/null +++ b/src/super/i-block/modules/async-render/modules/iter.ts @@ -0,0 +1,475 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Range from 'core/range'; + +import type { ComponentElement } from 'super/i-block/i-block'; +import type { TaskParams, TaskDesc } from 'super/i-block/modules/async-render/interface'; + +import Super from 'super/i-block/modules/async-render/modules/base'; + +export * from 'super/i-block/modules/async-render/modules/base'; +export * from 'super/i-block/modules/async-render/interface'; + +export default class AsyncRender extends Super { + //#if runtime has component/async-render + + /** + * Creates an asynchronous render stream from the specified value. + * This method helps to optimize the rendering of a component by splitting big render tasks into little. + * + * @param value + * @param [sliceOrOpts] - elements per chunk, `[start position, elements per chunk]` or additional options + * @param [opts] - additional options + * + * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & TaskDesc)` + * @emits `localEmitter.asyncRenderComplete(e: TaskParams & TaskDesc)` + * + * @example + * ``` + * /// Asynchronous rendering of components, only five elements per chunk + * < template v-for = el in asyncRender.iterate(largeList, 5) + * < my-component :data = el + * ``` + */ + iterate( + value: unknown, + sliceOrOpts: number | [number?, number?] | TaskParams = 1, + opts: TaskParams = {} + ): unknown[] { + if (value == null) { + return []; + } + + if (Object.isPlainObject(sliceOrOpts)) { + opts = sliceOrOpts; + sliceOrOpts = []; + } + + const + {filter} = opts; + + let + iterable = this.getIterable(value, filter != null); + + let + startIterPos, + elsPerChunk; + + if (Object.isArray(sliceOrOpts)) { + startIterPos = sliceOrOpts[0]; + elsPerChunk = sliceOrOpts[1]; + + } else { + elsPerChunk = sliceOrOpts; + } + + startIterPos ??= 0; + elsPerChunk ??= 1; + + const + iterableIsPromise = Object.isPromise(iterable); + + const + firstRenderEls = [], + asyncRenderEls = []; + + let + iterator: Iterator, + lastSyncEl: IteratorResult; + + let + syncI = 0, + syncTotal = 0; + + if (!iterableIsPromise) { + iterator = iterable[Symbol.iterator](); + + // eslint-disable-next-line no-multi-assign + for (let o = iterator, el = lastSyncEl = o.next(); !el.done; el = o.next(), syncI++) { + if (startIterPos > 0) { + startIterPos--; + continue; + } + + const + val = el.value; + + let + valIsPromise = Object.isPromise(val), + canRender = !valIsPromise; + + if (canRender && filter != null) { + canRender = filter.call(this.component, val, syncI, { + iterable, + i: syncI, + total: syncTotal + }); + + if (Object.isPromise(canRender)) { + valIsPromise = true; + canRender = false; + + } else if (!Object.isTruly(canRender)) { + canRender = false; + } + } + + if (canRender) { + syncTotal++; + firstRenderEls.push(val); + + } else if (valIsPromise) { + asyncRenderEls.push(val); + } + + if (syncTotal >= elsPerChunk || valIsPromise) { + break; + } + } + } + + const { + async: $a, + localEmitter + } = this; + + let + render: CanUndef, + target: CanUndef; + + this.ctx.$once('[[V-FOR-CB]]', setRender); + this.ctx.$once('[[V-ASYNC-TARGET]]', setTarget); + + const + BREAK = {}; + + $a.setImmediate(async () => { + this.ctx.$off('[[V-FOR-CB]]', setRender); + this.ctx.$off('[[V-ASYNC-TARGET]]', setTarget); + + if (render == null || target == null) { + return; + } + + const + weight = opts.weight ?? 1, + newIterator = createIterator(); + + let + i = 0, + total = syncTotal, + chunkTotal = 0, + chunkI = 0, + awaiting = 0; + + let + group = 'asyncComponents', + renderBuffer = []; + + let + lastTask, + lastEvent; + + for (let o = newIterator, el = o.next(); !el.done; el = o.next()) { + if (opts.group != null) { + group = `asyncComponents:${opts.group}:${chunkI}`; + } + + let + val = el.value; + + const + valIsPromise = Object.isPromise(val); + + if (valIsPromise) { + try { + // eslint-disable-next-line require-atomic-updates + val = await $a.promise(>val, {group}); + + if (val === BREAK) { + break; + } + + } catch (err) { + if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { + break; + } + + stderr(err); + continue; + } + } + + const cb = (chunk, desc, cb) => { + const res = []; + + Object.forEach(chunk, (el) => { + const node = this.ctx.$renderEngine.r.render(render(el)); + target.el.appendChild(node); + res.push(node); + }); + + cb(res); + }; + + const resolveTask = (filter?: boolean) => { + if (filter === false) { + return; + } + + total++; + chunkTotal++; + renderBuffer.push(val); + + lastTask = () => { + lastTask = null; + awaiting++; + + const task = () => { + const desc: TaskDesc = { + async: $a, + renderGroup: group + }; + + cb(renderBuffer, desc, (els: Node[]) => { + chunkI++; + chunkTotal = 0; + renderBuffer = []; + + awaiting--; + lastEvent = {...opts, ...desc}; + localEmitter.emit('asyncRenderChunkComplete', lastEvent); + + $a.worker(() => { + const destroyEl = (el: CanUndef) => { + if (el == null) { + return; + } + + if (el[this.asyncLabel] != null) { + delete el[this.asyncLabel]; + $a.worker(() => destroyEl(el), {group}); + + } else { + const + els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; + + if (opts.destructor?.(el, els) !== true) { + this.destroy(el, els); + } + } + }; + + for (let i = 0; i < els.length; i++) { + destroyEl(els[i]); + } + }, {group}); + }); + }; + + return this.createTask(task, {group, weight}); + }; + + if (!valIsPromise && chunkTotal < elsPerChunk) { + return; + } + + return lastTask(); + }; + + try { + if (filter != null) { + const needRender = filter.call(this.ctx, val, i, { + iterable, + i: syncI + i + 1, + chunk: chunkI, + total + }); + + if (Object.isPromise(needRender)) { + await $a.promise(needRender, {group}) + .then((res) => resolveTask(res === undefined || Object.isTruly(res))); + + } else { + const + res = resolveTask(Object.isTruly(needRender)); + + if (res != null) { + await res; + } + } + + } else { + const + res = resolveTask(); + + if (res != null) { + await res; + } + } + + } catch (err) { + if (err?.type === 'clearAsync' && err.link.group === group) { + break; + } + + stderr(err); + continue; + } + + i++; + } + + if (lastTask != null) { + awaiting++; + + const + res = lastTask(); + + if (res != null) { + await res; + } + } + + if (awaiting <= 0) { + localEmitter.emit('asyncRenderComplete', lastEvent); + + } else { + const id = localEmitter.on('asyncRenderChunkComplete', () => { + if (awaiting <= 0) { + localEmitter.emit('asyncRenderComplete', lastEvent); + localEmitter.off(id); + } + }); + } + + function createIterator() { + if (iterableIsPromise) { + const next = () => { + if (Object.isPromise(iterable)) { + return { + done: false, + value: iterable + .then((v) => { + iterable = v; + iterator = v[Symbol.iterator](); + + const + el = iterator.next(); + + if (el.done) { + return BREAK; + } + + return el.value; + }) + + .catch((err) => { + stderr(err); + return BREAK; + }) + }; + } + + return iterator.next(); + }; + + return {next}; + } + + let + i = 0; + + const next = () => { + if (asyncRenderEls.length === 0 && lastSyncEl.done) { + return lastSyncEl; + } + + if (i < asyncRenderEls.length) { + return { + value: asyncRenderEls[i++], + done: false + }; + } + + return iterator.next(); + }; + + return {next}; + } + }); + + return firstRenderEls; + + function setRender(r: Function) { + return render = r; + } + + function setTarget(r: Element) { + return target = r; + } + } + + /** + * Returns an iterable object based on the passed value + * + * @param obj + * @param [hasFilter] - true if the passed object will be filtered + */ + protected getIterable(obj: unknown, hasFilter?: boolean): CanPromise> { + if (obj == null) { + return []; + } + + if (obj === true) { + if (hasFilter) { + return new Range(0, Infinity); + } + + return []; + } + + if (obj === false) { + if (hasFilter) { + return new Range(0, -Infinity); + } + + return []; + } + + if (Object.isNumber(obj)) { + return new Range(0, [obj]); + } + + if (Object.isArray(obj)) { + return obj; + } + + if (Object.isString(obj)) { + return obj.letters(); + } + + if (Object.isPromise(obj)) { + return obj.then(this.getIterable.bind(this)); + } + + if (typeof obj === 'object') { + if (Object.isIterable(obj)) { + return obj; + } + + return Object.entries(obj); + } + + return [obj]; + } + + //#endif +} + From 868b42b655a9b929b364d3aa7b8cab78ebaacd2f Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 16:54:55 +0300 Subject: [PATCH 0150/2313] refactor: split `iter` into separated files --- .../modules/async-render/modules/interface.ts | 26 ++ .../async-render/modules/iter-helpers.ts | 205 +++++++++ .../modules/async-render/modules/iter.ts | 395 +++++------------- 3 files changed, 335 insertions(+), 291 deletions(-) create mode 100644 src/super/i-block/modules/async-render/modules/interface.ts create mode 100644 src/super/i-block/modules/async-render/modules/iter-helpers.ts diff --git a/src/super/i-block/modules/async-render/modules/interface.ts b/src/super/i-block/modules/async-render/modules/interface.ts new file mode 100644 index 0000000000..d1e981bc1a --- /dev/null +++ b/src/super/i-block/modules/async-render/modules/interface.ts @@ -0,0 +1,26 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { TaskFilter } from 'super/i-block/modules/async-render/interface'; + +export interface IterOptions { + start?: number; + perChunk?: number; + filter?: TaskFilter; +} + +export interface IterDescriptor { + isAsync: boolean; + + readI: number; + readTotal: number; + readEls: unknown[]; + + iterable: CanPromise; + iterator: IterableIterator | AsyncIterableIterator; +} diff --git a/src/super/i-block/modules/async-render/modules/iter-helpers.ts b/src/super/i-block/modules/async-render/modules/iter-helpers.ts new file mode 100644 index 0000000000..72e5e6a192 --- /dev/null +++ b/src/super/i-block/modules/async-render/modules/iter-helpers.ts @@ -0,0 +1,205 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Range from 'core/range'; +import Super from 'super/i-block/modules/async-render/modules/base'; + +import type { IterOptions, IterDescriptor } from 'super/i-block/modules/async-render/modules/interface'; + +export * from 'super/i-block/modules/async-render/modules/base'; +export * from 'super/i-block/modules/async-render/interface'; + +export default class AsyncRender extends Super { + //#if runtime has component/async-render + + /** + * Returns an iterator descriptor object based on the passed value and options + * + * @param value + * @param [start] - start positions to iterate + * @param [perChunk] - elements per one iteration chunk + * @param [filter] - function to filter iteration elements + */ + protected getIterDescriptor(value: unknown, {start = 0, perChunk = 1, filter}: IterOptions = {}): IterDescriptor { + const + iterable = this.getIterable(value, filter != null), + isAsyncIterable = Object.isPromise(iterable) || Object.isAsyncIterable(iterable), + syncIterator: Iterator = isAsyncIterable ? [].values() : iterable[Symbol.iterator](); + + const + readEls: unknown[] = [], + discardedReadEls: unknown[] = []; + + let + readI = 0, + readTotal = 0, + lastReadValue: CanUndef> = undefined; + + if (!isAsyncIterable) { + // eslint-disable-next-line no-multi-assign + for (let o = syncIterator, el = lastReadValue = o.next(); !el.done; el = o.next(), readI++) { + if (start > 0) { + start--; + continue; + } + + const + iterVal = el.value; + + let + valIsPromise = Object.isPromise(iterVal), + canRender = !valIsPromise; + + if (canRender && filter != null) { + canRender = filter.call(this.component, iterVal, readI, { + iterable, + i: readI, + total: readTotal + }); + + if (Object.isPromise(canRender)) { + valIsPromise = true; + canRender = false; + + } else if (!Object.isTruly(canRender)) { + canRender = false; + } + } + + if (canRender) { + readTotal++; + readEls.push(iterVal); + + } else if (valIsPromise) { + discardedReadEls.push(iterVal); + } + + if (readTotal >= perChunk || valIsPromise) { + break; + } + } + } + + return { + isAsync: isAsyncIterable, + + readI, + readTotal, + readEls, + + iterable, + iterator: createFinalIterator() + }; + + function createFinalIterator() { + if (isAsyncIterable) { + const next = () => { + if (Object.isPromise(iterable)) { + return iterable.then((i) => { + const iter = i[Object.isAsyncIterable(i) ? Symbol.asyncIterator : Symbol.iterator](); + return Promise.resolve(iter.next()); + }); + } + + return Promise.resolve(syncIterator.next()); + }; + + return { + [Symbol.asyncIterator]() { + return this; + }, + + next + }; + } + + let + i = 0; + + const next = () => { + if (discardedReadEls.length === 0 && lastReadValue?.done) { + return lastReadValue; + } + + if (i < discardedReadEls.length) { + return { + value: discardedReadEls[i++], + done: false + }; + } + + return syncIterator.next(); + }; + + return { + [Symbol.iterator]() { + return this; + }, + + next + }; + } + } + + /** + * Returns an iterable structure based on the passed value + * + * @param value + * @param [hasFilter] - true if the passed value will be filtered + */ + protected getIterable(value: unknown, hasFilter?: boolean): CanPromise | Iterable> { + if (value == null) { + return []; + } + + if (value === true) { + if (hasFilter) { + return new Range(0, Infinity); + } + + return []; + } + + if (value === false) { + if (hasFilter) { + return new Range(0, -Infinity); + } + + return []; + } + + if (Object.isNumber(value)) { + return new Range(0, [value]); + } + + if (Object.isArray(value)) { + return value; + } + + if (Object.isString(value)) { + return value.letters(); + } + + if (Object.isPromise(value)) { + return value.then(this.getIterable.bind(this)); + } + + if (typeof value === 'object') { + if (Object.isIterable(value) || Object.isAsyncIterable(value)) { + return value; + } + + return Object.entries(value); + } + + return [value]; + } + + //#endif +} + diff --git a/src/super/i-block/modules/async-render/modules/iter.ts b/src/super/i-block/modules/async-render/modules/iter.ts index ea92c8cd74..0c637615e1 100644 --- a/src/super/i-block/modules/async-render/modules/iter.ts +++ b/src/super/i-block/modules/async-render/modules/iter.ts @@ -6,12 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import Range from 'core/range'; - import type { ComponentElement } from 'super/i-block/i-block'; import type { TaskParams, TaskDesc } from 'super/i-block/modules/async-render/interface'; -import Super from 'super/i-block/modules/async-render/modules/base'; +import Super from 'super/i-block/modules/async-render/modules/iter-helpers'; export * from 'super/i-block/modules/async-render/modules/base'; export * from 'super/i-block/modules/async-render/interface'; @@ -46,6 +44,11 @@ export default class AsyncRender extends Super { return []; } + const { + async: $a, + localEmitter + } = this; + if (Object.isPlainObject(sliceOrOpts)) { opts = sliceOrOpts; sliceOrOpts = []; @@ -55,89 +58,19 @@ export default class AsyncRender extends Super { {filter} = opts; let - iterable = this.getIterable(value, filter != null); - - let - startIterPos, - elsPerChunk; + start, + perChunk; if (Object.isArray(sliceOrOpts)) { - startIterPos = sliceOrOpts[0]; - elsPerChunk = sliceOrOpts[1]; + start = sliceOrOpts[0]; + perChunk = sliceOrOpts[1]; } else { - elsPerChunk = sliceOrOpts; + perChunk = sliceOrOpts; } - startIterPos ??= 0; - elsPerChunk ??= 1; - - const - iterableIsPromise = Object.isPromise(iterable); - const - firstRenderEls = [], - asyncRenderEls = []; - - let - iterator: Iterator, - lastSyncEl: IteratorResult; - - let - syncI = 0, - syncTotal = 0; - - if (!iterableIsPromise) { - iterator = iterable[Symbol.iterator](); - - // eslint-disable-next-line no-multi-assign - for (let o = iterator, el = lastSyncEl = o.next(); !el.done; el = o.next(), syncI++) { - if (startIterPos > 0) { - startIterPos--; - continue; - } - - const - val = el.value; - - let - valIsPromise = Object.isPromise(val), - canRender = !valIsPromise; - - if (canRender && filter != null) { - canRender = filter.call(this.component, val, syncI, { - iterable, - i: syncI, - total: syncTotal - }); - - if (Object.isPromise(canRender)) { - valIsPromise = true; - canRender = false; - - } else if (!Object.isTruly(canRender)) { - canRender = false; - } - } - - if (canRender) { - syncTotal++; - firstRenderEls.push(val); - - } else if (valIsPromise) { - asyncRenderEls.push(val); - } - - if (syncTotal >= elsPerChunk || valIsPromise) { - break; - } - } - } - - const { - async: $a, - localEmitter - } = this; + iter = this.getIterDescriptor(value, {start, perChunk, filter}); let render: CanUndef, @@ -146,9 +79,6 @@ export default class AsyncRender extends Super { this.ctx.$once('[[V-FOR-CB]]', setRender); this.ctx.$once('[[V-ASYNC-TARGET]]', setTarget); - const - BREAK = {}; - $a.setImmediate(async () => { this.ctx.$off('[[V-FOR-CB]]', setRender); this.ctx.$off('[[V-ASYNC-TARGET]]', setTarget); @@ -158,14 +88,13 @@ export default class AsyncRender extends Super { } const - weight = opts.weight ?? 1, - newIterator = createIterator(); + weight = opts.weight ?? 1; let i = 0, - total = syncTotal, - chunkTotal = 0, chunkI = 0, + total = iter.readTotal, + chunkTotal = 0, awaiting = 0; let @@ -176,118 +105,101 @@ export default class AsyncRender extends Super { lastTask, lastEvent; - for (let o = newIterator, el = o.next(); !el.done; el = o.next()) { - if (opts.group != null) { - group = `asyncComponents:${opts.group}:${chunkI}`; - } - - let - val = el.value; - - const - valIsPromise = Object.isPromise(val); - - if (valIsPromise) { - try { - // eslint-disable-next-line require-atomic-updates - val = await $a.promise(>val, {group}); - - if (val === BREAK) { - break; - } - - } catch (err) { - if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { - break; - } - - stderr(err); - continue; + const doIter = async (val: unknown) => { + try { + if (opts.group != null) { + group = `asyncComponents:${opts.group}:${chunkI}`; } - } - - const cb = (chunk, desc, cb) => { - const res = []; - Object.forEach(chunk, (el) => { - const node = this.ctx.$renderEngine.r.render(render(el)); - target.el.appendChild(node); - res.push(node); - }); + const + valIsPromise = Object.isPromise(val); - cb(res); - }; - - const resolveTask = (filter?: boolean) => { - if (filter === false) { - return; + if (valIsPromise) { + // eslint-disable-next-line require-atomic-updates + val = await $a.promise(Object.cast(val), {group}); } - total++; - chunkTotal++; - renderBuffer.push(val); + const cb = (chunk, desc, cb) => { + const res = []; - lastTask = () => { - lastTask = null; - awaiting++; - - const task = () => { - const desc: TaskDesc = { - async: $a, - renderGroup: group - }; + Object.forEach(chunk, (el) => { + const node = this.ctx.$renderEngine.r.render(render(el)); + target.el.appendChild(node); + res.push(node); + }); - cb(renderBuffer, desc, (els: Node[]) => { - chunkI++; - chunkTotal = 0; - renderBuffer = []; + cb(res); + }; - awaiting--; - lastEvent = {...opts, ...desc}; - localEmitter.emit('asyncRenderChunkComplete', lastEvent); + const resolveTask = (filter?: boolean) => { + if (filter === false) { + return; + } - $a.worker(() => { - const destroyEl = (el: CanUndef) => { - if (el == null) { - return; - } + total++; + chunkTotal++; + renderBuffer.push(val); + + lastTask = () => { + lastTask = null; + awaiting++; + + const task = () => { + const desc: TaskDesc = { + async: $a, + renderGroup: group + }; + + cb(renderBuffer, desc, (els: Node[]) => { + chunkI++; + chunkTotal = 0; + renderBuffer = []; + + awaiting--; + lastEvent = {...opts, ...desc}; + localEmitter.emit('asyncRenderChunkComplete', lastEvent); + + $a.worker(() => { + const destroyEl = (el: CanUndef) => { + if (el == null) { + return; + } - if (el[this.asyncLabel] != null) { - delete el[this.asyncLabel]; - $a.worker(() => destroyEl(el), {group}); + if (el[this.asyncLabel] != null) { + delete el[this.asyncLabel]; + $a.worker(() => destroyEl(el), {group}); - } else { - const - els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; + } else { + const + els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; - if (opts.destructor?.(el, els) !== true) { - this.destroy(el, els); + if (opts.destructor?.(el, els) !== true) { + this.destroy(el, els); + } } + }; + + for (let i = 0; i < els.length; i++) { + destroyEl(els[i]); } - }; + }, {group}); + }); + }; - for (let i = 0; i < els.length; i++) { - destroyEl(els[i]); - } - }, {group}); - }); + return this.createTask(task, {group, weight}); }; - return this.createTask(task, {group, weight}); - }; - - if (!valIsPromise && chunkTotal < elsPerChunk) { - return; - } + if (!valIsPromise && chunkTotal < perChunk) { + return; + } - return lastTask(); - }; + return lastTask(); + }; - try { if (filter != null) { const needRender = filter.call(this.ctx, val, i, { - iterable, - i: syncI + i + 1, + iterable: iter.iterable, + i: iter.readI + i + 1, chunk: chunkI, total }); @@ -314,16 +226,26 @@ export default class AsyncRender extends Super { } } + i++; + } catch (err) { - if (err?.type === 'clearAsync' && err.link.group === group) { - break; + if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { + return false; } stderr(err); - continue; + } + }; + + if (Object.isAsyncIterable(iter.iterator)) { + for await (const el of iter.iterator) { + await doIter(el); } - i++; + } else { + for (const el of iter.iterator) { + await doIter(el); + } } if (lastTask != null) { @@ -348,64 +270,9 @@ export default class AsyncRender extends Super { } }); } - - function createIterator() { - if (iterableIsPromise) { - const next = () => { - if (Object.isPromise(iterable)) { - return { - done: false, - value: iterable - .then((v) => { - iterable = v; - iterator = v[Symbol.iterator](); - - const - el = iterator.next(); - - if (el.done) { - return BREAK; - } - - return el.value; - }) - - .catch((err) => { - stderr(err); - return BREAK; - }) - }; - } - - return iterator.next(); - }; - - return {next}; - } - - let - i = 0; - - const next = () => { - if (asyncRenderEls.length === 0 && lastSyncEl.done) { - return lastSyncEl; - } - - if (i < asyncRenderEls.length) { - return { - value: asyncRenderEls[i++], - done: false - }; - } - - return iterator.next(); - }; - - return {next}; - } }); - return firstRenderEls; + return iter.readEls; function setRender(r: Function) { return render = r; @@ -416,60 +283,6 @@ export default class AsyncRender extends Super { } } - /** - * Returns an iterable object based on the passed value - * - * @param obj - * @param [hasFilter] - true if the passed object will be filtered - */ - protected getIterable(obj: unknown, hasFilter?: boolean): CanPromise> { - if (obj == null) { - return []; - } - - if (obj === true) { - if (hasFilter) { - return new Range(0, Infinity); - } - - return []; - } - - if (obj === false) { - if (hasFilter) { - return new Range(0, -Infinity); - } - - return []; - } - - if (Object.isNumber(obj)) { - return new Range(0, [obj]); - } - - if (Object.isArray(obj)) { - return obj; - } - - if (Object.isString(obj)) { - return obj.letters(); - } - - if (Object.isPromise(obj)) { - return obj.then(this.getIterable.bind(this)); - } - - if (typeof obj === 'object') { - if (Object.isIterable(obj)) { - return obj; - } - - return Object.entries(obj); - } - - return [obj]; - } - //#endif } From 5b3e9d9331a1bc17f9eca41b53e2c0fec6e62f2e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 19 May 2022 18:28:43 +0300 Subject: [PATCH 0151/2313] refactor: better code structure --- .../i-block/modules/async-render/interface.ts | 52 +-- .../modules/async-render/modules/base.ts | 2 - .../modules/async-render/modules/interface.ts | 26 -- .../async-render/modules/iter-helpers.ts | 5 +- .../modules/async-render/modules/iter.ts | 323 ++++++++++-------- 5 files changed, 214 insertions(+), 194 deletions(-) delete mode 100644 src/super/i-block/modules/async-render/modules/interface.ts diff --git a/src/super/i-block/modules/async-render/interface.ts b/src/super/i-block/modules/async-render/interface.ts index 8e7c324b2f..a893ccd1a2 100644 --- a/src/super/i-block/modules/async-render/interface.ts +++ b/src/super/i-block/modules/async-render/interface.ts @@ -6,24 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type Async from 'core/async'; - -export interface TaskI { - iterable: Iterable; - i: number; - total: number; - chunk?: number; -} - -export interface TaskFilter { - (): CanPromise; - (el: EL, i: I, task: TaskI): CanPromise; -} - -export interface ElementDestructor { - (el: Node, childComponentEls: Element[]): AnyToIgnore; -} - export interface TaskParams { /** * If true, then rendered chunks are inserted into DOM on the `requestAnimationFrame` callback. @@ -91,7 +73,35 @@ export interface TaskParams; - renderGroup: string; +export interface TaskI { + iterable: Iterable; + i: number; + total: number; + chunk?: number; +} + +export interface TaskFilter { + (): CanPromise; + (el: EL, i: I, task: TaskI): CanPromise; +} + +export interface ElementDestructor { + (el: Node, childComponentEls: Element[]): AnyToIgnore; +} + +export interface IterOptions { + start?: number; + perChunk?: number; + filter?: TaskFilter; +} + +export interface IterDescriptor { + isAsync: boolean; + + readI: number; + readTotal: number; + readEls: unknown[]; + + iterable: CanPromise; + iterator: IterableIterator | AsyncIterableIterator; } diff --git a/src/super/i-block/modules/async-render/modules/base.ts b/src/super/i-block/modules/async-render/modules/base.ts index 6a3a4a9ae2..95b1dc2473 100644 --- a/src/super/i-block/modules/async-render/modules/base.ts +++ b/src/super/i-block/modules/async-render/modules/base.ts @@ -18,8 +18,6 @@ import type { ComponentElement } from 'super/i-block/i-block'; import Friend from 'super/i-block/modules/friend'; import type { TaskParams } from 'super/i-block/modules/async-render/interface'; -export * from 'super/i-block/modules/async-render/interface'; - export default class AsyncRender extends Friend { //#if runtime has component/async-render diff --git a/src/super/i-block/modules/async-render/modules/interface.ts b/src/super/i-block/modules/async-render/modules/interface.ts deleted file mode 100644 index d1e981bc1a..0000000000 --- a/src/super/i-block/modules/async-render/modules/interface.ts +++ /dev/null @@ -1,26 +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 { TaskFilter } from 'super/i-block/modules/async-render/interface'; - -export interface IterOptions { - start?: number; - perChunk?: number; - filter?: TaskFilter; -} - -export interface IterDescriptor { - isAsync: boolean; - - readI: number; - readTotal: number; - readEls: unknown[]; - - iterable: CanPromise; - iterator: IterableIterator | AsyncIterableIterator; -} diff --git a/src/super/i-block/modules/async-render/modules/iter-helpers.ts b/src/super/i-block/modules/async-render/modules/iter-helpers.ts index 72e5e6a192..e5f5ecd2f8 100644 --- a/src/super/i-block/modules/async-render/modules/iter-helpers.ts +++ b/src/super/i-block/modules/async-render/modules/iter-helpers.ts @@ -7,12 +7,11 @@ */ import Range from 'core/range'; -import Super from 'super/i-block/modules/async-render/modules/base'; -import type { IterOptions, IterDescriptor } from 'super/i-block/modules/async-render/modules/interface'; +import Super from 'super/i-block/modules/async-render/modules/base'; +import type { IterOptions, IterDescriptor } from 'super/i-block/modules/async-render/interface'; export * from 'super/i-block/modules/async-render/modules/base'; -export * from 'super/i-block/modules/async-render/interface'; export default class AsyncRender extends Super { //#if runtime has component/async-render diff --git a/src/super/i-block/modules/async-render/modules/iter.ts b/src/super/i-block/modules/async-render/modules/iter.ts index 0c637615e1..5d6b57593c 100644 --- a/src/super/i-block/modules/async-render/modules/iter.ts +++ b/src/super/i-block/modules/async-render/modules/iter.ts @@ -6,33 +6,38 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentElement } from 'super/i-block/i-block'; -import type { TaskParams, TaskDesc } from 'super/i-block/modules/async-render/interface'; +import symbolGenerator from 'core/symbol'; +import type { ComponentElement, VNode } from 'super/i-block/i-block'; import Super from 'super/i-block/modules/async-render/modules/iter-helpers'; +import type { TaskParams } from 'super/i-block/modules/async-render/interface'; -export * from 'super/i-block/modules/async-render/modules/base'; -export * from 'super/i-block/modules/async-render/interface'; +export * from 'super/i-block/modules/async-render/modules/iter-helpers'; + +export const + $$ = symbolGenerator(); export default class AsyncRender extends Super { //#if runtime has component/async-render /** * Creates an asynchronous render stream from the specified value. - * This method helps to optimize the rendering of a component by splitting big render tasks into little. + * This method helps to optimize component rendering by splitting big render tasks into little. * * @param value - * @param [sliceOrOpts] - elements per chunk, `[start position, elements per chunk]` or additional options + * @param [sliceOrOpts] - elements per chunk or `[start position, elements per chunk]` or additional options * @param [opts] - additional options * - * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & TaskDesc)` - * @emits `localEmitter.asyncRenderComplete(e: TaskParams & TaskDesc)` + * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & {renderGroup: string})` + * @emits `localEmitter.asyncRenderComplete(e: TaskParams & {renderGroup: string})` * * @example * ``` - * /// Asynchronous rendering of components, only five elements per chunk - * < template v-for = el in asyncRender.iterate(largeList, 5) - * < my-component :data = el + * /// Where to append asynchronous elements + * < .target v-async-target + * /// Asynchronous rendering of components: only five elements per chunk + * < template v-for = el in asyncRender.iterate(largeList, 5) + * < my-component :data = el * ``` */ iterate( @@ -44,7 +49,13 @@ export default class AsyncRender extends Super { return []; } + const + that = this; + const { + ctx, + ctx: {$renderEngine: {r}}, + async: $a, localEmitter } = this; @@ -55,7 +66,7 @@ export default class AsyncRender extends Super { } const - {filter} = opts; + {filter, weight = 1} = opts; let start, @@ -73,144 +84,88 @@ export default class AsyncRender extends Super { iter = this.getIterDescriptor(value, {start, perChunk, filter}); let - render: CanUndef, - target: CanUndef; - - this.ctx.$once('[[V-FOR-CB]]', setRender); - this.ctx.$once('[[V-ASYNC-TARGET]]', setTarget); + toVNode: AnyFunction>, + target: VNode; - $a.setImmediate(async () => { - this.ctx.$off('[[V-FOR-CB]]', setRender); - this.ctx.$off('[[V-ASYNC-TARGET]]', setTarget); + ctx.$once('[[V-FOR-CB]]', setVNodeCompiler); + ctx.$once('[[V-ASYNC-TARGET]]', setTarget); - if (render == null || target == null) { - return; - } + let + i = 0, + chunkI = 0; - const - weight = opts.weight ?? 1; + let + total = iter.readTotal, + chunkTotal = 0; - let - i = 0, - chunkI = 0, - total = iter.readTotal, - chunkTotal = 0, - awaiting = 0; + let + awaiting = 0; - let - group = 'asyncComponents', - renderBuffer = []; + let + group = 'asyncComponents', + valsToRender: unknown[] = []; - let - lastTask, - lastEvent; + let + lastTask, + lastEvent; - const doIter = async (val: unknown) => { - try { - if (opts.group != null) { - group = `asyncComponents:${opts.group}:${chunkI}`; - } + $a.setImmediate(async () => { + ctx.$off('[[V-FOR-CB]]', setVNodeCompiler); + ctx.$off('[[V-ASYNC-TARGET]]', setTarget); - const - valIsPromise = Object.isPromise(val); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (target == null) { + throw new ReferenceError('There is no host node to append asynchronously render elements'); + } - if (valIsPromise) { - // eslint-disable-next-line require-atomic-updates - val = await $a.promise(Object.cast(val), {group}); - } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (toVNode == null) { + return; + } - const cb = (chunk, desc, cb) => { - const res = []; + // eslint-disable-next-line no-constant-condition + while (true) { + if (opts.group != null) { + group = `asyncComponents:${opts.group}:${chunkI}`; + } - Object.forEach(chunk, (el) => { - const node = this.ctx.$renderEngine.r.render(render(el)); - target.el.appendChild(node); - res.push(node); - }); + let + el = iter.iterator.next(); - cb(res); - }; + try { + el = Object.isPromise(el) ? await $a.promise(el, {group}) : el; - const resolveTask = (filter?: boolean) => { - if (filter === false) { - return; - } + if (el.done) { + break; + } - total++; - chunkTotal++; - renderBuffer.push(val); - - lastTask = () => { - lastTask = null; - awaiting++; - - const task = () => { - const desc: TaskDesc = { - async: $a, - renderGroup: group - }; - - cb(renderBuffer, desc, (els: Node[]) => { - chunkI++; - chunkTotal = 0; - renderBuffer = []; - - awaiting--; - lastEvent = {...opts, ...desc}; - localEmitter.emit('asyncRenderChunkComplete', lastEvent); - - $a.worker(() => { - const destroyEl = (el: CanUndef) => { - if (el == null) { - return; - } - - if (el[this.asyncLabel] != null) { - delete el[this.asyncLabel]; - $a.worker(() => destroyEl(el), {group}); - - } else { - const - els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; - - if (opts.destructor?.(el, els) !== true) { - this.destroy(el, els); - } - } - }; - - for (let i = 0; i < els.length; i++) { - destroyEl(els[i]); - } - }, {group}); - }); - }; - - return this.createTask(task, {group, weight}); - }; - - if (!valIsPromise && chunkTotal < perChunk) { - return; - } + } catch (err) { + stderr(err); + break; + } - return lastTask(); - }; + try { + const + iterVal = Object.isPromise(el.value) ? await $a.promise(el.value, {group}) : el.value; if (filter != null) { - const needRender = filter.call(this.ctx, val, i, { - iterable: iter.iterable, + const needRender = filter.call(this.ctx, iterVal, i, { + total, + i: iter.readI + i + 1, chunk: chunkI, - total + + iterable: iter.iterable }); if (Object.isPromise(needRender)) { - await $a.promise(needRender, {group}) - .then((res) => resolveTask(res === undefined || Object.isTruly(res))); + await $a.promise(needRender, {group}).then( + (res) => resolveTask(iterVal, res === undefined || Object.isTruly(res)) + ); } else { const - res = resolveTask(Object.isTruly(needRender)); + res = resolveTask(iterVal, Object.isTruly(needRender)); if (res != null) { await res; @@ -219,7 +174,7 @@ export default class AsyncRender extends Super { } else { const - res = resolveTask(); + res = resolveTask(iterVal); if (res != null) { await res; @@ -230,22 +185,11 @@ export default class AsyncRender extends Super { } catch (err) { if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { - return false; + break; } stderr(err); } - }; - - if (Object.isAsyncIterable(iter.iterator)) { - for await (const el of iter.iterator) { - await doIter(el); - } - - } else { - for (const el of iter.iterator) { - await doIter(el); - } } if (lastTask != null) { @@ -274,12 +218,107 @@ export default class AsyncRender extends Super { return iter.readEls; - function setRender(r: Function) { - return render = r; + function setVNodeCompiler(c: AnyFunction) { + toVNode = c; + } + + function setTarget(t: VNode) { + target = t; } - function setTarget(r: Element) { - return target = r; + function resolveTask(iterVal: unknown, filter?: boolean) { + if (filter === false) { + return; + } + + total++; + chunkTotal++; + valsToRender.push(iterVal); + + lastTask = () => { + lastTask = null; + awaiting++; + + return that.createTask(task, {group, weight}); + }; + + if (!Object.isPromise(iterVal) && chunkTotal < perChunk) { + return; + } + + return lastTask(); + + function task() { + const + renderedVNodes: Node[] = []; + + for (let i = 0; i < valsToRender.length; i++) { + const + el = valsToRender[i], + vnodes = toVNode(el); + + if (Object.isArray(vnodes)) { + for (let i = 0; i < vnodes.length; i++) { + renderVNode(vnodes[i]); + } + + } else { + renderVNode(vnodes); + } + } + + valsToRender = []; + + chunkI++; + chunkTotal = 0; + awaiting--; + + lastEvent = {...opts, renderGroup: group}; + localEmitter.emit('asyncRenderChunkComplete', lastEvent); + + $a.worker(destructor, {group}); + + function renderVNode(vnode: VNode) { + let + renderedVnode: Node; + + if (vnode.el != null) { + vnode.el[Object.cast($$.cached)] = true; + renderedVnode = Object.cast(vnode.el); + + } else { + renderedVnode = r.render(vnode); + } + + renderedVNodes.push(renderedVnode); + target.el?.appendChild(renderedVnode); + } + + function destructor() { + for (let i = 0; i < renderedVNodes.length; i++) { + destroyNode(renderedVNodes[i]); + } + + function destroyNode(el: CanUndef) { + if (el == null) { + return; + } + + if (el[$$.cached] != null) { + delete el[$$.cached]; + $a.worker(() => destroyNode(el), {group}); + + } else { + const + els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; + + if (opts.destructor?.(el, els) !== true) { + that.destroy(el, els); + } + } + } + } + } } } From 325a69417b81917903967ac09ee884bdb7515222 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Sat, 21 May 2022 10:45:18 +0300 Subject: [PATCH 0152/2313] refactor: removed legacy --- src/core/component/construct/states/before-create.ts | 2 -- src/core/component/traverse/index.ts | 2 +- src/super/i-block/modules/field/index.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts index 76d30090d6..599936dfd7 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/construct/states/before-create.ts @@ -8,7 +8,6 @@ import Async from 'core/async'; -import { asyncLabel } from 'core/component/const'; import { getComponentContext } from 'core/component/context'; import { forkMeta } from 'core/component/meta'; @@ -54,7 +53,6 @@ export function beforeCreateState( unsafe.$refHandlers = {}; unsafe.$async = new Async(component); - unsafe.$asyncLabel = asyncLabel; const parent = unsafe.$parent, diff --git a/src/core/component/traverse/index.ts b/src/core/component/traverse/index.ts index 68247b60ff..5fc5d5e7a1 100644 --- a/src/core/component/traverse/index.ts +++ b/src/core/component/traverse/index.ts @@ -24,7 +24,7 @@ export function getNormalParent(component: ComponentInterface): CanUndef Date: Sat, 21 May 2022 10:45:33 +0300 Subject: [PATCH 0153/2313] refactor: better naming --- .../directives/async/target/index.ts | 2 +- src/core/component/render/wrappers.ts | 2 +- .../async-render/modules/iter-helpers.ts | 2 +- .../modules/async-render/modules/iter.ts | 20 +++++++++---------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/core/component/directives/async/target/index.ts b/src/core/component/directives/async/target/index.ts index c22431f13b..ed2477dada 100644 --- a/src/core/component/directives/async/target/index.ts +++ b/src/core/component/directives/async/target/index.ts @@ -19,6 +19,6 @@ export * from 'core/component/directives/async/async/interface'; ComponentEngine.directive('async-target', { beforeCreate(opts: DirectiveBinding, vnode: VNode): void { const ctx = Object.cast(opts.instance); - ctx.$emit('[[V-ASYNC-TARGET]]', vnode); + ctx.$emit('[[V_ASYNC_TARGET]]', vnode); } }); diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index ab89cfedca..6a51588595 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -78,7 +78,7 @@ export function wrapRenderList(original: T): T { src: Iterable | Dictionary, cb: AnyFunction ) { - this.$emit('[[V-FOR-CB]]', cb); + this.$emit('[[V_FOR_CB]]', cb); return original(src, cb); }); } diff --git a/src/super/i-block/modules/async-render/modules/iter-helpers.ts b/src/super/i-block/modules/async-render/modules/iter-helpers.ts index e5f5ecd2f8..4dfc92551f 100644 --- a/src/super/i-block/modules/async-render/modules/iter-helpers.ts +++ b/src/super/i-block/modules/async-render/modules/iter-helpers.ts @@ -105,7 +105,7 @@ export default class AsyncRender extends Super { }); } - return Promise.resolve(syncIterator.next()); + return Promise.resolve(iterable[Symbol.asyncIterator]().next()); }; return { diff --git a/src/super/i-block/modules/async-render/modules/iter.ts b/src/super/i-block/modules/async-render/modules/iter.ts index 5d6b57593c..42c595c110 100644 --- a/src/super/i-block/modules/async-render/modules/iter.ts +++ b/src/super/i-block/modules/async-render/modules/iter.ts @@ -87,11 +87,11 @@ export default class AsyncRender extends Super { toVNode: AnyFunction>, target: VNode; - ctx.$once('[[V-FOR-CB]]', setVNodeCompiler); - ctx.$once('[[V-ASYNC-TARGET]]', setTarget); + ctx.$once('[[V_FOR_CB]]', setVNodeCompiler); + ctx.$once('[[V_ASYNC_TARGET]]', setTarget); let - i = 0, + iterI = iter.readI + 1, chunkI = 0; let @@ -110,8 +110,8 @@ export default class AsyncRender extends Super { lastEvent; $a.setImmediate(async () => { - ctx.$off('[[V-FOR-CB]]', setVNodeCompiler); - ctx.$off('[[V-ASYNC-TARGET]]', setTarget); + ctx.$off('[[V_FOR_CB]]', setVNodeCompiler); + ctx.$off('[[V_ASYNC_TARGET]]', setTarget); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (target == null) { @@ -124,6 +124,7 @@ export default class AsyncRender extends Super { } // eslint-disable-next-line no-constant-condition + // TODO: add description while (true) { if (opts.group != null) { group = `asyncComponents:${opts.group}:${chunkI}`; @@ -149,12 +150,9 @@ export default class AsyncRender extends Super { iterVal = Object.isPromise(el.value) ? await $a.promise(el.value, {group}) : el.value; if (filter != null) { - const needRender = filter.call(this.ctx, iterVal, i, { + const needRender = filter.call(this.ctx, iterVal, iterI, { total, - - i: iter.readI + i + 1, chunk: chunkI, - iterable: iter.iterable }); @@ -181,7 +179,7 @@ export default class AsyncRender extends Super { } } - i++; + iterI++; } catch (err) { if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { @@ -255,7 +253,7 @@ export default class AsyncRender extends Super { for (let i = 0; i < valsToRender.length; i++) { const el = valsToRender[i], - vnodes = toVNode(el); + vnodes = toVNode(el, iterI); if (Object.isArray(vnodes)) { for (let i = 0; i < vnodes.length; i++) { From e118b1bc6e3788d7eddf2b6fe1f35ff9aba08297 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Sat, 21 May 2022 10:45:56 +0300 Subject: [PATCH 0154/2313] refactor: removed flyweight support --- src/super/i-block/i-block.ts | 182 +++++++---------------------------- 1 file changed, 37 insertions(+), 145 deletions(-) diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index c0a0659c08..04e1916c13 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -17,7 +17,6 @@ import symbolGenerator from 'core/symbol'; import SyncPromise from 'core/promise/sync'; import log, { LogMessageOptions } from 'core/log'; -import { deprecated } from 'core/functools/deprecation'; import { EventEmitter2 as EventEmitter } from 'eventemitter2'; import config from 'config'; @@ -65,9 +64,7 @@ import { Hook, ComponentInterface, - UnsafeGetter, - - VNode + UnsafeGetter } from 'core/component'; @@ -261,7 +258,7 @@ export default abstract class iBlock extends ComponentInterface { * Component stage value * @see [[iBlock.stageProp]] */ - @computed({replace: false}) + @computed() get stage(): CanUndef { return this.field.get('stageStore'); } @@ -295,7 +292,7 @@ export default abstract class iBlock extends ComponentInterface { /** * Group name of the current stage */ - @computed({replace: false}) + @computed() get stageGroup(): string { return `stage.${this.stage}`; } @@ -313,7 +310,6 @@ export default abstract class iBlock extends ComponentInterface { * @see [[iBlock.modsProp]] */ @system({ - replace: false, merge: mergeMods, init: initMods }) @@ -521,44 +517,11 @@ export default abstract class iBlock extends ComponentInterface { @prop({type: Object, required: false}) readonly p?: Dictionary; - /** - * Additional classes for the component elements. - * It can be useful if you need to attach some extra classes to internal component elements. - * Be sure you know what you are doing because this mechanism is tied to an internal component markup. - * - * @example - * ```js - * // Key names are tied with component elements, - * // and values contain a CSS class or list of classes we want to add - * - * { - * foo: 'bla', - * bar: ['bla', 'baz'] - * } - * ``` - */ @prop({type: Object, required: false}) - readonly classes?: Dictionary>; + override readonly classes?: Dictionary>; - /** - * Additional styles for the component elements. - * It can be useful if you need to attach some extra styles to internal component elements. - * Be sure you know what you are doing because this mechanism is tied to an internal component markup. - * - * @example - * ```js - * // Key names are tied with component elements, - * // and values contains a CSS style string, a style object or list of style strings - * - * { - * foo: 'color: red', - * bar: {color: 'blue'}, - * baz: ['color: red', 'background: green'] - * } - * ``` - */ @prop({type: Object, required: false}) - readonly styles?: Styles; + override readonly styles?: Dictionary | Dictionary>; /** * Link to a `i18n` function that will be used to localize string literals @@ -603,7 +566,7 @@ export default abstract class iBlock extends ComponentInterface { * *) destroyed - a component was destroyed: * this status can intersect with some hooks, like `beforeDestroy` or `destroyed`. */ - @computed({replace: false}) + @computed() get componentStatus(): ComponentStatus { return this.shadowComponentStatusStore ?? this.field.get('componentStatusStore') ?? 'unloaded'; } @@ -644,8 +607,6 @@ export default abstract class iBlock extends ComponentInterface { } } - // @deprecated - this.emit(`status-${value}`, value); this.emit(`componentStatus:${value}`, value, oldValue); this.emit('componentStatusChange', value, oldValue); } @@ -727,7 +688,7 @@ export default abstract class iBlock extends ComponentInterface { * True if the current component is completely ready to work. * The `ready` status is mean, that component was mounted an all data provider are loaded. */ - @computed({replace: false}) + @computed() get isReady(): boolean { return Boolean(readyStatuses[this.componentStatus]); } @@ -735,7 +696,7 @@ export default abstract class iBlock extends ComponentInterface { /** * True if the current component is a functional */ - @computed({replace: false}) + @computed() get isFunctional(): boolean { return this.meta.params.functional === true; } @@ -743,15 +704,15 @@ export default abstract class iBlock extends ComponentInterface { /** * True if the current component is a functional or flyweight */ - @computed({replace: false}) + @computed() get isNotRegular(): boolean { - return Boolean(this.isFunctional || this.isFlyweight); + return Boolean(this.isFunctional); } /** * True if the current component is rendered by using server-side rendering */ - @computed({replace: false}) + @computed() get isSSR(): boolean { return this.$renderEngine.supports.ssr; } @@ -763,7 +724,7 @@ export default abstract class iBlock extends ComponentInterface { * and you specify to the outer component some theme modifier. * This modifier will recursively provide to all child components. */ - @computed({replace: false}) + @computed() get baseMods(): CanUndef> { const m = this.mods; @@ -1070,7 +1031,6 @@ export default abstract class iBlock extends ComponentInterface { * @see [[iBlock.stageProp]] */ @field({ - replace: false, forceUpdate: false, functionalWatching: false, init: (o) => o.sync.link>((val) => { @@ -1111,7 +1071,6 @@ export default abstract class iBlock extends ComponentInterface { */ @field({ merge: true, - replace: false, functionalWatching: false, init: () => Object.create({}) }) @@ -1129,7 +1088,7 @@ export default abstract class iBlock extends ComponentInterface { * that can emit re-rendering of the component. * Don't use this getter outside the component template. */ - @computed({cache: true, replace: false}) + @computed({cache: true}) protected get m(): Readonly { return getWatchableMods(this); } @@ -1137,7 +1096,7 @@ export default abstract class iBlock extends ComponentInterface { /** * Cache object for `opt.ifOnce` */ - @system({merge: true, replace: false}) + @system({merge: true}) protected readonly ifOnceStore: Dictionary = {}; /** @@ -1146,7 +1105,6 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ merge: true, - replace: false, init: () => Object.createDict() }) @@ -1159,24 +1117,11 @@ export default abstract class iBlock extends ComponentInterface { @field({merge: true}) protected watchTmp: Dictionary = {}; - /** - * A render temporary cache. - * It's used with the `renderKey` directive. - */ - @system({ - merge: true, - replace: false, - init: () => Object.createDict() - }) - - protected override renderTmp!: Dictionary; - /** * Cache of watched values */ @system({ merge: true, - replace: false, init: () => Object.createDict() }) @@ -1185,7 +1130,7 @@ export default abstract class iBlock extends ComponentInterface { /** * Link to the current component */ - @computed({replace: false}) + @computed() protected get self(): this { return this; } @@ -1218,15 +1163,6 @@ export default abstract class iBlock extends ComponentInterface { protected readonly localEmitter!: EventEmitterWrapper; - /** - * @deprecated - * @see [[iBlock.localEmitter]] - */ - @deprecated({renamedTo: 'localEmitter'}) - get localEvent(): EventEmitterWrapper { - return this.localEmitter; - } - /** * Event emitter of a parent component */ @@ -1239,15 +1175,6 @@ export default abstract class iBlock extends ComponentInterface { protected readonly parentEmitter!: ReadonlyEventEmitterWrapper; - /** - * @deprecated - * @see [[iBlock.parentEmitter]] - */ - @deprecated({renamedTo: 'parentEmitter'}) - get parentEvent(): ReadonlyEventEmitterWrapper { - return this.parentEmitter; - } - /** * Event emitter of the root component */ @@ -1260,15 +1187,6 @@ export default abstract class iBlock extends ComponentInterface { protected readonly rootEmitter!: EventEmitterWrapper; - /** - * @deprecated - * @see [[iBlock.rootEmitter]] - */ - @deprecated({renamedTo: 'rootEmitter'}) - get rootEvent(): ReadonlyEventEmitterWrapper { - return this.rootEmitter; - } - /** * The global event emitter of an application. * It can be used to provide external events to a component. @@ -1282,15 +1200,6 @@ export default abstract class iBlock extends ComponentInterface { protected readonly globalEmitter!: EventEmitterWrapper; - /** - * @deprecated - * @see [[iBlock.globalEmitter]] - */ - @deprecated({renamedTo: 'globalEmitter'}) - get globalEvent(): ReadonlyEventEmitterWrapper { - return this.globalEmitter; - } - /** * A map of extra helpers. * It can be useful to provide some helper functions to a component. @@ -1298,7 +1207,6 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - replace: true, init: () => { //#if runtime has core/helpers return helpers; @@ -1319,7 +1227,6 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - replace: true, init: () => { //#if runtime has core/browser return browser; @@ -1340,18 +1247,11 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - replace: true, init: () => presets }) protected readonly presets!: typeof presets; - /** @see [[iBlock.presets]] */ - @deprecated({renamedTo: 'presets'}) - get preset(): typeof presets { - return this.presets; - } - /** * Number of `beforeReady` event listeners: * it's used to optimize component initializing @@ -1369,7 +1269,7 @@ export default abstract class iBlock extends ComponentInterface { /** * Alias for `i18n` */ - @computed({replace: false}) + @computed() protected get t(): this['i18n'] { return this.i18n; } @@ -1379,8 +1279,7 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ atom: true, - unique: true, - replace: true + unique: true }) protected readonly l: typeof l = globalThis.l; @@ -1391,7 +1290,6 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - replace: true, init: () => console }) @@ -1403,7 +1301,6 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - replace: true, init: () => location }) @@ -1415,7 +1312,6 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - replace: true, init: () => globalThis }) @@ -1591,7 +1487,7 @@ export default abstract class iBlock extends ComponentInterface { opts?: AsyncWatchOptions ): void; - @p({replace: false}) + @p() watch( path: WatchPath | object, optsOrHandler: AsyncWatchOptions | RawWatchHandler, @@ -1600,7 +1496,7 @@ export default abstract class iBlock extends ComponentInterface { const {async: $a} = this; - if (this.isFlyweight || this.isSSR) { + if (this.isSSR) { return; } @@ -1675,7 +1571,7 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param args */ - @p({replace: false}) + @p() emit(event: string | ComponentEvent, ...args: unknown[]): void { const eventDecl = Object.isString(event) ? {event} : event, @@ -1714,7 +1610,7 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param args */ - @p({replace: false}) + @p() emitError(event: string, ...args: unknown[]): void { this.emit({event, type: 'error'}, ...args); } @@ -1725,7 +1621,7 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param args */ - @p({replace: false}) + @p() dispatch(event: string | ComponentEvent, ...args: unknown[]): void { const eventDecl = Object.isString(event) ? {event} : event, @@ -1787,7 +1683,7 @@ export default abstract class iBlock extends ComponentInterface { * @param handler * @param [opts] - additional options */ - @p({replace: false}) + @p() on(event: string, handler: ProxyCb, opts?: AsyncOptions): void { event = event.dasherize(); @@ -1807,7 +1703,7 @@ export default abstract class iBlock extends ComponentInterface { * @param handler * @param [opts] - additional options */ - @p({replace: false}) + @p() once(event: string, handler: ProxyCb, opts?: AsyncOptions): void { event = event.dasherize(); @@ -1826,7 +1722,7 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param [opts] - additional options */ - @p({replace: false}) + @p() promisifyOnce(event: string, opts?: AsyncOptions): Promise { return this.async.promisifyOnce(this, event.dasherize(), opts); } @@ -1847,7 +1743,7 @@ export default abstract class iBlock extends ComponentInterface { */ off(opts: ClearOptionsId): void; - @p({replace: false}) + @p() off(eventOrParams?: string | ClearOptionsId, handler?: Function): void { const e = eventOrParams; @@ -1885,7 +1781,7 @@ export default abstract class iBlock extends ComponentInterface { opts?: WaitDecoratorOptions ): CanPromise>; - @p({replace: false}) + @p() waitStatus>( status: ComponentStatus, cbOrOpts?: F | WaitDecoratorOptions, @@ -2159,7 +2055,7 @@ export default abstract class iBlock extends ComponentInterface { */ setMod(name: string, value: unknown): CanPromise; - @p({replace: false}) + @p() setMod(nodeOrName: Element | string, name: string | unknown, value?: unknown): CanPromise { if (Object.isString(nodeOrName)) { const res = this.lfc.execCbAfterBlockReady(() => this.block!.setMod(nodeOrName, name)); @@ -2187,7 +2083,7 @@ export default abstract class iBlock extends ComponentInterface { */ removeMod(name: string, value?: unknown): CanPromise; - @p({replace: false}) + @p() removeMod(nodeOrName: Element | string, name?: string | unknown, value?: unknown): CanPromise { if (Object.isString(nodeOrName)) { const res = this.lfc.execCbAfterBlockReady(() => this.block!.removeMod(nodeOrName, name)); @@ -2220,7 +2116,7 @@ export default abstract class iBlock extends ComponentInterface { * console.log(document.documentElement.classList.contains('b-az-foo-bla')); * ``` */ - @p({replace: false}) + @p() setRootMod(name: string, value: unknown): boolean { return this.r.setRootMod(name, value, this); } @@ -2244,7 +2140,7 @@ export default abstract class iBlock extends ComponentInterface { * console.log(document.documentElement.classList.contains('b-az-foo-bla') === false); * ``` */ - @p({replace: false}) + @p() removeRootMod(name: string, value?: unknown): boolean { return this.r.removeRootMod(name, value, this); } @@ -2261,7 +2157,7 @@ export default abstract class iBlock extends ComponentInterface { * console.log(this.getRootMod('foo') === 'bla-bar'); * ``` */ - @p({replace: false}) + @p() getRootMod(name: string): CanUndef { return this.r.getRootMod(name, this); } @@ -2283,7 +2179,7 @@ export default abstract class iBlock extends ComponentInterface { * @param ctxOrOpts * @param details */ - @p({replace: false}) + @p() override log(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void { let context = ctxOrOpts, @@ -2419,7 +2315,7 @@ export default abstract class iBlock extends ComponentInterface { * @param ref - ref name * @param [opts] - additional options */ - @p({replace: false}) + @p() protected waitRef>(ref: string, opts?: AsyncOptions): Promise { let that = this; @@ -2467,7 +2363,7 @@ export default abstract class iBlock extends ComponentInterface { * Initializes an instance of the `Block` class for the current component */ @hook('mounted') - @p({replace: false}) + @p() protected initBlockInstance(): void { if (this.block != null) { const @@ -2561,7 +2457,7 @@ export default abstract class iBlock extends ComponentInterface { } protected override onCreatedHook(): void { - if (this.isFlyweight || this.isSSR) { + if (this.isSSR) { this.componentStatusStore = 'ready'; this.isReadyOnce = true; } @@ -2579,10 +2475,6 @@ export default abstract class iBlock extends ComponentInterface { try { await this.nextTick({label: $$.onUpdateHook}); - if (this.isFlyweight) { - this.$el?.component?.onUnbindHook(); - } - this.onBindHook(); this.onInsertedHook(); @@ -2620,7 +2512,7 @@ export default abstract class iBlock extends ComponentInterface { /** * Hook handler: component will be destroyed */ - @p({replace: false}) + @p() protected beforeDestroy(): void { this.componentStatus = 'destroyed'; this.async.clearAll().locked = true; From c0678f1ad8fe4f97a79b729304a07aae6168a9d8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 12:03:22 +0300 Subject: [PATCH 0155/2313] docs: improved doc --- .../i-block/modules/async-render/interface.ts | 158 +++++++++++++----- 1 file changed, 119 insertions(+), 39 deletions(-) diff --git a/src/super/i-block/modules/async-render/interface.ts b/src/super/i-block/modules/async-render/interface.ts index a893ccd1a2..a288a1974d 100644 --- a/src/super/i-block/modules/async-render/interface.ts +++ b/src/super/i-block/modules/async-render/interface.ts @@ -6,27 +6,41 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export interface TaskParams { +/** + * Additional options for a render task + * + * @typeparam El - a data element to render + * @typeparam D - a data collection to render + */ +export interface TaskOptions { /** - * If true, then rendered chunks are inserted into DOM on the `requestAnimationFrame` callback. - * It may optimize the process of browser rendering. + * The weight of one render chunk. + * At the same tick can be rendered chunks with the accumulated weight no more than the `TASKS_PER_TICK` constant. + * + * @see core/component/render/daemon + */ + weight?: number; + + /** + * If true, then all rendered fragments are inserted into the DOM by using a `requestAnimationFrame` callback. + * This can optimize the browser rendering process. * * @default `false` */ useRAF?: boolean; /** - * A group name to manual clearing of pending tasks via `async`. - * Providing this value disables automatically canceling of rendering task on the `update` hook. + * A group name to manual clearing of pending tasks via the `async` module. + * Providing this value disables automatically cleanup of render tasks on the `update` hook. * * @example * ``` - * /// Iterate over only even values - * < .bla v-for = el in asyncRender.iterate(100, 10, {group: 'listRendering'}) - * {{ el }} + * < .bla v-async-target + * < template v-for = el in asyncRender.iterate(100, 10, {group: 'listRendering'}) + * {{ el }} * - * /// Notice that we use RegExp to clear tasks. - * /// Because each group has a group based on a template `asyncComponents:listRendering:${chunkIndex}`. + * /// We should use a RegExp to clear tasks, + * /// because each group has a group based on a template `asyncComponents:listRendering:${chunkIndex}`. * < button @click = async.clearAll({group: /:listRendering/}) * Cancel rendering * ``` @@ -34,26 +48,22 @@ export interface TaskParams el % 2 === 0}) * {{ el }} * - * /// Render each element only after the previous with the specified delay + * /// Render each element with the specified delay * < .bla v-for = el in asyncRender.iterate(100, {filter: (el) => async.sleep(100)}) * {{ el }} * - * /// Render a chunk on the specified event + * /// Render each element after the specified event * < .bla v-for = el in asyncRender.iterate(100, 20, {filter: (el) => promisifyOnce('renderNextChunk')}) * {{ el }} * @@ -61,47 +71,117 @@ export interface TaskParams; + filter?: TaskFilter; /** - * The destructor of a rendered element. - * It will be invoked before removing each async rendered element from DOM. - * - * - If the function returns `true` then the `destroy` method of the `asyncRender` module will not be called - * - Any value other than `true` will cause the `destroy` method to be called + * The destructor of a rendered fragment. + * It will be called before each asynchronously rendered fragment is removed from the DOM. + * If the function returns true, the internal destructor of the `asyncRender` module won’t be called. */ - destructor?: ElementDestructor; + destructor?: NodeDestructor; } -export interface TaskI { +/** + * An element of the render task + */ +export interface TaskEl { + /** + * The original structure that we iterate + */ iterable: Iterable; + + /** + * An iteration index + */ i: number; - total: number; - chunk?: number; -} -export interface TaskFilter { - (): CanPromise; - (el: EL, i: I, task: TaskI): CanPromise; -} + /** + * Number of rendered tasks + */ + total: number; -export interface ElementDestructor { - (el: Node, childComponentEls: Element[]): AnyToIgnore; + /** + * An index of the render chunk that own this operation + */ + chunk?: number; } +/** + * Additional options to render an iterable structure + */ export interface IterOptions { + /** + * A start index to iterate + */ start?: number; + + /** + * How many fragments can be rendered at the same time + */ perChunk?: number; + + /** + * A function to filter elements to render + */ filter?: TaskFilter; } +/** + * A filter function for render tasks + */ +export interface TaskFilter { + (): CanPromise; + + /** + * @param el - a data element to render + * @param i - an iteration index + * @param task - an element of the render task + */ + (el: E, i: number, task: TaskEl): CanPromise; +} + +/** + * A function to destroy the unmounted node + */ +export interface NodeDestructor { + /** + * @param node - a node to remove + * @param childComponentEls - root elements of the child components + */ + (node: Node, childComponentEls: Element[]): void; +} + +/** + * A descriptor of the iterable-based rendering structure + */ export interface IterDescriptor { + /** + * Is this iterator asynchronous or not + */ isAsync: boolean; + /** + * An index of the last synchronously read element for the first render + */ readI: number; + + /** + * Number of synchronously read elements for the first render + */ readTotal: number; + + /** + * An array of the synchronously read elements for the first render + */ readEls: unknown[]; + /** + * The original structure that we iterate + */ iterable: CanPromise; - iterator: IterableIterator | AsyncIterableIterator; + + /** + * An iterator for the structure that we iterate + */ + iterator: AnyIterableIterator; } From ab111fa481a21d3611842344fbf7b2299ea9ff85 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 13:35:16 +0300 Subject: [PATCH 0156/2313] review: use new tree-shake friendly api --- .../i-block/modules/async-render/class.ts | 23 + .../i-block/modules/async-render/index.ts | 10 +- .../i-block/modules/async-render/interface.ts | 2 +- .../i-block/modules/async-render/iter.ts | 322 +++++++++++++ .../modules/async-render/modules/base.ts | 182 -------- .../async-render/modules/iter-helpers.ts | 204 --------- .../modules/async-render/modules/iter.ts | 428 +++++++----------- .../modules/async-render/modules/render.ts | 92 ++++ .../i-block/modules/async-render/render.ts | 90 ++++ 9 files changed, 693 insertions(+), 660 deletions(-) create mode 100644 src/super/i-block/modules/async-render/class.ts create mode 100644 src/super/i-block/modules/async-render/iter.ts delete mode 100644 src/super/i-block/modules/async-render/modules/base.ts delete mode 100644 src/super/i-block/modules/async-render/modules/iter-helpers.ts create mode 100644 src/super/i-block/modules/async-render/modules/render.ts create mode 100644 src/super/i-block/modules/async-render/render.ts diff --git a/src/super/i-block/modules/async-render/class.ts b/src/super/i-block/modules/async-render/class.ts new file mode 100644 index 0000000000..44a16d451e --- /dev/null +++ b/src/super/i-block/modules/async-render/class.ts @@ -0,0 +1,23 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type iBlock from 'super/i-block/i-block'; + +import Friend from 'super/i-block/modules/friend'; + +export default class AsyncRender extends Friend { + constructor(component: iBlock) { + super(component); + + this.meta.hooks.beforeUpdate.push({ + fn: () => this.async.clearAll({ + group: 'asyncComponents' + }) + }); + } +} diff --git a/src/super/i-block/modules/async-render/index.ts b/src/super/i-block/modules/async-render/index.ts index 4f83b992d9..06aed98fc2 100644 --- a/src/super/i-block/modules/async-render/index.ts +++ b/src/super/i-block/modules/async-render/index.ts @@ -11,12 +11,8 @@ * @packageDocumentation */ -import Super from 'super/i-block/modules/async-render/modules/iter'; +export { default } from 'super/i-block/modules/async-render/class'; -export * from 'super/i-block/modules/async-render/modules/iter'; +export * from 'super/i-block/modules/async-render/iter'; +export * from 'super/i-block/modules/async-render/render'; export * from 'super/i-block/modules/async-render/interface'; - -/** - * Class provides API to render chunks of a component template asynchronously - */ -export default class AsyncRender extends Super {} diff --git a/src/super/i-block/modules/async-render/interface.ts b/src/super/i-block/modules/async-render/interface.ts index a288a1974d..0b8c1c6bc2 100644 --- a/src/super/i-block/modules/async-render/interface.ts +++ b/src/super/i-block/modules/async-render/interface.ts @@ -148,7 +148,7 @@ export interface NodeDestructor { * @param node - a node to remove * @param childComponentEls - root elements of the child components */ - (node: Node, childComponentEls: Element[]): void; + (node: Node, childComponentEls: Element[]): AnyToBoolean; } /** diff --git a/src/super/i-block/modules/async-render/iter.ts b/src/super/i-block/modules/async-render/iter.ts new file mode 100644 index 0000000000..f0dcc274c1 --- /dev/null +++ b/src/super/i-block/modules/async-render/iter.ts @@ -0,0 +1,322 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { ComponentElement, VNode } from 'super/i-block/i-block'; +import type AsyncRender from 'super/i-block/modules/async-render/class'; + +import { addRenderTask, destroyNode as nodeDestructor } from 'super/i-block/modules/async-render/modules/render'; +import { getIterDescriptor } from 'super/i-block/modules/async-render/modules/iter'; + +import type { TaskOptions } from 'super/i-block/modules/async-render/interface'; + +const + isCached = Symbol('Is cached') + +/** + * Creates an asynchronous render stream from the specified value. + * It returns a list of element to the first synchronous render. + * + * This function helps optimize component rendering by splitting big render tasks into smaller ones. + * + * @param value + * @param [sliceOrOpts] - elements per chunk or `[start position, elements per chunk]` or additional options + * @param [opts] - additional options + * + * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & {renderGroup: string})` + * @emits `localEmitter.asyncRenderComplete(e: TaskParams & {renderGroup: string})` + * + * @example + * ``` + * /// Where to append asynchronous elements + * < .target v-async-target + * /// Asynchronous rendering of components: only five elements per chunk + * < template v-for = el in asyncRender.iterate(largeList, 5) + * < my-component :data = el + * ``` + */ +export function iterate( + this: AsyncRender, + value: unknown, + sliceOrOpts: number | [number?, number?] | TaskOptions = 1, + opts: TaskOptions = {} +): unknown[] { + if (value == null) { + return []; + } + + const + that = this; + + const { + ctx, + ctx: {$renderEngine: {r}}, + + async: $a, + localEmitter + } = this; + + if (Object.isPlainObject(sliceOrOpts)) { + opts = sliceOrOpts; + sliceOrOpts = []; + } + + const + {filter, weight = 1} = opts; + + let + start, + perChunk; + + if (Object.isArray(sliceOrOpts)) { + start = sliceOrOpts[0]; + perChunk = sliceOrOpts[1]; + + } else { + perChunk = sliceOrOpts; + } + + const + iter = getIterDescriptor.call(this, value, {start, perChunk, filter}); + + let + toVNode: AnyFunction>, + target: VNode; + + ctx.$once('[[V_FOR_CB]]', setVNodeCompiler); + ctx.$once('[[V_ASYNC_TARGET]]', setTarget); + + let + iterI = iter.readI + 1, + chunkI = 0; + + let + total = iter.readTotal, + chunkTotal = 0; + + let + awaiting = 0; + + let + group = 'asyncComponents', + valsToRender: unknown[] = []; + + let + lastTask, + lastEvent; + + $a.setImmediate(async () => { + ctx.$off('[[V_FOR_CB]]', setVNodeCompiler); + ctx.$off('[[V_ASYNC_TARGET]]', setTarget); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (target == null) { + throw new ReferenceError('There is no host node to append asynchronously render elements'); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (toVNode == null) { + return; + } + + // eslint-disable-next-line no-constant-condition + // Using `while` instead of `for of` helps to iterate over synchronous and asynchronous iterators with a single loop + while (true) { + if (opts.group != null) { + group = `asyncComponents:${opts.group}:${chunkI}`; + } + + let + el = iter.iterator.next(); + + try { + el = Object.isPromise(el) ? await $a.promise(el, {group}) : el; + + if (el.done) { + break; + } + + } catch (err) { + stderr(err); + break; + } + + try { + const + iterVal = Object.isPromise(el.value) ? await $a.promise(el.value, {group}) : el.value; + + if (filter != null) { + const needRender = filter.call(this.ctx, iterVal, iterI, { + total, + chunk: chunkI, + iterable: iter.iterable + }); + + if (Object.isPromise(needRender)) { + await $a.promise(needRender, {group}).then( + (res) => resolveTask(iterVal, res === undefined || Object.isTruly(res)) + ); + + } else { + const + res = resolveTask(iterVal, Object.isTruly(needRender)); + + if (res != null) { + await res; + } + } + + } else { + const + res = resolveTask(iterVal); + + if (res != null) { + await res; + } + } + + iterI++; + + } catch (err) { + if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { + break; + } + + stderr(err); + } + } + + if (lastTask != null) { + awaiting++; + + const + res = lastTask(); + + if (res != null) { + await res; + } + } + + if (awaiting <= 0) { + localEmitter.emit('asyncRenderComplete', lastEvent); + + } else { + const id = localEmitter.on('asyncRenderChunkComplete', () => { + if (awaiting <= 0) { + localEmitter.emit('asyncRenderComplete', lastEvent); + localEmitter.off(id); + } + }); + } + }); + + return iter.readEls; + + function setVNodeCompiler(c: AnyFunction) { + toVNode = c; + } + + function setTarget(t: VNode) { + target = t; + } + + function resolveTask(iterVal: unknown, filter?: boolean) { + if (filter === false) { + return; + } + + total++; + chunkTotal++; + valsToRender.push(iterVal); + + lastTask = () => { + lastTask = null; + awaiting++; + + return addRenderTask.call(that, task, {group, weight}); + }; + + if (!Object.isPromise(iterVal) && chunkTotal < perChunk) { + return; + } + + return lastTask(); + + function task() { + const + renderedVNodes: Node[] = []; + + for (let i = 0; i < valsToRender.length; i++) { + const + el = valsToRender[i], + vnodes = toVNode(el, iterI); + + if (Object.isArray(vnodes)) { + for (let i = 0; i < vnodes.length; i++) { + renderVNode(vnodes[i]); + } + + } else { + renderVNode(vnodes); + } + } + + valsToRender = []; + + chunkI++; + chunkTotal = 0; + awaiting--; + + lastEvent = {...opts, renderGroup: group}; + localEmitter.emit('asyncRenderChunkComplete', lastEvent); + + $a.worker(destructor, {group}); + + function renderVNode(vnode: VNode) { + let + renderedVnode: Node; + + if (vnode.el != null) { + vnode.el[Object.cast(isCached)] = true; + renderedVnode = Object.cast(vnode.el); + + } else { + renderedVnode = r.render(vnode); + } + + renderedVNodes.push(renderedVnode); + target.el?.appendChild(renderedVnode); + } + + function destructor() { + for (let i = 0; i < renderedVNodes.length; i++) { + destroyNode(renderedVNodes[i]); + } + + function destroyNode(el: CanUndef) { + if (el == null) { + return; + } + + if (el[isCached] != null) { + delete el[isCached]; + $a.worker(() => destroyNode(el), {group}); + + } else { + const + els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; + + if (opts.destructor?.(el, els) !== true) { + nodeDestructor.call(that, el, els); + } + } + } + } + } + } +} + diff --git a/src/super/i-block/modules/async-render/modules/base.ts b/src/super/i-block/modules/async-render/modules/base.ts deleted file mode 100644 index 95b1dc2473..0000000000 --- a/src/super/i-block/modules/async-render/modules/base.ts +++ /dev/null @@ -1,182 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import SyncPromise from 'core/promise/sync'; - -//#if runtime has component/async-render -import { queue, restart, deferRestart } from 'core/component/render/daemon'; -//#endif - -import type iBlock from 'super/i-block/i-block'; -import type { ComponentElement } from 'super/i-block/i-block'; - -import Friend from 'super/i-block/modules/friend'; -import type { TaskParams } from 'super/i-block/modules/async-render/interface'; - -export default class AsyncRender extends Friend { - //#if runtime has component/async-render - - constructor(component: iBlock) { - super(component); - - this.meta.hooks.beforeUpdate.push({ - fn: () => this.async.clearAll({ - group: 'asyncComponents' - }) - }); - } - - /** - * Restarts the `asyncRender` daemon to force rendering - */ - forceRender(): void { - restart(); - this.localEmitter.emit('forceRender'); - } - - /** - * Restarts the `asyncRender` daemon to force rendering - * (runs on the next tick) - */ - deferForceRender(): void { - deferRestart(); - this.localEmitter.emit('forceRender'); - } - - /** - * Returns a function that returns a promise that will be resolved after firing the `forceRender` event. - * The method can take an element name as the first parameter. This element will be dropped before resolving. - * - * Notice, the initial component rendering is mean the same as `forceRender`. - * The method is useful to re-render a function component without touching the parent state. - * - * @param elementToDrop - element to drop before resolving the promise - * (if the element is passed as a function, it would be invoked) - * - * @example - * ``` - * < button @click = asyncRender.forceRender() - * Re-render the component - * - * < .&__wrapper - * < template v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')}) - * < .&__content - * {{ Math.random() }} - * ``` - */ - waitForceRender( - elementToDrop?: string | ((ctx: this['component']) => CanPromise>) - ): () => CanPromise { - return () => { - const - canImmediateRender = this.lfc.isBeforeCreate() || this.hook === 'beforeMount'; - - if (canImmediateRender) { - return true; - } - - return this.localEmitter.promisifyOnce('forceRender').then(async () => { - if (elementToDrop != null) { - let - el; - - if (Object.isFunction(elementToDrop)) { - el = await elementToDrop(this.ctx); - - } else { - el = elementToDrop; - } - - if (Object.isString(el)) { - this.block?.element(el)?.remove(); - - } else { - el?.remove(); - } - } - - return true; - }); - }; - } - - /** - * Removes the given element from the DOM tree and destroys all tied components - * - * @param el - * @param [childComponentEls] - list of child component nodes - */ - protected destroy(el: Node, childComponentEls: Element[] = []): void { - el.parentNode?.removeChild(el); - - for (let i = 0; i < childComponentEls.length; i++) { - const - el = childComponentEls[i]; - - try { - (>el).component?.unsafe.$destroy(); - - } catch (err) { - stderr(err); - } - } - - try { - (>el).component?.unsafe.$destroy(); - - } catch (err) { - stderr(err); - } - } - - /** - * Creates a render task by the specified parameters - * - * @param taskFn - * @param [params] - */ - protected createTask(taskFn: AnyFunction, params: TaskParams = {}): Promise { - const - {async: $a} = this; - - const - group = params.group ?? 'asyncComponents'; - - return new SyncPromise((resolve, reject) => { - const task = { - weight: params.weight, - - fn: $a.proxy(() => { - const cb = () => { - taskFn(); - resolve(); - return true; - }; - - if (params.useRAF) { - return $a.animationFrame({group}).then(cb); - } - - return cb(); - - }, { - group, - single: false, - onClear: (err) => { - queue.delete(task); - reject(err); - } - }) - }; - - queue.add(task); - }); - } - - //#endif -} diff --git a/src/super/i-block/modules/async-render/modules/iter-helpers.ts b/src/super/i-block/modules/async-render/modules/iter-helpers.ts deleted file mode 100644 index 4dfc92551f..0000000000 --- a/src/super/i-block/modules/async-render/modules/iter-helpers.ts +++ /dev/null @@ -1,204 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import Range from 'core/range'; - -import Super from 'super/i-block/modules/async-render/modules/base'; -import type { IterOptions, IterDescriptor } from 'super/i-block/modules/async-render/interface'; - -export * from 'super/i-block/modules/async-render/modules/base'; - -export default class AsyncRender extends Super { - //#if runtime has component/async-render - - /** - * Returns an iterator descriptor object based on the passed value and options - * - * @param value - * @param [start] - start positions to iterate - * @param [perChunk] - elements per one iteration chunk - * @param [filter] - function to filter iteration elements - */ - protected getIterDescriptor(value: unknown, {start = 0, perChunk = 1, filter}: IterOptions = {}): IterDescriptor { - const - iterable = this.getIterable(value, filter != null), - isAsyncIterable = Object.isPromise(iterable) || Object.isAsyncIterable(iterable), - syncIterator: Iterator = isAsyncIterable ? [].values() : iterable[Symbol.iterator](); - - const - readEls: unknown[] = [], - discardedReadEls: unknown[] = []; - - let - readI = 0, - readTotal = 0, - lastReadValue: CanUndef> = undefined; - - if (!isAsyncIterable) { - // eslint-disable-next-line no-multi-assign - for (let o = syncIterator, el = lastReadValue = o.next(); !el.done; el = o.next(), readI++) { - if (start > 0) { - start--; - continue; - } - - const - iterVal = el.value; - - let - valIsPromise = Object.isPromise(iterVal), - canRender = !valIsPromise; - - if (canRender && filter != null) { - canRender = filter.call(this.component, iterVal, readI, { - iterable, - i: readI, - total: readTotal - }); - - if (Object.isPromise(canRender)) { - valIsPromise = true; - canRender = false; - - } else if (!Object.isTruly(canRender)) { - canRender = false; - } - } - - if (canRender) { - readTotal++; - readEls.push(iterVal); - - } else if (valIsPromise) { - discardedReadEls.push(iterVal); - } - - if (readTotal >= perChunk || valIsPromise) { - break; - } - } - } - - return { - isAsync: isAsyncIterable, - - readI, - readTotal, - readEls, - - iterable, - iterator: createFinalIterator() - }; - - function createFinalIterator() { - if (isAsyncIterable) { - const next = () => { - if (Object.isPromise(iterable)) { - return iterable.then((i) => { - const iter = i[Object.isAsyncIterable(i) ? Symbol.asyncIterator : Symbol.iterator](); - return Promise.resolve(iter.next()); - }); - } - - return Promise.resolve(iterable[Symbol.asyncIterator]().next()); - }; - - return { - [Symbol.asyncIterator]() { - return this; - }, - - next - }; - } - - let - i = 0; - - const next = () => { - if (discardedReadEls.length === 0 && lastReadValue?.done) { - return lastReadValue; - } - - if (i < discardedReadEls.length) { - return { - value: discardedReadEls[i++], - done: false - }; - } - - return syncIterator.next(); - }; - - return { - [Symbol.iterator]() { - return this; - }, - - next - }; - } - } - - /** - * Returns an iterable structure based on the passed value - * - * @param value - * @param [hasFilter] - true if the passed value will be filtered - */ - protected getIterable(value: unknown, hasFilter?: boolean): CanPromise | Iterable> { - if (value == null) { - return []; - } - - if (value === true) { - if (hasFilter) { - return new Range(0, Infinity); - } - - return []; - } - - if (value === false) { - if (hasFilter) { - return new Range(0, -Infinity); - } - - return []; - } - - if (Object.isNumber(value)) { - return new Range(0, [value]); - } - - if (Object.isArray(value)) { - return value; - } - - if (Object.isString(value)) { - return value.letters(); - } - - if (Object.isPromise(value)) { - return value.then(this.getIterable.bind(this)); - } - - if (typeof value === 'object') { - if (Object.isIterable(value) || Object.isAsyncIterable(value)) { - return value; - } - - return Object.entries(value); - } - - return [value]; - } - - //#endif -} - diff --git a/src/super/i-block/modules/async-render/modules/iter.ts b/src/super/i-block/modules/async-render/modules/iter.ts index 42c595c110..2e95552f4b 100644 --- a/src/super/i-block/modules/async-render/modules/iter.ts +++ b/src/super/i-block/modules/async-render/modules/iter.ts @@ -6,320 +6,216 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import symbolGenerator from 'core/symbol'; -import type { ComponentElement, VNode } from 'super/i-block/i-block'; - -import Super from 'super/i-block/modules/async-render/modules/iter-helpers'; -import type { TaskParams } from 'super/i-block/modules/async-render/interface'; - -export * from 'super/i-block/modules/async-render/modules/iter-helpers'; - -export const - $$ = symbolGenerator(); - -export default class AsyncRender extends Super { - //#if runtime has component/async-render - - /** - * Creates an asynchronous render stream from the specified value. - * This method helps to optimize component rendering by splitting big render tasks into little. - * - * @param value - * @param [sliceOrOpts] - elements per chunk or `[start position, elements per chunk]` or additional options - * @param [opts] - additional options - * - * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & {renderGroup: string})` - * @emits `localEmitter.asyncRenderComplete(e: TaskParams & {renderGroup: string})` - * - * @example - * ``` - * /// Where to append asynchronous elements - * < .target v-async-target - * /// Asynchronous rendering of components: only five elements per chunk - * < template v-for = el in asyncRender.iterate(largeList, 5) - * < my-component :data = el - * ``` - */ - iterate( - value: unknown, - sliceOrOpts: number | [number?, number?] | TaskParams = 1, - opts: TaskParams = {} - ): unknown[] { - if (value == null) { - return []; - } - - const - that = this; - - const { - ctx, - ctx: {$renderEngine: {r}}, - - async: $a, - localEmitter - } = this; - - if (Object.isPlainObject(sliceOrOpts)) { - opts = sliceOrOpts; - sliceOrOpts = []; - } - - const - {filter, weight = 1} = opts; - - let - start, - perChunk; - - if (Object.isArray(sliceOrOpts)) { - start = sliceOrOpts[0]; - perChunk = sliceOrOpts[1]; - - } else { - perChunk = sliceOrOpts; - } - - const - iter = this.getIterDescriptor(value, {start, perChunk, filter}); - - let - toVNode: AnyFunction>, - target: VNode; +import Range from 'core/range'; +import { seq } from 'core/iter/combinators'; - ctx.$once('[[V_FOR_CB]]', setVNodeCompiler); - ctx.$once('[[V_ASYNC_TARGET]]', setTarget); +import type AsyncRender from 'super/i-block/modules/async-render/class'; +import type { IterOptions, IterDescriptor } from 'super/i-block/modules/async-render/interface'; - let - iterI = iter.readI + 1, - chunkI = 0; - - let - total = iter.readTotal, - chunkTotal = 0; - - let - awaiting = 0; - - let - group = 'asyncComponents', - valsToRender: unknown[] = []; +/** + * Returns an iterator descriptor based on the passed value and options + * + * @param value + * @param [start] + * @param [perChunk] + * @param [filter] + */ +export function getIterDescriptor( + this: AsyncRender, + value: unknown, + {start = 0, perChunk = 1, filter}: IterOptions = {} +): IterDescriptor { + const + iterable = getIterable.call(this, value, filter != null); + + let + iterator: AnyIterableIterator, + isAsyncIterable = false; + + const + readEls: unknown[] = [], + discardedReadEls: unknown[] = []; + + let + readI = 0, + readTotal = 0, + lastReadValue: CanUndef> = undefined; + + if (Object.isAsyncIterable(iterable)) { + isAsyncIterable = true; + iterator = Object.cast(iterable[Symbol.asyncIterator]()); + + } else if (Object.isPromise(iterable)) { + isAsyncIterable = true; let - lastTask, - lastEvent; + innerIter, + pendedResult; - $a.setImmediate(async () => { - ctx.$off('[[V_FOR_CB]]', setVNodeCompiler); - ctx.$off('[[V_ASYNC_TARGET]]', setTarget); + iterator = { + [Symbol.asyncIterator]() { + return this; + }, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (target == null) { - throw new ReferenceError('There is no host node to append asynchronously render elements'); - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (toVNode == null) { - return; - } - - // eslint-disable-next-line no-constant-condition - // TODO: add description - while (true) { - if (opts.group != null) { - group = `asyncComponents:${opts.group}:${chunkI}`; + next: () => { + if (innerIter != null) { + return Promise.resolve(innerIter.next()); } - let - el = iter.iterator.next(); - - try { - el = Object.isPromise(el) ? await $a.promise(el, {group}) : el; - - if (el.done) { - break; - } - - } catch (err) { - stderr(err); - break; + if (pendedResult != null) { + return pendedResult; } - try { - const - iterVal = Object.isPromise(el.value) ? await $a.promise(el.value, {group}) : el.value; - - if (filter != null) { - const needRender = filter.call(this.ctx, iterVal, iterI, { - total, - chunk: chunkI, - iterable: iter.iterable - }); + const res = (>iterable).then(async (v) => { + const i = await getIterable.call(this, v); + innerIter = i[Object.isAsyncIterable(i) ? Symbol.asyncIterator : Symbol.iterator](); + return innerIter.next(); + }); - if (Object.isPromise(needRender)) { - await $a.promise(needRender, {group}).then( - (res) => resolveTask(iterVal, res === undefined || Object.isTruly(res)) - ); + pendedResult = res; + return res; + } + }; - } else { - const - res = resolveTask(iterVal, Object.isTruly(needRender)); + } else { + iterator = >iterable[Symbol.iterator](); - if (res != null) { - await res; - } - } + // eslint-disable-next-line no-multi-assign + for (let o = iterator, el = lastReadValue = o.next(); !el.done; el = o.next(), readI++) { + if (start > 0) { + start--; + continue; + } - } else { - const - res = resolveTask(iterVal); + const + iterVal = el.value; - if (res != null) { - await res; - } - } + let + valIsPromise = Object.isPromise(iterVal), + canRender = !valIsPromise; - iterI++; + if (canRender && filter != null) { + canRender = filter.call(this.component, iterVal, readI, { + iterable, + i: readI, + total: readTotal + }); - } catch (err) { - if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { - break; - } + if (Object.isPromise(canRender)) { + valIsPromise = true; + canRender = false; - stderr(err); + } else if (!Object.isTruly(canRender)) { + canRender = false; } } - if (lastTask != null) { - awaiting++; - - const - res = lastTask(); + if (canRender) { + readTotal++; + readEls.push(iterVal); - if (res != null) { - await res; - } + } else if (valIsPromise) { + discardedReadEls.push(iterVal); } - if (awaiting <= 0) { - localEmitter.emit('asyncRenderComplete', lastEvent); - - } else { - const id = localEmitter.on('asyncRenderChunkComplete', () => { - if (awaiting <= 0) { - localEmitter.emit('asyncRenderComplete', lastEvent); - localEmitter.off(id); - } - }); + if (readTotal >= perChunk || valIsPromise) { + break; } - }); - - return iter.readEls; - - function setVNodeCompiler(c: AnyFunction) { - toVNode = c; } + } - function setTarget(t: VNode) { - target = t; - } + return { + isAsync: isAsyncIterable, - function resolveTask(iterVal: unknown, filter?: boolean) { - if (filter === false) { - return; - } + readI, + readTotal, + readEls, - total++; - chunkTotal++; - valsToRender.push(iterVal); + iterable, + iterator: createFinalIterator() + }; - lastTask = () => { - lastTask = null; - awaiting++; + function createFinalIterator() { + if (Object.isAsyncIterator(iterator)) { + return iterator; + } - return that.createTask(task, {group, weight}); - }; + const + innerIter = seq(discardedReadEls, iterator); - if (!Object.isPromise(iterVal) && chunkTotal < perChunk) { - return; + const next = () => { + if (discardedReadEls.length === 0 && lastReadValue?.done) { + return lastReadValue; } - return lastTask(); + return innerIter.next(); + }; - function task() { - const - renderedVNodes: Node[] = []; + return { + [Symbol.iterator]() { + return this; + }, - for (let i = 0; i < valsToRender.length; i++) { - const - el = valsToRender[i], - vnodes = toVNode(el, iterI); - - if (Object.isArray(vnodes)) { - for (let i = 0; i < vnodes.length; i++) { - renderVNode(vnodes[i]); - } + next + }; + } +} - } else { - renderVNode(vnodes); - } - } +/** + * Returns an iterable structure based on the passed value + * + * @param value + * @param [hasFilter] - true if the passed value will be filtered + */ +export function getIterable( + this: AsyncRender, + value: unknown, + hasFilter?: boolean +): CanPromise { + if (value == null) { + return []; + } - valsToRender = []; + if (value === true) { + if (hasFilter) { + return new Range(0, Infinity); + } - chunkI++; - chunkTotal = 0; - awaiting--; + return []; + } - lastEvent = {...opts, renderGroup: group}; - localEmitter.emit('asyncRenderChunkComplete', lastEvent); + if (value === false) { + if (hasFilter) { + return new Range(0, -Infinity); + } - $a.worker(destructor, {group}); + return []; + } - function renderVNode(vnode: VNode) { - let - renderedVnode: Node; + if (Object.isNumber(value)) { + return new Range(0, [value]); + } - if (vnode.el != null) { - vnode.el[Object.cast($$.cached)] = true; - renderedVnode = Object.cast(vnode.el); + if (Object.isArray(value)) { + return value; + } - } else { - renderedVnode = r.render(vnode); - } + if (Object.isString(value)) { + return value.letters(); + } - renderedVNodes.push(renderedVnode); - target.el?.appendChild(renderedVnode); - } + if (Object.isPromise(value)) { + return value.then(getIterable.bind(this)); + } - function destructor() { - for (let i = 0; i < renderedVNodes.length; i++) { - destroyNode(renderedVNodes[i]); - } - - function destroyNode(el: CanUndef) { - if (el == null) { - return; - } - - if (el[$$.cached] != null) { - delete el[$$.cached]; - $a.worker(() => destroyNode(el), {group}); - - } else { - const - els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : []; - - if (opts.destructor?.(el, els) !== true) { - that.destroy(el, els); - } - } - } - } - } + if (typeof value === 'object') { + if (Object.isIterable(value) || Object.isAsyncIterable(value)) { + return value; } + + return Object.entries(value); } - //#endif + return [value]; } diff --git a/src/super/i-block/modules/async-render/modules/render.ts b/src/super/i-block/modules/async-render/modules/render.ts new file mode 100644 index 0000000000..574f7d0180 --- /dev/null +++ b/src/super/i-block/modules/async-render/modules/render.ts @@ -0,0 +1,92 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import SyncPromise from 'core/promise/sync'; +import { queue } from 'core/component/render/daemon'; + +import type iBlock from 'super/i-block/i-block'; +import type { ComponentElement } from 'super/i-block/i-block'; + +import type AsyncRender from 'super/i-block/modules/async-render/class'; +import type { TaskOptions } from 'super/i-block/modules/async-render/interface'; + +/** + * Adds a new render task to the global render queue and returns a promise. + * The promise will be resolved when the added task is completed. + * + * @param task - a task function to execute + * @param [opts] - additional options + */ +export function addRenderTask( + this: AsyncRender, + task: Function, + opts: TaskOptions = {} +): Promise { + const + $a = this.async, + group = opts.group ?? 'asyncComponents'; + + return new SyncPromise((resolve, reject) => { + const taskDesc = { + weight: opts.weight, + + fn: $a.proxy(() => { + const cb = () => { + task(); + resolve(); + return true; + }; + + if (opts.useRAF) { + return $a.animationFrame({group}).then(cb); + } + + return cb(); + + }, { + group, + single: false, + onClear: (err) => { + queue.delete(taskDesc); + reject(err); + } + }) + }; + + queue.add(taskDesc); + }); +} + +/** + * Removes the given node from the DOM tree and destroys all tied components + * + * @param node - a node to remove + * @param [childComponentEls] - root elements of the child components + */ +export function destroyNode(this: AsyncRender, node: Node, childComponentEls: Element[] = []): void { + node.parentNode?.removeChild(node); + + for (let i = 0; i < childComponentEls.length; i++) { + const + el = childComponentEls[i]; + + try { + (>el).component?.unsafe.$destroy(); + + } catch (err) { + stderr(err); + } + } + + try { + (>node).component?.unsafe.$destroy(); + + } catch (err) { + stderr(err); + } +} diff --git a/src/super/i-block/modules/async-render/render.ts b/src/super/i-block/modules/async-render/render.ts new file mode 100644 index 0000000000..91c9dd3ca6 --- /dev/null +++ b/src/super/i-block/modules/async-render/render.ts @@ -0,0 +1,90 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { restart, deferRestart } from 'core/component/render/daemon'; + +import type AsyncRender from 'super/i-block/modules/async-render/class'; + +/** + * Restarts the `asyncRender` daemon to force rendering of async chunks + */ +export function forceRender(this: AsyncRender): void { + restart(); + this.localEmitter.emit('forceRender'); +} + +/** + * Creates a task to restart the `asyncRender` daemon on the next tick + * @see forceRender + */ +export function deferForceRender(this: AsyncRender): void { + deferRestart(); + this.localEmitter.emit('forceRender'); +} + +/** + * A factory to create filters for `AsyncRender`, it returns a new function. + * The new function can return a boolean or promise. If the function returns a promise, + * it will be resolved after firing a `forceRender` event. + * + * The main function can take an element name as the first parameter. + * This element will be dropped before resolving the resulting promise. + * + * Notice, the initial component rendering is mean the same as `forceRender`. + * This function is useful to re-render a functional component without touching the parent state. + * + * @param elementToDrop - an element to drop before resolving the promise + * (if the element is passed as a function, it would be invoked) + * + * @example + * ``` + * < button @click = asyncRender.forceRender() + * Re-render the component + * + * < .&__wrapper v-async-target + * < template v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')}) + * < .&__content + * {{ Math.random() }} + * ``` + */ +export function waitForceRender( + this: AsyncRender, + elementToDrop?: string | ((ctx: AsyncRender['component']) => CanPromise>) +): () => CanPromise { + return () => { + const + canImmediateRender = this.lfc.isBeforeCreate() || this.hook === 'beforeMount'; + + if (canImmediateRender) { + return true; + } + + return this.localEmitter.promisifyOnce('forceRender').then(async () => { + if (elementToDrop != null) { + let + el; + + if (Object.isFunction(elementToDrop)) { + el = await elementToDrop(this.ctx); + + } else { + el = elementToDrop; + } + + if (Object.isString(el)) { + this.block?.element(el)?.remove(); + + } else { + el?.remove(); + } + } + + return true; + }); + }; +} From 0cc3d67752eb2d22a917f393f1090e1ab4f06d00 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:14:38 +0300 Subject: [PATCH 0157/2313] refactor: moved to `super/i-block/friend` and removed legacy --- .../i-block/{modules => }/friend/index.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) rename src/super/i-block/{modules => }/friend/index.ts (88%) diff --git a/src/super/i-block/modules/friend/index.ts b/src/super/i-block/friend/index.ts similarity index 88% rename from src/super/i-block/modules/friend/index.ts rename to src/super/i-block/friend/index.ts index 2d3901c6cd..50f16337b5 100644 --- a/src/super/i-block/modules/friend/index.ts +++ b/src/super/i-block/friend/index.ts @@ -7,34 +7,33 @@ */ /** - * [[include:super/i-block/modules/friend/README.md]] + * [[include:super/i-block/friend/README.md]] * @packageDocumentation */ import iBlock from 'super/i-block/i-block'; /** - * Class that friendly to a component - * @typeparam T - component + * Superclass to create classes friendly to the main component class */ export default class Friend { /** - * Type: component instance + * Type: the main component instance */ readonly C!: iBlock; /** - * Type: component context + * Type: the main component context */ readonly CTX!: this['C']['unsafe']; /** - * Component instance + * The main component instance */ readonly component: this['C']; /** - * Component context + * The main component context */ protected readonly ctx: this['CTX']; @@ -58,11 +57,6 @@ export default class Friend { return this.ctx.componentStatus; } - /** @see [[iBlock.$asyncLabel]] */ - get asyncLabel(): symbol { - return this.ctx.$asyncLabel; - } - /** @see [[iBlock.hook]] */ get hook(): this['CTX']['hook'] { return this.ctx.hook; @@ -130,7 +124,7 @@ export default class Friend { constructor(component: iBlock) { if (!(Object.get(component, 'instance') instanceof iBlock)) { - throw new TypeError("The specified component isn't inherited from iBlock"); + throw new TypeError("The specified component isn't inherited from `iBlock`"); } this.ctx = component.unsafe; From 3c7c62fc7a81f0085eeff614177162cbe7967c5c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:15:04 +0300 Subject: [PATCH 0158/2313] docs: added usage --- src/super/i-block/friend/README.md | 38 ++++++++++++++++++++++ src/super/i-block/modules/friend/README.md | 3 -- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/super/i-block/friend/README.md delete mode 100644 src/super/i-block/modules/friend/README.md diff --git a/src/super/i-block/friend/README.md b/src/super/i-block/friend/README.md new file mode 100644 index 0000000000..ea818542f7 --- /dev/null +++ b/src/super/i-block/friend/README.md @@ -0,0 +1,38 @@ +# super/i-block/friend + +This module provides a superclass create classes friendly to the main component class, +i.e. it can use private or protected methods and properties. + +__b-example/theme.ts__ + +```typescript +import Friend from 'super/i-block/friend'; + +export default class Theme extends Friend { + theme: string = this.ctx.initialTheme; + + setTheme(newTheme: string): void { + this.theme = newTheme; + } +} +``` + +__b-example/b-example.ts__ + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +import Theme from 'b-example/theme'; + +@component() +export default class bExample extends iBlock { + protected initialTheme?: string; + + @system((o) => new Theme(o)) + theme: Theme; + + created() { + this.theme.setTheme('demo'); + } +} +``` diff --git a/src/super/i-block/modules/friend/README.md b/src/super/i-block/modules/friend/README.md deleted file mode 100644 index 7f33cad016..0000000000 --- a/src/super/i-block/modules/friend/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# super/i-block/modules/friend - -This module provides a class that is friendly to [[iBlock]], i.e. it can use private and protected methods and properties. From 732f63d592c663c1c0d3bdc8a443e15419cd6b92 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:15:25 +0300 Subject: [PATCH 0159/2313] refactor: fixed link to friend --- src/super/i-block/modules/analytics/index.ts | 2 +- src/super/i-block/modules/block/index.ts | 2 +- src/super/i-block/modules/daemons/index.ts | 2 +- src/super/i-block/modules/dom/index.ts | 2 +- src/super/i-block/modules/field/index.ts | 2 +- src/super/i-block/modules/lfc/index.ts | 2 +- src/super/i-block/modules/module-loader/index.ts | 2 +- src/super/i-block/modules/opt/index.ts | 2 +- src/super/i-block/modules/provide/index.ts | 2 +- src/super/i-block/modules/state/index.ts | 2 +- src/super/i-block/modules/storage/index.ts | 2 +- src/super/i-block/modules/sync/index.ts | 2 +- src/super/i-block/modules/vdom/index.ts | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/super/i-block/modules/analytics/index.ts b/src/super/i-block/modules/analytics/index.ts index 1acaee3672..8922096f2d 100644 --- a/src/super/i-block/modules/analytics/index.ts +++ b/src/super/i-block/modules/analytics/index.ts @@ -15,7 +15,7 @@ import * as analytics from 'core/analytics'; //#endif -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; /** * Class provides some methods to work with analytic engines diff --git a/src/super/i-block/modules/block/index.ts b/src/super/i-block/modules/block/index.ts index 0938421a04..877fbe6189 100644 --- a/src/super/i-block/modules/block/index.ts +++ b/src/super/i-block/modules/block/index.ts @@ -14,7 +14,7 @@ import type iBlock from 'super/i-block/i-block'; import { fakeCtx, modRgxpCache, elRxp } from 'super/i-block/modules/block/const'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import type { ModsTable, ModsNTable } from 'super/i-block/modules/mods'; import type { diff --git a/src/super/i-block/modules/daemons/index.ts b/src/super/i-block/modules/daemons/index.ts index 65f98096d2..4cc370214f 100644 --- a/src/super/i-block/modules/daemons/index.ts +++ b/src/super/i-block/modules/daemons/index.ts @@ -13,7 +13,7 @@ import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import { wait } from 'super/i-block/modules/decorators'; import type { diff --git a/src/super/i-block/modules/dom/index.ts b/src/super/i-block/modules/dom/index.ts index 3fd5dc7c43..add39cbc49 100644 --- a/src/super/i-block/modules/dom/index.ts +++ b/src/super/i-block/modules/dom/index.ts @@ -23,7 +23,7 @@ import type { ComponentElement } from 'core/component'; import iBlock from 'super/i-block/i-block'; import Block from 'super/i-block/modules/block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import { componentRgxp } from 'super/i-block/modules/dom/const'; import { ElCb, inViewInstanceStore, DOMManipulationOptions } from 'super/i-block/modules/dom/interface'; diff --git a/src/super/i-block/modules/field/index.ts b/src/super/i-block/modules/field/index.ts index afa9dcd05c..026ca3fc20 100644 --- a/src/super/i-block/modules/field/index.ts +++ b/src/super/i-block/modules/field/index.ts @@ -15,7 +15,7 @@ import { unwrap } from 'core/object/watch'; import { getPropertyInfo } from 'core/component'; import iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import type { KeyGetter, ValueGetter } from 'super/i-block/modules/field/interface'; diff --git a/src/super/i-block/modules/lfc/index.ts b/src/super/i-block/modules/lfc/index.ts index 22f00e26c1..8926dab1e3 100644 --- a/src/super/i-block/modules/lfc/index.ts +++ b/src/super/i-block/modules/lfc/index.ts @@ -14,7 +14,7 @@ import SyncPromise from 'core/promise/sync'; import type { AsyncOptions } from 'core/async'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import { statuses } from 'super/i-block/const'; diff --git a/src/super/i-block/modules/module-loader/index.ts b/src/super/i-block/modules/module-loader/index.ts index 1d8e376342..e412291ebb 100644 --- a/src/super/i-block/modules/module-loader/index.ts +++ b/src/super/i-block/modules/module-loader/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import { cache, cachedModules } from 'super/i-block/modules/module-loader/const'; import type { Module } from 'super/i-block/modules/module-loader/interface'; diff --git a/src/super/i-block/modules/opt/index.ts b/src/super/i-block/modules/opt/index.ts index 655e00ae13..4d51d784a3 100644 --- a/src/super/i-block/modules/opt/index.ts +++ b/src/super/i-block/modules/opt/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import { literalCache } from 'super/i-block/modules/opt/const'; import type { IfOnceValue } from 'super/i-block/modules/opt/interface'; diff --git a/src/super/i-block/modules/provide/index.ts b/src/super/i-block/modules/provide/index.ts index 4fe6b6829d..04c3a23780 100644 --- a/src/super/i-block/modules/provide/index.ts +++ b/src/super/i-block/modules/provide/index.ts @@ -13,7 +13,7 @@ import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import Block from 'super/i-block/modules/block'; import { classesCache, modsCache } from 'super/i-block/modules/provide/const'; diff --git a/src/super/i-block/modules/state/index.ts b/src/super/i-block/modules/state/index.ts index d8c07aa947..cd6d7f3ee4 100644 --- a/src/super/i-block/modules/state/index.ts +++ b/src/super/i-block/modules/state/index.ts @@ -14,7 +14,7 @@ import symbolGenerator from 'core/symbol'; import iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; export * from 'super/i-block/modules/state/interface'; diff --git a/src/super/i-block/modules/storage/index.ts b/src/super/i-block/modules/storage/index.ts index 4f3148b383..13f3cc1cf1 100644 --- a/src/super/i-block/modules/storage/index.ts +++ b/src/super/i-block/modules/storage/index.ts @@ -16,7 +16,7 @@ import { asyncLocal, factory, AsyncStorageNamespace } from 'core/kv-storage'; //#endif import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; /** * Class to work with a local storage diff --git a/src/super/i-block/modules/sync/index.ts b/src/super/i-block/modules/sync/index.ts index ac3a7dd87e..c70bb7b0f3 100644 --- a/src/super/i-block/modules/sync/index.ts +++ b/src/super/i-block/modules/sync/index.ts @@ -26,7 +26,7 @@ import { import type iBlock from 'super/i-block/i-block'; import { statuses } from 'super/i-block/const'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import type { diff --git a/src/super/i-block/modules/vdom/index.ts b/src/super/i-block/modules/vdom/index.ts index c4236bb034..0f7d840005 100644 --- a/src/super/i-block/modules/vdom/index.ts +++ b/src/super/i-block/modules/vdom/index.ts @@ -14,7 +14,7 @@ import type { VNode } from 'core/component'; import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'super/i-block/friend'; import Opt from 'super/i-block/modules/opt'; import Field from 'super/i-block/modules/field'; From d975e0afb9886399660a22265aa4ada7ee184eaf Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:15:43 +0300 Subject: [PATCH 0160/2313] chore: :up: --- src/super/i-block/{modules => }/friend/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) rename src/super/i-block/{modules => }/friend/CHANGELOG.md (81%) diff --git a/src/super/i-block/modules/friend/CHANGELOG.md b/src/super/i-block/friend/CHANGELOG.md similarity index 81% rename from src/super/i-block/modules/friend/CHANGELOG.md rename to src/super/i-block/friend/CHANGELOG.md index 8e4c560cea..bd0dbcac03 100644 --- a/src/super/i-block/modules/friend/CHANGELOG.md +++ b/src/super/i-block/friend/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :house: Internal + +* Refactoring + ## v3.0.0-rc.46 (2020-07-31) #### :house: Internal From 0bdd1db2b0a03e019be9dd8d18092b547853903c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:16:42 +0300 Subject: [PATCH 0161/2313] chore: fixed grammar --- src/super/i-block/friend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/super/i-block/friend/README.md b/src/super/i-block/friend/README.md index ea818542f7..71f770bf79 100644 --- a/src/super/i-block/friend/README.md +++ b/src/super/i-block/friend/README.md @@ -1,6 +1,6 @@ # super/i-block/friend -This module provides a superclass create classes friendly to the main component class, +This module provides a superclass to create classes friendly to the main component class, i.e. it can use private or protected methods and properties. __b-example/theme.ts__ From e461f8d0bcbc368c33fd0717e454b33b7d7206af Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:23:14 +0300 Subject: [PATCH 0162/2313] refactor: moved the module to `friends/friend` --- src/{super/i-block => friends}/friend/CHANGELOG.md | 4 ++++ src/{super/i-block => friends}/friend/README.md | 4 ++-- src/{super/i-block => friends}/friend/index.ts | 2 +- src/super/i-block/modules/analytics/index.ts | 2 +- src/super/i-block/modules/block/index.ts | 2 +- src/super/i-block/modules/daemons/index.ts | 2 +- src/super/i-block/modules/dom/index.ts | 2 +- src/super/i-block/modules/field/index.ts | 2 +- src/super/i-block/modules/lfc/index.ts | 2 +- src/super/i-block/modules/module-loader/index.ts | 2 +- src/super/i-block/modules/opt/index.ts | 2 +- src/super/i-block/modules/provide/index.ts | 2 +- src/super/i-block/modules/state/index.ts | 2 +- src/super/i-block/modules/storage/index.ts | 2 +- src/super/i-block/modules/sync/index.ts | 2 +- src/super/i-block/modules/vdom/index.ts | 2 +- 16 files changed, 20 insertions(+), 16 deletions(-) rename src/{super/i-block => friends}/friend/CHANGELOG.md (83%) rename src/{super/i-block => friends}/friend/README.md (91%) rename src/{super/i-block => friends}/friend/index.ts (98%) diff --git a/src/super/i-block/friend/CHANGELOG.md b/src/friends/friend/CHANGELOG.md similarity index 83% rename from src/super/i-block/friend/CHANGELOG.md rename to src/friends/friend/CHANGELOG.md index bd0dbcac03..a5ca377a55 100644 --- a/src/super/i-block/friend/CHANGELOG.md +++ b/src/friends/friend/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :boom: Breaking Change + +* Moved the module to `friends/friend` + #### :house: Internal * Refactoring diff --git a/src/super/i-block/friend/README.md b/src/friends/friend/README.md similarity index 91% rename from src/super/i-block/friend/README.md rename to src/friends/friend/README.md index 71f770bf79..a2b7a165f0 100644 --- a/src/super/i-block/friend/README.md +++ b/src/friends/friend/README.md @@ -1,4 +1,4 @@ -# super/i-block/friend +# friends/friend This module provides a superclass to create classes friendly to the main component class, i.e. it can use private or protected methods and properties. @@ -6,7 +6,7 @@ i.e. it can use private or protected methods and properties. __b-example/theme.ts__ ```typescript -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; export default class Theme extends Friend { theme: string = this.ctx.initialTheme; diff --git a/src/super/i-block/friend/index.ts b/src/friends/friend/index.ts similarity index 98% rename from src/super/i-block/friend/index.ts rename to src/friends/friend/index.ts index 50f16337b5..25748a9656 100644 --- a/src/super/i-block/friend/index.ts +++ b/src/friends/friend/index.ts @@ -7,7 +7,7 @@ */ /** - * [[include:super/i-block/friend/README.md]] + * [[include:friends/friend/README.md]] * @packageDocumentation */ diff --git a/src/super/i-block/modules/analytics/index.ts b/src/super/i-block/modules/analytics/index.ts index 8922096f2d..d2248400ea 100644 --- a/src/super/i-block/modules/analytics/index.ts +++ b/src/super/i-block/modules/analytics/index.ts @@ -15,7 +15,7 @@ import * as analytics from 'core/analytics'; //#endif -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; /** * Class provides some methods to work with analytic engines diff --git a/src/super/i-block/modules/block/index.ts b/src/super/i-block/modules/block/index.ts index 877fbe6189..e993fc481e 100644 --- a/src/super/i-block/modules/block/index.ts +++ b/src/super/i-block/modules/block/index.ts @@ -14,7 +14,7 @@ import type iBlock from 'super/i-block/i-block'; import { fakeCtx, modRgxpCache, elRxp } from 'super/i-block/modules/block/const'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import type { ModsTable, ModsNTable } from 'super/i-block/modules/mods'; import type { diff --git a/src/super/i-block/modules/daemons/index.ts b/src/super/i-block/modules/daemons/index.ts index 4cc370214f..6840bec93e 100644 --- a/src/super/i-block/modules/daemons/index.ts +++ b/src/super/i-block/modules/daemons/index.ts @@ -13,7 +13,7 @@ import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import { wait } from 'super/i-block/modules/decorators'; import type { diff --git a/src/super/i-block/modules/dom/index.ts b/src/super/i-block/modules/dom/index.ts index add39cbc49..3048eeea65 100644 --- a/src/super/i-block/modules/dom/index.ts +++ b/src/super/i-block/modules/dom/index.ts @@ -23,7 +23,7 @@ import type { ComponentElement } from 'core/component'; import iBlock from 'super/i-block/i-block'; import Block from 'super/i-block/modules/block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import { componentRgxp } from 'super/i-block/modules/dom/const'; import { ElCb, inViewInstanceStore, DOMManipulationOptions } from 'super/i-block/modules/dom/interface'; diff --git a/src/super/i-block/modules/field/index.ts b/src/super/i-block/modules/field/index.ts index 026ca3fc20..813ed8431c 100644 --- a/src/super/i-block/modules/field/index.ts +++ b/src/super/i-block/modules/field/index.ts @@ -15,7 +15,7 @@ import { unwrap } from 'core/object/watch'; import { getPropertyInfo } from 'core/component'; import iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import type { KeyGetter, ValueGetter } from 'super/i-block/modules/field/interface'; diff --git a/src/super/i-block/modules/lfc/index.ts b/src/super/i-block/modules/lfc/index.ts index 8926dab1e3..7fa0fdb6d8 100644 --- a/src/super/i-block/modules/lfc/index.ts +++ b/src/super/i-block/modules/lfc/index.ts @@ -14,7 +14,7 @@ import SyncPromise from 'core/promise/sync'; import type { AsyncOptions } from 'core/async'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import { statuses } from 'super/i-block/const'; diff --git a/src/super/i-block/modules/module-loader/index.ts b/src/super/i-block/modules/module-loader/index.ts index e412291ebb..6d5b9ebebb 100644 --- a/src/super/i-block/modules/module-loader/index.ts +++ b/src/super/i-block/modules/module-loader/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import { cache, cachedModules } from 'super/i-block/modules/module-loader/const'; import type { Module } from 'super/i-block/modules/module-loader/interface'; diff --git a/src/super/i-block/modules/opt/index.ts b/src/super/i-block/modules/opt/index.ts index 4d51d784a3..03e003e4e8 100644 --- a/src/super/i-block/modules/opt/index.ts +++ b/src/super/i-block/modules/opt/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import { literalCache } from 'super/i-block/modules/opt/const'; import type { IfOnceValue } from 'super/i-block/modules/opt/interface'; diff --git a/src/super/i-block/modules/provide/index.ts b/src/super/i-block/modules/provide/index.ts index 04c3a23780..d4803e51ee 100644 --- a/src/super/i-block/modules/provide/index.ts +++ b/src/super/i-block/modules/provide/index.ts @@ -13,7 +13,7 @@ import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import Block from 'super/i-block/modules/block'; import { classesCache, modsCache } from 'super/i-block/modules/provide/const'; diff --git a/src/super/i-block/modules/state/index.ts b/src/super/i-block/modules/state/index.ts index cd6d7f3ee4..c640fa9b66 100644 --- a/src/super/i-block/modules/state/index.ts +++ b/src/super/i-block/modules/state/index.ts @@ -14,7 +14,7 @@ import symbolGenerator from 'core/symbol'; import iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; export * from 'super/i-block/modules/state/interface'; diff --git a/src/super/i-block/modules/storage/index.ts b/src/super/i-block/modules/storage/index.ts index 13f3cc1cf1..dad910c18f 100644 --- a/src/super/i-block/modules/storage/index.ts +++ b/src/super/i-block/modules/storage/index.ts @@ -16,7 +16,7 @@ import { asyncLocal, factory, AsyncStorageNamespace } from 'core/kv-storage'; //#endif import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; /** * Class to work with a local storage diff --git a/src/super/i-block/modules/sync/index.ts b/src/super/i-block/modules/sync/index.ts index c70bb7b0f3..09fbac712c 100644 --- a/src/super/i-block/modules/sync/index.ts +++ b/src/super/i-block/modules/sync/index.ts @@ -26,7 +26,7 @@ import { import type iBlock from 'super/i-block/i-block'; import { statuses } from 'super/i-block/const'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import type { diff --git a/src/super/i-block/modules/vdom/index.ts b/src/super/i-block/modules/vdom/index.ts index 0f7d840005..4aa99b8432 100644 --- a/src/super/i-block/modules/vdom/index.ts +++ b/src/super/i-block/modules/vdom/index.ts @@ -14,7 +14,7 @@ import type { VNode } from 'core/component'; import type iBlock from 'super/i-block/i-block'; -import Friend from 'super/i-block/friend'; +import Friend from 'friends/friend'; import Opt from 'super/i-block/modules/opt'; import Field from 'super/i-block/modules/field'; From 5df607ebde48831f715c1f89bb44cfa54dc0cafd Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 14:26:33 +0300 Subject: [PATCH 0163/2313] refactor: moved the module to `friends/async-render` --- .../async-render/CHANGELOG.md | 7 +++++++ .../modules => friends}/async-render/README.md | 2 +- .../modules => friends}/async-render/class.ts | 4 ++-- src/friends/async-render/index.ts | 18 ++++++++++++++++++ .../async-render/interface.ts | 0 .../modules => friends}/async-render/iter.ts | 8 ++++---- .../async-render/modules/iter.ts | 4 ++-- .../async-render/modules/render.ts | 4 ++-- .../modules => friends}/async-render/render.ts | 2 +- .../async-render/test/index.js | 0 .../i-block/modules/async-render/index.ts | 18 ------------------ 11 files changed, 37 insertions(+), 30 deletions(-) rename src/{super/i-block/modules => friends}/async-render/CHANGELOG.md (94%) rename src/{super/i-block/modules => friends}/async-render/README.md (99%) rename src/{super/i-block/modules => friends}/async-render/class.ts (89%) create mode 100644 src/friends/async-render/index.ts rename src/{super/i-block/modules => friends}/async-render/interface.ts (100%) rename src/{super/i-block/modules => friends}/async-render/iter.ts (94%) rename src/{super/i-block/modules => friends}/async-render/modules/iter.ts (95%) rename src/{super/i-block/modules => friends}/async-render/modules/render.ts (92%) rename src/{super/i-block/modules => friends}/async-render/render.ts (97%) rename src/{super/i-block/modules => friends}/async-render/test/index.js (100%) delete mode 100644 src/super/i-block/modules/async-render/index.ts diff --git a/src/super/i-block/modules/async-render/CHANGELOG.md b/src/friends/async-render/CHANGELOG.md similarity index 94% rename from src/super/i-block/modules/async-render/CHANGELOG.md rename to src/friends/async-render/CHANGELOG.md index a5e7a2b9dc..c09c59dfd1 100644 --- a/src/super/i-block/modules/async-render/CHANGELOG.md +++ b/src/friends/async-render/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Moved the module to `friends/async-render` +* The module has been rewritten to a new tree-shake friendly API + ## v3.18.0 (2022-03-04) #### :boom: Breaking Change diff --git a/src/super/i-block/modules/async-render/README.md b/src/friends/async-render/README.md similarity index 99% rename from src/super/i-block/modules/async-render/README.md rename to src/friends/async-render/README.md index 0e2feebf4a..8386c79d93 100644 --- a/src/super/i-block/modules/async-render/README.md +++ b/src/friends/async-render/README.md @@ -1,4 +1,4 @@ -# super/i-block/modules/async-render +# friends/async-render This module provides a class to render chunks of a component template asynchronously. It helps to optimize component' rendering. diff --git a/src/super/i-block/modules/async-render/class.ts b/src/friends/async-render/class.ts similarity index 89% rename from src/super/i-block/modules/async-render/class.ts rename to src/friends/async-render/class.ts index 44a16d451e..3482d7a92f 100644 --- a/src/super/i-block/modules/async-render/class.ts +++ b/src/friends/async-render/class.ts @@ -6,9 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type iBlock from 'super/i-block/i-block'; +import Friend from 'friends/friend'; -import Friend from 'super/i-block/modules/friend'; +import type iBlock from 'super/i-block/i-block'; export default class AsyncRender extends Friend { constructor(component: iBlock) { diff --git a/src/friends/async-render/index.ts b/src/friends/async-render/index.ts new file mode 100644 index 0000000000..c0b8805272 --- /dev/null +++ b/src/friends/async-render/index.ts @@ -0,0 +1,18 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:friends/async-render/README.md]] + * @packageDocumentation + */ + +export { default } from 'friends/async-render/class'; + +export * from 'friends/async-render/iter'; +export * from 'friends/async-render/render'; +export * from 'friends/async-render/interface'; diff --git a/src/super/i-block/modules/async-render/interface.ts b/src/friends/async-render/interface.ts similarity index 100% rename from src/super/i-block/modules/async-render/interface.ts rename to src/friends/async-render/interface.ts diff --git a/src/super/i-block/modules/async-render/iter.ts b/src/friends/async-render/iter.ts similarity index 94% rename from src/super/i-block/modules/async-render/iter.ts rename to src/friends/async-render/iter.ts index f0dcc274c1..6e48089dca 100644 --- a/src/super/i-block/modules/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -7,12 +7,12 @@ */ import type { ComponentElement, VNode } from 'super/i-block/i-block'; -import type AsyncRender from 'super/i-block/modules/async-render/class'; +import type AsyncRender from 'friends/async-render/class'; -import { addRenderTask, destroyNode as nodeDestructor } from 'super/i-block/modules/async-render/modules/render'; -import { getIterDescriptor } from 'super/i-block/modules/async-render/modules/iter'; +import { addRenderTask, destroyNode as nodeDestructor } from 'friends/async-render/modules/render'; +import { getIterDescriptor } from 'friends/async-render/modules/iter'; -import type { TaskOptions } from 'super/i-block/modules/async-render/interface'; +import type { TaskOptions } from 'friends/async-render/interface'; const isCached = Symbol('Is cached') diff --git a/src/super/i-block/modules/async-render/modules/iter.ts b/src/friends/async-render/modules/iter.ts similarity index 95% rename from src/super/i-block/modules/async-render/modules/iter.ts rename to src/friends/async-render/modules/iter.ts index 2e95552f4b..4023649c86 100644 --- a/src/super/i-block/modules/async-render/modules/iter.ts +++ b/src/friends/async-render/modules/iter.ts @@ -9,8 +9,8 @@ import Range from 'core/range'; import { seq } from 'core/iter/combinators'; -import type AsyncRender from 'super/i-block/modules/async-render/class'; -import type { IterOptions, IterDescriptor } from 'super/i-block/modules/async-render/interface'; +import type AsyncRender from 'friends/async-render/class'; +import type { IterOptions, IterDescriptor } from 'friends/async-render/interface'; /** * Returns an iterator descriptor based on the passed value and options diff --git a/src/super/i-block/modules/async-render/modules/render.ts b/src/friends/async-render/modules/render.ts similarity index 92% rename from src/super/i-block/modules/async-render/modules/render.ts rename to src/friends/async-render/modules/render.ts index 574f7d0180..c8067881ba 100644 --- a/src/super/i-block/modules/async-render/modules/render.ts +++ b/src/friends/async-render/modules/render.ts @@ -12,8 +12,8 @@ import { queue } from 'core/component/render/daemon'; import type iBlock from 'super/i-block/i-block'; import type { ComponentElement } from 'super/i-block/i-block'; -import type AsyncRender from 'super/i-block/modules/async-render/class'; -import type { TaskOptions } from 'super/i-block/modules/async-render/interface'; +import type AsyncRender from 'friends/async-render/class'; +import type { TaskOptions } from 'friends/async-render/interface'; /** * Adds a new render task to the global render queue and returns a promise. diff --git a/src/super/i-block/modules/async-render/render.ts b/src/friends/async-render/render.ts similarity index 97% rename from src/super/i-block/modules/async-render/render.ts rename to src/friends/async-render/render.ts index 91c9dd3ca6..5d9f8d55fd 100644 --- a/src/super/i-block/modules/async-render/render.ts +++ b/src/friends/async-render/render.ts @@ -8,7 +8,7 @@ import { restart, deferRestart } from 'core/component/render/daemon'; -import type AsyncRender from 'super/i-block/modules/async-render/class'; +import type AsyncRender from 'friends/async-render/class'; /** * Restarts the `asyncRender` daemon to force rendering of async chunks diff --git a/src/super/i-block/modules/async-render/test/index.js b/src/friends/async-render/test/index.js similarity index 100% rename from src/super/i-block/modules/async-render/test/index.js rename to src/friends/async-render/test/index.js diff --git a/src/super/i-block/modules/async-render/index.ts b/src/super/i-block/modules/async-render/index.ts deleted file mode 100644 index 06aed98fc2..0000000000 --- a/src/super/i-block/modules/async-render/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:super/i-block/modules/async-render/README.md]] - * @packageDocumentation - */ - -export { default } from 'super/i-block/modules/async-render/class'; - -export * from 'super/i-block/modules/async-render/iter'; -export * from 'super/i-block/modules/async-render/render'; -export * from 'super/i-block/modules/async-render/interface'; From f415fed781d99667dc7e12733b3f91d1941126c1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 16:20:59 +0300 Subject: [PATCH 0164/2313] docs: improved doc --- src/friends/async-render/README.md | 270 ++++++++++++++++---------- src/friends/async-render/interface.ts | 29 +-- src/friends/async-render/iter.ts | 6 +- src/friends/async-render/render.ts | 7 +- 4 files changed, 194 insertions(+), 118 deletions(-) diff --git a/src/friends/async-render/README.md b/src/friends/async-render/README.md index 8386c79d93..fcafc65ae2 100644 --- a/src/friends/async-render/README.md +++ b/src/friends/async-render/README.md @@ -1,168 +1,214 @@ # friends/async-render -This module provides a class to render chunks of a component template asynchronously. -It helps to optimize component' rendering. +This module provides a class to render component fragments asynchronously. +It helps to optimize component rendering. + +## How to include this module to your component + +By default, any components that inherited from [[iBlock]] have the `asyncRender` property. +But to use module methods, you should attach them explicitly to enable tree-shake code optimizations. +Just place a necessary import declaration within your component file. + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; +import AsyncRender, { iterate, forceRender } from 'friends/async-render'; + +// Import `iterate` and `forceRender` methods +AsyncRender.addToPrototype(iterate, forceRender); + +@component() +export default class bExample extends iBlock {} +``` ## How does it work? -The class brings a new method to create iterable objects: `asyncRender.iterate`. -This method should be used with the `v-for` directive. +The class brings a new method to create iterable objects: `asyncRender.iterate` that should be used with the `v-for` directive. +Don't forget to declare where to mount dynamically rendered fragments. +To do it, place your code within another node that marked by the `v-async-target` directive. ``` -// The first ten elements are rendered synchronously. -// After that, the rest elements will be split into chunks by ten elements and rendered asynchronously. -// The rendering of async chunks does not force re-rendering of the main component template. -< .bla v-for = el in asyncRender.iterate(myData, 10) - {{ el }} +< .container v-async-target + /// The first ten elements are rendered synchronously. + /// After that, the rest elements will be split into chunks by ten elements and rendered asynchronously. + /// The rendering of async fragments does not force re-rendering of the main component template. + < .&__item v-for = el in asyncRender.iterate(myData, 10) + {{ el }} ``` As we see in the example, `iterate` splits iteration into separated chunks. Basically, the first chunk is rendered immediate, but the rest - asynchronously. -Using the second parameter, we can manage how many items should be contained per one render chunk (by default, the chunk size is equal to one). -Also, it is possible to skip a number of elements from the start. To do it, provide the second parameter as a tuple, -where the first parameter is a number to skip, the second one is a render chunk' size. +Using the second parameter, we can manage how many fragments can be placed within one render chunk (by default, the chunk size is equal to one). +Also, it is possible to skip several elements from the start. To do it, provide the second parameter as a tuple, +where the first parameter is a number to skip, the second one is a render chunk size. ``` -/// Skip the first fifteen elements and render by three elements per chunk -< .bla v-for = el in asyncRender.iterate(myData, [15, 3]) - {{ el }} +< .container v-async-target + /// Skip the first fifteen elements and render by three elements per chunk + < .&__item v-for = el in asyncRender.iterate(myData, [15, 3]) + {{ el }} ``` -The parameter to iterate can be defined as any valid iterable JavaScript value, like arrays, maps, or sets. +The parameter to iterate can be any iterable JS value, like arrays, maps, or sets. ``` -< .bla v-for = el in asyncRender.iterate([1, 2, 3]) - {{ el }} +< .container v-async-target + < .&__item v-for = el in asyncRender.iterate([1, 2, 3]) + {{ el }} + +< .container v-async-target + < .&__item v-for = el in asyncRender.iterate(new Set([1, 2, 3])) + {{ el }} + +< .container v-async-target + /// All built-in JS iterators are iterable objects + < .&__item v-for = el in asyncRender.iterate(new Map([['a', 1], ['b', 2]]).entries()) + {{ el }} +``` -< .bla v-for = el in asyncRender.iterate(new Set([1, 2, 3])) - {{ el }} +Any string values are iterated by graphemes or letters, but not Unicode symbols. -/// All JS iterators are iterable objects -< .bla v-for = el in asyncRender.iterate(new Map([['a', 1], ['b', 2]]).entries()) - {{ el }} +``` +< .container v-async-target + /// 1, 😃, à, 🇷🇺, 👩🏽‍❤️‍💋‍👨 + < .&__item v-for = letter in asyncRender.iterate('1😃à🇷🇺👩🏽‍❤️‍💋‍👨') + {{ letter }} ``` -The string values are iterated by graphemes or letters, but not Unicode symbols. +Any iterable element can be a promise. In that case, it will be rendered after resolving. ``` -/// 1, 😃, à, 🇷🇺, 👩🏽‍❤️‍💋‍👨 -< .bla v-for = letter in asyncRender.iterate('1😃à🇷🇺👩🏽‍❤️‍💋‍👨') - {{ letter }} +< .container v-async-target + < .&__item v-for = user in asyncRender.iterate([fetch('/user/1'), fetch('/user/2')]) + {{ user }} ``` -Any iterable element can return a promise. In that case, it will be rendered after resolving. +Or, we can use asynchronous iterable structures. ``` -< .bla v-for = user in asyncRender.iterate([fetch('/user/1'), fetch('/user/2')]) - {{ user }} +< .container v-async-target + < .&__item v-for = user in asyncRender.iterate(asyncEventStream) + {{ user }} ``` -In addition to elements with promises, `asyncRender.iterate` can take a promise that returns a valid value to iterate. +In addition to elements with promises or asynchronous iterable structures, +`asyncRender.iterate` can take a promise that returns a valid value to iterate. ``` -< .bla v-for = user in asyncRender.iterate(fetch('/users')) - {{ user }} +< .container v-async-target + < .&__item v-for = user in asyncRender.iterate(fetch('/users')) + {{ user }} ``` -Also, the method supports iterating over JS object (without prototype' values). +Also, the method supports iterating over JS objects. ``` -// To iterate objects is used `Object.entries` -< .bla v-for = [key, value] in asyncRender.iterate({a: 1, b: 2}) - {{ key }} = {{ value }} +< .container v-async-target + /// To iterate objects is used `Object.entries` + < .&__item v-for = [key, value] in asyncRender.iterate({a: 1, b: 2}) + {{ key }} = {{ value }} ``` Finally, the method can create ranges and iterate through them if provided a value as a number. ``` -< .bla v-for = i in asyncRender.iterate(10) - {{ i }} - -/// `true` is an alias for `Infinity` -< .bla v-for = i in asyncRender.iterate(true) - {{ i }} - -/// `false` is an alias for `-Infinity` -< .bla v-for = i in asyncRender.iterate(false) - {{ i }} +< .container v-async-target + < .&__item v-for = i in asyncRender.iterate(10) + {{ i }} + +< .container v-async-target + /// `true` is an alias for `Infinity` + < .&__item v-for = i in asyncRender.iterate(true) + {{ i }} + +< .container v-async-target + /// `false` is an alias for `-Infinity` + < .&__item v-for = i in asyncRender.iterate(false) + {{ i }} ``` -`null` and `undefined` are cast to an empty iterator. It is useful when you provide a promise to iterate that can return a null value. +`null` and `undefined` are cast to the empty iterator. It is useful when you provide a promise to iterate that can return a nullish value. ``` -< .bla v-for = el in asyncRender.iterate(null) - {{ i }} - -/// It's ok if `fetch` returns `null` -< .bla v-for = el in asyncRender.iterate(fetch('/users')) - {{ i }} +< .container v-async-target + < .&__item v-for = el in asyncRender.iterate(null) + {{ i }} + +< .container v-async-target + /// It's ok if `fetch` returns `null` + < .&__item v-for = el in asyncRender.iterate(fetch('/users')) + {{ i }} ``` The rest primitive types are cast to a single-element iterator. ``` -< .bla v-for = el in asyncRender.iterate(Symbol('foo')) - {{ el }} +< .container v-async-target + < .&__item v-for = el in asyncRender.iterate(Symbol('foo')) + {{ el }} ``` ## Events -| EventName | Description | Payload description | Payload | -|----------------------------|----------------------------------------------------------|---------------------|-------------------------| -| `asyncRenderChunkComplete` | One async chunk has been rendered | Task description | `TaskParams & TaskDesc` | -| `asyncRenderComplete` | All async chunks from one render task have been rendered | Task description | `TaskParams & TaskDesc` | +| EventName | Description | Payload description | Payload | +|----------------------------|----------------------------------------------------------|---------------------|--------------| +| `asyncRenderChunkComplete` | One async chunk has been rendered | Task description | `TaskParams` | +| `asyncRenderComplete` | All async chunks from one render task have been rendered | Task description | `TaskParams` | + +## Additional options of iterations -## Additional parameters of iterations +As you have already known, you can specify a chunk size to render as the second parameter of `asyncRender.iterate`. +If the passed value to iterate not a promise, asynchronous iterable structure, or not contains promises as elements, +the first chunk can be rendered synchronously. Also, the iteration method can take an object with additional options to iterate. -As you have already known, you can specify a chunk' size to render as the second parameter of `asyncRender.iterate`. -If the passes value to iterate not a promise or not contains promises as elements, the first chunk can be rendered synchronously. -Also, the iteration method can take an object with additional parameters to iterate. +### [weight = `1`] + +The weight of one render chunk. +At the same tick can be rendered chunks with the accumulated weight no more than the `TASKS_PER_TICK` constant. +See `core/component/render/daemon` for more information. ### [useRaf = `false`] -If true, then rendered chunks are inserted into DOM on the `requestAnimationFrame` callback. -It may optimize the process of browser rendering. +If true, then all rendered fragments are inserted into the DOM by using a `requestAnimationFrame` callback. +This can optimize the browser rendering process. ### [group] -A group name to manual clearing of pending tasks via `async`. -Providing this value disables automatically canceling of rendering task on the `update` hook. +A group name to manual clearing of pending tasks via the `async` module. +Providing this value disables automatically cleanup of render tasks on the `update` hook. ``` -/// Iterate over only even values -< .bla v-for = el in asyncRender.iterate(100, 10, {group: 'listRendering'}) - {{ el }} +< .container v-async-target + < .&__item v-for = el in asyncRender.iterate(100, 10, {group: 'listRendering'}) + {{ el }} -/// Notice that we use RegExp to clear tasks. -/// Because each group has a group based on a template `asyncComponents:listRendering:${chunkIndex}`. +/// We should use a RegExp to clear tasks, +/// because each group has a group based on a template `asyncComponents:listRendering:${chunkIndex}`. < button @click = async.clearAll({group: /:listRendering/}) Cancel rendering ``` -### [weight = `1`] - -Weight of the one rendering chunk. -In the one tick can be rendered chunks with accumulated weight no more than 5. -See `core/render` for more information. - ### [filter] -A function to filter elements to iterate. If it returns a promise, the rendering will wait for resolving. -If the promise' value is equal to `undefined`, it will cast to `true`. +A function to filter elements to render. +If it returns a promise, the rendering process will wait for the promise to resolve. +If the promise is resolved with `undefined`, the value will be interpreted as `true`. ``` -/// Iterate over only even values -< .bla v-for = el in asyncRender.iterate(100, 5, {filter: (el) => el % 2 === 0}) - {{ el }} +< .container v-async-target + /// Render only even values + < .&__item v-for = el in asyncRender.iterate(100, 5, {filter: (el) => el % 2 === 0}) + {{ el }} -/// Render each element only after the previous with the specified delay -< .bla v-for = el in asyncRender.iterate(100, {filter: (el) => async.sleep(100)}) - {{ el }} +< .container v-async-target + /// Render each element with the specified delay + < .&__item v-for = el in asyncRender.iterate(100, {filter: (el) => async.sleep(100)}) + {{ el }} -/// Render a chunk on the specified event -< .bla v-for = el in asyncRender.iterate(100, 20, {filter: (el) => promisifyOnce('renderNextChunk')}) - {{ el }} +< .container v-async-target + /// Render each element after the specified event + < .&__item v-for = el in asyncRender.iterate(100, 20, {filter: (el) => promisifyOnce('renderNextChunk')}) + {{ el }} < button @click = emit('renderNextChunk') Render the next chunk @@ -170,46 +216,66 @@ If the promise' value is equal to `undefined`, it will cast to `true`. ### [destructor] -The destructor of a rendered element. If the destructor returns `true` then the `destroy` method of the `asyncRender` module will not be called. -It will be invoked before removing each async rendered element from DOM. +The destructor of a rendered fragment. +It will be called before each asynchronously rendered fragment is removed from the DOM. +If the function returns true, the internal destructor of the `asyncRender` module won’t be called. -## Helpers +## Methods ### forceRender -Restarts the async render daemon to force rendering. +Restarts the `asyncRender` daemon to force rendering of async chunks. +See `core/component/render/daemon` for more information. ### deferForceRender -Restarts the `asyncRender` daemon to force rendering (runs on the next tick). +Creates a task to restart the `asyncRender` daemon on the next tick. +See `core/component/render/daemon` for more information. ### waitForceRender -Returns a function that returns a promise that will be resolved after firing the `forceRender` event. -The method can take an element name as the first parameter. This element will be dropped before resolving. +A factory to create filters for `AsyncRender`, it returns a new function. +The new function can return a boolean or promise. If the function returns a promise, it will be resolved after firing a `forceRender` event. + +The main function can take an element name as the first parameter. +This element will be dropped before resolving the resulting promise. -Notice, the initial rendering of a component is mean the same as `forceRender`. -The method is useful to re-render a non-regular component (functional or flyweight) without touching the parent state. +Notice, the initial component rendering is mean the same as `forceRender`. +This function is useful to re-render a functional component without touching the parent state. ``` < button @click = asyncRender.forceRender() Re-render the component -< .&__wrapper +< .container v-async-target < template v-for = el in asyncRender.iterate(true, { & filter: asyncRender.waitForceRender('content') }) . < .&__content {{ Math.random() }} -< .&__wrapper +< .container v-async-target < template v-for = el in asyncRender.iterate(true, { & filter: asyncRender.waitForceRender((ctx) => ctx.$el.querySelector('.foo')) }) . - < .foo + < .&__content {{ Math.random() }} ``` +### iterate + +Creates an asynchronous render stream from the specified value. +It returns a list of element to the first synchronous render. +This function helps optimize component rendering by splitting big render tasks into smaller ones. + +``` +/// Where to append asynchronous elements +< .container v-async-target + /// Asynchronous rendering of components: only five elements per chunk + < template v-for = el in asyncRender.iterate(largeList, 5) + < my-component :data = el +``` + ## Snakeskin helpers ### loadModules diff --git a/src/friends/async-render/interface.ts b/src/friends/async-render/interface.ts index 0b8c1c6bc2..a8dd1f2704 100644 --- a/src/friends/async-render/interface.ts +++ b/src/friends/async-render/interface.ts @@ -35,8 +35,8 @@ export interface TaskOptions { * * @example * ``` - * < .bla v-async-target - * < template v-for = el in asyncRender.iterate(100, 10, {group: 'listRendering'}) + * < .container v-async-target + * < .&__item v-for = el in asyncRender.iterate(100, 10, {group: 'listRendering'}) * {{ el }} * * /// We should use a RegExp to clear tasks, @@ -55,17 +55,20 @@ export interface TaskOptions { * * @example * ``` - * /// Render only even values - * < .bla v-for = el in asyncRender.iterate(100, 5, {filter: (el) => el % 2 === 0}) - * {{ el }} + * < .container v-async-target + * /// Render only even values + * < .&__item v-for = el in asyncRender.iterate(100, 5, {filter: (el) => el % 2 === 0}) + * {{ el }} * - * /// Render each element with the specified delay - * < .bla v-for = el in asyncRender.iterate(100, {filter: (el) => async.sleep(100)}) - * {{ el }} + * < .container v-async-target + * /// Render each element with the specified delay + * < .&__item v-for = el in asyncRender.iterate(100, {filter: (el) => async.sleep(100)}) + * {{ el }} * - * /// Render each element after the specified event - * < .bla v-for = el in asyncRender.iterate(100, 20, {filter: (el) => promisifyOnce('renderNextChunk')}) - * {{ el }} + * < .container v-async-target + * /// Render each element after the specified event + * < .&__item v-for = el in asyncRender.iterate(100, 20, {filter: (el) => promisifyOnce('renderNextChunk')}) + * {{ el }} * * < button @click = emit('renderNextChunk') * Render the next chunk @@ -81,6 +84,10 @@ export interface TaskOptions { destructor?: NodeDestructor; } +export interface TaskParams extends TaskOptions { + renderGroup: string; +} + /** * An element of the render task */ diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index 6e48089dca..b37f0e6138 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -27,13 +27,13 @@ const * @param [sliceOrOpts] - elements per chunk or `[start position, elements per chunk]` or additional options * @param [opts] - additional options * - * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & {renderGroup: string})` - * @emits `localEmitter.asyncRenderComplete(e: TaskParams & {renderGroup: string})` + * @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams)` + * @emits `localEmitter.asyncRenderComplete(e: TaskParams)` * * @example * ``` * /// Where to append asynchronous elements - * < .target v-async-target + * < .container v-async-target * /// Asynchronous rendering of components: only five elements per chunk * < template v-for = el in asyncRender.iterate(largeList, 5) * < my-component :data = el diff --git a/src/friends/async-render/render.ts b/src/friends/async-render/render.ts index 5d9f8d55fd..b01e7c68b1 100644 --- a/src/friends/async-render/render.ts +++ b/src/friends/async-render/render.ts @@ -12,6 +12,7 @@ import type AsyncRender from 'friends/async-render/class'; /** * Restarts the `asyncRender` daemon to force rendering of async chunks + * @see core/component/render/daemon */ export function forceRender(this: AsyncRender): void { restart(); @@ -20,7 +21,9 @@ export function forceRender(this: AsyncRender): void { /** * Creates a task to restart the `asyncRender` daemon on the next tick + * * @see forceRender + * @see core/component/render/daemon */ export function deferForceRender(this: AsyncRender): void { deferRestart(); @@ -46,8 +49,8 @@ export function deferForceRender(this: AsyncRender): void { * < button @click = asyncRender.forceRender() * Re-render the component * - * < .&__wrapper v-async-target - * < template v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')}) + * < .container v-async-target + * < .&__item v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')}) * < .&__content * {{ Math.random() }} * ``` From b25249cd48634fff889ff4605ab07da5b35ccd5e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 16:49:44 +0300 Subject: [PATCH 0165/2313] fix: fixed TS & ESLint errors --- src/friends/async-render/interface.ts | 7 +------ src/friends/async-render/iter.ts | 13 +++++++++---- src/friends/async-render/modules/iter.ts | 11 +++++------ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/friends/async-render/interface.ts b/src/friends/async-render/interface.ts index a8dd1f2704..bac2e90941 100644 --- a/src/friends/async-render/interface.ts +++ b/src/friends/async-render/interface.ts @@ -95,12 +95,7 @@ export interface TaskEl { /** * The original structure that we iterate */ - iterable: Iterable; - - /** - * An iteration index - */ - i: number; + iterable: CanPromise>; /** * Number of rendered tasks diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index b37f0e6138..528651bfbd 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -15,7 +15,7 @@ import { getIterDescriptor } from 'friends/async-render/modules/iter'; import type { TaskOptions } from 'friends/async-render/interface'; const - isCached = Symbol('Is cached') + isCached = Symbol('Is cached'); /** * Creates an asynchronous render stream from the specified value. @@ -123,15 +123,15 @@ export function iterate( return; } - // eslint-disable-next-line no-constant-condition // Using `while` instead of `for of` helps to iterate over synchronous and asynchronous iterators with a single loop + // eslint-disable-next-line no-constant-condition while (true) { if (opts.group != null) { group = `asyncComponents:${opts.group}:${chunkI}`; } let - el = iter.iterator.next(); + el: CanPromise> = iter.iterator.next(); try { el = Object.isPromise(el) ? await $a.promise(el, {group}) : el; @@ -158,6 +158,7 @@ export function iterate( if (Object.isPromise(needRender)) { await $a.promise(needRender, {group}).then( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (res) => resolveTask(iterVal, res === undefined || Object.isTruly(res)) ); @@ -182,7 +183,11 @@ export function iterate( iterI++; } catch (err) { - if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) { + if ( + Object.get(err, 'type') === 'clearAsync' && + Object.get(err, 'reason') === 'group' && + Object.get(err, 'link.group') === 'group' + ) { break; } diff --git a/src/friends/async-render/modules/iter.ts b/src/friends/async-render/modules/iter.ts index 4023649c86..8a633d4916 100644 --- a/src/friends/async-render/modules/iter.ts +++ b/src/friends/async-render/modules/iter.ts @@ -66,7 +66,7 @@ export function getIterDescriptor( return pendedResult; } - const res = (>iterable).then(async (v) => { + const res = iterable.then(async (v) => { const i = await getIterable.call(this, v); innerIter = i[Object.isAsyncIterable(i) ? Symbol.asyncIterator : Symbol.iterator](); return innerIter.next(); @@ -95,18 +95,17 @@ export function getIterDescriptor( canRender = !valIsPromise; if (canRender && filter != null) { - canRender = filter.call(this.component, iterVal, readI, { + const filterRes = filter.call(this.component, iterVal, readI, { iterable, - i: readI, total: readTotal }); - if (Object.isPromise(canRender)) { + if (Object.isPromise(filterRes)) { valIsPromise = true; canRender = false; - } else if (!Object.isTruly(canRender)) { - canRender = false; + } else { + canRender = Object.isTruly(filterRes); } } From 8c2dd5ea99055a83eaa8c686a7be9fafa6d15e8d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 1 Jun 2022 17:27:19 +0300 Subject: [PATCH 0166/2313] fix: added virtual methods --- src/friends/async-render/class.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/friends/async-render/class.ts b/src/friends/async-render/class.ts index 3482d7a92f..18f1aa09e5 100644 --- a/src/friends/async-render/class.ts +++ b/src/friends/async-render/class.ts @@ -7,10 +7,18 @@ */ import Friend from 'friends/friend'; - import type iBlock from 'super/i-block/i-block'; -export default class AsyncRender extends Friend { +import type * as iter from 'friends/async-render/iter'; +import type * as render from 'friends/async-render/render'; + +interface AsyncRender { + forceRender: typeof render.forceRender; + deferForceRender: typeof render.deferForceRender; + iterate: typeof iter.iterate; +} + +class AsyncRender extends Friend { constructor(component: iBlock) { super(component); @@ -21,3 +29,5 @@ export default class AsyncRender extends Friend { }); } } + +export default AsyncRender; From b41d3089314952de119253726c8276242f129c97 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:06:19 +0300 Subject: [PATCH 0167/2313] fix: fixed TS errors --- src/core/component/directives/attrs/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/attrs/index.ts b/src/core/component/directives/attrs/index.ts index a50e00dcb8..2f01d51916 100644 --- a/src/core/component/directives/attrs/index.ts +++ b/src/core/component/directives/attrs/index.ts @@ -223,11 +223,11 @@ ComponentEngine.directive('attrs', { registeredKeyModifiers = Object.keys(Object.select(flags, keyModifiers)); if (registeredModifiers.length > 0) { - attrVal = r?.withModifiers.call(ctx, attrVal, registeredKeyModifiers); + attrVal = r?.withModifiers.call(ctx, Object.cast(attrVal), registeredKeyModifiers); } if (registeredKeyModifiers.length > 0) { - attrVal = r?.withKeys.call(ctx, attrVal, registeredKeyModifiers); + attrVal = r?.withKeys.call(ctx, Object.cast(attrVal), registeredKeyModifiers); } } From 312f742faeb10fbe4d64c1e74cc48e4bc0f6dd48 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:06:37 +0300 Subject: [PATCH 0168/2313] feat: added a new hook `beforeCreate` --- src/core/component/directives/hook/README.md | 5 +++-- src/core/component/directives/hook/index.ts | 18 +++++++++++------- .../component/directives/hook/interface.ts | 14 ++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/core/component/directives/hook/README.md b/src/core/component/directives/hook/README.md index 88340474cd..cb4220a400 100644 --- a/src/core/component/directives/hook/README.md +++ b/src/core/component/directives/hook/README.md @@ -1,13 +1,14 @@ # core/component/directives/hook This module brings a directive to provide any directive hooks into a component. -This directive is extremely useful to combine with a flyweight component because it does not have API to -attach the hook listeners. +This directive is extremely useful to combine with functional components because they don't have API to +attach any hook listeners. ## Usage ``` < div v-hook = { & + beforeCreate: (opts, vnode) => console.log(opts, vnode), created: (el, opts, vnode, oldVnode) => console.log(el, opts, vnode, oldVnode), beforeMount: onBeforeMount, mounted: onMounted, diff --git a/src/core/component/directives/hook/index.ts b/src/core/component/directives/hook/index.ts index 53c14389e8..aadb2f062f 100644 --- a/src/core/component/directives/hook/index.ts +++ b/src/core/component/directives/hook/index.ts @@ -19,31 +19,35 @@ import type { DirectiveOptions } from 'core/component/directives/hook/interface' export * from 'core/component/directives/hook/interface'; ComponentEngine.directive('hook', { + beforeCreate(opts: DirectiveOptions): void { + opts.value?.beforeCreate?.apply(opts.value, Object.cast(arguments)); + }, + created(el: Element, opts: DirectiveOptions): void { - opts.value?.created?.apply(opts.value, arguments); + opts.value?.created?.apply(opts.value, Object.cast(arguments)); }, beforeMount(el: Element, opts: DirectiveOptions): void { - opts.value?.beforeMount?.apply(opts.value, arguments); + opts.value?.beforeMount?.apply(opts.value, Object.cast(arguments)); }, mounted(el: Element, opts: DirectiveOptions): void { - opts.value?.mounted?.apply(opts.value, arguments); + opts.value?.mounted?.apply(opts.value, Object.cast(arguments)); }, beforeUpdate(el: Element, opts: DirectiveOptions): void { - opts.value?.beforeUpdate?.apply(opts.value, arguments); + opts.value?.beforeUpdate?.apply(opts.value, Object.cast(arguments)); }, updated(el: Element, opts: DirectiveOptions): void { - opts.value?.updated?.apply(opts.value, arguments); + opts.value?.updated?.apply(opts.value, Object.cast(arguments)); }, beforeUnmount(el: Element, opts: DirectiveOptions): void { - opts.value?.beforeUnmount?.apply(opts.value, arguments); + opts.value?.beforeUnmount?.apply(opts.value, Object.cast(arguments)); }, unmounted(el: Element, opts: DirectiveOptions): void { - opts.value?.unmounted?.apply(opts.value, arguments); + opts.value?.unmounted?.apply(opts.value, Object.cast(arguments)); } }); diff --git a/src/core/component/directives/hook/interface.ts b/src/core/component/directives/hook/interface.ts index 3452754706..ffa35193d7 100644 --- a/src/core/component/directives/hook/interface.ts +++ b/src/core/component/directives/hook/interface.ts @@ -6,16 +6,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { DirectiveBinding, DirectiveHook } from 'core/component/engines'; +import type { DirectiveBinding, DirectiveHook, ObjectDirective } from 'core/component/engines'; export interface DirectiveOptions extends DirectiveBinding> {} -export interface DirectiveValue { - created?: DirectiveHook; - beforeMount?: DirectiveHook; - mounted?: DirectiveHook; - beforeUpdate?: DirectiveHook; - updated?: DirectiveHook; - beforeUnmount?: DirectiveHook; - unmounted?: DirectiveHook; -} +export type DirectiveValue = Overwrite, { + beforeCreate?: DirectiveHook; +}>; From ed71d79d20e9c720b4e9d63eb775806b75e72793 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:12:46 +0300 Subject: [PATCH 0169/2313] :art: --- src/core/component/directives/attrs/README.md | 2 +- src/core/component/directives/hook/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/attrs/README.md b/src/core/component/directives/attrs/README.md index 1363f21c0d..9f11d46a60 100644 --- a/src/core/component/directives/attrs/README.md +++ b/src/core/component/directives/attrs/README.md @@ -1,6 +1,6 @@ # core/component/directives/attrs -This module brings a directive to set any component or tag attributes from the passed dictionary. +This module provides a directive to set any component or tag attributes from the passed dictionary. ## Usage diff --git a/src/core/component/directives/hook/README.md b/src/core/component/directives/hook/README.md index cb4220a400..ffdc67058e 100644 --- a/src/core/component/directives/hook/README.md +++ b/src/core/component/directives/hook/README.md @@ -1,6 +1,6 @@ # core/component/directives/hook -This module brings a directive to provide any directive hooks into a component. +This module provides a directive to provide any directive hooks into a component. This directive is extremely useful to combine with functional components because they don't have API to attach any hook listeners. From 383f74535386e5a5aec13ba7b8efaf9d9cf5b7e0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:13:05 +0300 Subject: [PATCH 0170/2313] refactor: removed redundant exports --- src/core/component/directives/image/index.ts | 2 +- src/core/component/directives/in-view/README.md | 2 +- src/core/component/directives/in-view/adapter.ts | 9 --------- src/core/component/directives/in-view/helpers.ts | 9 --------- src/core/component/directives/in-view/index.ts | 2 +- 5 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 src/core/component/directives/in-view/adapter.ts delete mode 100644 src/core/component/directives/in-view/helpers.ts diff --git a/src/core/component/directives/image/index.ts b/src/core/component/directives/image/index.ts index aae491fbe9..824fee3178 100644 --- a/src/core/component/directives/image/index.ts +++ b/src/core/component/directives/image/index.ts @@ -16,7 +16,7 @@ import { ImageLoader } from 'core/dom/image'; import { ComponentEngine, VNode } from 'core/component/engines'; import type { DirectiveOptions } from 'core/component/directives/image/interface'; -export * from 'core/dom/image'; +export * from 'core/component/directives/image/interface'; ComponentEngine.directive('image', { mounted(el: HTMLElement, {value}: DirectiveOptions, vnode: VNode): void { diff --git a/src/core/component/directives/in-view/README.md b/src/core/component/directives/in-view/README.md index c51911c52b..fb5f18bdb7 100644 --- a/src/core/component/directives/in-view/README.md +++ b/src/core/component/directives/in-view/README.md @@ -14,7 +14,7 @@ This module provides a directive to track elements entering or leaving the viewp }, { threshold: 0.5, delay: 1000, - callback: () => console.log('half of the element in the viewport for 1 second') + callback: () => console.log('Half of the element was visible for one second') }] . ``` diff --git a/src/core/component/directives/in-view/adapter.ts b/src/core/component/directives/in-view/adapter.ts deleted file mode 100644 index a952641a63..0000000000 --- a/src/core/component/directives/in-view/adapter.ts +++ /dev/null @@ -1,9 +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/dom/in-view/adapter'; diff --git a/src/core/component/directives/in-view/helpers.ts b/src/core/component/directives/in-view/helpers.ts deleted file mode 100644 index d70f979b3c..0000000000 --- a/src/core/component/directives/in-view/helpers.ts +++ /dev/null @@ -1,9 +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/dom/in-view/helpers'; diff --git a/src/core/component/directives/in-view/index.ts b/src/core/component/directives/in-view/index.ts index 0038400db1..e597768f30 100644 --- a/src/core/component/directives/in-view/index.ts +++ b/src/core/component/directives/in-view/index.ts @@ -14,7 +14,7 @@ import { InView, Adaptee, InViewDirectiveOptions } from 'core/dom/in-view'; import { ComponentEngine } from 'core/component/engines'; -export * from 'core/dom/in-view'; +export * from 'core/component/directives/in-view/interface'; ComponentEngine.directive('in-view', { mounted(el: Element, {value}: InViewDirectiveOptions): void { From b47686791dceb881e1a955bdbbe7bc9c25761a67 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:20:49 +0300 Subject: [PATCH 0171/2313] refactor: removed redundant exports & refactoring --- src/core/component/directives/resize-observer/const.ts | 2 +- .../component/directives/resize-observer/helpers.ts | 4 ++-- src/core/component/directives/resize-observer/index.ts | 10 +++++----- src/core/component/directives/tag/README.md | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/component/directives/resize-observer/const.ts b/src/core/component/directives/resize-observer/const.ts index af58abb6a7..75da1d8d15 100644 --- a/src/core/component/directives/resize-observer/const.ts +++ b/src/core/component/directives/resize-observer/const.ts @@ -7,4 +7,4 @@ */ export const - DIRECTIVE_BIND: unique symbol = Symbol('Indicator for observable bound via a directive'); + DIRECTIVE = Symbol('An indicator that observer was initialized via the directive'); diff --git a/src/core/component/directives/resize-observer/helpers.ts b/src/core/component/directives/resize-observer/helpers.ts index d9e38e37ed..9aebf5bb4d 100644 --- a/src/core/component/directives/resize-observer/helpers.ts +++ b/src/core/component/directives/resize-observer/helpers.ts @@ -9,7 +9,7 @@ import type { ResizeWatcherInitOptions } from 'core/dom/resize-observer'; import type { ComponentInterface } from 'core/component/interface'; -import { DIRECTIVE_BIND } from 'core/component/directives/resize-observer/const'; +import { DIRECTIVE } from 'core/component/directives/resize-observer/const'; import type { ResizeWatcherObservable, ResizeWatcherObserverOptions } from 'core/component/directives/resize-observer/interface'; /** @@ -21,7 +21,7 @@ export function setCreatedViaDirectiveFlag(observable: Nullable { - if (observable[DIRECTIVE_BIND] === true) { + store.forEach((observable) => { + if (observable[DIRECTIVE] === true) { observable.destructor(); } }); diff --git a/src/core/component/directives/tag/README.md b/src/core/component/directives/tag/README.md index 8e2d5a534f..92812693b8 100644 --- a/src/core/component/directives/tag/README.md +++ b/src/core/component/directives/tag/README.md @@ -1,6 +1,6 @@ # core/component/directives/tag -This module brings a directive to specify dynamically a tag name to create. +This module provides a directive to specify dynamically a tag name to create. ## Usage From 5b445ff46f9969e2ada6bb2247a862187d34ddd3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:25:39 +0300 Subject: [PATCH 0172/2313] :art: --- src/friends/async-render/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/friends/async-render/interface.ts b/src/friends/async-render/interface.ts index bac2e90941..735122ab63 100644 --- a/src/friends/async-render/interface.ts +++ b/src/friends/async-render/interface.ts @@ -30,7 +30,7 @@ export interface TaskOptions { useRAF?: boolean; /** - * A group name to manual clearing of pending tasks via the `async` module. + * A group name to manual clearing of pending tasks via the [[Async]] module. * Providing this value disables automatically cleanup of render tasks on the `update` hook. * * @example From 1b113ffaf00321baf6d43ed487f5425b81f0aa5c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:33:04 +0300 Subject: [PATCH 0173/2313] refactor: removed redundant code --- src/core/component/directives/update-on/README.md | 3 ++- .../component/directives/update-on/engines/index.ts | 2 ++ src/core/component/directives/update-on/index.ts | 3 --- .../component/directives/update-on/interface.ts | 13 ++++++------- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/core/component/directives/update-on/README.md b/src/core/component/directives/update-on/README.md index 78ed4d151d..4a26b9fb3a 100644 --- a/src/core/component/directives/update-on/README.md +++ b/src/core/component/directives/update-on/README.md @@ -1,7 +1,8 @@ # core/component/directives/update-on This module provides a directive to manually update an element using various event(s) from multiple emitters. -Use this directive if you want to update some parts of your template without re-rendering of a whole template or with functional components. +Use this directive if you want to update some fragments of your template without re-rendering of the whole template or +with functional components. ## Usage diff --git a/src/core/component/directives/update-on/engines/index.ts b/src/core/component/directives/update-on/engines/index.ts index 514f71257d..fe1b755fe7 100644 --- a/src/core/component/directives/update-on/engines/index.ts +++ b/src/core/component/directives/update-on/engines/index.ts @@ -14,6 +14,8 @@ import { ID_ATTRIBUTE } from 'core/component/directives/update-on/const'; import type { ComponentInterface } from 'core/component'; import type { DirectiveValue } from 'core/component/directives/update-on/interface'; +export * from 'core/component/directives/update-on/interface'; + export const $$ = symbolGenerator(); diff --git a/src/core/component/directives/update-on/index.ts b/src/core/component/directives/update-on/index.ts index a57fc45050..1f70f1f063 100644 --- a/src/core/component/directives/update-on/index.ts +++ b/src/core/component/directives/update-on/index.ts @@ -14,9 +14,6 @@ import type { DirectiveOptions, DirectiveValue } from 'core/component/directives export * from 'core/component/directives/update-on/const'; export * from 'core/component/directives/update-on/interface'; -/** - * Directive to manually update an element by using special events - */ ComponentEngine.directive('update-on', { mounted(el: Element, {value}: DirectiveOptions, vnode: VNode): void { add(el, value, vnode); diff --git a/src/core/component/directives/update-on/interface.ts b/src/core/component/directives/update-on/interface.ts index a013a333d0..c4c1e7bf3b 100644 --- a/src/core/component/directives/update-on/interface.ts +++ b/src/core/component/directives/update-on/interface.ts @@ -18,7 +18,7 @@ export interface DirectiveValue { * The event emitter. * * It can be specified as a simple event emitter, a promise or string. - * In a string case, the string represents a name of the component property to watch. + * In the case of a string, the string represents a name of the component property to watch. * * Also, the emitter can be provided as a function. In that case, it will be invoked, * and the emitter is taken from the result. @@ -26,13 +26,12 @@ export interface DirectiveValue { emitter: EventEmitterLike | Function | Promise | string; /** - * Name of the event to listen + * A name of the event to listen */ event?: CanArray; /** - * Group name of the operation - * (for Async) + * A group name to manual clearing of pending tasks via the [[Async]] module */ group?: string; @@ -43,17 +42,17 @@ export interface DirectiveValue { single?: boolean; /** - * Additional options for an event emitter or watcher + * Additional options for the event emitter or watcher */ options?: Dictionary | WatchOptions; /** - * Function to handle events + * A function to handle events */ handler: Function; /** - * Function to handle error (if the emitter is specified as a promise) + * A function to handle errors (if the emitter is specified as a promise) */ errorHandler?: Function; } From 4b508ddf48cca88fec041557f8916a75f805823c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 11:36:15 +0300 Subject: [PATCH 0174/2313] refactor: removed optional imports --- src/core/component/directives/index.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 89abd05f83..7f8b11eb04 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -6,22 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -//#if runtime has directives/in-view -import 'core/component/directives/in-view'; -//#endif - -//#if runtime has directives/resize-observer -import 'core/component/directives/resize-observer'; -//#endif - -//#if runtime has directives/image -import 'core/component/directives/image'; -//#endif - -//#if runtime has directives/update-on -import 'core/component/directives/update-on'; -//#endif - import 'core/component/directives/tag'; import 'core/component/directives/hook'; import 'core/component/directives/attrs'; From aa12d753abcd829e751fdd3589cb2bf8d7951930 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 12:06:58 +0300 Subject: [PATCH 0175/2313] fix: don't fire events for nodes without a context --- src/core/component/directives/async/target/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/component/directives/async/target/index.ts b/src/core/component/directives/async/target/index.ts index ed2477dada..9a3dc3b233 100644 --- a/src/core/component/directives/async/target/index.ts +++ b/src/core/component/directives/async/target/index.ts @@ -18,7 +18,13 @@ export * from 'core/component/directives/async/async/interface'; ComponentEngine.directive('async-target', { beforeCreate(opts: DirectiveBinding, vnode: VNode): void { - const ctx = Object.cast(opts.instance); + const + ctx = Object.cast>(opts.instance); + + if (ctx == null) { + return; + } + ctx.$emit('[[V_ASYNC_TARGET]]', vnode); } }); From e994ff2fc38af4333abe0b1fd8f27ba9f074be21 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 2 Jun 2022 16:48:11 +0300 Subject: [PATCH 0176/2313] refactor: moved to core & new tree-shake friendly API --- .../module-loader/CHANGELOG.md | 7 + src/core/module-loader/README.md | 35 +++ .../modules => core}/module-loader/const.ts | 4 +- src/core/module-loader/index.ts | 279 +++++++++++++++++ src/core/module-loader/interface.ts | 40 +++ .../module-loader/test/index.js | 0 .../i-block/modules/module-loader/README.md | 3 - .../i-block/modules/module-loader/index.ts | 286 ------------------ .../modules/module-loader/interface.ts | 16 - 9 files changed, 363 insertions(+), 307 deletions(-) rename src/{super/i-block/modules => core}/module-loader/CHANGELOG.md (78%) create mode 100644 src/core/module-loader/README.md rename src/{super/i-block/modules => core}/module-loader/const.ts (66%) create mode 100644 src/core/module-loader/index.ts create mode 100644 src/core/module-loader/interface.ts rename src/{super/i-block/modules => core}/module-loader/test/index.js (100%) delete mode 100644 src/super/i-block/modules/module-loader/README.md delete mode 100644 src/super/i-block/modules/module-loader/index.ts delete mode 100644 src/super/i-block/modules/module-loader/interface.ts diff --git a/src/super/i-block/modules/module-loader/CHANGELOG.md b/src/core/module-loader/CHANGELOG.md similarity index 78% rename from src/super/i-block/modules/module-loader/CHANGELOG.md rename to src/core/module-loader/CHANGELOG.md index 04d29e126c..e064ba0ce3 100644 --- a/src/super/i-block/modules/module-loader/CHANGELOG.md +++ b/src/core/module-loader/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Moved the module to `core/module-loader` +* The module has been rewritten to a new tree-shake friendly API + ## v3.0.0 (2021-07-27) #### :house: Internal diff --git a/src/core/module-loader/README.md b/src/core/module-loader/README.md new file mode 100644 index 0000000000..a2416b1ac5 --- /dev/null +++ b/src/core/module-loader/README.md @@ -0,0 +1,35 @@ +# core/module-loader + +This module provides a class to manage of dynamically loaded modules. + +## Usage + +```js +import * as moduleLoader from 'core/module-loader'; + +// Adds a new module to the loading queue. +// The download won't start unless explicitly requested. +moduleLoader.add({ + id: 'form/b-button', + load: () => import('form/b-button') +}); + +moduleLoader.add({ + id: 'form/b-input', + load: () => import('form/b-input'), + + // When the download is requested, it will only start a second after the request + wait: () => new Promise((r) => setTimeout(r, 1000)) +}); + +console.log(moduleLoader.size()); // 2 + +console.log(moduleLoader.has('form/b-input')); // true +console.log(moduleLoader.has('form/b-textarea')); // false + +(async () => { + for await (const module of moduleLoader.values('form/b-input', 'form/b-textarea')) { + + } +})(); +``` diff --git a/src/super/i-block/modules/module-loader/const.ts b/src/core/module-loader/const.ts similarity index 66% rename from src/super/i-block/modules/module-loader/const.ts rename to src/core/module-loader/const.ts index 9fea9664a5..3f5809b114 100644 --- a/src/super/i-block/modules/module-loader/const.ts +++ b/src/core/module-loader/const.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Module } from 'super/i-block/modules/module-loader/interface'; +import type { Module } from 'core/module-loader/interface'; export const cache = new Map(), - cachedModules = []; + cachedModules: Module[] = []; diff --git a/src/core/module-loader/index.ts b/src/core/module-loader/index.ts new file mode 100644 index 0000000000..3eb797d68d --- /dev/null +++ b/src/core/module-loader/index.ts @@ -0,0 +1,279 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/module-loader/README.md]] + * @packageDocumentation + */ + +import { cache, cachedModules } from 'core/module-loader/const'; +import type { Module, ResolvedModule } from 'core/module-loader/interface'; + +export * from 'core/module-loader/interface'; + +let + resolve, + cursor; + +/** + * Returns the number of added modules + */ +export function size(): number { + return cachedModules.length; +} + +/** + * Returns true if a module by the passed identifier already exists in the cache + * @param id + */ +export function has(id: unknown): boolean { + return cache.has(id); +} + +/** + * Adds the specified modules to a queue to load. + * The method returns the number of added modules in the cache. + * + * @param modules + */ +export function add(...modules: Module[]): number { + for (let i = 0; i < modules.length; i++) { + const + module = modules[i]; + + if (module.id != null) { + if (has(module.id)) { + continue; + } + + cache.set(module.id, module); + } + + if (Object.isFunction(resolve)) { + resolve(module); + } + } + + return cache.size; +} + +/** + * Loads the specified modules. + * The method returns false if there is nothing to load. + */ +export function load(...modules: Module[]): CanPromise { + const + toLoad: Array> = []; + + for (let i = 0; i < modules.length; i++) { + const + module = modules[i], + resolvedModule = resolveModule(module); + + if (Object.isPromise(resolvedModule)) { + toLoad.push(resolvedModule); + + if (module.id != null) { + cache.set(module.id, module); + } + + cachedModules.push(module); + } + } + + if (toLoad.length === 0) { + return false; + } + + return Promise.all(toLoad).then(() => true); +} + +/** + * Returns an iterator over the added modules. + * If there are no provided identifiers to check, the iterator will never stop. + * + * @param [ids] - module identifiers to filter + */ +export function values(...ids: unknown[]): IterableIterator> { + let + cachedLength = cachedModules.length; + + let + iterPos = 0, + done = false; + + const + idsSet = new Set(ids), + subTasks: Array> = [], + subValues: Array> = []; + + const iterator = { + [Symbol.iterator]() { + return this; + }, + + next() { + if (done) { + return { + done: true, + value: undefined + }; + } + + if (ids.length > 0) { + for (let o = idsSet.values(), el = o.next(); !el.done; el = o.next()) { + const + id = el.value, + module = cache.get(id); + + if (module != null) { + idsSet.delete(id); + + const + resolvedModule = getResolvedModule(module); + + if (Object.isPromise(resolvedModule)) { + subTasks.push(resolvedModule); + } + + subValues.push(resolvedModule); + + if (idsSet.size === 0) { + done = true; + + return { + done: false, + value: subTasks.length > 0 ? + Promise.all(subTasks).then(() => Promise.allSettled(subValues)) : + subValues + }; + } + } + } + + } else if (iterPos !== cachedLength) { + return { + done: false, + value: getResolvedModule(cachedModules[iterPos++]) + }; + } + + if (cursor != null) { + if (ids.length > 0) { + return { + done: false, + value: cursor.value.then((module) => { + if (done) { + return module; + } + + return iterator.next().value; + }) + }; + } + + return cursor; + } + + cursor = { + done: false, + value: new Promise((r) => { + resolve = (module: Module) => { + const + resolvedModule = getResolvedModule(module); + + if (Object.isPromise(resolvedModule)) { + return resolvedModule.then(r); + } + + r(resolvedModule); + }; + }) + }; + + return cursor; + } + }; + + return iterator; + + function getResolvedModule(module: Module): CanPromise { + cursor = undefined; + resolve = undefined; + + if (ids.length > 0 && idsSet.has(module.id)) { + idsSet.delete(module.id); + done = idsSet.size === 0; + + } else if (cachedLength !== cachedModules.length) { + iterPos++; + cachedLength = cachedModules.length; + } + + return resolveModule(module); + } +} + +/** + * Resolves the specified module: if the module already exists in the cache, the function simply returns it. + * Otherwise, the module will be loaded. + * + * @param module + */ +function resolveModule(module: Module): CanPromise { + let + resolvedModule: ResolvedModule; + + if (module.id != null) { + resolvedModule = Object.cast(cache.get(module.id) ?? module); + + } else { + resolvedModule = Object.cast(module); + } + + let + promise; + + switch (resolvedModule.status) { + case 'loaded': + break; + + case 'pending': + promise = resolvedModule.promise; + break; + + default: { + resolvedModule.status = 'pending'; + + resolvedModule.promise = new Promise((r) => { + if (module.wait) { + r(module.wait().then(module.load.bind(module))); + + } else { + r(module.load()); + } + }); + + promise = resolvedModule.promise + .then(() => { + module.status = 'loaded'; + }) + + .catch((err) => { + stderr(err); + module.status = 'failed'; + }); + } + } + + if (promise != null) { + return promise.then(() => resolvedModule); + } + + return resolvedModule; +} diff --git a/src/core/module-loader/interface.ts b/src/core/module-loader/interface.ts new file mode 100644 index 0000000000..13b76a083f --- /dev/null +++ b/src/core/module-loader/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 + */ + +/** + * A structure to define a module to load + */ +export interface Module extends Dictionary { + /** + * The module identifier + */ + id?: unknown; + + /** + * A function to load the module + */ + load(): Promise; + + /** + * A function that returns a promise. + * The module loading won't start until this promise is fulfilled. + */ + wait?(): Promise; +} + +export interface ResolvedModule extends Module { + /** + * The module loading status + */ + status: 'pending' | 'loaded' | 'failed'; + + /** + * The module loading promise + */ + promise: Promise; +} diff --git a/src/super/i-block/modules/module-loader/test/index.js b/src/core/module-loader/test/index.js similarity index 100% rename from src/super/i-block/modules/module-loader/test/index.js rename to src/core/module-loader/test/index.js diff --git a/src/super/i-block/modules/module-loader/README.md b/src/super/i-block/modules/module-loader/README.md deleted file mode 100644 index 3d9e7db225..0000000000 --- a/src/super/i-block/modules/module-loader/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# super/i-block/modules/module-loader - -This module provides a class to load the dynamic dependencies of components. diff --git a/src/super/i-block/modules/module-loader/index.ts b/src/super/i-block/modules/module-loader/index.ts deleted file mode 100644 index 6d5b9ebebb..0000000000 --- a/src/super/i-block/modules/module-loader/index.ts +++ /dev/null @@ -1,286 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:super/i-block/modules/module-loader/README.md]] - * @packageDocumentation - */ - -import Friend from 'friends/friend'; - -import { cache, cachedModules } from 'super/i-block/modules/module-loader/const'; -import type { Module } from 'super/i-block/modules/module-loader/interface'; - -export * from 'super/i-block/modules/module-loader/interface'; - -let - resolve, - cursor; - -/** - * Class to load the dynamic dependencies of the component - */ -export default class ModuleLoader extends Friend { - /** - * Number of added modules - */ - get size(): number { - return cachedModules.length; - } - - /** - * Returns true if the specified module already exists in the cache - * @param module - */ - has(module: Module): boolean { - return module.id != null ? cache.has(module.id) : false; - } - - /** - * Adds the specified modules to a queue to load. - * The method returns the number of added modules in the cache. - * - * @param modules - */ - add(...modules: Module[]): number { - for (let i = 0; i < modules.length; i++) { - const - module = modules[i]; - - if (module.id != null) { - if (this.has(module)) { - continue; - } - - cache.set(module.id, module); - } - - if (Object.isFunction(resolve)) { - resolve(module); - } - } - - return cache.size; - } - - /** - * Loads the specified modules. - * The method returns false if there is nothing to load. - */ - load(...modules: Module[]): CanPromise { - const - toLoad = >>[]; - - for (let i = 0; i < modules.length; i++) { - const - module = modules[i], - val = this.resolveModule(module); - - if (Object.isPromise(val)) { - toLoad.push(val); - - if (module.id != null) { - cache.set(module.id, module); - } - - cachedModules.push(module); - } - } - - if (toLoad.length === 0) { - return false; - } - - return this.async.promise(Promise.all(toLoad).then(() => true)); - } - - [Symbol.iterator](): IterableIterator> { - return this.values(); - } - - /** - * Returns an iterator to iterate the added modules. - * If there is no provided id to check, the iterator will never stop. - * The method should be used with [[AsyncRender]]. - * - * @param [ids] - module identifiers to filter - */ - values(...ids: unknown[]): IterableIterator> { - const - {async: $a} = this; - - let - iterPos = 0, - done = false, - cachedLength = cachedModules.length; - - const - idsSet = new Set(ids), - subTasks = >>[], - subValues = >>[]; - - const iterator = { - [Symbol.iterator]: () => iterator, - - next: () => { - if (done) { - return { - done: true, - value: undefined - }; - } - - const initModule = (module: Module) => { - cursor = undefined; - resolve = undefined; - - if (ids.length > 0 && idsSet.has(module.id)) { - idsSet.delete(module.id); - done = idsSet.size === 0; - - } else if (cachedLength !== cachedModules.length) { - iterPos++; - cachedLength = cachedModules.length; - } - - return this.resolveModule(module); - }; - - if (ids.length > 0) { - for (let o = idsSet.values(), el = o.next(); !el.done; el = o.next()) { - const - id = el.value, - module = cache.get(id); - - if (module != null) { - idsSet.delete(id); - - const - val = initModule(module); - - if (Object.isPromise(val)) { - subTasks.push(val); - } - - subValues.push(val); - - if (idsSet.size === 0) { - done = true; - - return { - done: false, - value: subTasks.length > 0 ? - $a.promise(Promise.all(subTasks).then(() => Promise.allSettled(subValues))) : - subValues - }; - } - } - } - - } else if (iterPos !== cachedLength) { - return { - done: false, - value: initModule(cachedModules[iterPos++]) - }; - } - - if (cursor != null) { - if (ids.length > 0) { - return { - done: false, - value: cursor.value.then((module) => { - if (done) { - return module; - } - - return iterator.next().value; - }) - }; - } - - return cursor; - } - - cursor = { - done: false, - value: $a.promise(new Promise((r) => { - resolve = (module: Module) => { - const - val = initModule(module); - - if (Object.isPromise(val)) { - return val.then(r); - } - - r(val); - }; - })) - }; - - return cursor; - } - }; - - return iterator; - } - - /** - * Resolves the specified module: if the module already exists in the cache, the method simply returns it. - * Otherwise, the module will be loaded. - * - * @param module - */ - protected resolveModule(module: Module): CanPromise { - const - {async: $a} = this; - - if (module.id != null) { - module = cache.get(module.id) ?? module; - } - - let - promise; - - switch (module.status) { - case 'loaded': - break; - - case 'pending': - promise = module.promise; - break; - - default: { - module.status = 'pending'; - module.promise = $a.promise(new Promise((r) => { - if (module.wait) { - r($a.promise(module.wait()).then(module.load.bind(module))); - - } else { - r(module.load()); - } - })); - - promise = module.promise - .then(() => { - module.status = 'loaded'; - }) - - .catch((err) => { - stderr(err); - module.status = 'failed'; - }); - } - } - - if (promise != null) { - return promise.then(() => module); - } - - return module; - } -} diff --git a/src/super/i-block/modules/module-loader/interface.ts b/src/super/i-block/modules/module-loader/interface.ts deleted file mode 100644 index c51c7a418f..0000000000 --- a/src/super/i-block/modules/module-loader/interface.ts +++ /dev/null @@ -1,16 +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 Module extends Dictionary { - id?: unknown; - status?: 'pending' | 'loaded' | 'failed'; - import?: unknown; - promise?: CanArray>; - load(): Promise; - wait?(): Promise; -} From 56c6f555b27cb281d10c4095149cf9073329c848 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 3 Jun 2022 14:51:50 +0300 Subject: [PATCH 0177/2313] refactor: split interface into separated files --- src/friends/async-render/interface/index.ts | 10 +++ src/friends/async-render/interface/iter.ts | 64 +++++++++++++++++++ .../{interface.ts => interface/task.ts} | 55 ---------------- 3 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 src/friends/async-render/interface/index.ts create mode 100644 src/friends/async-render/interface/iter.ts rename src/friends/async-render/{interface.ts => interface/task.ts} (79%) diff --git a/src/friends/async-render/interface/index.ts b/src/friends/async-render/interface/index.ts new file mode 100644 index 0000000000..2f3baf6a55 --- /dev/null +++ b/src/friends/async-render/interface/index.ts @@ -0,0 +1,10 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +export * from 'friends/async-render/interface/task'; +export * from 'friends/async-render/interface/iter'; diff --git a/src/friends/async-render/interface/iter.ts b/src/friends/async-render/interface/iter.ts new file mode 100644 index 0000000000..168deac302 --- /dev/null +++ b/src/friends/async-render/interface/iter.ts @@ -0,0 +1,64 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { TaskFilter } from 'friends/async-render/interface/task'; + +/** + * Additional options to render an iterable structure + */ +export interface IterOptions { + /** + * A start index to iterate + */ + start?: number; + + /** + * How many fragments can be rendered at the same time + */ + perChunk?: number; + + /** + * A function to filter elements to render + */ + filter?: TaskFilter; +} + +/** + * A descriptor of the iterable-based rendering structure + */ +export interface IterDescriptor { + /** + * Is this iterator asynchronous or not + */ + isAsync: boolean; + + /** + * An index of the last synchronously read element for the first render + */ + readI: number; + + /** + * Number of synchronously read elements for the first render + */ + readTotal: number; + + /** + * An array of the synchronously read elements for the first render + */ + readEls: unknown[]; + + /** + * The original structure that we iterate + */ + iterable: CanPromise; + + /** + * An iterator for the structure that we iterate + */ + iterator: AnyIterableIterator; +} diff --git a/src/friends/async-render/interface.ts b/src/friends/async-render/interface/task.ts similarity index 79% rename from src/friends/async-render/interface.ts rename to src/friends/async-render/interface/task.ts index 735122ab63..5371a609b0 100644 --- a/src/friends/async-render/interface.ts +++ b/src/friends/async-render/interface/task.ts @@ -108,26 +108,6 @@ export interface TaskEl { chunk?: number; } -/** - * Additional options to render an iterable structure - */ -export interface IterOptions { - /** - * A start index to iterate - */ - start?: number; - - /** - * How many fragments can be rendered at the same time - */ - perChunk?: number; - - /** - * A function to filter elements to render - */ - filter?: TaskFilter; -} - /** * A filter function for render tasks */ @@ -152,38 +132,3 @@ export interface NodeDestructor { */ (node: Node, childComponentEls: Element[]): AnyToBoolean; } - -/** - * A descriptor of the iterable-based rendering structure - */ -export interface IterDescriptor { - /** - * Is this iterator asynchronous or not - */ - isAsync: boolean; - - /** - * An index of the last synchronously read element for the first render - */ - readI: number; - - /** - * Number of synchronously read elements for the first render - */ - readTotal: number; - - /** - * An array of the synchronously read elements for the first render - */ - readEls: unknown[]; - - /** - * The original structure that we iterate - */ - iterable: CanPromise; - - /** - * An iterator for the structure that we iterate - */ - iterator: AnyIterableIterator; -} From 5967a0622d88d696ce59c3e5f3117f4f42357d99 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 3 Jun 2022 16:06:21 +0300 Subject: [PATCH 0178/2313] refactor: moved to `friends/module-loader` & new tree-shake friendly API --- .../module-loader/CHANGELOG.md | 2 +- src/friends/module-loader/api.ts | 94 +++++++++++++++++++ src/friends/module-loader/class.ts | 27 ++++++ src/{core => friends}/module-loader/const.ts | 5 +- src/friends/module-loader/helpers.ts | 74 +++++++++++++++ src/friends/module-loader/index.ts | 17 ++++ .../module-loader/interface.ts | 0 .../module-loader/test/index.js | 0 8 files changed, 215 insertions(+), 4 deletions(-) rename src/{core => friends}/module-loader/CHANGELOG.md (94%) create mode 100644 src/friends/module-loader/api.ts create mode 100644 src/friends/module-loader/class.ts rename src/{core => friends}/module-loader/const.ts (57%) create mode 100644 src/friends/module-loader/helpers.ts create mode 100644 src/friends/module-loader/index.ts rename src/{core => friends}/module-loader/interface.ts (100%) rename src/{core => friends}/module-loader/test/index.js (100%) diff --git a/src/core/module-loader/CHANGELOG.md b/src/friends/module-loader/CHANGELOG.md similarity index 94% rename from src/core/module-loader/CHANGELOG.md rename to src/friends/module-loader/CHANGELOG.md index e064ba0ce3..c79c3af94a 100644 --- a/src/core/module-loader/CHANGELOG.md +++ b/src/friends/module-loader/CHANGELOG.md @@ -13,7 +13,7 @@ Changelog #### :boom: Breaking Change -* Moved the module to `core/module-loader` +* Moved the module to `friends/module-loader` * The module has been rewritten to a new tree-shake friendly API ## v3.0.0 (2021-07-27) diff --git a/src/friends/module-loader/api.ts b/src/friends/module-loader/api.ts new file mode 100644 index 0000000000..cbdb71dd8c --- /dev/null +++ b/src/friends/module-loader/api.ts @@ -0,0 +1,94 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type ModuleLoader from 'friends/module-loader/class'; + +import { cache } from 'friends/module-loader/const'; +import { resolveModule } from 'friends/module-loader/helpers'; + +import type { Module } from 'friends/module-loader/interface'; + +export * from 'friends/module-loader/interface'; + +/** + * Adds the specified modules to a load bucket by the specified name. + * Notice, adding modules don’t force them to load. To load the created bucket, use the `loadBucket` method. + * The function returns the number of added modules in the bucket. + * + * @param bucketName + * @param modules + */ +export function addModulesToBucket(this: ModuleLoader, bucketName: string, ...modules: Module[]): number { + let + bucket = this.loadBuckets.get(bucketName); + + if (bucket == null) { + bucket = new Set(); + this.loadBuckets.set(bucketName, bucket); + } + + for (let i = 0; i < modules.length; i++) { + const + module = modules[i]; + + if (module.id != null) { + if (!cache.has(module.id)) { + cache.set(module.id, module); + } + } + + bucket.add(module); + } + + return bucket.size; +} + +/** + * Loads the specified modules. + * If some modules are already loaded, they won’t be loaded twice. + * If all specified modules are already loaded, the function returns a simple value, but not a promise. + * The resulting value is designed to use with [[AsyncRender]]. + * + * @param modules + */ +export function load(this: ModuleLoader, ...modules: Module[]): CanPromise> { + const + tasks: Array> = []; + + for (let i = 0; i < modules.length; i++) { + const + module = modules[i], + resolvedModule = resolveModule.call(this, module); + + if (Object.isPromise(resolvedModule)) { + tasks.push(resolvedModule); + } + } + + const + i = [modules].values(); + + if (tasks.length === 0) { + return i; + } + + return this.async.promise(Promise.all(tasks)).then(() => i); +} + +/** + * Loads a bucket of modules by the specified name. + * If some modules are already loaded, they won’t be loaded twice. + * If all specified modules are already loaded, the function returns a simple value, but not a promise. + * The resulting value is designed to use with [[AsyncRender]]. + * + * @param bucketName + */ +export function loadBucket(this: ModuleLoader, bucketName: string): CanPromise> { + const bucket = this.loadBuckets.get(bucketName) ?? new Set(); + return load.call(this, ...bucket); +} diff --git a/src/friends/module-loader/class.ts b/src/friends/module-loader/class.ts new file mode 100644 index 0000000000..e3f88cf1a4 --- /dev/null +++ b/src/friends/module-loader/class.ts @@ -0,0 +1,27 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Friend from 'friends/friend'; + +import type * as api from 'friends/module-loader/api'; +import type { Module } from 'friends/module-loader/interface'; + +interface ModuleLoader { + load: typeof api.load; + loadBucket: typeof api.loadBucket; + addModulesToLoadBucket: typeof api.addModulesToBucket; +} + +class ModuleLoader extends Friend { + /** + * A map of registered buckets to load + */ + protected loadBuckets: Map> = new Map(); +} + +export default ModuleLoader; diff --git a/src/core/module-loader/const.ts b/src/friends/module-loader/const.ts similarity index 57% rename from src/core/module-loader/const.ts rename to src/friends/module-loader/const.ts index 3f5809b114..ff9958133b 100644 --- a/src/core/module-loader/const.ts +++ b/src/friends/module-loader/const.ts @@ -6,8 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Module } from 'core/module-loader/interface'; +import type { Module } from 'friends/module-loader/interface'; export const - cache = new Map(), - cachedModules: Module[] = []; + cache = new Map(); diff --git a/src/friends/module-loader/helpers.ts b/src/friends/module-loader/helpers.ts new file mode 100644 index 0000000000..5fc0a2ffde --- /dev/null +++ b/src/friends/module-loader/helpers.ts @@ -0,0 +1,74 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { cache } from 'friends/module-loader/const'; + +import type ModuleLoader from 'friends/module-loader/class'; +import type { Module, ResolvedModule } from 'friends/module-loader/interface'; + +/** + * Resolves the specified module: if the module already exists in the cache, the function simply returns it. + * Otherwise, the module will be loaded. + * + * @param module + */ +export function resolveModule(this: ModuleLoader, module: Module): CanPromise { + const + {async: $a} = this; + + let + resolvedModule: ResolvedModule; + + if (module.id != null) { + resolvedModule = Object.cast(cache.get(module.id) ?? module); + + } else { + resolvedModule = Object.cast(module); + } + + let + promise; + + switch (resolvedModule.status) { + case 'loaded': + break; + + case 'pending': + promise = resolvedModule.promise; + break; + + default: { + resolvedModule.status = 'pending'; + + resolvedModule.promise = $a.promise(new Promise((r) => { + if (module.wait) { + r($a.promise(module.wait()).then(module.load.bind(module))); + + } else { + r(module.load()); + } + })); + + promise = resolvedModule.promise + .then(() => { + module.status = 'loaded'; + }) + + .catch((err) => { + stderr(err); + module.status = 'failed'; + }); + } + } + + if (promise != null) { + return promise.then(() => resolvedModule); + } + + return resolvedModule; +} diff --git a/src/friends/module-loader/index.ts b/src/friends/module-loader/index.ts new file mode 100644 index 0000000000..d59ad825ba --- /dev/null +++ b/src/friends/module-loader/index.ts @@ -0,0 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:friends/module-loader/README.md]] + * @packageDocumentation + */ + +export { default } from 'friends/module-loader/class'; + +export * from 'friends/module-loader/api'; +export * from 'friends/module-loader/interface'; diff --git a/src/core/module-loader/interface.ts b/src/friends/module-loader/interface.ts similarity index 100% rename from src/core/module-loader/interface.ts rename to src/friends/module-loader/interface.ts diff --git a/src/core/module-loader/test/index.js b/src/friends/module-loader/test/index.js similarity index 100% rename from src/core/module-loader/test/index.js rename to src/friends/module-loader/test/index.js From aafc21e4a4ce84dd6d4775f4f165dc8c292945a6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 3 Jun 2022 16:48:55 +0300 Subject: [PATCH 0179/2313] docs: improved doc --- src/core/module-loader/README.md | 35 ---------------- src/friends/module-loader/README.md | 61 +++++++++++++++++++++++++++ src/friends/module-loader/api.ts | 64 ++++++++++++++--------------- 3 files changed, 93 insertions(+), 67 deletions(-) delete mode 100644 src/core/module-loader/README.md create mode 100644 src/friends/module-loader/README.md diff --git a/src/core/module-loader/README.md b/src/core/module-loader/README.md deleted file mode 100644 index a2416b1ac5..0000000000 --- a/src/core/module-loader/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# core/module-loader - -This module provides a class to manage of dynamically loaded modules. - -## Usage - -```js -import * as moduleLoader from 'core/module-loader'; - -// Adds a new module to the loading queue. -// The download won't start unless explicitly requested. -moduleLoader.add({ - id: 'form/b-button', - load: () => import('form/b-button') -}); - -moduleLoader.add({ - id: 'form/b-input', - load: () => import('form/b-input'), - - // When the download is requested, it will only start a second after the request - wait: () => new Promise((r) => setTimeout(r, 1000)) -}); - -console.log(moduleLoader.size()); // 2 - -console.log(moduleLoader.has('form/b-input')); // true -console.log(moduleLoader.has('form/b-textarea')); // false - -(async () => { - for await (const module of moduleLoader.values('form/b-input', 'form/b-textarea')) { - - } -})(); -``` diff --git a/src/friends/module-loader/README.md b/src/friends/module-loader/README.md new file mode 100644 index 0000000000..8599081e30 --- /dev/null +++ b/src/friends/module-loader/README.md @@ -0,0 +1,61 @@ +# friends/module-loader + +This module provides a class to manage dynamically loaded modules. +Basically, this module is used with [[AsyncRender]]. + +## How to include this module to your component + +By default, any components that inherited from [[iBlock]] have the `moduleLoader` property. +But to use module methods, you should attach them explicitly to enable tree-shake code optimizations. +Just place a necessary import declaration within your component file. + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; +import ModuleLoader, { load } from 'friends/module-loader'; + +// Import the `load` method +ModuleLoader.addToPrototype(load); + +@component() +export default class bExample extends iBlock {} +``` + +## Usage + +The module API is designed to use with [[AsyncRender]]. +For instance, you can dynamically require dependencies when it needed and render a fragment that uses these modules asynchronously. + +``` +/// Don't forget to declare where to mount dynamically rendered fragments +< .container v-async-target + {{ moduleLoader.add('form', {id: 'form/b-button', load: () => import('form/b-button')}) }} + {{ moduleLoader.add('form', {id: 'form/b-input', load: () => import('form/b-input')}) }} + + < .form v-for = _ in asyncRender.iterate(moduleLoader.loadBucket('form')) + < b-button + Press on me! + + < b-input :placeholder = 'Enter your name' +``` + +## Methods + +### load + +Loads the specified modules. +If some modules are already loaded, they won’t be loaded twice. +If all specified modules are already loaded, the function returns a simple value, but not a promise. +The resulting value is designed to use with [[AsyncRender]]. + +### addModulesToBucket + +Adds the specified modules to a load bucket by the specified name. +Notice, adding modules don’t force them to load. To load the created bucket, use the `loadBucket` method. +The function returns the number of added modules in the bucket. + +### loadBucket + +Loads a bucket of modules by the specified name. +If some modules are already loaded, they won’t be loaded twice. +If all specified modules are already loaded, the function returns a simple value, but not a promise. +The resulting value is designed to use with [[AsyncRender]]. diff --git a/src/friends/module-loader/api.ts b/src/friends/module-loader/api.ts index cbdb71dd8c..d53fac9222 100644 --- a/src/friends/module-loader/api.ts +++ b/src/friends/module-loader/api.ts @@ -15,6 +15,38 @@ import type { Module } from 'friends/module-loader/interface'; export * from 'friends/module-loader/interface'; +/** + * Loads the specified modules. + * If some modules are already loaded, they won’t be loaded twice. + * If all specified modules are already loaded, the function returns a simple value, but not a promise. + * The resulting value is designed to use with [[AsyncRender]]. + * + * @param modules + */ +export function load(this: ModuleLoader, ...modules: Module[]): CanPromise> { + const + tasks: Array> = []; + + for (let i = 0; i < modules.length; i++) { + const + module = modules[i], + resolvedModule = resolveModule.call(this, module); + + if (Object.isPromise(resolvedModule)) { + tasks.push(resolvedModule); + } + } + + const + i = [modules].values(); + + if (tasks.length === 0) { + return i; + } + + return this.async.promise(Promise.all(tasks)).then(() => i); +} + /** * Adds the specified modules to a load bucket by the specified name. * Notice, adding modules don’t force them to load. To load the created bucket, use the `loadBucket` method. @@ -48,38 +80,6 @@ export function addModulesToBucket(this: ModuleLoader, bucketName: string, ...mo return bucket.size; } -/** - * Loads the specified modules. - * If some modules are already loaded, they won’t be loaded twice. - * If all specified modules are already loaded, the function returns a simple value, but not a promise. - * The resulting value is designed to use with [[AsyncRender]]. - * - * @param modules - */ -export function load(this: ModuleLoader, ...modules: Module[]): CanPromise> { - const - tasks: Array> = []; - - for (let i = 0; i < modules.length; i++) { - const - module = modules[i], - resolvedModule = resolveModule.call(this, module); - - if (Object.isPromise(resolvedModule)) { - tasks.push(resolvedModule); - } - } - - const - i = [modules].values(); - - if (tasks.length === 0) { - return i; - } - - return this.async.promise(Promise.all(tasks)).then(() => i); -} - /** * Loads a bucket of modules by the specified name. * If some modules are already loaded, they won’t be loaded twice. From 29e8c31cb8eeea3365f180d010c140319ffbffb1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 15:04:27 +0300 Subject: [PATCH 0180/2313] feat: added a new directive `v-render` --- .../component/directives/render/CHANGELOG.md | 16 ++++ .../component/directives/render/README.md | 44 ++++++++++ src/core/component/directives/render/index.ts | 86 +++++++++++++++++++ .../component/directives/render/interface.ts | 11 +++ src/core/component/render/components/index.ts | 9 -- 5 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 src/core/component/directives/render/CHANGELOG.md create mode 100644 src/core/component/directives/render/README.md create mode 100644 src/core/component/directives/render/index.ts create mode 100644 src/core/component/directives/render/interface.ts delete mode 100644 src/core/component/render/components/index.ts diff --git a/src/core/component/directives/render/CHANGELOG.md b/src/core/component/directives/render/CHANGELOG.md new file mode 100644 index 0000000000..b18e802496 --- /dev/null +++ b/src/core/component/directives/render/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/component/directives/render/README.md b/src/core/component/directives/render/README.md new file mode 100644 index 0000000000..b50e663733 --- /dev/null +++ b/src/core/component/directives/render/README.md @@ -0,0 +1,44 @@ +# core/component/directives/render + +This module provides a directive to create a composition of multiple functions that return VNode-s without using JSX. + +## Usage + +### Replacing a VNode with another one + +If you use the directive with a `template` tag without custom properties, a VNode that passed to `v-render` will replace the original VNode. +If the passed value is undefined or null, the directive will do nothing. + +``` +< template v-render = myFragment + This content is used when the value passed to `v-render` is undefined or null. +``` + +### Adding child nodes + +If you use the directive with a regular tag, a VNode that passed to `v-render` +will replace all children VNode-s of the original VNode. Also, in this case, you can provide a list of VNode-s to insert. +If the passed value is undefined or null, the directive will do nothing. + +``` +< div v-render = myFragment + This content is used when the value passed to `v-render` is undefined or null. + +< div v-render = [myFragment1, myFragment2] + This content is used when the value passed to `v-render` is undefined or null. +``` + +### Adding component slots + +If you use the directive with a component, a VNode that passed to `v-render` will replace the default or named +(if the name passed via the `slot` attribute) children slot of the original VNode. +Also, in this case, you can provide a list of VNode-s or slots to insert. +If the passed value is undefined or null, the directive will do nothing. + +``` +< b-button v-render = mySlot + This content is used when the value passed to `v-render` is undefined or null. + +< b-button v-render = [mySlot1, mySlot2] + This content is used when the value passed to `v-render` is undefined or null. +``` diff --git a/src/core/component/directives/render/index.ts b/src/core/component/directives/render/index.ts new file mode 100644 index 0000000000..8fe9f1dfaf --- /dev/null +++ b/src/core/component/directives/render/index.ts @@ -0,0 +1,86 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:core/component/directives/render/README.md]] + * @packageDocumentation + */ + +import { ComponentEngine, VNode } from 'core/component/engines'; +import type { DirectiveOptions } from 'core/component/directives/render/interface'; + +export * from 'core/component/directives/render/interface'; + +ComponentEngine.directive('render', { + beforeCreate(opts: DirectiveOptions, vnode: VNode): CanUndef { + const + newVNode = opts.value, + originalChildren = vnode.children; + + if (newVNode != null) { + const canReplaceVNode = + vnode.type === 'template' && + !Object.isArray(newVNode) && + Object.size(vnode.props) === 0; + + if (canReplaceVNode) { + return newVNode; + } + + if (Object.isString(vnode.type)) { + vnode.children = Array.concat([], newVNode); + + } else { + const slots = Object.isPlainObject(originalChildren) ? Object.reject(originalChildren, /^_/) : {}; + vnode.children = slots; + + if (Object.isArray(newVNode)) { + if (isSlot(newVNode[0])) { + for (let i = 0; i < newVNode.length; i++) { + const + el = newVNode[i], + slot = el.props?.slot; + + if (slot != null) { + slots[slot] = () => el.children ?? getDefSlotFromChildren(slot); + } + } + + return; + } + + } else if (isSlot(newVNode)) { + const {slot} = newVNode.props!; + slots[slot] = () => newVNode.children ?? getDefSlotFromChildren(slot); + return; + } + + slots.default = () => newVNode; + } + } + + function isSlot(vnode: CanUndef): boolean { + return vnode?.type === 'template' && vnode.props?.slot != null; + } + + function getDefSlotFromChildren(slotName: string): unknown { + if (Object.isPlainObject(originalChildren)) { + const + slot = originalChildren[slotName]; + + if (Object.isFunction(slot)) { + return slot(); + } + + return slot; + } + + return originalChildren; + } + } +}); diff --git a/src/core/component/directives/render/interface.ts b/src/core/component/directives/render/interface.ts new file mode 100644 index 0000000000..a61bb8a15c --- /dev/null +++ b/src/core/component/directives/render/interface.ts @@ -0,0 +1,11 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { DirectiveBinding, VNode } from 'core/component/engines'; + +export interface DirectiveOptions extends DirectiveBinding>> {} diff --git a/src/core/component/render/components/index.ts b/src/core/component/render/components/index.ts deleted file mode 100644 index afe7eee6e6..0000000000 --- a/src/core/component/render/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import 'core/component/render/components/v-render'; From e40e570c4c49e19161cdd8e91b6dfd2a2f31d03e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 15:23:46 +0300 Subject: [PATCH 0181/2313] refactor: review the module structure & moved some logic to directives --- .../component/render/components/v-render.ts | 23 -------- src/core/component/render/const.ts | 9 ---- src/core/component/render/daemon/index.ts | 16 +++--- src/core/component/render/daemon/interface.ts | 3 +- .../render/{vnode.ts => helpers/attrs.ts} | 19 +------ src/core/component/render/helpers/index.ts | 11 ++++ .../{helpers.ts => helpers/normalizers.ts} | 53 ------------------ src/core/component/render/helpers/props.ts | 54 +++++++++++++++++++ src/core/component/render/index.ts | 4 -- src/core/component/render/interface.ts | 13 ----- src/core/component/render/wrappers.ts | 30 +++++------ 11 files changed, 90 insertions(+), 145 deletions(-) delete mode 100644 src/core/component/render/components/v-render.ts delete mode 100644 src/core/component/render/const.ts rename src/core/component/render/{vnode.ts => helpers/attrs.ts} (75%) create mode 100644 src/core/component/render/helpers/index.ts rename src/core/component/render/{helpers.ts => helpers/normalizers.ts} (64%) create mode 100644 src/core/component/render/helpers/props.ts delete mode 100644 src/core/component/render/interface.ts diff --git a/src/core/component/render/components/v-render.ts b/src/core/component/render/components/v-render.ts deleted file mode 100644 index f7ea6904be..0000000000 --- a/src/core/component/render/components/v-render.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 - */ - -import type { VNode, VNodeTypes, VNodeProps } from 'core/component/engines'; - -import { components } from 'core/component/render/const'; -import type { CreateVNode } from 'core/component/render/interface'; - -export default vRender; -components['v-render'] = vRender; - -function vRender(createVNode: CreateVNode, type: VNodeTypes, props: Dictionary & VNodeProps): CanUndef { - if (type !== 'v-render' || props.from == null) { - return undefined; - } - - return Object.cast(props.from); -} diff --git a/src/core/component/render/const.ts b/src/core/component/render/const.ts deleted file mode 100644 index ade318dd3c..0000000000 --- a/src/core/component/render/const.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export const components: Dictionary = Object.createDict(); diff --git a/src/core/component/render/daemon/index.ts b/src/core/component/render/daemon/index.ts index 0661ef6a17..60d09702d5 100644 --- a/src/core/component/render/daemon/index.ts +++ b/src/core/component/render/daemon/index.ts @@ -23,6 +23,8 @@ import { } from 'core/component/render/daemon/const'; +import type { Task } from 'core/component/render/daemon/interface'; + export * from 'core/component/render/daemon/const'; export * from 'core/component/render/daemon/interface'; @@ -30,15 +32,14 @@ let inProgress = false, isStarted = false; -queue.add = function add(...args: unknown[]): T { - const - res = addToQueue(...args); +queue.add = function add(task: Task): typeof queue { + addToQueue(task); if (!isStarted) { run(); } - return res; + return this; }; /** @@ -51,8 +52,7 @@ export function restart(): void { } /** - * Restarts the render daemon - * (it runs on the next tick) + * Creates a task to restart the render daemon on the next tick */ export function deferRestart(): void { isStarted = false; @@ -79,8 +79,6 @@ function run(): void { if (done <= 0 || Date.now() - time > DELAY) { await daemon.idle({timeout: DELAY}); time = Date.now(); - - // eslint-disable-next-line require-atomic-updates done = TASKS_PER_TICK; } @@ -92,7 +90,7 @@ function run(): void { } const - canRender = val.fn(); + canRender = val.useRAF ? daemon.animationFrame().then(val.task) : val.task(); const exec = (canRender) => { if (Object.isTruly(canRender)) { diff --git a/src/core/component/render/daemon/interface.ts b/src/core/component/render/daemon/interface.ts index f015f1c433..9a9a718b9e 100644 --- a/src/core/component/render/daemon/interface.ts +++ b/src/core/component/render/daemon/interface.ts @@ -7,6 +7,7 @@ */ export interface Task { - fn: Function; + task: Function; + useRAF?: boolean; weight?: number; } diff --git a/src/core/component/render/vnode.ts b/src/core/component/render/helpers/attrs.ts similarity index 75% rename from src/core/component/render/vnode.ts rename to src/core/component/render/helpers/attrs.ts index 305ca55acb..6ced2be596 100644 --- a/src/core/component/render/vnode.ts +++ b/src/core/component/render/helpers/attrs.ts @@ -7,25 +7,10 @@ */ import type { VNode } from 'core/component/engines'; +import { mergeProps } from 'core/component/render/helpers/props'; -import { components } from 'core/component/render/const'; -import { mergeProps } from 'core/component/render/helpers'; - -import type { CreateVNode } from 'core/component/render/interface'; import type { ComponentInterface } from 'core/component/interface'; -/** - * Creates a new VNode by the specified parameters with applying internal component directives, like `v-render`. - * The function takes an original function to create VNodes and list of arguments and returns a new VNode. - * - * @param createVNode - original function to create a VNode - * @param type - VNode type - * @param args - operation arguments - */ -export function createVNodeWithDirectives(createVNode: CreateVNode, type: string, ...args: any[]): VNode { - return components[type]?.(createVNode, type, ...args) ?? createVNode(...args); -} - const staticAttrsCache: Dictionary = Object.createDict(); @@ -104,7 +89,7 @@ export function interpolateStaticAttrs(this: Compo if (Object.isArray(children)) { for (let i = 0; i < children.length; i++) { - interpolateStaticAttrs.call(this, children[i]); + interpolateStaticAttrs.call(this, Object.cast(children[i])); } } diff --git a/src/core/component/render/helpers/index.ts b/src/core/component/render/helpers/index.ts new file mode 100644 index 0000000000..d35edf6b22 --- /dev/null +++ b/src/core/component/render/helpers/index.ts @@ -0,0 +1,11 @@ +/*! + * 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/render/helpers/normalizers'; +export * from 'core/component/render/helpers/props'; +export * from 'core/component/render/helpers/attrs'; diff --git a/src/core/component/render/helpers.ts b/src/core/component/render/helpers/normalizers.ts similarity index 64% rename from src/core/component/render/helpers.ts rename to src/core/component/render/helpers/normalizers.ts index af08404b80..239c225397 100644 --- a/src/core/component/render/helpers.ts +++ b/src/core/component/render/helpers/normalizers.ts @@ -6,59 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -/** - * Returns true if a component by the passed name is special (v-render, etc.) or functional - * @param componentName - */ -export function isSpecialComponent(componentName: string): boolean { - return componentName === 'v-render' || componentName.endsWith('-functional'); -} - -const - isHandler = /^on[^a-z]/; - -/** - * Merges the specified props into one and returns it - * @param args - */ -export function mergeProps(...args: Dictionary[]): Dictionary { - const - res: Dictionary = {}; - - for (let i = 0; i < args.length; i++) { - const - toMerge = args[i]; - - for (const key in toMerge) { - if (key === 'class') { - if (res.class !== toMerge.class) { - res.class = normalizeClass(Object.cast([res.class, toMerge.class])); - } - - } else if (key === 'style') { - res.style = normalizeStyle(Object.cast([res.style, toMerge.style])); - - } else if (isHandler.test(key)) { - const - existing = res[key], - incoming = toMerge[key]; - - if ( - existing !== incoming && - !(Object.isArray(existing) && existing.includes(incoming)) - ) { - res[key] = Object.isTruly(existing) ? ([]).concat(existing, incoming) : incoming; - } - - } else if (key !== '') { - res[key] = toMerge[key]; - } - } - } - - return res; -} - /** * Normalizes the passed class attribute and returns the result * @param classValue diff --git a/src/core/component/render/helpers/props.ts b/src/core/component/render/helpers/props.ts new file mode 100644 index 0000000000..5aa897534f --- /dev/null +++ b/src/core/component/render/helpers/props.ts @@ -0,0 +1,54 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { normalizeClass, normalizeStyle } from 'core/component/render/helpers/normalizers'; + +const + isHandler = /^on[^a-z]/; + +/** + * Merges the specified props into one and returns it + * @param args + */ +export function mergeProps(...args: Dictionary[]): Dictionary { + const + res: Dictionary = {}; + + for (let i = 0; i < args.length; i++) { + const + toMerge = args[i]; + + for (const key in toMerge) { + if (key === 'class') { + if (res.class !== toMerge.class) { + res.class = normalizeClass(Object.cast([res.class, toMerge.class])); + } + + } else if (key === 'style') { + res.style = normalizeStyle(Object.cast([res.style, toMerge.style])); + + } else if (isHandler.test(key)) { + const + existing = res[key], + incoming = toMerge[key]; + + if ( + existing !== incoming && + !(Object.isArray(existing) && existing.includes(incoming)) + ) { + res[key] = Object.isTruly(existing) ? ([]).concat(existing, incoming) : incoming; + } + + } else if (key !== '') { + res[key] = toMerge[key]; + } + } + } + + return res; +} diff --git a/src/core/component/render/index.ts b/src/core/component/render/index.ts index 7df99d8894..66161b2163 100644 --- a/src/core/component/render/index.ts +++ b/src/core/component/render/index.ts @@ -11,10 +11,6 @@ * @packageDocumentation */ -import 'core/component/render/components'; - export * from 'core/component/render/api'; -export * from 'core/component/render/vnode'; export * from 'core/component/render/wrappers'; export * from 'core/component/render/helpers'; -export * from 'core/component/render/interface'; diff --git a/src/core/component/render/interface.ts b/src/core/component/render/interface.ts deleted file mode 100644 index a9b36db8c4..0000000000 --- a/src/core/component/render/interface.ts +++ /dev/null @@ -1,13 +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 { VNode } from 'core/component/engines'; - -export interface CreateVNode { - (...args: any[]): VNode; -} diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index 6a51588595..36dcfcb941 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -1,3 +1,5 @@ +/* eslint-disable prefer-spread, prefer-rest-params */ + /*! * V4Fire Client Core * https://github.com/V4Fire/Client @@ -26,36 +28,31 @@ import type { } from 'core/component/engines'; import { registerComponent } from 'core/component/register'; -import { createVNodeWithDirectives, interpolateStaticAttrs } from 'core/component/render/vnode'; -import { isSpecialComponent } from 'core/component/render/helpers'; +import { interpolateStaticAttrs } from 'core/component/render/helpers'; import type { ComponentInterface } from 'core/component/interface'; export function wrapCreateVNode(original: T): T { - return Object.cast(function createVNode(this: ComponentInterface, type: string, ...args: unknown[]) { - const vnode = createVNodeWithDirectives(original, type, ...args); - return interpolateStaticAttrs.call(this, vnode); + return Object.cast(function createVNode(this: ComponentInterface) { + return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); }); } export function wrapCreateElementVNode(original: T): T { return Object.cast(function createElementVNode(this: ComponentInterface) { - // eslint-disable-next-line prefer-spread - return interpolateStaticAttrs.call(this, original.apply(null, arguments)); + return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); }); } export function wrapCreateBlock(original: T): T { return Object.cast(function wrapCreateBlock(this: ComponentInterface) { - // eslint-disable-next-line prefer-spread - return interpolateStaticAttrs.call(this, original.apply(null, arguments)); + return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); }); } export function wrapCreateElementBlock(original: T): T { return Object.cast(function createElementBlock(this: ComponentInterface) { - // eslint-disable-next-line prefer-spread - return interpolateStaticAttrs.call(this, original.apply(null, arguments)); + return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); }); } @@ -63,10 +60,6 @@ export function wrapResolveComponent { - if (isSpecialComponent(name)) { - return name; - } - registerComponent(name); return original(name, ...args); }); @@ -98,7 +91,12 @@ export function wrapWithDirectives(original: T) if (Object.isDictionary(dir)) { if (Object.isFunction(dir.beforeCreate)) { - dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); + const + newVnode = dir.beforeCreate({value, arg, modifiers, dir, instance: this}, vnode); + + if (newVnode != null) { + vnode = newVnode; + } if (Object.keys(dir).length > 1 && cantIgnoreDir) { resolvedDirs.push(decl); From 87c92ca50000087971d9dddda37c982bb8d3b777 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 16:19:43 +0300 Subject: [PATCH 0182/2313] refactor: added return type --- src/core/component/directives/tag/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/directives/tag/index.ts b/src/core/component/directives/tag/index.ts index 4641ccbd4a..bb29912e2b 100644 --- a/src/core/component/directives/tag/index.ts +++ b/src/core/component/directives/tag/index.ts @@ -17,7 +17,7 @@ import type { DirectiveOptions } from 'core/component/directives/tag/interface'; export * from 'core/component/directives/tag/interface'; ComponentEngine.directive('tag', { - beforeCreate(opts: DirectiveOptions, vnode: VNode) { + beforeCreate(opts: DirectiveOptions, vnode: VNode): void { vnode.type = opts.value ?? vnode.type; } }); From e40f78cad8723050db56abee08f3b338db61dd6b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 17:45:19 +0300 Subject: [PATCH 0183/2313] docs: improved docs --- .../interface/component/component.ts | 117 +++++++++--------- .../component/interface/component/types.ts | 6 +- .../component/interface/component/unsafe.ts | 13 +- src/core/component/interface/index.ts | 1 - src/core/component/interface/life-cycle.ts | 2 +- src/core/component/interface/link.ts | 10 +- src/core/component/interface/mod.ts | 12 +- src/core/component/interface/watch.ts | 68 +++++----- 8 files changed, 118 insertions(+), 111 deletions(-) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 03e6c7db5b..30bf20ae5c 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -42,42 +42,41 @@ import type { UnsafeGetter, UnsafeComponentInterface } from 'core/component/inte */ export abstract class ComponentInterface { /** - * Type: root component + * Type: the root component */ readonly Root!: ComponentInterface; /** - * Type: base super class for all components + * Type: the base super class for all components */ readonly Component!: ComponentInterface; /** - * Unique component string identifier + * The component string unique identifier */ readonly componentId!: string; /** - * Name of the component without special postfixes like `-functional` + * The component name without special postfixes like `-functional` */ readonly componentName!: string; /** - * Link to a class instance that is used to describe the component. - * Basically, this parameter is used for `instanceof` checks and to get default values of properties. - * Mind, every kind of components has only the one instance of that kind, - * i.e. one instance is shared between components of the same type. + * A link to the component class instance. + * Basically, this parameter is mainly used for `instanceof` checks and to get component default property values. + * Mind, all components of the same type refer to the one shareable class instance. */ readonly instance!: this; /** - * Additional classes for the component elements. - * It can be useful if you need to attach some extra classes to internal component elements. - * Be sure you know what you are doing because this mechanism is tied to an internal component markup. + * Additional classes for component elements. + * This option can be useful if you need to attach some extra classes to the internal component elements. + * Be sure you know what you are doing because this mechanism is tied to the private component markup. * * @example * ```js - * // Key names are tied with component elements, - * // and values contain a CSS class or list of classes we want to add + * // Key names are tied with the component elements + * // Values contain a CSS class or list of classes we want to add * * { * foo: 'bla', @@ -88,14 +87,14 @@ export abstract class ComponentInterface { readonly classes?: Dictionary>; /** - * Additional styles for the component elements. - * It can be useful if you need to attach some extra styles to internal component elements. - * Be sure you know what you are doing because this mechanism is tied to an internal component markup. + * Additional styles for component elements. + * This option can be useful if you need to attach some extra styles to the internal component elements. + * Be sure you know what you are doing because this mechanism is tied to the private component markup. * * @example * ```js * // Key names are tied with component elements, - * // and values contains a CSS style string, a style object or list of style strings + * // Values contains a CSS style string, a style object or list of style strings * * { * foo: 'color: red', @@ -107,7 +106,7 @@ export abstract class ComponentInterface { readonly styles?: Dictionary | Dictionary>; /** - * Name of the active component hook + * The active component hook name */ get hook(): Hook { return 'beforeRuntime'; @@ -125,159 +124,161 @@ export abstract class ComponentInterface { } /** - * Name of the active rendering group to use with async rendering - */ - readonly renderGroup?: string; - - /** - * API to invoke unsafely of internal properties of the component. - * This parameter helps to avoid TS errors of using protected properties and methods outside from the component class. - * It's useful to create component’ friendly classes. + * An API for unsafely invoking of some internal properties of the component. + * This parameter allows to avoid TS errors while using protected properties and methods outside from the main class. + * Use it when you need to decompose the component class into a composition of friendly classes. */ get unsafe(): UnsafeGetter> { return Object.cast(this); } /** - * Link to a DOM element that is the root for the component + * A link to the component root element */ readonly $el?: ComponentElement; /** - * Raw parameters of the component with which it was created by an engine + * Raw options of the component with which it was created by an engine */ readonly $options!: ComponentOptions; /** - * Map of initialized input properties of the component + * A dictionary with the initialized component props */ readonly $props!: Dictionary; /** - * Link to the root component of the application + * A link to the root component */ readonly $root!: this['Root']; /** - * Link to a parent component of the current component + * A link to the component parent */ readonly $parent?: this['Component']; /** - * Link to the closest non-functional parent component of the current component + * A link to the closest non-functional parent component */ readonly $normalParent?: this['Component']; /** - * Link to a parent component if the current component was dynamically created and mounted + * A link to the component parent if the current component was dynamically created and mounted */ readonly $remoteParent?: this['Component']; /** - * API of the used rendering engine + * An API of the used render engine */ readonly $renderEngine!: RenderEngine; /** * A link to the component meta object. - * This object contains all information of component properties, methods and other stuff. - * It's used to create a "real" component by the used engine and some optimizations based on reflect. + * This object contains all information of the component properties, methods and other stuff. + * It's used to create a "real" component by the used render engine. */ protected readonly meta!: ComponentMeta; /** - * Number that increments on every re-rendering of the component + * A number that is incremented each time the component is re-rendered */ protected renderCounter!: number; /** - * Temporary unique component string identifier for functional components + * A temporary string identifier of the component + * (only for functional components) */ protected readonly $componentId?: string; /** - * Map of watchable component properties that can force re-rendering + * A dictionary with the watchable component properties that can force re-rendering */ protected readonly $fields!: Dictionary; /** - * Map of watchable component properties that can't force re-rendering + * A dictionary with the watchable component properties that can't force re-rendering */ protected readonly $systemFields!: Dictionary; /** - * Map of component properties that were modified and need to synchronize + * A dictionary with component properties that have been modified and need to be synchronized * (only for functional components) */ protected readonly $modifiedFields!: Dictionary; /** - * Map of component attributes that aren't recognized as input properties + * A dictionary with component attributes that aren't recognized as input properties */ protected readonly $attrs!: Dictionary; /** - * Map of external listeners of component events + * A dictionary with external listeners of the component events */ protected readonly $listeners!: Dictionary>; /** - * Map of references to elements that have a "ref" attribute + * A dictionary with references to component elements that have the "ref" attribute */ protected readonly $refs!: Dictionary; /** - * Map of handlers that wait appearing of references to elements that have a "ref" attribute + * A dictionary with handlers that wait appearing of references to elements that have the "ref" attribute */ protected readonly $refHandlers!: Dictionary; /** - * Map of available render slots + * A dictionary with available render slots */ protected readonly $slots!: Slots; /** - * Name of the active property to initialize + * The active property name to initialize */ protected readonly $activeField?: string; /** - * Cache for component links + * The cache for component links */ protected readonly $syncLinkCache!: SyncLinkCache; /** - * API to tie and control async operations + * An API to tie and control async operations + */ + protected readonly async!: Async; + + /** + * An API to tie and control async operations + * (this parameter is used by a render engine) */ protected readonly $async!: Async; /** - * Promise of the component initializing + * A promise of the component initializing */ protected $initializer?: Promise; /** * Logs an event with the specified context * - * @param ctxOrOpts - logging context or logging options (logLevel, context) + * @param ctxOrOpts - the logging context or logging options * @param [details] - event details */ log?(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void; /** * Activates the component. - * The deactivated component won't load data from providers during initializing. + * The deactivated component won't load data from its providers during initializing. * * Basically, you don't need to think about the component activation, * because it's automatically synchronized with `keep-alive` or the component prop. * - * @param [force] - if true, then the component will be forced to activate, even if it is already activated + * @param [force] - if true, then the component will be forced to activate, even if it's already activated */ activate(force?: boolean): void {} /** * Deactivates the component. - * The deactivated component won't load data from providers during initializing. + * The deactivated component won't load data from its providers during initializing. * * Basically, you don't need to think about the component activation, * because it's automatically synchronized with `keep-alive` or the component prop. @@ -307,7 +308,7 @@ export abstract class ComponentInterface { protected $destroy(): void {} /** - * Sets a new reactive value to the specified property of an object + * Sets a new reactive value to the specified property of the passed object * * @param object * @param key @@ -318,7 +319,7 @@ export abstract class ComponentInterface { } /** - * Deletes the specified reactive property from an object + * Deletes the specified reactive property from the passed object * * @param object * @param key @@ -398,7 +399,7 @@ export abstract class ComponentInterface { } /** - * Detaches an event listeners from the component + * Detaches the specified event listeners from the component * * @param [event] * @param [handler] @@ -408,7 +409,7 @@ export abstract class ComponentInterface { } /** - * Emits a component event + * Emits the specified component event * * @param event * @param args diff --git a/src/core/component/interface/component/types.ts b/src/core/component/interface/component/types.ts index e0494e2ab5..50d5629ee0 100644 --- a/src/core/component/interface/component/types.ts +++ b/src/core/component/interface/component/types.ts @@ -9,21 +9,21 @@ import type { ComponentMeta } from 'core/component/meta'; /** - * Constructor function of a component + * A component constructor function */ export interface ComponentConstructor { new(): T; } /** - * Root DOM element that is tied with a component + * A component root DOM element */ export type ComponentElement = Element & { component?: T; }; /** - * Base context of a functional component + * The base context of a functional component */ export interface FunctionalCtx { componentName: string; diff --git a/src/core/component/interface/component/unsafe.ts b/src/core/component/interface/component/unsafe.ts index 28dddeb442..713533d188 100644 --- a/src/core/component/interface/component/unsafe.ts +++ b/src/core/component/interface/component/unsafe.ts @@ -9,19 +9,19 @@ import type { ComponentInterface } from 'core/component/interface/component/component'; /** - * A helper structure to pack the unsafe interface: - * it fixes some ambiguous TS warnings + * A helper structure to pack the unsafe interface. + * It fixes some ambiguous TS warnings. */ export type UnsafeGetter = Dictionary & U['CTX'] & U & {unsafe: any}; /** - * A special interface to provide access to protected properties and methods outside the component. - * It's used to create the "friendly classes" feature. + * A special interface to provide access for protected properties and methods outside the main component. + * It's used to create a "friendly classes" feature. */ export interface UnsafeComponentInterface { /** - * Type: context type + * Type: the context type */ readonly CTX: CTX; @@ -63,6 +63,9 @@ export interface UnsafeComponentInterface { /** - * Link path + * The link path */ path: string; /** - * Synchronize the link value and with all tied objects - * @param [value] - new value to set + * Synchronizes the link value with all tied objects + * @param [value] - a new value to set */ sync(value?: T): void; } /** - * Map of registered links + * A dictionary with all registered links */ export type SyncLinkCache = Map< string | object, diff --git a/src/core/component/interface/mod.ts b/src/core/component/interface/mod.ts index 9ae026db9a..5106865f79 100644 --- a/src/core/component/interface/mod.ts +++ b/src/core/component/interface/mod.ts @@ -9,24 +9,24 @@ import type { PARENT } from 'core/component/const'; /** - * Available "pure" types of a modifier value + * Modifier value primitives */ export type ModVal = string | boolean | number; /** - * Available types of a modifier value. - * If a value wrapped with an array, it interprets as a value by default. + * Modifier value types. + * If a value is wrapped by an array, it interprets as the value by default. */ export type ModDeclVal = CanArray; /** - * Expanded available types of a modifier value with support a feature of referring to a parent. - * If a value wrapped with an array, it interprets as a value by default. + * Expanded modifier types with parent reference support. + * If a value is wrapped by an array, it interprets as the value by default. */ export type ExpandedModDeclVal = ModDeclVal | typeof PARENT; /** - * Dictionary of registered modifiers and their possible values + * A dictionary with registered modifiers and their possible values * * @example * ```typescript diff --git a/src/core/component/interface/watch.ts b/src/core/component/interface/watch.ts index b25e9ee841..58f3b4e1a1 100644 --- a/src/core/component/interface/watch.ts +++ b/src/core/component/interface/watch.ts @@ -32,18 +32,20 @@ export interface FieldWatcher< B = A > extends WatchOptions { /** - * Handler that is invoked on watcher events + * A handler that is invoked on watcher events */ handler: WatchHandler; /** - * If false, the watcher won't be registered for functional/flyweight components + * If false, the watcher won't be registered for functional components * @default `true` */ functional?: boolean; /** - * If false, then the handler that is invoked on watcher events does not take any arguments from the event + * If false, then the handler that is invoked on the watcher events does not take any arguments from + * events it is listening for + * * @default `true` */ provideArgs?: boolean; @@ -55,20 +57,20 @@ export interface WatchObject< B = A > extends WatchOptions { /** - * Group name of a watcher - * (provided to Async) + * A name of the group the watcher belongs to. + * The parameter is provided to [[Async]]. */ group?: Group; /** - * Label of a watcher - * (provided to Async) + * A label associated with the watcher. + * The parameter is provided to [[Async]]. */ label?: Label; /** - * Join strategy of a watcher - * (provided to Async) + * A strategy type to join conflict tasks. + * The parameter is provided to [[Async]]. */ join?: Join; @@ -79,35 +81,31 @@ export interface WatchObject< single?: boolean; /** - * If false, the watcher won't be registered for functional/flyweight components + * If false, the watcher won't be registered for functional components * @default `true` */ functional?: boolean; /** - * Additional options for an event emitter - * (only if you listen an event) + * Additional options for the used event emitter */ options?: Dictionary; /** - * A name of a component method that is registered as a handler to the watcher - */ - method?: string; - - /** - * Additional arguments to the operation + * Additional arguments for the used event emitter */ args?: unknown[]; /** - * If false, then the handler that is invoked on watcher events does not take any arguments from the event + * If false, then the handler that is invoked on the watcher events does not take any arguments from + * events it is listening for + * * @default `true` */ provideArgs?: boolean; /*** - * Wrapper for a handler + * A wrapper function for the registered handler * * @example * ```typescript @@ -127,7 +125,12 @@ export interface WatchObject< wrapper?: WatchWrapper; /** - * Handler (or a name of a component method) that is invoked on watcher events + * A component method name that is registered as a handler to the watcher + */ + method?: string; + + /** + * A handler (or component method name) that is invoked on watcher events */ handler: string | WatchHandler; } @@ -138,13 +141,13 @@ export interface MethodWatcher< B = A > extends WatchOptions { /** - * Path to a component property to watch or event to listen + * A path to a component property to watch or event to listen */ path?: string; /** - * Group name of the watcher - * (provided to Async) + * A name of the group the watcher belongs to. + * The parameter is provided to [[Async]]. */ group?: Group; @@ -155,30 +158,31 @@ export interface MethodWatcher< single?: boolean; /** - * If false, the watcher won't be registered for functional/flyweight components + * If false, the watcher won't be registered for functional components * @default `true` */ functional?: boolean; /** - * Additional options for an event emitter - * (only if you listen an event) + * Additional options for the used event emitter */ options?: Dictionary; /** - * Additional arguments for the operation (their provides to an event emitter when attaching listeners) + * Additional arguments for the used event emitter */ args?: CanArray; /** - * If false, then the handler that is invoked on watcher events does not take any arguments from the event + * If false, then the handler that is invoked on the watcher events does not take any arguments from + * events it is listening for + * * @default `true` */ provideArgs?: boolean; /*** - * Wrapper for a handler + * A wrapper function for the registered handler * * @example * ```typescript @@ -204,8 +208,8 @@ export type WatchPath = {ctx: object; path?: RawWatchPath}; export interface RawWatchHandler { - (a: A, b?: B, params?: WatchHandlerParams): AnyToIgnore; - (this: CTX, a: A, b?: B, params?: WatchHandlerParams): AnyToIgnore; + (a: A, b?: B, params?: WatchHandlerParams): void; + (this: CTX, a: A, b?: B, params?: WatchHandlerParams): void; } export interface WatchHandler { From 0cd553c57bd5d57ed9b17ba8f61e4f6f30c2d5de Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 17:47:33 +0300 Subject: [PATCH 0184/2313] refactor: moved `async` to `ComponentInterface` --- src/core/component/construct/states/before-create.ts | 1 + src/super/i-block/i-block.ts | 11 ----------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts index 599936dfd7..a731069fb1 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/construct/states/before-create.ts @@ -52,6 +52,7 @@ export function beforeCreateState( unsafe.$modifiedFields = {}; unsafe.$refHandlers = {}; + unsafe.async = new Async(component); unsafe.$async = new Async(component); const diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index 04e1916c13..be7c2d99ca 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -958,17 +958,6 @@ export default abstract class iBlock extends ComponentInterface { protected readonly storage!: Storage; - /** - * API to wrap async operations - */ - @system({ - atom: true, - unique: true, - init: (ctx) => new Async(ctx) - }) - - protected readonly async!: Async; - /** * API for the component state. * This property provides a bunch of helper methods to initialize the component state. From 6f2931ef062e523054345501df1c3264080ab243 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 17:54:01 +0300 Subject: [PATCH 0185/2313] refactor: created a new module `core/module-loader` --- src/core/module-loader/CHANGELOG.md | 16 ++ src/core/module-loader/README.md | 3 + src/{friends => core}/module-loader/const.ts | 2 +- src/core/module-loader/index.ts | 208 ++---------------- .../module-loader/interface.ts | 6 - 5 files changed, 33 insertions(+), 202 deletions(-) create mode 100644 src/core/module-loader/CHANGELOG.md create mode 100644 src/core/module-loader/README.md rename src/{friends => core}/module-loader/const.ts (77%) rename src/{friends => core}/module-loader/interface.ts (80%) diff --git a/src/core/module-loader/CHANGELOG.md b/src/core/module-loader/CHANGELOG.md new file mode 100644 index 0000000000..b18e802496 --- /dev/null +++ b/src/core/module-loader/CHANGELOG.md @@ -0,0 +1,16 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Initial release diff --git a/src/core/module-loader/README.md b/src/core/module-loader/README.md new file mode 100644 index 0000000000..596ebdca9d --- /dev/null +++ b/src/core/module-loader/README.md @@ -0,0 +1,3 @@ +# core/module-loader + +This module provides a class to manage dynamically loaded modules. diff --git a/src/friends/module-loader/const.ts b/src/core/module-loader/const.ts similarity index 77% rename from src/friends/module-loader/const.ts rename to src/core/module-loader/const.ts index ff9958133b..024318d31b 100644 --- a/src/friends/module-loader/const.ts +++ b/src/core/module-loader/const.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Module } from 'friends/module-loader/interface'; +import type { Module } from 'core/module-loader/interface'; export const cache = new Map(); diff --git a/src/core/module-loader/index.ts b/src/core/module-loader/index.ts index 3eb797d68d..7b0028a8e3 100644 --- a/src/core/module-loader/index.ts +++ b/src/core/module-loader/index.ts @@ -11,64 +11,22 @@ * @packageDocumentation */ -import { cache, cachedModules } from 'core/module-loader/const'; +import { cache } from 'core/module-loader/const'; import type { Module, ResolvedModule } from 'core/module-loader/interface'; +export * from 'core/module-loader/const'; export * from 'core/module-loader/interface'; -let - resolve, - cursor; - -/** - * Returns the number of added modules - */ -export function size(): number { - return cachedModules.length; -} - -/** - * Returns true if a module by the passed identifier already exists in the cache - * @param id - */ -export function has(id: unknown): boolean { - return cache.has(id); -} - /** - * Adds the specified modules to a queue to load. - * The method returns the number of added modules in the cache. + * Loads the specified modules. + * If some modules are already loaded, they won’t be loaded twice. + * If all specified modules are already loaded, the function returns a simple value, but not a promise. * * @param modules */ -export function add(...modules: Module[]): number { - for (let i = 0; i < modules.length; i++) { - const - module = modules[i]; - - if (module.id != null) { - if (has(module.id)) { - continue; - } - - cache.set(module.id, module); - } - - if (Object.isFunction(resolve)) { - resolve(module); - } - } - - return cache.size; -} - -/** - * Loads the specified modules. - * The method returns false if there is nothing to load. - */ -export function load(...modules: Module[]): CanPromise { +export function load(...modules: Module[]): CanPromise { const - toLoad: Array> = []; + tasks: Array> = []; for (let i = 0; i < modules.length; i++) { const @@ -76,147 +34,15 @@ export function load(...modules: Module[]): CanPromise { resolvedModule = resolveModule(module); if (Object.isPromise(resolvedModule)) { - toLoad.push(resolvedModule); - - if (module.id != null) { - cache.set(module.id, module); - } - - cachedModules.push(module); + tasks.push(resolvedModule); } } - if (toLoad.length === 0) { - return false; + if (tasks.length === 0) { + return; } - return Promise.all(toLoad).then(() => true); -} - -/** - * Returns an iterator over the added modules. - * If there are no provided identifiers to check, the iterator will never stop. - * - * @param [ids] - module identifiers to filter - */ -export function values(...ids: unknown[]): IterableIterator> { - let - cachedLength = cachedModules.length; - - let - iterPos = 0, - done = false; - - const - idsSet = new Set(ids), - subTasks: Array> = [], - subValues: Array> = []; - - const iterator = { - [Symbol.iterator]() { - return this; - }, - - next() { - if (done) { - return { - done: true, - value: undefined - }; - } - - if (ids.length > 0) { - for (let o = idsSet.values(), el = o.next(); !el.done; el = o.next()) { - const - id = el.value, - module = cache.get(id); - - if (module != null) { - idsSet.delete(id); - - const - resolvedModule = getResolvedModule(module); - - if (Object.isPromise(resolvedModule)) { - subTasks.push(resolvedModule); - } - - subValues.push(resolvedModule); - - if (idsSet.size === 0) { - done = true; - - return { - done: false, - value: subTasks.length > 0 ? - Promise.all(subTasks).then(() => Promise.allSettled(subValues)) : - subValues - }; - } - } - } - - } else if (iterPos !== cachedLength) { - return { - done: false, - value: getResolvedModule(cachedModules[iterPos++]) - }; - } - - if (cursor != null) { - if (ids.length > 0) { - return { - done: false, - value: cursor.value.then((module) => { - if (done) { - return module; - } - - return iterator.next().value; - }) - }; - } - - return cursor; - } - - cursor = { - done: false, - value: new Promise((r) => { - resolve = (module: Module) => { - const - resolvedModule = getResolvedModule(module); - - if (Object.isPromise(resolvedModule)) { - return resolvedModule.then(r); - } - - r(resolvedModule); - }; - }) - }; - - return cursor; - } - }; - - return iterator; - - function getResolvedModule(module: Module): CanPromise { - cursor = undefined; - resolve = undefined; - - if (ids.length > 0 && idsSet.has(module.id)) { - idsSet.delete(module.id); - done = idsSet.size === 0; - - } else if (cachedLength !== cachedModules.length) { - iterPos++; - cachedLength = cachedModules.length; - } - - return resolveModule(module); - } + return Promise.all(tasks).then(() => undefined); } /** @@ -225,7 +51,7 @@ export function values(...ids: unknown[]): IterableIterator> { * * @param module */ -function resolveModule(module: Module): CanPromise { +export function resolveModule(module: Module): CanPromise { let resolvedModule: ResolvedModule; @@ -249,15 +75,7 @@ function resolveModule(module: Module): CanPromise { default: { resolvedModule.status = 'pending'; - - resolvedModule.promise = new Promise((r) => { - if (module.wait) { - r(module.wait().then(module.load.bind(module))); - - } else { - r(module.load()); - } - }); + resolvedModule.promise = module.load(); promise = resolvedModule.promise .then(() => { diff --git a/src/friends/module-loader/interface.ts b/src/core/module-loader/interface.ts similarity index 80% rename from src/friends/module-loader/interface.ts rename to src/core/module-loader/interface.ts index 13b76a083f..de54d73ea9 100644 --- a/src/friends/module-loader/interface.ts +++ b/src/core/module-loader/interface.ts @@ -19,12 +19,6 @@ export interface Module extends Dictionary { * A function to load the module */ load(): Promise; - - /** - * A function that returns a promise. - * The module loading won't start until this promise is fulfilled. - */ - wait?(): Promise; } export interface ResolvedModule extends Module { From 4ac3e41e7b221e8c744072716ad7802c03f48d07 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 6 Jun 2022 17:54:55 +0300 Subject: [PATCH 0186/2313] refactor: updated interface --- src/core/component/directives/hook/interface.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/component/directives/hook/interface.ts b/src/core/component/directives/hook/interface.ts index ffa35193d7..0d1e6dc083 100644 --- a/src/core/component/directives/hook/interface.ts +++ b/src/core/component/directives/hook/interface.ts @@ -6,10 +6,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { DirectiveBinding, DirectiveHook, ObjectDirective } from 'core/component/engines'; +import type { + + DirectiveBinding, + DirectiveHook, + ObjectDirective, + + VNode + +} from 'core/component/engines'; export interface DirectiveOptions extends DirectiveBinding> {} export type DirectiveValue = Overwrite, { - beforeCreate?: DirectiveHook; + beforeCreate?(...args: Parameters): CanUndef; }>; From 5659c8dc8584637147d7b5e9f3f68dac656344e0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 17:22:49 +0300 Subject: [PATCH 0187/2313] refactor: moved to `directives` & added doc --- .../{async/async => async-target}/CHANGELOG.md | 0 .../component/directives/async-target/README.md | 15 +++++++++++++++ .../{async/target => async-target}/index.ts | 4 ++-- .../{async/async => async-target}/interface.ts | 6 +----- .../component/directives/async/target/README.md | 2 -- 5 files changed, 18 insertions(+), 9 deletions(-) rename src/core/component/directives/{async/async => async-target}/CHANGELOG.md (100%) create mode 100644 src/core/component/directives/async-target/README.md rename src/core/component/directives/{async/target => async-target}/index.ts (82%) rename src/core/component/directives/{async/async => async-target}/interface.ts (64%) delete mode 100644 src/core/component/directives/async/target/README.md diff --git a/src/core/component/directives/async/async/CHANGELOG.md b/src/core/component/directives/async-target/CHANGELOG.md similarity index 100% rename from src/core/component/directives/async/async/CHANGELOG.md rename to src/core/component/directives/async-target/CHANGELOG.md diff --git a/src/core/component/directives/async-target/README.md b/src/core/component/directives/async-target/README.md new file mode 100644 index 0000000000..566248a746 --- /dev/null +++ b/src/core/component/directives/async-target/README.md @@ -0,0 +1,15 @@ +# core/component/directives/async-target + +This module provides a directive to mark an element where should be appended dynamically render fragments. +You should use it with the [[AsyncRender]] module. + +## Usage + +``` +< .container v-async-target + /// The first ten elements are rendered synchronously. + /// After that, the rest elements will be split into chunks by ten elements and rendered asynchronously. + /// The rendering of async fragments does not force re-rendering of the main component template. + < .&__item v-for = el in asyncRender.iterate(myData, 10) + {{ el }} +``` diff --git a/src/core/component/directives/async/target/index.ts b/src/core/component/directives/async-target/index.ts similarity index 82% rename from src/core/component/directives/async/target/index.ts rename to src/core/component/directives/async-target/index.ts index 9a3dc3b233..8915a1a172 100644 --- a/src/core/component/directives/async/target/index.ts +++ b/src/core/component/directives/async-target/index.ts @@ -7,14 +7,14 @@ */ /** - * [[include:core/component/directives/async/target/README.md]] + * [[include:core/component/directives/async-target/README.md]] * @packageDocumentation */ import { ComponentEngine, VNode, DirectiveBinding } from 'core/component/engines'; import type { ComponentInterface } from 'core/component/interface'; -export * from 'core/component/directives/async/async/interface'; +export * from 'core/component/directives/async-target/interface'; ComponentEngine.directive('async-target', { beforeCreate(opts: DirectiveBinding, vnode: VNode): void { diff --git a/src/core/component/directives/async/async/interface.ts b/src/core/component/directives/async-target/interface.ts similarity index 64% rename from src/core/component/directives/async/async/interface.ts rename to src/core/component/directives/async-target/interface.ts index 525e416fb6..a04c6e5265 100644 --- a/src/core/component/directives/async/async/interface.ts +++ b/src/core/component/directives/async-target/interface.ts @@ -8,8 +8,4 @@ import type { DirectiveBinding } from 'core/component/engines'; -export interface DirectiveOptions extends DirectiveBinding> {} - -export interface DirectiveValue { - -} +export interface DirectiveOptions extends DirectiveBinding {} diff --git a/src/core/component/directives/async/target/README.md b/src/core/component/directives/async/target/README.md deleted file mode 100644 index b9e6cff111..0000000000 --- a/src/core/component/directives/async/target/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# core/component/directives/async/target - From d92d0e469387728785a164a5704ca3149bd6a143 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 17:23:09 +0300 Subject: [PATCH 0188/2313] refactor: removed the directive --- .../directives/async/async/README.md | 2 -- .../component/directives/async/async/index.ts | 24 ------------------- 2 files changed, 26 deletions(-) delete mode 100644 src/core/component/directives/async/async/README.md delete mode 100644 src/core/component/directives/async/async/index.ts diff --git a/src/core/component/directives/async/async/README.md b/src/core/component/directives/async/async/README.md deleted file mode 100644 index 5230072f3e..0000000000 --- a/src/core/component/directives/async/async/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# core/component/directives/async/async - diff --git a/src/core/component/directives/async/async/index.ts b/src/core/component/directives/async/async/index.ts deleted file mode 100644 index f7a492349d..0000000000 --- a/src/core/component/directives/async/async/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/directives/async/async/README.md]] - * @packageDocumentation - */ - -import { ComponentEngine } from 'core/component/engines'; - -import type { DirectiveOptions } from 'core/component/directives/async/async/interface'; - -export * from 'core/component/directives/async/async/interface'; - -ComponentEngine.directive('async', { - created(node: Element, opts: DirectiveOptions): void { - - } -}); From 588f7edd5fbd491aabd8e9396d6cddf6cf39ee45 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 17:23:39 +0300 Subject: [PATCH 0189/2313] refactor: removed dead code --- src/core/component/directives/async/index.ts | 10 ---------- .../directives/async/target/CHANGELOG.md | 16 ---------------- 2 files changed, 26 deletions(-) delete mode 100644 src/core/component/directives/async/index.ts delete mode 100644 src/core/component/directives/async/target/CHANGELOG.md diff --git a/src/core/component/directives/async/index.ts b/src/core/component/directives/async/index.ts deleted file mode 100644 index 0d87420af3..0000000000 --- a/src/core/component/directives/async/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import 'core/component/directives/async/target'; -import 'core/component/directives/async/async'; diff --git a/src/core/component/directives/async/target/CHANGELOG.md b/src/core/component/directives/async/target/CHANGELOG.md deleted file mode 100644 index b18e802496..0000000000 --- a/src/core/component/directives/async/target/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.??.?? (2022-??-??) - -#### :rocket: New Feature - -* Initial release From dfd74e3f1f7f304d5f1b600b887186521011b49e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 17:25:09 +0300 Subject: [PATCH 0190/2313] fix: fixed return type --- src/core/component/engines/vue3/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/engines/vue3/interface.ts b/src/core/component/engines/vue3/interface.ts index 5ff74f9dcd..44ff0b13cb 100644 --- a/src/core/component/engines/vue3/interface.ts +++ b/src/core/component/engines/vue3/interface.ts @@ -27,7 +27,7 @@ export interface ObjectDirective extends SuperObjectDirective< this: ResolveDirective & ObjectDirective, binding: DirectiveBinding, vnode: VNode - ): void; + ): CanVoid; } export declare type Directive = From 417ef89fa2758e348724bcefc7988a7cbf1a1dc4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 17:32:14 +0300 Subject: [PATCH 0191/2313] feat: added a new wrapper for async cases --- src/core/component/engines/vue3/render.ts | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index b6a5d7b97b..c472006d99 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -20,6 +20,9 @@ import { renderList as superRenderList, withDirectives as superWithDirectives, + // @ts-ignore (private) + withAsyncContext as superWithAsyncContext, + VNode } from 'vue'; @@ -83,6 +86,12 @@ export { export { interpolateStaticAttrs }; +type Awaitable = () => PromiseLike; + +export function withAsyncContext(awaitable: T): [Awaited>, Function] { + return superWithAsyncContext(awaitable); +} + export const resolveComponent = wrapResolveComponent(superResolveComponent), resolveDynamicComponent = wrapResolveComponent(superResolveDynamicComponent); @@ -116,15 +125,28 @@ export function render(vnode: VNode, parent?: ComponentInterface): Node; export function render(vnodes: VNode[], parent?: ComponentInterface): Node[]; export function render(vnode: CanArray, parent?: ComponentInterface): CanArray { const vue = new Vue({ - render: () => vnode + render: () => vnode, + beforeCreate() { + if (parent != null) { + this.root = Object.create(parent.$root); + + Object.defineProperty(this.root, '$remoteParent', { + configurable: true, + enumerable: true, + writable: true, + value: parent + }); + + Object.defineProperty(this, 'unsafe', { + configurable: true, + enumerable: true, + writable: true, + value: this.root + }); + } + } }); - if (parent != null) { - Object.set(vue, '$root', Object.create(parent.$root)); - Object.set(vue, '$root.$remoteParent', parent); - Object.set(vue, '$root.unsafe', vue['$root']); - } - const el = document.createElement('div'), root = vue.mount(el); From 57cfb46a10a1d4de6bc17b7a7cf43d7bf8efae6d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 17:58:34 +0300 Subject: [PATCH 0192/2313] refactor: joined module to the one --- src/core/module-loader/CHANGELOG.md | 16 --- src/core/module-loader/README.md | 3 - src/core/module-loader/index.ts | 97 ------------------- src/friends/module-loader/api.ts | 55 ++++++++++- src/friends/module-loader/class.ts | 2 +- src/{core => friends}/module-loader/const.ts | 2 +- src/friends/module-loader/helpers.ts | 74 -------------- src/friends/module-loader/index.ts | 1 - .../module-loader/interface.ts | 0 9 files changed, 54 insertions(+), 196 deletions(-) delete mode 100644 src/core/module-loader/CHANGELOG.md delete mode 100644 src/core/module-loader/README.md delete mode 100644 src/core/module-loader/index.ts rename src/{core => friends}/module-loader/const.ts (77%) delete mode 100644 src/friends/module-loader/helpers.ts rename src/{core => friends}/module-loader/interface.ts (100%) diff --git a/src/core/module-loader/CHANGELOG.md b/src/core/module-loader/CHANGELOG.md deleted file mode 100644 index b18e802496..0000000000 --- a/src/core/module-loader/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.??.?? (2022-??-??) - -#### :rocket: New Feature - -* Initial release diff --git a/src/core/module-loader/README.md b/src/core/module-loader/README.md deleted file mode 100644 index 596ebdca9d..0000000000 --- a/src/core/module-loader/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core/module-loader - -This module provides a class to manage dynamically loaded modules. diff --git a/src/core/module-loader/index.ts b/src/core/module-loader/index.ts deleted file mode 100644 index 7b0028a8e3..0000000000 --- a/src/core/module-loader/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/module-loader/README.md]] - * @packageDocumentation - */ - -import { cache } from 'core/module-loader/const'; -import type { Module, ResolvedModule } from 'core/module-loader/interface'; - -export * from 'core/module-loader/const'; -export * from 'core/module-loader/interface'; - -/** - * Loads the specified modules. - * If some modules are already loaded, they won’t be loaded twice. - * If all specified modules are already loaded, the function returns a simple value, but not a promise. - * - * @param modules - */ -export function load(...modules: Module[]): CanPromise { - const - tasks: Array> = []; - - for (let i = 0; i < modules.length; i++) { - const - module = modules[i], - resolvedModule = resolveModule(module); - - if (Object.isPromise(resolvedModule)) { - tasks.push(resolvedModule); - } - } - - if (tasks.length === 0) { - return; - } - - return Promise.all(tasks).then(() => undefined); -} - -/** - * Resolves the specified module: if the module already exists in the cache, the function simply returns it. - * Otherwise, the module will be loaded. - * - * @param module - */ -export function resolveModule(module: Module): CanPromise { - let - resolvedModule: ResolvedModule; - - if (module.id != null) { - resolvedModule = Object.cast(cache.get(module.id) ?? module); - - } else { - resolvedModule = Object.cast(module); - } - - let - promise; - - switch (resolvedModule.status) { - case 'loaded': - break; - - case 'pending': - promise = resolvedModule.promise; - break; - - default: { - resolvedModule.status = 'pending'; - resolvedModule.promise = module.load(); - - promise = resolvedModule.promise - .then(() => { - module.status = 'loaded'; - }) - - .catch((err) => { - stderr(err); - module.status = 'failed'; - }); - } - } - - if (promise != null) { - return promise.then(() => resolvedModule); - } - - return resolvedModule; -} diff --git a/src/friends/module-loader/api.ts b/src/friends/module-loader/api.ts index d53fac9222..768cd81cf9 100644 --- a/src/friends/module-loader/api.ts +++ b/src/friends/module-loader/api.ts @@ -9,9 +9,7 @@ import type ModuleLoader from 'friends/module-loader/class'; import { cache } from 'friends/module-loader/const'; -import { resolveModule } from 'friends/module-loader/helpers'; - -import type { Module } from 'friends/module-loader/interface'; +import type { Module, ResolvedModule } from 'friends/module-loader/interface'; export * from 'friends/module-loader/interface'; @@ -92,3 +90,54 @@ export function loadBucket(this: ModuleLoader, bucketName: string): CanPromise { + let + resolvedModule: ResolvedModule; + + if (module.id != null) { + resolvedModule = Object.cast(cache.get(module.id) ?? module); + + } else { + resolvedModule = Object.cast(module); + } + + let + promise; + + switch (resolvedModule.status) { + case 'loaded': + break; + + case 'pending': + promise = resolvedModule.promise; + break; + + default: { + resolvedModule.status = 'pending'; + resolvedModule.promise = module.load(); + + promise = resolvedModule.promise + .then(() => { + module.status = 'loaded'; + }) + + .catch((err) => { + stderr(err); + module.status = 'failed'; + }); + } + } + + if (promise != null) { + return promise.then(() => resolvedModule); + } + + return resolvedModule; +} diff --git a/src/friends/module-loader/class.ts b/src/friends/module-loader/class.ts index e3f88cf1a4..ca8e1ec57c 100644 --- a/src/friends/module-loader/class.ts +++ b/src/friends/module-loader/class.ts @@ -14,7 +14,7 @@ import type { Module } from 'friends/module-loader/interface'; interface ModuleLoader { load: typeof api.load; loadBucket: typeof api.loadBucket; - addModulesToLoadBucket: typeof api.addModulesToBucket; + addModulesToBucket: typeof api.addModulesToBucket; } class ModuleLoader extends Friend { diff --git a/src/core/module-loader/const.ts b/src/friends/module-loader/const.ts similarity index 77% rename from src/core/module-loader/const.ts rename to src/friends/module-loader/const.ts index 024318d31b..ff9958133b 100644 --- a/src/core/module-loader/const.ts +++ b/src/friends/module-loader/const.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { Module } from 'core/module-loader/interface'; +import type { Module } from 'friends/module-loader/interface'; export const cache = new Map(); diff --git a/src/friends/module-loader/helpers.ts b/src/friends/module-loader/helpers.ts deleted file mode 100644 index 5fc0a2ffde..0000000000 --- a/src/friends/module-loader/helpers.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { cache } from 'friends/module-loader/const'; - -import type ModuleLoader from 'friends/module-loader/class'; -import type { Module, ResolvedModule } from 'friends/module-loader/interface'; - -/** - * Resolves the specified module: if the module already exists in the cache, the function simply returns it. - * Otherwise, the module will be loaded. - * - * @param module - */ -export function resolveModule(this: ModuleLoader, module: Module): CanPromise { - const - {async: $a} = this; - - let - resolvedModule: ResolvedModule; - - if (module.id != null) { - resolvedModule = Object.cast(cache.get(module.id) ?? module); - - } else { - resolvedModule = Object.cast(module); - } - - let - promise; - - switch (resolvedModule.status) { - case 'loaded': - break; - - case 'pending': - promise = resolvedModule.promise; - break; - - default: { - resolvedModule.status = 'pending'; - - resolvedModule.promise = $a.promise(new Promise((r) => { - if (module.wait) { - r($a.promise(module.wait()).then(module.load.bind(module))); - - } else { - r(module.load()); - } - })); - - promise = resolvedModule.promise - .then(() => { - module.status = 'loaded'; - }) - - .catch((err) => { - stderr(err); - module.status = 'failed'; - }); - } - } - - if (promise != null) { - return promise.then(() => resolvedModule); - } - - return resolvedModule; -} diff --git a/src/friends/module-loader/index.ts b/src/friends/module-loader/index.ts index d59ad825ba..0ddbe29edb 100644 --- a/src/friends/module-loader/index.ts +++ b/src/friends/module-loader/index.ts @@ -14,4 +14,3 @@ export { default } from 'friends/module-loader/class'; export * from 'friends/module-loader/api'; -export * from 'friends/module-loader/interface'; diff --git a/src/core/module-loader/interface.ts b/src/friends/module-loader/interface.ts similarity index 100% rename from src/core/module-loader/interface.ts rename to src/friends/module-loader/interface.ts From 7a023fc4d6f111d14688d274d9842f8fd36e7c3b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 18:25:45 +0300 Subject: [PATCH 0193/2313] refactor: updated async API --- src/core/component/directives/index.ts | 2 +- src/core/component/engines/vue3/render.ts | 13 +++----- src/core/component/render/daemon/index.ts | 2 +- src/core/component/render/daemon/interface.ts | 1 - src/friends/async-render/iter.ts | 7 +++- src/friends/async-render/modules/render.ts | 30 ++++++++--------- src/super/i-block/i-block.ss | 33 +++++++------------ 7 files changed, 38 insertions(+), 50 deletions(-) diff --git a/src/core/component/directives/index.ts b/src/core/component/directives/index.ts index 7f8b11eb04..2cfc341cbd 100644 --- a/src/core/component/directives/index.ts +++ b/src/core/component/directives/index.ts @@ -9,4 +9,4 @@ import 'core/component/directives/tag'; import 'core/component/directives/hook'; import 'core/component/directives/attrs'; -import 'core/component/directives/async'; +import 'core/component/directives/async-target'; diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index c472006d99..04964ee225 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -20,9 +20,6 @@ import { renderList as superRenderList, withDirectives as superWithDirectives, - // @ts-ignore (private) - withAsyncContext as superWithAsyncContext, - VNode } from 'vue'; @@ -72,6 +69,10 @@ export { resolveTransitionHooks, withCtx, + + // @ts-ignore (private) + withAsyncContext, + withKeys, withModifiers, @@ -86,12 +87,6 @@ export { export { interpolateStaticAttrs }; -type Awaitable = () => PromiseLike; - -export function withAsyncContext(awaitable: T): [Awaited>, Function] { - return superWithAsyncContext(awaitable); -} - export const resolveComponent = wrapResolveComponent(superResolveComponent), resolveDynamicComponent = wrapResolveComponent(superResolveDynamicComponent); diff --git a/src/core/component/render/daemon/index.ts b/src/core/component/render/daemon/index.ts index 60d09702d5..d974202c41 100644 --- a/src/core/component/render/daemon/index.ts +++ b/src/core/component/render/daemon/index.ts @@ -90,7 +90,7 @@ function run(): void { } const - canRender = val.useRAF ? daemon.animationFrame().then(val.task) : val.task(); + canRender = val.task(); const exec = (canRender) => { if (Object.isTruly(canRender)) { diff --git a/src/core/component/render/daemon/interface.ts b/src/core/component/render/daemon/interface.ts index 9a9a718b9e..932dc76d2b 100644 --- a/src/core/component/render/daemon/interface.ts +++ b/src/core/component/render/daemon/interface.ts @@ -8,6 +8,5 @@ export interface Task { task: Function; - useRAF?: boolean; weight?: number; } diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index 528651bfbd..9201a88424 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -109,6 +109,9 @@ export function iterate( lastTask, lastEvent; + const + [_, setCurrentInstance] = r.withAsyncContext(() => undefined); + $a.setImmediate(async () => { ctx.$off('[[V_FOR_CB]]', setVNodeCompiler); ctx.$off('[[V_ASYNC_TARGET]]', setTarget); @@ -252,6 +255,8 @@ export function iterate( return lastTask(); function task() { + setCurrentInstance(); + const renderedVNodes: Node[] = []; @@ -290,7 +295,7 @@ export function iterate( renderedVnode = Object.cast(vnode.el); } else { - renderedVnode = r.render(vnode); + renderedVnode = ctx.vdom.render(vnode); } renderedVNodes.push(renderedVnode); diff --git a/src/friends/async-render/modules/render.ts b/src/friends/async-render/modules/render.ts index c8067881ba..8072fe620c 100644 --- a/src/friends/async-render/modules/render.ts +++ b/src/friends/async-render/modules/render.ts @@ -34,21 +34,7 @@ export function addRenderTask( return new SyncPromise((resolve, reject) => { const taskDesc = { weight: opts.weight, - - fn: $a.proxy(() => { - const cb = () => { - task(); - resolve(); - return true; - }; - - if (opts.useRAF) { - return $a.animationFrame({group}).then(cb); - } - - return cb(); - - }, { + task: $a.proxy(runTask, { group, single: false, onClear: (err) => { @@ -59,6 +45,20 @@ export function addRenderTask( }; queue.add(taskDesc); + + function runTask() { + const cb = () => { + task(); + resolve(); + return true; + }; + + if (opts.useRAF) { + return $a.animationFrame({group}).then(cb); + } + + return cb(); + } }); } diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index bc7b35e932..a846bc6153 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -68,16 +68,14 @@ ? content = opts ? opts = {} - : buble = require('buble') - : & - ids = [], - paths = [].concat(path || []) + buble = require('buble'), + paths = Array.concat([], path) . : & waitFor = opts.wait || 'undefined', - interpolatedWaitFor = (opts.wait ? buble.transform("`" + opts.wait + "`").code : 'undefined') + filter = (opts.wait ? buble.transform("`" + opts.wait + "`").code : 'undefined') .replace(/^\(?['"]/, '') .replace(/['"]\)?$/, '') .replace(/\\(['"])/g, '$1') @@ -89,21 +87,12 @@ interpolatedId = buble.transform("`" + id + "`").code . - ? ids.push(interpolatedId) - {{ void(moduleLoader.add({id: ${interpolatedId}, load: () => import('${path}'), wait: ${interpolatedWaitFor}})) }} - - : & - source, - filter - . - - - if ids.length - ? source = 'moduleLoader.values(...[' + ids.join(',') + '])' - ? filter = 'undefined' - - - else - ? source = '1' - ? filter = interpolatedWaitFor + {{ + void(moduleLoader.addModulesToBucket('global', { + id: ${interpolatedId}, + load: () => import('${path}') + })) + }} - if content != null - if opts.renderKey @@ -112,7 +101,7 @@ < template v-if = !field.get('ifOnceStore.' + ${renderKey}) {{ void(field.set('ifOnceStore.' + ${renderKey}, true)) }} - < template v-for = _ in asyncRender.iterate(${source}, 1, { & + < template v-for = _ in asyncRender.iterate(moduleLoader.loadBucket('global'), 1, { & useRaf: true, group: 'module:' + ${renderKey}, filter: ${filter} @@ -120,7 +109,7 @@ += content - else - < template v-for = _ in asyncRender.iterate(${source}, 1, {useRaf: true, filter: ${filter}}) + < template v-for = _ in asyncRender.iterate(moduleLoader.loadBucket('global'), 1, {useRaf: true, filter: ${filter}}) += content /** From 87a726c98bc6a3888358d1bef1cbcaa6d5fc67d9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 7 Jun 2022 18:29:32 +0300 Subject: [PATCH 0194/2313] fix: fixed modification of `$root` --- .../component/construct/states/before-create.ts | 15 ++++++++++++++- src/core/component/engines/vue3/render.ts | 7 ++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/construct/states/before-create.ts index a731069fb1..fb243868ac 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/construct/states/before-create.ts @@ -59,8 +59,21 @@ export function beforeCreateState( parent = unsafe.$parent, isFunctional = meta.params.functional === true; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (parent != null && parent.componentName == null) { - unsafe.$parent = unsafe.$root.unsafe.$remoteParent; + Object.defineProperty(unsafe, '$root', { + configurable: true, + enumerable: true, + writable: true, + value: unsafe.$root.unsafe + }); + + Object.defineProperty(unsafe, '$parent', { + configurable: true, + enumerable: true, + writable: true, + value: unsafe.$root.$remoteParent + }); } unsafe.$normalParent = getNormalParent(component); diff --git a/src/core/component/engines/vue3/render.ts b/src/core/component/engines/vue3/render.ts index 04964ee225..227854cc78 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -123,9 +123,10 @@ export function render(vnode: CanArray, parent?: ComponentInterface): Can render: () => vnode, beforeCreate() { if (parent != null) { - this.root = Object.create(parent.$root); + const + root = Object.create(parent.$root); - Object.defineProperty(this.root, '$remoteParent', { + Object.defineProperty(root, '$remoteParent', { configurable: true, enumerable: true, writable: true, @@ -136,7 +137,7 @@ export function render(vnode: CanArray, parent?: ComponentInterface): Can configurable: true, enumerable: true, writable: true, - value: this.root + value: root }); } } From 23900ecae66e79c2b4704bfda5f481c7bdd5fbd6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:01:31 +0300 Subject: [PATCH 0195/2313] docs: improved doc --- src/friends/module-loader/README.md | 150 ++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 8 deletions(-) diff --git a/src/friends/module-loader/README.md b/src/friends/module-loader/README.md index 8599081e30..c8a12f72f5 100644 --- a/src/friends/module-loader/README.md +++ b/src/friends/module-loader/README.md @@ -1,13 +1,12 @@ # friends/module-loader -This module provides a class to manage dynamically loaded modules. -Basically, this module is used with [[AsyncRender]]. +The module provides a class to manage dynamically loaded modules. ## How to include this module to your component -By default, any components that inherited from [[iBlock]] have the `moduleLoader` property. -But to use module methods, you should attach them explicitly to enable tree-shake code optimizations. -Just place a necessary import declaration within your component file. +By default, any component that inherited from [[iBlock]] has the `moduleLoader` property. +But to use module methods, attach them explicitly to enable tree-shake code optimizations. +Just place the necessary import declaration within your component file. ```typescript import iBlock, { component } from 'super/i-block/i-block'; @@ -20,6 +19,15 @@ ModuleLoader.addToPrototype(load); export default class bExample extends iBlock {} ``` +Or, if you're using a module with Snakeskin helpers, all dependencies will be installed automatically. + +``` +< .container v-async-target + += self.loadModules('form/b-button') + < b-button + Press on me! +``` + ## Usage The module API is designed to use with [[AsyncRender]]. @@ -28,8 +36,8 @@ For instance, you can dynamically require dependencies when it needed and render ``` /// Don't forget to declare where to mount dynamically rendered fragments < .container v-async-target - {{ moduleLoader.add('form', {id: 'form/b-button', load: () => import('form/b-button')}) }} - {{ moduleLoader.add('form', {id: 'form/b-input', load: () => import('form/b-input')}) }} + {{ moduleLoader.addToBucket('form', {id: 'form/b-button', load: () => import('form/b-button')}) }} + {{ moduleLoader.addToBucket('form', {id: 'form/b-input', load: () => import('form/b-input')}) }} < .form v-for = _ in asyncRender.iterate(moduleLoader.loadBucket('form')) < b-button @@ -38,6 +46,26 @@ For instance, you can dynamically require dependencies when it needed and render < b-input :placeholder = 'Enter your name' ``` +### Using Snakeskin helpers + +Manual use of the module within templates forces you to write a lot of boilerplate code. +To fix this, you should use the special Snakeskin helpers available to all [[iBlock]] heirs. + +``` +< .container v-async-target + += self.loadModules(['form/b-button', 'form/b-input']) + < b-button + Press on me! + + < b-input :placeholder = 'Enter your name' + + += self.render({wait: 'promisifyOnce.bind(null, 'forceRender')'}) + Hello there! + + < button @click = emit('forceRender') + Show hidden content +``` + ## Methods ### load @@ -47,15 +75,121 @@ If some modules are already loaded, they won’t be loaded twice. If all specified modules are already loaded, the function returns a simple value, but not a promise. The resulting value is designed to use with [[AsyncRender]]. -### addModulesToBucket +```js +const modules = await this.moduleLoader.load( + { + id: 'form/b-button', + load: () => import('form/b-button') + }, + + { + // If you don't provide a module identifier, it won't be cached + load: () => import('form/b-input') + }, +); + +[[/* Module 1 */, /* Module 2 */]] +console.log([...modules]); +``` + +### addToBucket Adds the specified modules to a load bucket by the specified name. Notice, adding modules don’t force them to load. To load the created bucket, use the `loadBucket` method. The function returns the number of added modules in the bucket. +```js +this.moduleLoader.addToBucket('form', { + id: 'form/b-button', + load: () => import('form/b-button') +}); + +this.moduleLoader.addToBucket('form', { + // If you don't provide a module identifier, it won't be cached + load: () => import('form/b-input') +}); +``` + ### loadBucket Loads a bucket of modules by the specified name. If some modules are already loaded, they won’t be loaded twice. If all specified modules are already loaded, the function returns a simple value, but not a promise. The resulting value is designed to use with [[AsyncRender]]. + +```js +this.moduleLoader.addToBucket('form', { + id: 'form/b-button', + load: () => import('form/b-button') +}); + +await this.moduleLoader.addToBucket('form', { + // If you don't provide a module identifier, it won't be cached + load: () => import('form/b-input') +}); + +[[/* Module 1 */, /* Module 2 */]] +console.log([...await this.moduleLoader.loadBucket('form')]); +``` + +## Snakeskin helpers + +### loadModules + +Loads modules by the specified paths and dynamically inserted the provided content when it loaded. + +``` +< .container v-async-target + /// You can provide a path to module or list of paths + += self.loadModules(['form/b-button', 'form/b-input']) + < b-button + Press on me! + + < b-input :placeholder = 'Enter your name' +``` + +#### Enabling one time rendering + +Providing the `renderKey` option you declare this template fragment should be rendered once, +i.e. it won’t be re-rendered during the component state change. Mind, the render key should be unique. + +``` +< .container v-async-target + += self.loadModules('form/b-button', {renderKey: 'Login Form'}) + < b-button + Login +``` + +#### Conditional rendering + +If you want render a fragment only after some event, provide the `wait` option. +This option expects a function that returns a promise. + +``` +< .container v-async-target + += self.loadModules(['form/b-button', 'form/b-input'], {wait: promisifyOnce.bind(null, 'forceRender')}) + < b-button + Press on me! + + < b-input :placeholder = 'Enter your name' + +< button @click = emit('forceRender') + Show form +``` + +### render + +This helper is like `loadModules`, but without loading of modules. +You can use it when you need to create asynchronous conditional rendering of template fragments. + +``` +< .container v-async-target + += self.render({renderKey: 'Login Form', wait: promisifyOnce.bind(null, 'forceRender')}) + < b-button + Press on me! + + < b-input :placeholder = 'Enter your name' + +< button @click = emit('forceRender') + Show form +``` From 3d3ec4ab5f493740602f09ef6047e77cbcf570c5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:01:52 +0300 Subject: [PATCH 0196/2313] refactor: `addModulesToBucket` -> `addToBucket` --- src/friends/module-loader/api.ts | 8 ++++---- src/friends/module-loader/class.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/friends/module-loader/api.ts b/src/friends/module-loader/api.ts index 768cd81cf9..09d43301a6 100644 --- a/src/friends/module-loader/api.ts +++ b/src/friends/module-loader/api.ts @@ -53,13 +53,13 @@ export function load(this: ModuleLoader, ...modules: Module[]): CanPromise> { - const bucket = this.loadBuckets.get(bucketName) ?? new Set(); + const bucket = this.moduleBuckets.get(bucketName) ?? new Set(); return load.call(this, ...bucket); } diff --git a/src/friends/module-loader/class.ts b/src/friends/module-loader/class.ts index ca8e1ec57c..80d5e1c29b 100644 --- a/src/friends/module-loader/class.ts +++ b/src/friends/module-loader/class.ts @@ -14,14 +14,14 @@ import type { Module } from 'friends/module-loader/interface'; interface ModuleLoader { load: typeof api.load; loadBucket: typeof api.loadBucket; - addModulesToBucket: typeof api.addModulesToBucket; + addToBucket: typeof api.addToBucket; } class ModuleLoader extends Friend { /** - * A map of registered buckets to load + * A dictionary with registered buckets to load */ - protected loadBuckets: Map> = new Map(); + protected moduleBuckets: Map> = new Map(); } export default ModuleLoader; From 73868b5b9b242c05f1db001e6ca279cd3a8f7653 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:02:30 +0300 Subject: [PATCH 0197/2313] refactor: removed runtime check to avoid circular dependencies --- src/friends/friend/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/friends/friend/index.ts b/src/friends/friend/index.ts index 25748a9656..31cbf06d73 100644 --- a/src/friends/friend/index.ts +++ b/src/friends/friend/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import iBlock from 'super/i-block/i-block'; +import type iBlock from 'super/i-block/i-block'; /** * Superclass to create classes friendly to the main component class @@ -123,10 +123,6 @@ export default class Friend { } constructor(component: iBlock) { - if (!(Object.get(component, 'instance') instanceof iBlock)) { - throw new TypeError("The specified component isn't inherited from `iBlock`"); - } - this.ctx = component.unsafe; this.component = component; } From fbb46b382363356c93b762a845bf84a53f208ad3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:03:39 +0300 Subject: [PATCH 0198/2313] doc: moved some doc to `module-loader` --- src/friends/async-render/README.md | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/friends/async-render/README.md b/src/friends/async-render/README.md index fcafc65ae2..dea5856b4e 100644 --- a/src/friends/async-render/README.md +++ b/src/friends/async-render/README.md @@ -5,9 +5,9 @@ It helps to optimize component rendering. ## How to include this module to your component -By default, any components that inherited from [[iBlock]] have the `asyncRender` property. -But to use module methods, you should attach them explicitly to enable tree-shake code optimizations. -Just place a necessary import declaration within your component file. +By default, any component that inherited from [[iBlock]] has the `asyncRender` property. +But to use module methods, attach them explicitly to enable tree-shake code optimizations. +Just place the necessary import declaration within your component file. ```typescript import iBlock, { component } from 'super/i-block/i-block'; @@ -275,26 +275,3 @@ This function helps optimize component rendering by splitting big render tasks i < template v-for = el in asyncRender.iterate(largeList, 5) < my-component :data = el ``` - -## Snakeskin helpers - -### loadModules - -Loads modules by the specified paths and dynamically inserted the provided content when it loaded. - -``` -+= self.loadModules('form/b-button') - < b-button - Hello world - -/// `renderKey` is necessary to prevent any chunk' re-rendering after the first rendering of a template -/// `wait` is a function to defer the process of loading, it should return a promise with a non-false value -+= self.loadModules(['form/b-button', 'form/b-input'], {renderKey: 'controls', wait: 'promisifyOnce.bind(null, "needLoad")'}) - < b-button - Hello world - - < b-input - -< button @click = emit('needLoad') - Force load -``` From 22c9314070dad00005ea3931ac530c1122f8bf6b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:06:11 +0300 Subject: [PATCH 0199/2313] :art: --- src/friends/async-render/iter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index 9201a88424..95f4803888 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -329,4 +329,3 @@ export function iterate( } } } - From b4d7d453996a2fe1c90a08ad379d6caf16b9c37e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:10:02 +0300 Subject: [PATCH 0200/2313] doc: fixed mistakes --- src/friends/module-loader/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/friends/module-loader/README.md b/src/friends/module-loader/README.md index c8a12f72f5..6f8857f8c2 100644 --- a/src/friends/module-loader/README.md +++ b/src/friends/module-loader/README.md @@ -163,11 +163,11 @@ i.e. it won’t be re-rendered during the component state change. Mind, the rend #### Conditional rendering If you want render a fragment only after some event, provide the `wait` option. -This option expects a function that returns a promise. +This option expects a string expression (cause it code-generation) with a function that returns a promise. ``` < .container v-async-target - += self.loadModules(['form/b-button', 'form/b-input'], {wait: promisifyOnce.bind(null, 'forceRender')}) + += self.loadModules(['form/b-button', 'form/b-input'], {wait: 'promisifyOnce.bind(null, "forceRender")'}) < b-button Press on me! @@ -184,7 +184,7 @@ You can use it when you need to create asynchronous conditional rendering of tem ``` < .container v-async-target - += self.render({renderKey: 'Login Form', wait: promisifyOnce.bind(null, 'forceRender')}) + += self.render({renderKey: 'Login Form', wait: 'promisifyOnce.bind(null, "forceRender")'}) < b-button Press on me! From a206cfba05923378d30bed94010e474db83cdbae Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:10:46 +0300 Subject: [PATCH 0201/2313] refactor: improved SS helpers to load modules --- src/super/i-block/i-block.ss | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index a846bc6153..022e244436 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -44,9 +44,9 @@ - return name.split('.').slice(-1)[0].dasherize() /** - * Loads modules by the specified paths and dynamically inserted the provided content when it loaded + * Loads modules by the specified paths and dynamically inserted the provided content when them are loaded * - * @param {(string|!Array)} path - path or an array of paths + * @param {(string|!Array)} path - the module path or list of paths * @param {{renderKey: string, wait: string}=} [opts] - additional options * @param {string=} [content] * @@ -88,7 +88,11 @@ . {{ - void(moduleLoader.addModulesToBucket('global', { + void(require('friends/module-loader').default.addToPrototype(require('friends/module-loader'))) + }} + + {{ + void(moduleLoader.addToBucket('global', { id: ${interpolatedId}, load: () => import('${path}') })) From 5a871003e3db503dc44ace75c42897821b1da477 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:11:52 +0300 Subject: [PATCH 0202/2313] refactor: added `asyncRender` & `moduleLoader` to unsafe; removed `async` & `renderTmp` from unsafe --- src/super/i-block/interface.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/super/i-block/interface.ts b/src/super/i-block/interface.ts index c0a9c21ea7..9b26c37ca4 100644 --- a/src/super/i-block/interface.ts +++ b/src/super/i-block/interface.ts @@ -84,7 +84,10 @@ export interface UnsafeIBlock extends UnsafeCompone block: CTX['block']; // @ts-ignore (access) - async: CTX['async']; + asyncRender: CTX['asyncRender']; + + // @ts-ignore (access) + moduleLoader: CTX['moduleLoader']; // @ts-ignore (access) sync: CTX['sync']; @@ -113,9 +116,6 @@ export interface UnsafeIBlock extends UnsafeCompone // @ts-ignore (access) watchTmp: CTX['watchTmp']; - // @ts-ignore (access) - renderTmp: CTX['renderTmp']; - // @ts-ignore (access) ifOnceStore: CTX['ifOnceStore']; From d45d81fe2abad7a9be0706e2fc362d3be8444390 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:13:06 +0300 Subject: [PATCH 0203/2313] refactor: :up: --- src/super/i-block/i-block.ts | 21 +++++++------------ src/super/i-data/i-data.ts | 1 - .../modules/theme/theme-manager.ts | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index be7c2d99ca..d3e7fa9675 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -84,16 +84,16 @@ import DOM from 'super/i-block/modules/dom'; import VDOM from 'super/i-block/modules/vdom'; import Lfc from 'super/i-block/modules/lfc'; -import AsyncRender from 'super/i-block/modules/async-render'; +import AsyncRender from 'friends/async-render'; import Sync, { AsyncWatchOptions } from 'super/i-block/modules/sync'; import Block from 'super/i-block/modules/block'; import Field from 'super/i-block/modules/field'; -import Provide, { classesCache, Classes, Styles } from 'super/i-block/modules/provide'; +import Provide, { classesCache, Classes } from 'super/i-block/modules/provide'; import State, { ConverterCallType } from 'super/i-block/modules/state'; import Storage from 'super/i-block/modules/storage'; -import ModuleLoader, { Module } from 'super/i-block/modules/module-loader'; +import ModuleLoader, { Module } from 'friends/module-loader'; import { @@ -160,16 +160,16 @@ export * from 'super/i-block/interface'; export * from 'super/i-block/modules/block'; export * from 'super/i-block/modules/field'; export * from 'super/i-block/modules/state'; -export * from 'super/i-block/modules/module-loader'; +export * from 'friends/module-loader'; export * from 'super/i-block/modules/daemons'; export * from 'super/i-block/modules/event-emitter'; export * from 'super/i-block/modules/sync'; -export * from 'super/i-block/modules/async-render'; +export * from 'friends/async-render'; export * from 'super/i-block/modules/decorators'; -export { default as Friend } from 'super/i-block/modules/friend'; +export { default as Friend } from 'friends/friend'; export { @@ -667,7 +667,7 @@ export default abstract class iBlock extends ComponentInterface { */ get r(): this['$root'] { const r = this.$root; - return r.$remoteParent?.$root ?? r; + return r.unsafe.$remoteParent?.$root ?? r; } /** @@ -1129,7 +1129,6 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ atom: true, - after: 'async', unique: true, init: (o, d) => wrapEventEmitter(d.async, o) }) @@ -1141,7 +1140,6 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ atom: true, - after: 'async', unique: true, init: (o, d) => wrapEventEmitter(d.async, new EventEmitter({ maxListeners: 1e3, @@ -1157,7 +1155,6 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ atom: true, - after: 'async', unique: true, init: (o, d) => wrapEventEmitter(d.async, () => o.$parent, true) }) @@ -1169,7 +1166,6 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ atom: true, - after: 'async', unique: true, init: (o, d) => wrapEventEmitter(d.async, o.r) }) @@ -1182,7 +1178,6 @@ export default abstract class iBlock extends ComponentInterface { */ @system({ atom: true, - after: 'async', unique: true, init: (o, d) => wrapEventEmitter(d.async, globalEmitter) }) @@ -1929,7 +1924,7 @@ export default abstract class iBlock extends ComponentInterface { [], // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - this.moduleLoader.load(...this.dependencies) || [], + this.moduleLoader.load?.(...this.dependencies) || [], // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions this.state.initFromStorage() || [] diff --git a/src/super/i-data/i-data.ts b/src/super/i-data/i-data.ts index 99ef651626..bf70b5ce77 100644 --- a/src/super/i-data/i-data.ts +++ b/src/super/i-data/i-data.ts @@ -262,7 +262,6 @@ export default abstract class iData extends iBlock implements iProgress { */ @system({ atom: true, - after: 'async', unique: true, init: (o, d) => wrapEventEmitter(d.async, () => o.dp?.emitter, true) }) diff --git a/src/super/i-static-page/modules/theme/theme-manager.ts b/src/super/i-static-page/modules/theme/theme-manager.ts index 8bd1ec9544..724a9373d2 100644 --- a/src/super/i-static-page/modules/theme/theme-manager.ts +++ b/src/super/i-static-page/modules/theme/theme-manager.ts @@ -9,7 +9,7 @@ import type iBlock from 'super/i-block/i-block'; import type iStaticPage from 'super/i-static-page/i-static-page'; -import Friend from 'super/i-block/modules/friend'; +import Friend from 'friends/friend'; /** * Class to manage interface themes From cca80194bdd3bf2e1d89e1f283838f82f3ed1f08 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:43:41 +0300 Subject: [PATCH 0204/2313] fix: fixed the infinity loop within `on/off/once` methods --- src/super/i-block/i-block.ts | 52 +++++++----------------------------- 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index d3e7fa9675..6846fbba01 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -1130,7 +1130,11 @@ export default abstract class iBlock extends ComponentInterface { @system({ atom: true, unique: true, - init: (o, d) => wrapEventEmitter(d.async, o) + init: (o, d) => wrapEventEmitter(d.async, { + on: o.$on.bind(o), + once: o.$once.bind(o), + off: o.$off.bind(o) + }) }) protected readonly selfEmitter!: EventEmitterWrapper; @@ -1667,16 +1671,8 @@ export default abstract class iBlock extends ComponentInterface { * @param handler * @param [opts] - additional options */ - @p() on(event: string, handler: ProxyCb, opts?: AsyncOptions): void { - event = event.dasherize(); - - if (opts) { - this.async.on(this, event, handler, opts); - return; - } - - this.$on(event, handler); + this.selfEmitter.on(event.dasherize(), handler, opts); } /** @@ -1687,16 +1683,8 @@ export default abstract class iBlock extends ComponentInterface { * @param handler * @param [opts] - additional options */ - @p() once(event: string, handler: ProxyCb, opts?: AsyncOptions): void { - event = event.dasherize(); - - if (opts) { - this.async.once(this, event, handler, opts); - return; - } - - this.$once(event, handler); + this.selfEmitter.once(event.dasherize(), handler, opts); } /** @@ -1706,38 +1694,18 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param [opts] - additional options */ - @p() promisifyOnce(event: string, opts?: AsyncOptions): Promise { - return this.async.promisifyOnce(this, event.dasherize(), opts); + return this.selfEmitter.promisifyOnce(event.dasherize(), opts); } - /** - * Detaches an event listeners from the component - * - * @param [event] - * @param [handler] - */ - off(event?: string, handler?: Function): void; - /** * Detaches an event listeners from the component * * @see [[Async.off]] * @param [opts] - additional options */ - off(opts: ClearOptionsId): void; - - @p() - off(eventOrParams?: string | ClearOptionsId, handler?: Function): void { - const - e = eventOrParams; - - if (e == null || Object.isString(e)) { - this.$off(e?.dasherize(), handler); - return; - } - - this.async.off(e); + off(opts?: ClearOptionsId): void { + this.selfEmitter.off(opts); } /** From 586ce819ba02ed3c4d6a0dddefa81dcc453f71dc Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 8 Jun 2022 11:43:57 +0300 Subject: [PATCH 0205/2313] fix: load `async-render` --- src/super/i-block/i-block.ss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 022e244436..56b3b9b606 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -91,6 +91,10 @@ void(require('friends/module-loader').default.addToPrototype(require('friends/module-loader'))) }} + {{ + void(require('friends/async-render').default.addToPrototype(require('friends/async-render').iterate)) + }} + {{ void(moduleLoader.addToBucket('global', { id: ${interpolatedId}, From 6fb48342aaa8535976487f2251d2b1800d4c1ebd Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 15:19:27 +0300 Subject: [PATCH 0206/2313] refactor: using `vdom.render` --- src/friends/async-render/iter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index 95f4803888..86609563b8 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -6,7 +6,9 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import VDOM, { render } from 'friends/vdom'; import type { ComponentElement, VNode } from 'super/i-block/i-block'; + import type AsyncRender from 'friends/async-render/class'; import { addRenderTask, destroyNode as nodeDestructor } from 'friends/async-render/modules/render'; @@ -295,6 +297,7 @@ export function iterate( renderedVnode = Object.cast(vnode.el); } else { + VDOM.addToPrototype(render); renderedVnode = ctx.vdom.render(vnode); } From 37a3bdac70d4219e0569d39b3fbeb313b23b37f4 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 15:25:22 +0300 Subject: [PATCH 0207/2313] refactor: moved to `friends` & new tree-shake friendly API --- src/friends/vdom/CHANGELOG.md | 29 ++++ src/friends/vdom/README.md | 3 + src/friends/vdom/class.ts | 43 +++++ src/friends/vdom/index.ts | 19 +++ src/friends/vdom/interface.ts | 31 ++++ src/friends/vdom/render.ts | 145 +++++++++++++++++ src/friends/vdom/test/index.js | 278 +++++++++++++++++++++++++++++++++ src/friends/vdom/traverse.ts | 80 ++++++++++ src/friends/vdom/vnode.ts | 141 +++++++++++++++++ 9 files changed, 769 insertions(+) create mode 100644 src/friends/vdom/CHANGELOG.md create mode 100644 src/friends/vdom/README.md create mode 100644 src/friends/vdom/class.ts create mode 100644 src/friends/vdom/index.ts create mode 100644 src/friends/vdom/interface.ts create mode 100644 src/friends/vdom/render.ts create mode 100644 src/friends/vdom/test/index.js create mode 100644 src/friends/vdom/traverse.ts create mode 100644 src/friends/vdom/vnode.ts diff --git a/src/friends/vdom/CHANGELOG.md b/src/friends/vdom/CHANGELOG.md new file mode 100644 index 0000000000..ce871ae350 --- /dev/null +++ b/src/friends/vdom/CHANGELOG.md @@ -0,0 +1,29 @@ +Changelog +========= + +> **Tags:** +> - :boom: [Breaking Change] +> - :rocket: [New Feature] +> - :bug: [Bug Fix] +> - :memo: [Documentation] +> - :house: [Internal] +> - :nail_care: [Polish] + +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Moved the module to `friends/vdom` +* The module has been rewritten to a new tree-shake friendly API + +## v3.0.0 (2021-07-27) + +#### :house: Internal + +* Added tests + +## v3.0.0-rc.46 (2020-07-31) + +#### :house: Internal + +* Fixed ESLint warnings diff --git a/src/friends/vdom/README.md b/src/friends/vdom/README.md new file mode 100644 index 0000000000..03dd09b16c --- /dev/null +++ b/src/friends/vdom/README.md @@ -0,0 +1,3 @@ +# super/i-block/modules/vdom + +This module provides a class to work with a VDOM tree. diff --git a/src/friends/vdom/class.ts b/src/friends/vdom/class.ts new file mode 100644 index 0000000000..ce0f83e095 --- /dev/null +++ b/src/friends/vdom/class.ts @@ -0,0 +1,43 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Friend from 'friends/friend'; +import type iBlock from 'super/i-block/i-block'; + +import type * as traverse from 'friends/vdom/traverse'; +import type * as vnode from 'friends/vdom/vnode'; +import type * as render from 'friends/vdom/render'; + +interface VDOM { + closest: typeof traverse.closest; + findElFromVNode: typeof traverse.findElFromVNode; + create: typeof vnode.create; + render: typeof render.render; + getRenderFactory: typeof render.getRenderFactory; + getRenderFn: typeof render.getRenderFn; +} + +class VDOM extends Friend { + /** + * Sets the current component instance as active. + * This function is necessary to render components asynchronously. + */ + protected setInstance?: Function; + + constructor(component: iBlock) { + super(component); + + this.meta.hooks.mounted.push({ + fn: () => { + this.setInstance = this.ctx.$renderEngine.r.withAsyncContext(Promise.resolve.bind(Promise))[1]; + } + }); + } +} + +export default VDOM; diff --git a/src/friends/vdom/index.ts b/src/friends/vdom/index.ts new file mode 100644 index 0000000000..c4e29248b9 --- /dev/null +++ b/src/friends/vdom/index.ts @@ -0,0 +1,19 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:friends/vdom/README.md]] + * @packageDocumentation + */ + +export { default } from 'friends/vdom/class'; + +export * from 'friends/vdom/traverse'; +export * from 'friends/vdom/render'; +export * from 'friends/vdom/vnode'; +export * from 'friends/vdom/interface'; diff --git a/src/friends/vdom/interface.ts b/src/friends/vdom/interface.ts new file mode 100644 index 0000000000..7f18524cb8 --- /dev/null +++ b/src/friends/vdom/interface.ts @@ -0,0 +1,31 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type { VNode } from 'core/component/engines'; + +export type { RenderFactory, RenderFn } from 'core/component'; + +/** + * A structure to describe a VNode + */ +export interface VNodeDescriptor { + /** + * A simple tag name or component name + */ + type: string; + + /** + * A dictionary with attributes to pass to the created VNode + */ + attrs?: Dictionary; + + /** + * An array of children VNode descriptors or dictionary with slot functions + */ + children?: string | VNodeDescriptor[] | Dictionary CanArray)>; +} diff --git a/src/friends/vdom/render.ts b/src/friends/vdom/render.ts new file mode 100644 index 0000000000..912f4be6c8 --- /dev/null +++ b/src/friends/vdom/render.ts @@ -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 + */ + +import type { VNode } from 'core/component/engines'; +import type { ComponentInterface } from 'super/i-block'; + +import type VDOM from 'friends/vdom/class'; +import type { RenderFactory, RenderFn } from 'friends/vdom/interface'; + +/** + * Renders the specified VNode and returns the result + * @param vnode + * + * @example + * ```js + * const div = this.render(Vue.h('div', {class: 'foo'})); + * + * console.log(div.tagName); // DIV + * console.log(div.classList.contains('foo')); // true + * ``` + */ +export function render(this: VDOM, vnode: VNode): Node; + +/** + * Renders the specified list of VNode-s and returns the result + * @param vnodes + * + * @example + * ```js + * const divs = this.vdom.render([ + * Vue.h('div', {class: 'foo'}), + * Vue.h('div', {class: 'bar'}), + * ]); + * + * console.log(div[0].tagName); // DIV + * console.log(div[1].classList.contains('bar')); // true + * ``` + */ +export function render(this: VDOM, vnodes: VNode[]): Node[]; +export function render(this: VDOM, vnode: CanArray): CanArray { + return this.ctx.$renderEngine.r.render(Object.cast(vnode), this.ctx); +} + +/** + * Returns a render function factory by the specified path + * @param path - a path to the render factory + * + * @example + * ```js + * // Returns the main render factory of bExample + * this.getRenderFactory('bExample/'); + * this.getRenderFactory('bExample.index'); + * + * this.getRenderFactory('bExample.subTemplate'); + * ``` + */ +export function getRenderFactory(this: VDOM, path: string): CanUndef { + const + chunks = path.split('.'); + + if (path.endsWith('/')) { + const l = chunks.length - 1; + chunks[l] = chunks[l].slice(0, -1); + chunks.push('index'); + } + + if (chunks.length === 1) { + chunks.unshift(this.ctx.componentName); + + } else { + chunks[0] = chunks[0].dasherize(); + } + + const + tpl = TPLS[chunks[0]]; + + if (tpl == null) { + return; + } + + const + fn = Object.get(tpl, chunks.slice(1)); + + if (Object.isFunction(fn)) { + return fn; + } +} + +/** + * Returns a render function using the specified factory or path + * + * @param factoryOrPath - the render factory or a path to it + * @param [ctx] - a component context for rendering + * + * @example + * ```js + * this.getRenderFn(this.getRenderObject('bExample/')); + * this.getRenderFn('bExample.subTemplate'); + * ``` + */ +export function getRenderFn( + this: VDOM, + factoryOrPath: CanUndef | string, + ctx?: ComponentInterface +): RenderFn { + const + factory = Object.isString(factoryOrPath) ? getRenderFactory.call(this, factoryOrPath) : factoryOrPath; + + if (factory == null) { + return () => this.ctx.$renderEngine.r.createCommentVNode('loopback'); + } + + return (bindings) => { + const + cache = [], + instanceCtx = Object.create(ctx); + + if (bindings != null) { + for (let keys = Object.keys(bindings), i = 0; i < keys.length; i++) { + const + key = keys[i], + value = bindings[key]; + + if (key in ctx) { + Object.defineProperty(instanceCtx, key, { + configurable: true, + enumerable: true, + writable: true, + value + }); + + } else { + instanceCtx[key] = value; + } + } + } + + return Object.cast(factory(instanceCtx, cache)); + }; +} diff --git a/src/friends/vdom/test/index.js b/src/friends/vdom/test/index.js new file mode 100644 index 0000000000..b1d5c74f33 --- /dev/null +++ b/src/friends/vdom/test/index.js @@ -0,0 +1,278 @@ +// @ts-check + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +const + h = include('tests/helpers').default; + +/** + * Starts a test + * + * @param {Playwright.Page} page + * @param {object} params + * @returns {void} + */ +module.exports = (page, {browser, contextOpts}) => { + const initialUrl = page.url(); + + let + dummyComponent, + buttonComponent, + context, + vdom, + + DUMMY_COMPONENT_ID; + + const + BUTTON_TEXT = 'Hello ima button'; + + describe('`iBlock.vdom`', () => { + beforeEach(async () => { + context = await browser.newContext(contextOpts); + page = await context.newPage(); + + await page.goto(initialUrl); + + await page.evaluate((BUTTON_TEXT) => { + globalThis.renderComponents('b-dummy', [ + { + attrs: { + id: 'test-dummy' + }, + + content: { + default: { + tag: 'b-button', + attrs: { + id: 'test-button' + }, + content: BUTTON_TEXT + } + } + } + ]); + }, BUTTON_TEXT); + + [dummyComponent, buttonComponent] = await Promise.all([ + h.component.waitForComponent(page, '#test-dummy'), + h.component.waitForComponent(page, '#test-button') + ]); + + DUMMY_COMPONENT_ID = await dummyComponent.evaluate((ctx) => ctx.componentId); + vdom = await buttonComponent.evaluateHandle((ctx) => ctx.vdom); + }); + + afterEach(() => context.close()); + + describe('`getSlot`', () => { + it('returns `slot` if the slot exists', async () => { + const [hasSlot, slotText] = await vdom.evaluate((ctx) => [ + Boolean(ctx.getSlot('default')), + ctx.getSlot('default')[0].text + ]); + + expect(hasSlot).toBeTrue(); + expect(slotText).toBe(BUTTON_TEXT); + }); + + it('returns `undefined` if the does not exist', async () => { + const + slot = await vdom.evaluate((ctx) => ctx.getSlot('unreachableSlot')); + + expect(slot).toBeUndefined(); + }); + }); + + describe('`closest`', () => { + describe('component name is provided', () => { + it('returns the closest component instance', async () => { + const [cName, cId] = await vdom.evaluate((ctx) => [ + ctx.closest('b-dummy').componentName, + ctx.closest('b-dummy').componentId + ]); + + expect(cName).toBe('b-dummy'); + expect(cId).toBe(DUMMY_COMPONENT_ID); + }); + + it('returns `undefined` if there is no such a parent component', async () => { + const + closest = await vdom.evaluate((ctx) => ctx.closest('b-unreachable-component')); + + expect(closest).toBeUndefined(); + }); + }); + + describe('component constructor is provided', () => { + it('returns the closest component instance', async () => { + const [cName, cId] = await vdom.evaluate((ctx) => { + const + // @ts-ignore + dummyComponent = document.getElementById('test-dummy').component, + closest = ctx.closest(dummyComponent.componentInstances.bDummy); + + return [ + closest.componentName, + closest.componentId + ]; + }); + + expect(cName).toBe('b-dummy'); + expect(cId).toBe(DUMMY_COMPONENT_ID); + }); + + it('returns `undefined` if there is no such a parent component', async () => { + const closest = await vdom.evaluate((ctx) => { + const + // @ts-ignore + dummyComponent = document.getElementById('test-dummy').component; + + return ctx.closest(dummyComponent.componentInstances.bBottomSlide); + }); + + expect(closest).toBeUndefined(); + }); + }); + }); + + describe('`findElFromVNode`', () => { + it('returns an element if it presents in the provided `VNode`', async () => { + const hasEl = await dummyComponent.evaluate((ctx) => { + const + vNode = ctx.vdom.findElFromVNode(ctx._vnode, 'wrapper'), + className = 'b-dummy__wrapper', + {elm} = vNode; + + return vNode && vNode.data.staticClass === className && elm.classList.contains(className); + }); + + expect(hasEl).toBeTrue(); + }); + + it('returns `undefined` if an element does not presents in the provided `VNode`', async () => { + const + hasEl = await dummyComponent.evaluate((ctx) => ctx.vdom.findElFromVNode(ctx._vnode, 'unreachableSelector')); + + expect(hasEl).toBeUndefined(); + }); + }); + + describe('`render`', () => { + it('single `VNode` is provided', async () => { + await buttonComponent.evaluate((ctx) => { + const newButton = ctx.vdom.render(ctx.$createElement('b-button', { + attrs: { + 'v-attrs': { + id: 'new-button' + } + } + })); + + document.body.appendChild(newButton); + }); + + const + newButton = await h.component.waitForComponent(page, '#new-button'), + newButtonComponentName = await newButton.evaluate((ctx) => ctx.componentName); + + expect(newButton).toBeTruthy(); + expect(newButtonComponentName).toBe('b-button'); + }); + + it('array of `VNode`-s are provided', async () => { + await buttonComponent.evaluate((ctx) => { + const newButtons = ctx.vdom.render([ + ctx.$createElement('b-button', { + attrs: { + 'v-attrs': { + id: 'new-button-1' + } + } + }), + + ctx.$createElement('b-button', { + attrs: { + 'v-attrs': { + id: 'new-button-2' + } + } + }) + ]); + + document.body.append(...newButtons); + }); + + const [button1, button2] = await Promise.all([ + h.component.waitForComponent(page, '#new-button-1'), + h.component.waitForComponent(page, '#new-button-2') + ]); + + const [button1ComponentName, button2ComponentName] = await Promise.all([ + button1.evaluate((ctx) => ctx.componentName), + button2.evaluate((ctx) => ctx.componentName) + ]); + + expect(button1ComponentName).toBe('b-button'); + expect(button2ComponentName).toBe('b-button'); + }); + }); + + describe('`getRenderObject`', () => { + it('returns `RenderObject` if the specified template exists', async () => { + const isRenderObj = await vdom.evaluate((ctx) => { + const + renderObj = ctx.getRenderObject('b-button.index'); + + return Object.isFunction(renderObj.render) && 'staticRenderFns' in renderObj; + }); + + expect(isRenderObj).toBeTrue(); + }); + + it('returns `undefined` if the specified template does not exist', async () => { + const + notRenderObj = await vdom.evaluate((ctx) => ctx.getRenderObject('b-button.unreachableTemplate')); + + expect(notRenderObj).toBeUndefined(); + }); + }); + + describe('`bindRenderObject`', () => { + it('returns a placeholder if the provided template does not exist', async () => { + const + isPlaceholder = await vdom.evaluate((ctx) => ctx.bindRenderObject('bUnreachable.index')().tag === 'span'); + + expect(isPlaceholder).toBeTrue(); + }); + + it('returns a render function if the provided template exists', async () => { + const + componentName = await vdom.evaluate((ctx) => ctx.bindRenderObject('bButton.index')().context.componentName); + + expect(componentName).toBe('b-button'); + }); + }); + + describe('`execRenderObject`', () => { + it('returns a `VNode` if the specified template exists', async () => { + const + componentName = await vdom.evaluate((ctx) => ctx.execRenderObject('bButton.index').context.componentName); + + expect(componentName).toBe('b-button'); + }); + + it('returns a placeholder if the specified template does not exist', async () => { + const + isPlaceholder = await vdom.evaluate((ctx) => ctx.execRenderObject('bUnreachable.index').tag === 'span'); + + expect(isPlaceholder).toBeTrue(); + }); + }); + }); +}; diff --git a/src/friends/vdom/traverse.ts b/src/friends/vdom/traverse.ts new file mode 100644 index 0000000000..f51ebbcec8 --- /dev/null +++ b/src/friends/vdom/traverse.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 type VDOM from 'friends/vdom/class'; + +import type iBlock from 'super/i-block'; +import type { VNode } from 'super/i-block'; + +/** + * Returns a link to the closest parent component from the current + * @param component - the component name to search or a link to the component constructor + */ +export function closest( + this: VDOM, + component: string | ClassConstructor | Function +): CanUndef { + const + nm = Object.isString(component) ? component.dasherize() : undefined; + + let + el = this.ctx.$parent; + + while (el != null) { + if ((Object.isFunction(component) && el.instance instanceof component) || el.componentName === nm) { + return Object.cast(el); + } + + el = el.$parent; + } + + return undefined; +} + +/** + * Searches a VNode element by the specified name from the passed component virtual tree and context + * + * @param vtree + * @param elName - the element name to search + * @param [ctx] - the component context to resolve element names + */ +export function findElFromVNode( + this: VDOM, + vtree: VNode, + elName: string, + ctx: iBlock = this.component +): CanUndef { + const selector = ctx.provide.fullElName(elName); + return search(vtree); + + function search(vnode: VNode) { + const + data = vnode.data ?? {}; + + const classes = Object.fromArray( + Array.concat([], (data.staticClass ?? '').split(' '), data.class) + ); + + if (classes[selector] != null) { + return vnode; + } + + if (vnode.children != null) { + for (let i = 0; i < vnode.children.length; i++) { + const + res = search(vnode.children[i]); + + if (res != null) { + return res; + } + } + } + + return undefined; + } +} diff --git a/src/friends/vdom/vnode.ts b/src/friends/vdom/vnode.ts new file mode 100644 index 0000000000..12c8e7ab4a --- /dev/null +++ b/src/friends/vdom/vnode.ts @@ -0,0 +1,141 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { isComponent } from 'core/component'; + +import type { VNode, ObjectDirective } from 'core/component/engines'; +import type { ComponentInterface } from 'super/i-block'; + +import type VDOM from 'friends/vdom/class'; +import type { VNodeDescriptor } from 'friends/vdom/interface'; + +/** + * Creates a VNode by the specified descriptor + * + * @param descriptor + * @example + * ```js + * const vnode = this.vdom.create({ + * type: 'b-button', + * + * attrs: { + * exterior: 'warning', + * 'v-show': true, + * '@click': console.log + * }, + * + * children: {default: () => 'Press on me!'} + * }); + * ``` + */ +export function create(this: VDOM, descriptor: VNodeDescriptor): VNode; + +/** + * Creates a VNodes by the specified descriptors + * + * @param descriptors + * @example + * ```js + * const vnode = this.vdom.create([ + * { + * type: 'b-button', + * + * attrs: { + * exterior: 'warning', + * 'v-show': true, + * '@click': console.log + * }, + * + * children: {default: () => 'Press on me!'} + * }, + * + * { + * type: 'div', + * children: ['Hello div'] + * }, + * ]); + * ``` + */ +export function create(this: VDOM, descriptors: VNodeDescriptor[]): VNode[]; + +export function create(this: VDOM, descriptors: CanArray): CanArray { + if (Object.isArray(descriptors)) { + const + vnodes = new Array(descriptors.length); + + for (let i = 0; i < descriptors.length; i++) { + vnodes[i] = createVNode.call(this, descriptors[i]); + } + + return vnodes; + } + + return createVNode.call(this, descriptors); +} + +/** + * Creates a VNode by the specified descriptor + * + * @param type + * @param [attrs] + * @param [children] + */ +function createVNode( + this: VDOM, + {type, attrs, children}: VNodeDescriptor +): VNode { + this.setInstance?.(); + + const { + ctx, + ctx: {$renderEngine: {r}} + } = this; + + const + resolvedType = isComponent.test(type) ? r.resolveComponent.call(ctx, type) : type; + + let + resolvedChildren; + + if (children != null) { + if (Object.isArray(children)) { + resolvedChildren = new Array(children.length); + + for (let i = 0; i < children.length; i++) { + const el = children[i]; + resolvedChildren[i] = Object.isString(el) ? r.createTextVNode.call(ctx, el) : createVNode.call(this, el); + } + + } else { + resolvedChildren = children; + } + } + + const vnode = Object.isString(resolvedType) ? + r.createVNode.call(ctx, resolvedType, {}, resolvedChildren) : + r.createBlock.call(ctx, resolvedType, {}, resolvedChildren); + + if (attrs != null) { + const vAttrs: {beforeCreate: NonNullable} = Object.cast( + r.resolveDirective.call(ctx, 'attrs') + ); + + vAttrs.beforeCreate.call(Object.cast(vAttrs), { + arg: '', + modifiers: {}, + + value: attrs, + oldValue: undefined, + + dir: Object.cast(vAttrs), + instance: Object.cast(ctx) + }, vnode); + } + + return vnode; +} From 86507e72b90c2a045b867e79cccba8925df75949 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 15:47:47 +0300 Subject: [PATCH 0208/2313] fix: fixed render API --- src/friends/vdom/render.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/friends/vdom/render.ts b/src/friends/vdom/render.ts index 912f4be6c8..160ecefb5d 100644 --- a/src/friends/vdom/render.ts +++ b/src/friends/vdom/render.ts @@ -87,7 +87,7 @@ export function getRenderFactory(this: VDOM, path: string): CanUndef | string, - ctx?: ComponentInterface + ctx: ComponentInterface = this.ctx ): RenderFn { const factory = Object.isString(factoryOrPath) ? getRenderFactory.call(this, factoryOrPath) : factoryOrPath; @@ -140,6 +140,6 @@ export function getRenderFn( } } - return Object.cast(factory(instanceCtx, cache)); + return Object.cast(factory(instanceCtx, cache)()); }; } From b6132a5a3b7b505ff72ede6ec27302f5cd01c55d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 15:53:51 +0300 Subject: [PATCH 0209/2313] refactor: don't use `r` API --- src/friends/async-render/iter.ts | 8 +------- src/friends/vdom/class.ts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index 86609563b8..da9d9860f5 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -56,8 +56,6 @@ export function iterate( const { ctx, - ctx: {$renderEngine: {r}}, - async: $a, localEmitter } = this; @@ -111,9 +109,6 @@ export function iterate( lastTask, lastEvent; - const - [_, setCurrentInstance] = r.withAsyncContext(() => undefined); - $a.setImmediate(async () => { ctx.$off('[[V_FOR_CB]]', setVNodeCompiler); ctx.$off('[[V_ASYNC_TARGET]]', setTarget); @@ -257,8 +252,6 @@ export function iterate( return lastTask(); function task() { - setCurrentInstance(); - const renderedVNodes: Node[] = []; @@ -298,6 +291,7 @@ export function iterate( } else { VDOM.addToPrototype(render); + ctx.vdom.setInstance?.(); renderedVnode = ctx.vdom.render(vnode); } diff --git a/src/friends/vdom/class.ts b/src/friends/vdom/class.ts index ce0f83e095..5856b3f62e 100644 --- a/src/friends/vdom/class.ts +++ b/src/friends/vdom/class.ts @@ -27,7 +27,7 @@ class VDOM extends Friend { * Sets the current component instance as active. * This function is necessary to render components asynchronously. */ - protected setInstance?: Function; + setInstance?: Function; constructor(component: iBlock) { super(component); From 5a74de56b821200a849ea14032d4636075855885 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 17:05:11 +0300 Subject: [PATCH 0210/2313] fix: migration to Vue3 --- src/friends/vdom/traverse.ts | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/friends/vdom/traverse.ts b/src/friends/vdom/traverse.ts index f51ebbcec8..c0f4c7aae0 100644 --- a/src/friends/vdom/traverse.ts +++ b/src/friends/vdom/traverse.ts @@ -7,6 +7,7 @@ */ import type VDOM from 'friends/vdom/class'; +import { normalizeClass } from 'core/component'; import type iBlock from 'super/i-block'; import type { VNode } from 'super/i-block'; @@ -37,31 +38,32 @@ export function closest( } /** - * Searches a VNode element by the specified name from the passed component virtual tree and context + * Searches a VNode element by the specified element name from another VNode and context * - * @param vtree - * @param elName - the element name to search - * @param [ctx] - the component context to resolve element names + * @param name + * @param vnode + * @param [ctx] - a component context to resolve the passed element name */ export function findElFromVNode( this: VDOM, - vtree: VNode, - elName: string, + name: string, + vnode: VNode, ctx: iBlock = this.component ): CanUndef { - const selector = ctx.provide.fullElName(elName); - return search(vtree); + const selector = ctx.provide.fullElName(name); + return search(vnode); function search(vnode: VNode) { const - data = vnode.data ?? {}; + props = vnode.props ?? {}; - const classes = Object.fromArray( - Array.concat([], (data.staticClass ?? '').split(' '), data.class) - ); + if (props.class != null) { + const + classes = normalizeClass(props.class).split(' '); - if (classes[selector] != null) { - return vnode; + if (classes.includes(selector)) { + return vnode; + } } if (vnode.children != null) { From db320a0b9060bb330bc9a4200e7da8401cbe2d20 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 17:05:34 +0300 Subject: [PATCH 0211/2313] refactor: enable memoization --- src/friends/vdom/render.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/friends/vdom/render.ts b/src/friends/vdom/render.ts index 160ecefb5d..fdfd5b651a 100644 --- a/src/friends/vdom/render.ts +++ b/src/friends/vdom/render.ts @@ -115,11 +115,12 @@ export function getRenderFn( return () => this.ctx.$renderEngine.r.createCommentVNode('loopback'); } - return (bindings) => { - const - cache = [], - instanceCtx = Object.create(ctx); + const + cache = [], + instanceCtx = Object.create(ctx), + render = factory(instanceCtx, cache); + return (bindings) => { if (bindings != null) { for (let keys = Object.keys(bindings), i = 0; i < keys.length; i++) { const @@ -140,6 +141,6 @@ export function getRenderFn( } } - return Object.cast(factory(instanceCtx, cache)()); + return Object.cast(render()); }; } From 8362dc9eec951ac16125a626f94b5dc1164052e9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 17:05:58 +0300 Subject: [PATCH 0212/2313] redactor: review API --- src/friends/vdom/interface.ts | 19 ++++----- src/friends/vdom/vnode.ts | 79 ++++++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/friends/vdom/interface.ts b/src/friends/vdom/interface.ts index 7f18524cb8..4a48d525f8 100644 --- a/src/friends/vdom/interface.ts +++ b/src/friends/vdom/interface.ts @@ -10,15 +10,7 @@ import type { VNode } from 'core/component/engines'; export type { RenderFactory, RenderFn } from 'core/component'; -/** - * A structure to describe a VNode - */ -export interface VNodeDescriptor { - /** - * A simple tag name or component name - */ - type: string; - +export interface VNodeOptions { /** * A dictionary with attributes to pass to the created VNode */ @@ -27,5 +19,12 @@ export interface VNodeDescriptor { /** * An array of children VNode descriptors or dictionary with slot functions */ - children?: string | VNodeDescriptor[] | Dictionary CanArray)>; + children?: Array | Dictionary CanArray)>; +} + +export interface VNodeDescriptor extends VNodeOptions { + /** + * A simple tag name or component name + */ + type: string; } diff --git a/src/friends/vdom/vnode.ts b/src/friends/vdom/vnode.ts index 12c8e7ab4a..135dfaae02 100644 --- a/src/friends/vdom/vnode.ts +++ b/src/friends/vdom/vnode.ts @@ -12,17 +12,17 @@ import type { VNode, ObjectDirective } from 'core/component/engines'; import type { ComponentInterface } from 'super/i-block'; import type VDOM from 'friends/vdom/class'; -import type { VNodeDescriptor } from 'friends/vdom/interface'; +import type { VNodeOptions, VNodeDescriptor } from 'friends/vdom/interface'; /** - * Creates a VNode by the specified descriptor + * Creates a VNode with the specified parameters + * + * @param type - a simple tag name or component name + * @param [opts] - additional options * - * @param descriptor * @example * ```js - * const vnode = this.vdom.create({ - * type: 'b-button', - * + * const vnode = this.vdom.create('b-button', { * attrs: { * exterior: 'warning', * 'v-show': true, @@ -33,7 +33,7 @@ import type { VNodeDescriptor } from 'friends/vdom/interface'; * }); * ``` */ -export function create(this: VDOM, descriptor: VNodeDescriptor): VNode; +export function create(this: VDOM, type: string, opts?: VNodeOptions): VNode; /** * Creates a VNodes by the specified descriptors @@ -41,7 +41,7 @@ export function create(this: VDOM, descriptor: VNodeDescriptor): VNode; * @param descriptors * @example * ```js - * const vnode = this.vdom.create([ + * const vnode = this.vdom.create( * { * type: 'b-button', * @@ -57,25 +57,59 @@ export function create(this: VDOM, descriptor: VNodeDescriptor): VNode; * { * type: 'div', * children: ['Hello div'] + * } + * ); + * ``` + */ +export function create(this: VDOM, ...descriptors: VNodeDescriptor[]): VNode[]; + +/** + * Creates a VNodes by the specified descriptors + * + * @param descriptors + * @example + * ```js + * const vnode = this.vdom.create([ + * { + * type: 'b-button', + * + * attrs: { + * exterior: 'warning', + * 'v-show': true, + * '@click': console.log + * }, + * + * children: {default: () => 'Press on me!'} * }, + * + * { + * type: 'div', + * children: ['Hello div'] + * } * ]); * ``` */ export function create(this: VDOM, descriptors: VNodeDescriptor[]): VNode[]; -export function create(this: VDOM, descriptors: CanArray): CanArray { - if (Object.isArray(descriptors)) { - const - vnodes = new Array(descriptors.length); +export function create( + this: VDOM, + typeOrDesc?: string | CanArray, + ...descriptors: [VNodeOptions?] | VNodeDescriptor[] +): CanArray { + if (Object.isString(typeOrDesc)) { + return createVNode.call(this, typeOrDesc, descriptors[0]); + } - for (let i = 0; i < descriptors.length; i++) { - vnodes[i] = createVNode.call(this, descriptors[i]); - } + const + resolvedDescriptors = Array.concat([], typeOrDesc, Object.cast(descriptors)), + vnodes = new Array(resolvedDescriptors.length); - return vnodes; + for (let i = 0; i < resolvedDescriptors.length; i++) { + const el = resolvedDescriptors[i]; + vnodes[i] = createVNode.call(this, el.type, el); } - return createVNode.call(this, descriptors); + return vnodes; } /** @@ -87,7 +121,8 @@ export function create(this: VDOM, descriptors: CanArray): CanA */ function createVNode( this: VDOM, - {type, attrs, children}: VNodeDescriptor + type: string, + {attrs, children}: VNodeOptions = {} ): VNode { this.setInstance?.(); @@ -107,8 +142,12 @@ function createVNode( resolvedChildren = new Array(children.length); for (let i = 0; i < children.length; i++) { - const el = children[i]; - resolvedChildren[i] = Object.isString(el) ? r.createTextVNode.call(ctx, el) : createVNode.call(this, el); + const + el = children[i]; + + resolvedChildren[i] = Object.isString(el) ? + r.createTextVNode.call(ctx, el) : + createVNode.call(this, el.type, el); } } else { From cc21e675217ab4bba76ecdd44f96302fa864eabb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 17:07:25 +0300 Subject: [PATCH 0213/2313] :art: --- src/friends/vdom/traverse.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/friends/vdom/traverse.ts b/src/friends/vdom/traverse.ts index c0f4c7aae0..6ed3b5e364 100644 --- a/src/friends/vdom/traverse.ts +++ b/src/friends/vdom/traverse.ts @@ -40,17 +40,17 @@ export function closest( /** * Searches a VNode element by the specified element name from another VNode and context * - * @param name + * @param elName * @param vnode * @param [ctx] - a component context to resolve the passed element name */ export function findElFromVNode( this: VDOM, - name: string, + elName: string, vnode: VNode, ctx: iBlock = this.component ): CanUndef { - const selector = ctx.provide.fullElName(name); + const selector = ctx.provide.fullElName(elName); return search(vnode); function search(vnode: VNode) { From 5a4fd05836366df0edf6f1aaf9b5cd4806266a6a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 18:02:54 +0300 Subject: [PATCH 0214/2313] doc: improved docs --- src/friends/vdom/README.md | 157 ++++++++++++++++++++++++++++++++++- src/friends/vdom/render.ts | 40 ++++++--- src/friends/vdom/traverse.ts | 23 +++++ src/friends/vdom/vnode.ts | 4 +- 4 files changed, 208 insertions(+), 16 deletions(-) diff --git a/src/friends/vdom/README.md b/src/friends/vdom/README.md index 03dd09b16c..6223cdaf45 100644 --- a/src/friends/vdom/README.md +++ b/src/friends/vdom/README.md @@ -1,3 +1,158 @@ # super/i-block/modules/vdom -This module provides a class to work with a VDOM tree. +This module provides a class to low-level working with a component VDOM tree. + +## How to include this module to your component + +By default, any component that inherited from [[iBlock]] has the `vdom` property. +But to use module methods, attach them explicitly to enable tree-shake code optimizations. +Just place the necessary import declaration within your component file. + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; +import VDOM, { create, render } from 'friends/vdom'; + +// Import `create` and `render` methods +VDOM.addToPrototype(create, render); + +@component() +export default class bExample extends iBlock {} +``` + +## Methods + +### closest + +Returns a link to the closest parent component from the current. + +```js +// Returns a link to the closes `b-wrapper` component or undefined +console.log(this.vdom.closest('b-wrapper')); + +// By a constructor +console.log(this.vdom.closest('bWrapper')); +``` + +### findElFromVNode + +Searches a VNode element by the specified element name from another VNode and context. + +```js +const vnode = this.vdom.create('div', { + children: [ + { + type: 'div', + attrs: {class: this.block.getFullElName('elem')} + } + ] +}); + +console.log(this.vdom.findElFromVNode('elem', vnode)); +``` + +### create + +Creates a VNode or list of VNode-s with the specified parameters. + +```js +const vnode = this.vdom.create('b-button', { + attrs: { + exterior: 'warning', + 'v-show': true, + '@click': console.log + }, + + children: {default: () => 'Press on me!'} +}); + +const vnodesFromArgs = this.vdom.create( + { + type: 'b-button', + + attrs: { + exterior: 'warning', + 'v-show': true, + '@click': console.log + }, + + children: {default: () => 'Press on me!'} + }, + + { + type: 'div', + children: ['Hello div'] + } +); + +const vnodesFromArr = this.vdom.create([ + { + type: 'b-button', + + attrs: { + exterior: 'warning', + 'v-show': true, + '@click': console.log + }, + + children: {default: () => 'Press on me!'} + }, + + { + type: 'div', + children: ['Hello div'] + } +]); +``` + +### render + +Renders the specified VNode or a list of VNode-s and returns the result. + +```js +const div = this.vdom.render(this.create('div', {attrs: {class: 'foo'}})); + +console.log(div.tagName); // DIV +console.log(div.classList.contains('foo')); // true + +const divs = this.vdom.render(this.vdom.create( + {type: 'div', attrs: {class: 'foo'}}, + {type: 'div', attrs: {class: 'bar'}} +)); + +console.log(div[0].tagName); // DIV +console.log(div[1].classList.contains('bar')); // true +``` + +### getRenderFactory + +Returns a render function factory by the specified path. +This function is useful when you want to decompose your component template into separated render functions. + +```js +// Returns the main render factory of bExample +this.vdom.getRenderFactory('bExample/'); +this.vdom.getRenderFactory('bExample.index'); + +this.vdom.getRenderFactory('bExample.subTemplate'); +``` + +### getRenderFn + +Returns a render function using the specified factory or path. +This function is useful when you want to decompose your component template into separated render functions. + +``` +- namespace [%fileName%] + +- include 'super/i-static-page/i-static-page.component.ss'|b as placeholder + +- template sayHello() + < .hello + Hello {{ p.name }} + +- template index() extends ['i-static-page.component'].index + - block body + /// Invokes the passed render function and joins the result fragment with the main fragment. + /// Notice, you can pass parameters to another render function. + < .content v-render = vdom.getRenderFn('pV4ComponentsDemo.sayHello')({p: {name: 'Bob'}}) +``` diff --git a/src/friends/vdom/render.ts b/src/friends/vdom/render.ts index fdfd5b651a..cc65d1b738 100644 --- a/src/friends/vdom/render.ts +++ b/src/friends/vdom/render.ts @@ -18,7 +18,7 @@ import type { RenderFactory, RenderFn } from 'friends/vdom/interface'; * * @example * ```js - * const div = this.render(Vue.h('div', {class: 'foo'})); + * const div = this.vdom.render(this.create('div', {attrs: {class: 'foo'}})); * * console.log(div.tagName); // DIV * console.log(div.classList.contains('foo')); // true @@ -32,10 +32,10 @@ export function render(this: VDOM, vnode: VNode): Node; * * @example * ```js - * const divs = this.vdom.render([ - * Vue.h('div', {class: 'foo'}), - * Vue.h('div', {class: 'bar'}), - * ]); + * const divs = this.vdom.render(this.vdom.create( + * {type: 'div', attrs: {class: 'foo'}}, + * {type: 'div', attrs: {class: 'bar'}} + * )); * * console.log(div[0].tagName); // DIV * console.log(div[1].classList.contains('bar')); // true @@ -47,16 +47,18 @@ export function render(this: VDOM, vnode: CanArray): CanArray { } /** - * Returns a render function factory by the specified path + * Returns a render function factory by the specified path. + * This function is useful when you want to decompose your component template into separated render functions. + * * @param path - a path to the render factory * * @example * ```js * // Returns the main render factory of bExample - * this.getRenderFactory('bExample/'); - * this.getRenderFactory('bExample.index'); + * this.vdom.getRenderFactory('bExample/'); + * this.vdom.getRenderFactory('bExample.index'); * - * this.getRenderFactory('bExample.subTemplate'); + * this.vdom.getRenderFactory('bExample.subTemplate'); * ``` */ export function getRenderFactory(this: VDOM, path: string): CanUndef { @@ -92,15 +94,27 @@ export function getRenderFactory(this: VDOM, path: string): CanUndef( this: VDOM, @@ -43,6 +52,20 @@ export function closest( * @param elName * @param vnode * @param [ctx] - a component context to resolve the passed element name + * + * @example + * ```js + * const vnode = this.vdom.create('div', { + * children: [ + * { + * type: 'div', + * attrs: {class: this.block.getFullElName('elem')} + * } + * ] + * }); + * + * console.log(this.vdom.findElFromVNode('elem', vnode)); + * ``` */ export function findElFromVNode( this: VDOM, diff --git a/src/friends/vdom/vnode.ts b/src/friends/vdom/vnode.ts index 135dfaae02..4139bb1a03 100644 --- a/src/friends/vdom/vnode.ts +++ b/src/friends/vdom/vnode.ts @@ -41,7 +41,7 @@ export function create(this: VDOM, type: string, opts?: VNodeOptions): VNode; * @param descriptors * @example * ```js - * const vnode = this.vdom.create( + * const vnodes = this.vdom.create( * { * type: 'b-button', * @@ -69,7 +69,7 @@ export function create(this: VDOM, ...descriptors: VNodeDescriptor[]): VNode[]; * @param descriptors * @example * ```js - * const vnode = this.vdom.create([ + * const vnodes = this.vdom.create([ * { * type: 'b-button', * From 7f5b1058f2b7b0a1f3e0618f238499e445858f03 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 18:03:32 +0300 Subject: [PATCH 0215/2313] refactor: removed legacy --- src/super/i-block/modules/vdom/CHANGELOG.md | 22 -- src/super/i-block/modules/vdom/README.md | 3 - src/super/i-block/modules/vdom/const.ts | 12 - src/super/i-block/modules/vdom/index.ts | 274 ------------------ src/super/i-block/modules/vdom/interface.ts | 18 -- src/super/i-block/modules/vdom/test/index.js | 278 ------------------- 6 files changed, 607 deletions(-) delete mode 100644 src/super/i-block/modules/vdom/CHANGELOG.md delete mode 100644 src/super/i-block/modules/vdom/README.md delete mode 100644 src/super/i-block/modules/vdom/const.ts delete mode 100644 src/super/i-block/modules/vdom/index.ts delete mode 100644 src/super/i-block/modules/vdom/interface.ts delete mode 100644 src/super/i-block/modules/vdom/test/index.js diff --git a/src/super/i-block/modules/vdom/CHANGELOG.md b/src/super/i-block/modules/vdom/CHANGELOG.md deleted file mode 100644 index 05f179c040..0000000000 --- a/src/super/i-block/modules/vdom/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.0.0 (2021-07-27) - -#### :house: Internal - -* Added tests - -## v3.0.0-rc.46 (2020-07-31) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/super/i-block/modules/vdom/README.md b/src/super/i-block/modules/vdom/README.md deleted file mode 100644 index 03dd09b16c..0000000000 --- a/src/super/i-block/modules/vdom/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# super/i-block/modules/vdom - -This module provides a class to work with a VDOM tree. diff --git a/src/super/i-block/modules/vdom/const.ts b/src/super/i-block/modules/vdom/const.ts deleted file mode 100644 index c1f853f07d..0000000000 --- a/src/super/i-block/modules/vdom/const.ts +++ /dev/null @@ -1,12 +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 { RenderObject } from 'core/component'; - -export const - tplCache = Object.createDict(); diff --git a/src/super/i-block/modules/vdom/index.ts b/src/super/i-block/modules/vdom/index.ts deleted file mode 100644 index 4aa99b8432..0000000000 --- a/src/super/i-block/modules/vdom/index.ts +++ /dev/null @@ -1,274 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:super/i-block/modules/vdom/README.md]] - * @packageDocumentation - */ - -import type { VNode } from 'core/component'; - -import type iBlock from 'super/i-block/i-block'; -import Friend from 'friends/friend'; - -import Opt from 'super/i-block/modules/opt'; -import Field from 'super/i-block/modules/field'; -import Provide from 'super/i-block/modules/provide'; - -import { tplCache } from 'super/i-block/modules/vdom/const'; -import type { RenderFn, RenderPath, RenderContext } from 'super/i-block/modules/vdom/interface'; - -export * from 'super/i-block/modules/vdom/interface'; - -/** - * Class provides API to work with a VDOM tree - */ -export default class VDOM extends Friend { - /** - * Renders the specified VNode and returns the result - * @param vnode - * - * @example - * ```js - * const div = this.render(Vue.h('div', {class: 'foo'})); - * - * console.log(div.tagName); // DIV - * console.log(div.classList.contains('foo')); // true - * ``` - */ - render(vnode: VNode): Node; - - /** - * Renders the specified list of VNode-s and returns the result - * @param vnodes - * - * @example - * ```js - * const divs = this.render([ - * Vue.h('div', {class: 'foo'}), - * Vue.h('div', {class: 'bar'}), - * ]); - * - * console.log(div[0].tagName); // DIV - * console.log(div[1].classList.contains('bar')); // true - * ``` - */ - render(vnodes: VNode[]): Node[]; - render(vnode: CanArray): CanArray { - return this.ctx.$renderEngine.r.render(Object.cast(vnode), this.ctx); - } - - /** - * Returns a render object by the specified path - * @param path - path to a template - * - * @example - * ```js - * // Returns the main render object of bExample - * this.getRenderObject('bExample/'); - * this.getRenderObject('bExample.index'); - * - * this.getRenderObject('bExample.subTemplate'); - * ``` - */ - getRenderObject(path: string): CanUndef { - const - chunks = path.split('.'); - - if (path.endsWith('/')) { - const l = chunks.length - 1; - chunks[l] = chunks[l].slice(0, -1); - chunks.push('index'); - } - - if (chunks.length === 1) { - chunks.unshift(this.ctx.componentName); - - } else { - chunks[0] = chunks[0].dasherize(); - } - - const - key = chunks.join('.'), - cache = tplCache[key]; - - if (cache) { - return cache; - } - - const - tpl = TPLS[chunks[0]]; - - if (!tpl) { - return; - } - - const - fn = Object.get(tpl, chunks.slice(1)); - - if (Object.isFunction(fn)) { - return tplCache[key] = fn(); - } - } - - /** - * Returns a function that executes the specified render object - * - * @param objOrPath - render object or path to a template - * @param [ctx] - render context - * - * @example - * ```js - * this.bindRenderObject(this.getRenderObject('bExample/')); - * this.bindRenderObject('bExample.subTemplate'); - * ``` - */ - bindRenderObject(objOrPath: RenderPath, ctx?: RenderContext): RenderFn { - const - renderObj = Object.isString(objOrPath) ? this.getRenderObject(objOrPath) : objOrPath; - - if (!renderObj) { - return () => this.ctx.$createElement('span'); - } - - let - instanceCtx, - renderCtx; - - if (ctx && Object.isArray(ctx)) { - instanceCtx = ctx[0] ?? this.ctx; - renderCtx = ctx[1]; - - if (instanceCtx !== instanceCtx.provide.component) { - instanceCtx.field = new Field(instanceCtx); - instanceCtx.provide = new Provide(instanceCtx); - instanceCtx.opts = new Opt(instanceCtx); - } - - } else { - instanceCtx = this.ctx; - renderCtx = ctx; - } - - return (p) => { - instanceCtx = Object.create(instanceCtx); - instanceCtx.isVirtualTpl = true; - - if (p) { - for (let keys = Object.keys(p), i = 0; i < keys.length; i++) { - const - key = keys[i], - value = p[key]; - - if (key in instanceCtx) { - Object.defineProperty(instanceCtx, key, { - configurable: true, - enumerable: true, - writable: true, - value - }); - - } else { - instanceCtx[key] = value; - } - } - } - - const - vnode = execRenderObject(renderObj, instanceCtx); - - if (renderCtx != null) { - this.ctx.$renderEngine.patchVNode(vnode, instanceCtx, renderCtx); - } - - return vnode; - }; - } - - /** - * Executes the specified render object - * - * @param objOrPath - render object or path to a template - * @param [ctx] - render context - * - * @example - * ```js - * this.execRenderObject(this.getRenderObject('bExample/')); - * this.execRenderObject('bExample.subTemplate'); - * ``` - */ - execRenderObject(objOrPath: RenderPath, ctx?: RenderContext): VNode { - return this.bindRenderObject(objOrPath, ctx)(); - } - - /** - * Returns a link to the closest parent component from the current - * @param component - component name or a link to the component constructor - */ - closest(component: string | ClassConstructor | Function): CanUndef { - const - nm = Object.isString(component) ? component.dasherize() : undefined; - - let - el = >this.ctx.$parent; - - while (el) { - if ((Object.isFunction(component) && el.instance instanceof component) || el.componentName === nm) { - return el; - } - - el = >el.$parent; - } - - return undefined; - } - - /** - * Searches an element by the specified name from a virtual node - * - * @param vnode - * @param elName - * @param [ctx] - component context - */ - findElFromVNode( - vnode: VNode, - elName: string, - ctx: iBlock = this.component - ): CanUndef { - const - selector = ctx.provide.fullElName(elName); - - const search = (vnode) => { - const - data = vnode.data ?? {}; - - const classes = Object.fromArray( - Array.concat([], (data.staticClass ?? '').split(' '), data.class) - ); - - if (classes[selector] != null) { - return vnode; - } - - if (vnode.children != null) { - for (let i = 0; i < vnode.children.length; i++) { - const - res = search(vnode.children[i]); - - if (res != null) { - return res; - } - } - } - - return undefined; - }; - - return search(vnode); - } -} diff --git a/src/super/i-block/modules/vdom/interface.ts b/src/super/i-block/modules/vdom/interface.ts deleted file mode 100644 index 0ad70d943d..0000000000 --- a/src/super/i-block/modules/vdom/interface.ts +++ /dev/null @@ -1,18 +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 { VNode, RenderObject, RenderContext as ComponentRenderContext } from 'core/component'; - -export type RenderFn = (params?: Dictionary) => VNode; - -export type RenderContext = - ComponentRenderContext | - [Dictionary?] | - [Dictionary?, ComponentRenderContext?]; - -export type RenderPath = CanUndef | string; diff --git a/src/super/i-block/modules/vdom/test/index.js b/src/super/i-block/modules/vdom/test/index.js deleted file mode 100644 index b1d5c74f33..0000000000 --- a/src/super/i-block/modules/vdom/test/index.js +++ /dev/null @@ -1,278 +0,0 @@ -// @ts-check - -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -const - h = include('tests/helpers').default; - -/** - * Starts a test - * - * @param {Playwright.Page} page - * @param {object} params - * @returns {void} - */ -module.exports = (page, {browser, contextOpts}) => { - const initialUrl = page.url(); - - let - dummyComponent, - buttonComponent, - context, - vdom, - - DUMMY_COMPONENT_ID; - - const - BUTTON_TEXT = 'Hello ima button'; - - describe('`iBlock.vdom`', () => { - beforeEach(async () => { - context = await browser.newContext(contextOpts); - page = await context.newPage(); - - await page.goto(initialUrl); - - await page.evaluate((BUTTON_TEXT) => { - globalThis.renderComponents('b-dummy', [ - { - attrs: { - id: 'test-dummy' - }, - - content: { - default: { - tag: 'b-button', - attrs: { - id: 'test-button' - }, - content: BUTTON_TEXT - } - } - } - ]); - }, BUTTON_TEXT); - - [dummyComponent, buttonComponent] = await Promise.all([ - h.component.waitForComponent(page, '#test-dummy'), - h.component.waitForComponent(page, '#test-button') - ]); - - DUMMY_COMPONENT_ID = await dummyComponent.evaluate((ctx) => ctx.componentId); - vdom = await buttonComponent.evaluateHandle((ctx) => ctx.vdom); - }); - - afterEach(() => context.close()); - - describe('`getSlot`', () => { - it('returns `slot` if the slot exists', async () => { - const [hasSlot, slotText] = await vdom.evaluate((ctx) => [ - Boolean(ctx.getSlot('default')), - ctx.getSlot('default')[0].text - ]); - - expect(hasSlot).toBeTrue(); - expect(slotText).toBe(BUTTON_TEXT); - }); - - it('returns `undefined` if the does not exist', async () => { - const - slot = await vdom.evaluate((ctx) => ctx.getSlot('unreachableSlot')); - - expect(slot).toBeUndefined(); - }); - }); - - describe('`closest`', () => { - describe('component name is provided', () => { - it('returns the closest component instance', async () => { - const [cName, cId] = await vdom.evaluate((ctx) => [ - ctx.closest('b-dummy').componentName, - ctx.closest('b-dummy').componentId - ]); - - expect(cName).toBe('b-dummy'); - expect(cId).toBe(DUMMY_COMPONENT_ID); - }); - - it('returns `undefined` if there is no such a parent component', async () => { - const - closest = await vdom.evaluate((ctx) => ctx.closest('b-unreachable-component')); - - expect(closest).toBeUndefined(); - }); - }); - - describe('component constructor is provided', () => { - it('returns the closest component instance', async () => { - const [cName, cId] = await vdom.evaluate((ctx) => { - const - // @ts-ignore - dummyComponent = document.getElementById('test-dummy').component, - closest = ctx.closest(dummyComponent.componentInstances.bDummy); - - return [ - closest.componentName, - closest.componentId - ]; - }); - - expect(cName).toBe('b-dummy'); - expect(cId).toBe(DUMMY_COMPONENT_ID); - }); - - it('returns `undefined` if there is no such a parent component', async () => { - const closest = await vdom.evaluate((ctx) => { - const - // @ts-ignore - dummyComponent = document.getElementById('test-dummy').component; - - return ctx.closest(dummyComponent.componentInstances.bBottomSlide); - }); - - expect(closest).toBeUndefined(); - }); - }); - }); - - describe('`findElFromVNode`', () => { - it('returns an element if it presents in the provided `VNode`', async () => { - const hasEl = await dummyComponent.evaluate((ctx) => { - const - vNode = ctx.vdom.findElFromVNode(ctx._vnode, 'wrapper'), - className = 'b-dummy__wrapper', - {elm} = vNode; - - return vNode && vNode.data.staticClass === className && elm.classList.contains(className); - }); - - expect(hasEl).toBeTrue(); - }); - - it('returns `undefined` if an element does not presents in the provided `VNode`', async () => { - const - hasEl = await dummyComponent.evaluate((ctx) => ctx.vdom.findElFromVNode(ctx._vnode, 'unreachableSelector')); - - expect(hasEl).toBeUndefined(); - }); - }); - - describe('`render`', () => { - it('single `VNode` is provided', async () => { - await buttonComponent.evaluate((ctx) => { - const newButton = ctx.vdom.render(ctx.$createElement('b-button', { - attrs: { - 'v-attrs': { - id: 'new-button' - } - } - })); - - document.body.appendChild(newButton); - }); - - const - newButton = await h.component.waitForComponent(page, '#new-button'), - newButtonComponentName = await newButton.evaluate((ctx) => ctx.componentName); - - expect(newButton).toBeTruthy(); - expect(newButtonComponentName).toBe('b-button'); - }); - - it('array of `VNode`-s are provided', async () => { - await buttonComponent.evaluate((ctx) => { - const newButtons = ctx.vdom.render([ - ctx.$createElement('b-button', { - attrs: { - 'v-attrs': { - id: 'new-button-1' - } - } - }), - - ctx.$createElement('b-button', { - attrs: { - 'v-attrs': { - id: 'new-button-2' - } - } - }) - ]); - - document.body.append(...newButtons); - }); - - const [button1, button2] = await Promise.all([ - h.component.waitForComponent(page, '#new-button-1'), - h.component.waitForComponent(page, '#new-button-2') - ]); - - const [button1ComponentName, button2ComponentName] = await Promise.all([ - button1.evaluate((ctx) => ctx.componentName), - button2.evaluate((ctx) => ctx.componentName) - ]); - - expect(button1ComponentName).toBe('b-button'); - expect(button2ComponentName).toBe('b-button'); - }); - }); - - describe('`getRenderObject`', () => { - it('returns `RenderObject` if the specified template exists', async () => { - const isRenderObj = await vdom.evaluate((ctx) => { - const - renderObj = ctx.getRenderObject('b-button.index'); - - return Object.isFunction(renderObj.render) && 'staticRenderFns' in renderObj; - }); - - expect(isRenderObj).toBeTrue(); - }); - - it('returns `undefined` if the specified template does not exist', async () => { - const - notRenderObj = await vdom.evaluate((ctx) => ctx.getRenderObject('b-button.unreachableTemplate')); - - expect(notRenderObj).toBeUndefined(); - }); - }); - - describe('`bindRenderObject`', () => { - it('returns a placeholder if the provided template does not exist', async () => { - const - isPlaceholder = await vdom.evaluate((ctx) => ctx.bindRenderObject('bUnreachable.index')().tag === 'span'); - - expect(isPlaceholder).toBeTrue(); - }); - - it('returns a render function if the provided template exists', async () => { - const - componentName = await vdom.evaluate((ctx) => ctx.bindRenderObject('bButton.index')().context.componentName); - - expect(componentName).toBe('b-button'); - }); - }); - - describe('`execRenderObject`', () => { - it('returns a `VNode` if the specified template exists', async () => { - const - componentName = await vdom.evaluate((ctx) => ctx.execRenderObject('bButton.index').context.componentName); - - expect(componentName).toBe('b-button'); - }); - - it('returns a placeholder if the specified template does not exist', async () => { - const - isPlaceholder = await vdom.evaluate((ctx) => ctx.execRenderObject('bUnreachable.index').tag === 'span'); - - expect(isPlaceholder).toBeTrue(); - }); - }); - }); -}; From acc2c74adb1ebc4b9c9e8b3e621c3fc2289eacf8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 18:05:49 +0300 Subject: [PATCH 0216/2313] doc: fixed the header --- src/friends/vdom/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/friends/vdom/README.md b/src/friends/vdom/README.md index 6223cdaf45..1ce16ad4e9 100644 --- a/src/friends/vdom/README.md +++ b/src/friends/vdom/README.md @@ -1,4 +1,4 @@ -# super/i-block/modules/vdom +# friends/vdom This module provides a class to low-level working with a component VDOM tree. From 2152c9f0cb59c30b0c12c462000b0a59ea7219b9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 9 Jun 2022 22:55:17 +0300 Subject: [PATCH 0217/2313] doc: better doc --- src/friends/vdom/README.md | 1 + src/friends/vdom/traverse.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/friends/vdom/README.md b/src/friends/vdom/README.md index 1ce16ad4e9..ec3493b1aa 100644 --- a/src/friends/vdom/README.md +++ b/src/friends/vdom/README.md @@ -36,6 +36,7 @@ console.log(this.vdom.closest('bWrapper')); ### findElFromVNode Searches a VNode element by the specified element name from another VNode and context. +The function returns the found VNode or undefined. ```js const vnode = this.vdom.create('div', { diff --git a/src/friends/vdom/traverse.ts b/src/friends/vdom/traverse.ts index 1aa9e5eedd..06d27bf21a 100644 --- a/src/friends/vdom/traverse.ts +++ b/src/friends/vdom/traverse.ts @@ -47,7 +47,8 @@ export function closest( } /** - * Searches a VNode element by the specified element name from another VNode and context + * Searches a VNode element by the specified element name from another VNode and context. + * The function returns the found VNode or undefined. * * @param elName * @param vnode From df1ffc34c2cf3923e3913b6657d59f2e93a2407e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 13:09:32 +0300 Subject: [PATCH 0218/2313] refactor: renamed `finedElFromVNode` to `findElem` --- src/friends/vdom/README.md | 2 +- src/friends/vdom/class.ts | 2 +- src/friends/vdom/traverse.ts | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/friends/vdom/README.md b/src/friends/vdom/README.md index ec3493b1aa..7e7f538fc6 100644 --- a/src/friends/vdom/README.md +++ b/src/friends/vdom/README.md @@ -33,7 +33,7 @@ console.log(this.vdom.closest('b-wrapper')); console.log(this.vdom.closest('bWrapper')); ``` -### findElFromVNode +### findElem Searches a VNode element by the specified element name from another VNode and context. The function returns the found VNode or undefined. diff --git a/src/friends/vdom/class.ts b/src/friends/vdom/class.ts index 5856b3f62e..8c1a7cc8bb 100644 --- a/src/friends/vdom/class.ts +++ b/src/friends/vdom/class.ts @@ -15,7 +15,7 @@ import type * as render from 'friends/vdom/render'; interface VDOM { closest: typeof traverse.closest; - findElFromVNode: typeof traverse.findElFromVNode; + findElFromVNode: typeof traverse.findElem; create: typeof vnode.create; render: typeof render.render; getRenderFactory: typeof render.getRenderFactory; diff --git a/src/friends/vdom/traverse.ts b/src/friends/vdom/traverse.ts index 06d27bf21a..7838c22e48 100644 --- a/src/friends/vdom/traverse.ts +++ b/src/friends/vdom/traverse.ts @@ -50,8 +50,8 @@ export function closest( * Searches a VNode element by the specified element name from another VNode and context. * The function returns the found VNode or undefined. * - * @param elName - * @param vnode + * @param name - the element name to search + * @param where - the vnode where to search * @param [ctx] - a component context to resolve the passed element name * * @example @@ -68,14 +68,14 @@ export function closest( * console.log(this.vdom.findElFromVNode('elem', vnode)); * ``` */ -export function findElFromVNode( +export function findElem( this: VDOM, - elName: string, - vnode: VNode, + name: string, + where: VNode, ctx: iBlock = this.component ): CanUndef { - const selector = ctx.provide.fullElName(elName); - return search(vnode); + const selector = ctx.provide.fullElName(name); + return search(where); function search(vnode: VNode) { const From ebd7446dea260a7cc235b57db7b62bc3fa18ec83 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 13:24:49 +0300 Subject: [PATCH 0219/2313] feat: added a new decorator to define fake methods --- src/friends/async-render/class.ts | 8 +- src/friends/friend/class.ts | 124 +++++++++++++++++++++++++++++ src/friends/friend/decorators.ts | 31 ++++++++ src/friends/friend/index.ts | 118 +-------------------------- src/friends/module-loader/class.ts | 8 +- src/friends/vdom/class.ts | 15 +++- 6 files changed, 183 insertions(+), 121 deletions(-) create mode 100644 src/friends/friend/class.ts create mode 100644 src/friends/friend/decorators.ts diff --git a/src/friends/async-render/class.ts b/src/friends/async-render/class.ts index 18f1aa09e5..0033f9c6ad 100644 --- a/src/friends/async-render/class.ts +++ b/src/friends/async-render/class.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import Friend from 'friends/friend'; +import Friend, { fakeMethods } from 'friends/friend'; import type iBlock from 'super/i-block/i-block'; import type * as iter from 'friends/async-render/iter'; @@ -18,6 +18,12 @@ interface AsyncRender { iterate: typeof iter.iterate; } +@fakeMethods( + 'forceRender', + 'deferForceRender', + 'iterate' +) + class AsyncRender extends Friend { constructor(component: iBlock) { super(component); diff --git a/src/friends/friend/class.ts b/src/friends/friend/class.ts new file mode 100644 index 0000000000..a3b06997fa --- /dev/null +++ b/src/friends/friend/class.ts @@ -0,0 +1,124 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type iBlock from 'super/i-block/i-block'; + +/** + * Superclass to create classes friendly to the main component class + */ +export default class Friend { + /** + * Type: the main component instance + */ + readonly C!: iBlock; + + /** + * Type: the main component context + */ + readonly CTX!: this['C']['unsafe']; + + /** + * The main component instance + */ + readonly component: this['C']; + + /** + * The main component context + */ + protected readonly ctx: this['CTX']; + + /** @see [[iBlock.componentId]] */ + get componentId(): string { + return this.ctx.componentId; + } + + /** @see [[iBlock.componentName]] */ + get componentName(): string { + return this.ctx.componentName; + } + + /** @see [[iBlock.globalName]] */ + get globalName(): CanUndef { + return this.ctx.globalName; + } + + /** @see [[iBlock.componentStatus]] */ + get componentStatus(): this['CTX']['componentStatus'] { + return this.ctx.componentStatus; + } + + /** @see [[iBlock.hook]] */ + get hook(): this['CTX']['hook'] { + return this.ctx.hook; + } + + /** @see [[iBlock.$el]] */ + get node(): this['CTX']['$el'] { + return this.ctx.$el; + } + + /** @see [[iBlock.field]] */ + get field(): this['CTX']['field'] { + return this.ctx.field; + } + + /** @see [[iBlock.provide]] */ + get provide(): this['CTX']['provide'] { + return this.ctx.provide; + } + + /** @see [[iBlock.lfc]] */ + get lfc(): this['CTX']['lfc'] { + return this.ctx.lfc; + } + + /** @see [[iBlock.meta]] */ + protected get meta(): this['CTX']['meta'] { + return this.ctx.meta; + } + + /** @see [[iBlock.$activeField]] */ + protected get activeField(): CanUndef { + return this.ctx.$activeField; + } + + /** @see [[iBlock.localEmitter]] */ + protected get localEmitter(): this['CTX']['localEmitter'] { + return this.ctx.localEmitter; + } + + /** @see [[iBlock.async]] */ + protected get async(): this['CTX']['async'] { + return this.ctx.async; + } + + /** @see [[iBlock.storage]] */ + protected get storage(): this['CTX']['storage'] { + return this.ctx.storage; + } + + /** @see [[iBlock.block]] */ + protected get block(): this['CTX']['block'] { + return this.ctx.block; + } + + /** @see [[iBlock.$refs]] */ + protected get refs(): this['CTX']['$refs'] { + return this.ctx.$refs; + } + + /** @see [[iBlock.dom]] */ + protected get dom(): this['CTX']['dom'] { + return this.ctx.dom; + } + + constructor(component: iBlock) { + this.ctx = component.unsafe; + this.component = component; + } +} diff --git a/src/friends/friend/decorators.ts b/src/friends/friend/decorators.ts new file mode 100644 index 0000000000..84d4324310 --- /dev/null +++ b/src/friends/friend/decorators.ts @@ -0,0 +1,31 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * Adds the specified fake methods to a class + * + * @decorator + * @param methods - methods to fake + */ +export function fakeMethods(...methods: string[]): ClassDecorator { + return (target) => { + const + {prototype} = target; + + for (let i = 0; i < methods.length; i++) { + const + method = methods[i]; + + if (!Object.isFunction(prototype[method])) { + prototype[method] = () => Object.throw( + `This is a loopback method. To use the read \`${method}\` method, register it to the ${target.name} class.` + ); + } + } + }; +} diff --git a/src/friends/friend/index.ts b/src/friends/friend/index.ts index 31cbf06d73..25f0068d16 100644 --- a/src/friends/friend/index.ts +++ b/src/friends/friend/index.ts @@ -11,119 +11,5 @@ * @packageDocumentation */ -import type iBlock from 'super/i-block/i-block'; - -/** - * Superclass to create classes friendly to the main component class - */ -export default class Friend { - /** - * Type: the main component instance - */ - readonly C!: iBlock; - - /** - * Type: the main component context - */ - readonly CTX!: this['C']['unsafe']; - - /** - * The main component instance - */ - readonly component: this['C']; - - /** - * The main component context - */ - protected readonly ctx: this['CTX']; - - /** @see [[iBlock.componentId]] */ - get componentId(): string { - return this.ctx.componentId; - } - - /** @see [[iBlock.componentName]] */ - get componentName(): string { - return this.ctx.componentName; - } - - /** @see [[iBlock.globalName]] */ - get globalName(): CanUndef { - return this.ctx.globalName; - } - - /** @see [[iBlock.componentStatus]] */ - get componentStatus(): this['CTX']['componentStatus'] { - return this.ctx.componentStatus; - } - - /** @see [[iBlock.hook]] */ - get hook(): this['CTX']['hook'] { - return this.ctx.hook; - } - - /** @see [[iBlock.$el]] */ - get node(): this['CTX']['$el'] { - return this.ctx.$el; - } - - /** @see [[iBlock.field]] */ - get field(): this['CTX']['field'] { - return this.ctx.field; - } - - /** @see [[iBlock.provide]] */ - get provide(): this['CTX']['provide'] { - return this.ctx.provide; - } - - /** @see [[iBlock.lfc]] */ - get lfc(): this['CTX']['lfc'] { - return this.ctx.lfc; - } - - /** @see [[iBlock.meta]] */ - protected get meta(): this['CTX']['meta'] { - return this.ctx.meta; - } - - /** @see [[iBlock.$activeField]] */ - protected get activeField(): CanUndef { - return this.ctx.$activeField; - } - - /** @see [[iBlock.localEmitter]] */ - protected get localEmitter(): this['CTX']['localEmitter'] { - return this.ctx.localEmitter; - } - - /** @see [[iBlock.async]] */ - protected get async(): this['CTX']['async'] { - return this.ctx.async; - } - - /** @see [[iBlock.storage]] */ - protected get storage(): this['CTX']['storage'] { - return this.ctx.storage; - } - - /** @see [[iBlock.block]] */ - protected get block(): this['CTX']['block'] { - return this.ctx.block; - } - - /** @see [[iBlock.$refs]] */ - protected get refs(): this['CTX']['$refs'] { - return this.ctx.$refs; - } - - /** @see [[iBlock.dom]] */ - protected get dom(): this['CTX']['dom'] { - return this.ctx.dom; - } - - constructor(component: iBlock) { - this.ctx = component.unsafe; - this.component = component; - } -} +export { default } from 'friends/friend/class'; +export * from 'friends/friend/decorators'; diff --git a/src/friends/module-loader/class.ts b/src/friends/module-loader/class.ts index 80d5e1c29b..549865a7ce 100644 --- a/src/friends/module-loader/class.ts +++ b/src/friends/module-loader/class.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import Friend from 'friends/friend'; +import Friend, { fakeMethods } from 'friends/friend'; import type * as api from 'friends/module-loader/api'; import type { Module } from 'friends/module-loader/interface'; @@ -17,6 +17,12 @@ interface ModuleLoader { addToBucket: typeof api.addToBucket; } +@fakeMethods( + 'load', + 'loadBucket', + 'addToBucket' +) + class ModuleLoader extends Friend { /** * A dictionary with registered buckets to load diff --git a/src/friends/vdom/class.ts b/src/friends/vdom/class.ts index 8c1a7cc8bb..dea194de32 100644 --- a/src/friends/vdom/class.ts +++ b/src/friends/vdom/class.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import Friend from 'friends/friend'; +import Friend, { fakeMethods } from 'friends/friend'; import type iBlock from 'super/i-block/i-block'; import type * as traverse from 'friends/vdom/traverse'; @@ -15,13 +15,22 @@ import type * as render from 'friends/vdom/render'; interface VDOM { closest: typeof traverse.closest; - findElFromVNode: typeof traverse.findElem; + findElem: typeof traverse.findElem; create: typeof vnode.create; render: typeof render.render; getRenderFactory: typeof render.getRenderFactory; getRenderFn: typeof render.getRenderFn; } +@fakeMethods( + 'closest', + 'findElem', + 'create', + 'render', + 'getRenderFactory', + 'getRenderFn' +) + class VDOM extends Friend { /** * Sets the current component instance as active. @@ -34,7 +43,7 @@ class VDOM extends Friend { this.meta.hooks.mounted.push({ fn: () => { - this.setInstance = this.ctx.$renderEngine.r.withAsyncContext(Promise.resolve.bind(Promise))[1]; + this.setInstance = this.ctx.$renderEngine.r.withAsyncContext.call(this.ctx, Promise.resolve.bind(Promise))[1]; } }); } From 1d2f8e87eabc041a037090be0fa4c7c19e87eeea Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 13:26:08 +0300 Subject: [PATCH 0220/2313] chore: fixed a typo --- src/friends/friend/decorators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/friends/friend/decorators.ts b/src/friends/friend/decorators.ts index 84d4324310..ed6b1f12dc 100644 --- a/src/friends/friend/decorators.ts +++ b/src/friends/friend/decorators.ts @@ -23,7 +23,7 @@ export function fakeMethods(...methods: string[]): ClassDecorator { if (!Object.isFunction(prototype[method])) { prototype[method] = () => Object.throw( - `This is a loopback method. To use the read \`${method}\` method, register it to the ${target.name} class.` + `This is a loopback method. To use the real \`${method}\` method, register it to the ${target.name} class.` ); } } From 7a85a97ccb18a996cc08992e7d64da99d8bc2a5e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 13:26:34 +0300 Subject: [PATCH 0221/2313] :art: --- src/friends/friend/decorators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/friends/friend/decorators.ts b/src/friends/friend/decorators.ts index ed6b1f12dc..1c588e3863 100644 --- a/src/friends/friend/decorators.ts +++ b/src/friends/friend/decorators.ts @@ -23,7 +23,7 @@ export function fakeMethods(...methods: string[]): ClassDecorator { if (!Object.isFunction(prototype[method])) { prototype[method] = () => Object.throw( - `This is a loopback method. To use the real \`${method}\` method, register it to the ${target.name} class.` + `This is a loopback method. To use the real \`${method}\` method, register it to the \`${target.name}\` class.` ); } } From 898a48100cdfcd07c6f6c702896c073302ab3796 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 13:58:58 +0300 Subject: [PATCH 0222/2313] fix: some fixes --- src/friends/async-render/iter.ts | 3 ++- src/friends/friend/decorators.ts | 8 +++++--- src/super/i-block/i-block.ss | 1 - src/super/i-block/i-block.ts | 11 ++++++----- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/friends/async-render/iter.ts b/src/friends/async-render/iter.ts index da9d9860f5..c6faaa5de1 100644 --- a/src/friends/async-render/iter.ts +++ b/src/friends/async-render/iter.ts @@ -252,6 +252,8 @@ export function iterate( return lastTask(); function task() { + ctx.vdom.setInstance?.(); + const renderedVNodes: Node[] = []; @@ -291,7 +293,6 @@ export function iterate( } else { VDOM.addToPrototype(render); - ctx.vdom.setInstance?.(); renderedVnode = ctx.vdom.render(vnode); } diff --git a/src/friends/friend/decorators.ts b/src/friends/friend/decorators.ts index 1c588e3863..faf490ffae 100644 --- a/src/friends/friend/decorators.ts +++ b/src/friends/friend/decorators.ts @@ -22,9 +22,11 @@ export function fakeMethods(...methods: string[]): ClassDecorator { method = methods[i]; if (!Object.isFunction(prototype[method])) { - prototype[method] = () => Object.throw( - `This is a loopback method. To use the real \`${method}\` method, register it to the \`${target.name}\` class.` - ); + prototype[method] = function fake() { + Object.throw( + `This is a loopback method. To use the real \`${method}\` method, register it to the \`${target.name}\` class.` + ); + }; } } }; diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 56b3b9b606..b5b6be702f 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -196,7 +196,6 @@ - block root < ?.${self.name()} < _ v-attrs = rootAttrs | ${rootAttrs|!html} - /** * Generates an icon layout * diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index 6846fbba01..541f81da04 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -81,7 +81,7 @@ import Daemons, { DaemonsDict } from 'super/i-block/modules/daemons'; import Analytics from 'super/i-block/modules/analytics'; import DOM from 'super/i-block/modules/dom'; -import VDOM from 'super/i-block/modules/vdom'; +import VDOM from 'friends/vdom'; import Lfc from 'super/i-block/modules/lfc'; import AsyncRender from 'friends/async-render'; @@ -1888,16 +1888,17 @@ export default abstract class iBlock extends ComponentInterface { this.componentStatus = 'loading'; } - const tasks = >>Array.concat( + const tasks = >>Array.concat( [], - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - this.moduleLoader.load?.(...this.dependencies) || [], - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions this.state.initFromStorage() || [] ); + if (this.dependencies.length > 0) { + tasks.push(this.moduleLoader.load(...this.dependencies)); + } + if ( (this.isNotRegular || this.dontWaitRemoteProviders) && !this.$renderEngine.supports.ssr From 8cdc7bd23b590a76b96dd1dca1f54e4ad2b63556 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 15:38:03 +0300 Subject: [PATCH 0223/2313] fix: re-export interfaces --- src/friends/module-loader/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/friends/module-loader/index.ts b/src/friends/module-loader/index.ts index 0ddbe29edb..d59ad825ba 100644 --- a/src/friends/module-loader/index.ts +++ b/src/friends/module-loader/index.ts @@ -14,3 +14,4 @@ export { default } from 'friends/module-loader/class'; export * from 'friends/module-loader/api'; +export * from 'friends/module-loader/interface'; From 83e4f46b4a82362974658ef9dac57fed88be6e9c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 10 Jun 2022 15:38:29 +0300 Subject: [PATCH 0224/2313] refactor: moved the module to `friends/block` & new tree-shake friendly API --- .../modules => friends}/block/CHANGELOG.md | 7 + .../modules => friends}/block/README.md | 2 +- src/friends/block/class.ts | 287 +++++++++ .../modules => friends}/block/const.ts | 0 src/friends/block/element.ts | 172 +++++ src/friends/block/index.ts | 20 + .../modules => friends}/block/interface.ts | 12 +- .../modules => friends}/block/test/index.js | 0 src/friends/block/traverse.ts | 210 ++++++ src/super/i-block/modules/block/index.ts | 609 ------------------ 10 files changed, 703 insertions(+), 616 deletions(-) rename src/{super/i-block/modules => friends}/block/CHANGELOG.md (81%) rename src/{super/i-block/modules => friends}/block/README.md (87%) create mode 100644 src/friends/block/class.ts rename src/{super/i-block/modules => friends}/block/const.ts (100%) create mode 100644 src/friends/block/element.ts create mode 100644 src/friends/block/index.ts rename src/{super/i-block/modules => friends}/block/interface.ts (89%) rename src/{super/i-block/modules => friends}/block/test/index.js (100%) create mode 100644 src/friends/block/traverse.ts delete mode 100644 src/super/i-block/modules/block/index.ts diff --git a/src/super/i-block/modules/block/CHANGELOG.md b/src/friends/block/CHANGELOG.md similarity index 81% rename from src/super/i-block/modules/block/CHANGELOG.md rename to src/friends/block/CHANGELOG.md index dda0112020..ddac87ea7c 100644 --- a/src/super/i-block/modules/block/CHANGELOG.md +++ b/src/friends/block/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Moved the module to `friends/block` +* The module has been rewritten to a new tree-shake friendly API + ## v3.8.3 (2021-10-26) #### :bug: Bug Fix diff --git a/src/super/i-block/modules/block/README.md b/src/friends/block/README.md similarity index 87% rename from src/super/i-block/modules/block/README.md rename to src/friends/block/README.md index 34b63494bb..74988e5c0d 100644 --- a/src/super/i-block/modules/block/README.md +++ b/src/friends/block/README.md @@ -1,4 +1,4 @@ -# super/i-block/modules/block +# friends/block This module provides a class with some helper methods to work with a component DOM element by using BEM pattern. diff --git a/src/friends/block/class.ts b/src/friends/block/class.ts new file mode 100644 index 0000000000..cc5aa7b257 --- /dev/null +++ b/src/friends/block/class.ts @@ -0,0 +1,287 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import Friend, { fakeMethods } from 'friends/friend'; + +import type iBlock from 'super/i-block/i-block'; +import type { ModsNTable } from 'super/i-block/modules/mods'; + +import { modRgxpCache } from 'friends/block/const'; +import type { ModEvent, ModEventReason, SetModEvent } from 'friends/block/interface'; + +import type * as element from 'friends/block/element'; +import type * as traverse from 'friends/block/traverse'; + +interface Block { + getFullElName: typeof element.getFullElName; + getElMod: typeof element.getElMod; + setElMod: typeof element.setElMod; + removeElMod: typeof element.removeElMod; + + getBlockSelector: typeof traverse.getBlockSelector; + getElSelector: typeof traverse.getElSelector; + + element: typeof traverse.element; + elements: typeof traverse.elements; +} + +@fakeMethods( + 'getFullElName', + 'getElMod', + 'setElMod', + 'removeElMod', + + 'getBlockSelector', + 'getElSelector', + + 'element', + 'elements' +) + +class Block extends Friend { + /** + * A dictionary with applied modifiers + */ + protected readonly mods?: Dictionary>; + + constructor(component: iBlock) { + super(component); + this.mods = Object.createDict(); + + for (let m = component.mods, keys = Object.keys(m), i = 0; i < keys.length; i++) { + const name = keys[i]; + this.setMod(name, m[name], 'initSetMod'); + } + } + + /** + * Returns the full block name of the tied component + * + * @param [modName] - an additional modifier name + * @param [modValue] - an additional value name + * + * @example + * ```js + * // b-foo + * console.log(this.getFullBlockName()); + * + * // b-foo_focused_true + * console.log(this.getFullBlockName('focused', true)); + * ``` + */ + getFullBlockName(modName?: string, modValue?: unknown): string { + return this.componentName + (modName != null ? `_${modName.dasherize()}_${String(modValue).dasherize()}` : ''); + } + + /** + * Returns a value of the specified block modifier + * + * @param name - the modifier name + * @param [fromNode] - if true, then the modifier value will be taken from the tied DOM node instead of cache + * + * @example + * ```js + * console.log(this.getMod('focused')); + * console.log(this.getMod('focused', true)); + * ``` + */ + getMod(name: string, fromNode?: boolean): CanUndef { + const + {mods, node} = this; + + if (mods != null && !fromNode) { + return mods[name.camelize(false)]; + } + + if (node == null) { + return undefined; + } + + const + MOD_VALUE = 2; + + const + pattern = `(?:^| )(${this.getFullBlockName(name, '')}[^_ ]*)`, + modifierRgxp = modRgxpCache[pattern] ?? new RegExp(pattern), + matchedEl = modifierRgxp.exec(node.className); + + modRgxpCache[pattern] = modifierRgxp; + return matchedEl ? matchedEl[1].split('_')[MOD_VALUE] : undefined; + } + + /** + * Sets a block modifier to the current component. + * The method returns false if the modifier is already set. + * + * @param name - the modifier name to set + * @param value - the modifier value to set + * @param [reason] - a reason to set the modifier + * + * @example + * ```js + * this.setMod('focused', true); + * this.setMod('focused', true, 'removeMod'); + * ``` + */ + setMod(name: string, value: unknown, reason: ModEventReason = 'setMod'): boolean { + if (value == null) { + return false; + } + + name = name.camelize(false); + + const { + ctx, + mods, + node + } = this; + + const + normalizedValue = String(value).dasherize(), + oldValue = this.getMod(name); + + if (oldValue === normalizedValue) { + return false; + } + + const + isInit = reason === 'initSetMod'; + + let + prevValFromDOM, + needSync = false; + + if (isInit) { + prevValFromDOM = this.getMod(name, true); + needSync = prevValFromDOM !== normalizedValue; + } + + if (needSync) { + this.removeMod(name, prevValFromDOM, 'initSetMod'); + + } else if (!isInit) { + this.removeMod(name, undefined, 'setMod'); + } + + if (node != null && (!isInit || needSync)) { + node.classList.add(this.getFullBlockName(name, normalizedValue)); + } + + if (mods != null) { + mods[name] = normalizedValue; + } + + ctx.mods[name] = normalizedValue; + + if (!ctx.isFunctional) { + const + watchModsStore = ctx.field.get('watchModsStore'); + + if (watchModsStore != null && name in watchModsStore && watchModsStore[name] !== normalizedValue) { + delete Object.getPrototypeOf(watchModsStore)[name]; + ctx.field.set(`watchModsStore.${name}`, normalizedValue); + } + } + + if (!isInit) { + const event: SetModEvent = { + type: 'set', + event: 'block.mod.set', + reason, + name, + value: normalizedValue, + oldValue + }; + + this.localEmitter.emit(`block.mod.set.${name}.${normalizedValue}`, event); + ctx.emit(`mod:set:${name}:${normalizedValue}`, event); + } + + return true; + } + + /** + * Removes a block modifier from the current component. + * The method returns false if the block does not have this modifier. + * + * @param name - the modifier name to remove + * @param [value] - the modifier value to remove + * @param [reason] - a reason to remove the modifier + * + * @example + * ```js + * this.removeMod('focused'); + * this.removeMod('focused', true); + * this.removeMod('focused', true, 'setMod'); + * ``` + */ + removeMod(name: string, value?: unknown, reason: ModEventReason = 'removeMod'): boolean { + name = name.camelize(false); + value = value != null ? String(value).dasherize() : undefined; + + const { + ctx, + mods, + node + } = this; + + const + isInit = reason === 'initSetMod', + currentValue = this.getMod(name, isInit); + + if (currentValue === undefined || value !== undefined && currentValue !== value) { + return false; + } + + if (node != null) { + node.classList.remove(this.getFullBlockName(name, currentValue)); + } + + if (mods != null) { + mods[name] = undefined; + } + + const + needNotify = reason === 'removeMod'; + + if (needNotify) { + ctx.mods[name] = undefined; + + if (!ctx.isFunctional) { + const + watchModsStore = ctx.field.get('watchModsStore'); + + if (watchModsStore != null && name in watchModsStore && watchModsStore[name] != null) { + delete Object.getPrototypeOf(watchModsStore)[name]; + ctx.field.set(`watchModsStore.${name}`, undefined); + } + } + } + + if (!isInit) { + const event: ModEvent = { + type: 'remove', + event: 'block.mod.remove', + reason, + name, + value: currentValue + }; + + this.localEmitter + .emit(`block.mod.remove.${name}.${currentValue}`, event); + + if (needNotify) { + ctx.emit(`mod:remove:${name}:${currentValue}`, event); + } + } + + return true; + } +} + +export default Block; diff --git a/src/super/i-block/modules/block/const.ts b/src/friends/block/const.ts similarity index 100% rename from src/super/i-block/modules/block/const.ts rename to src/friends/block/const.ts diff --git a/src/friends/block/element.ts b/src/friends/block/element.ts new file mode 100644 index 0000000000..c02802ee75 --- /dev/null +++ b/src/friends/block/element.ts @@ -0,0 +1,172 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type Block from 'friends/block/class'; + +import { modRgxpCache, elRxp } from 'friends/block/const'; +import type { ModEventReason, ElementModEvent, SetElementModEvent } from 'friends/block/interface'; + +/** + * Returns the full name of the specified block element + * + * @param name - the element name + * @param [modName] - an additional modifier name + * @param [modValue] - an additional value name + * + * @example + * ```js + * // b-foo__bla + * console.log(this.getFullElName('bla')); + * + * // b-foo__bla_focused_true + * console.log(this.getBlockSelector('bla', 'focused', true)); + * ``` + */ +export function getFullElName(this: Block, name: string, modName?: string, modValue?: unknown): string { + const modStr = modName != null ? `_${modName.dasherize()}_${String(modValue).dasherize()}` : ''; + return `${this.componentName}__${name.dasherize()}${modStr}`; +} + +/** + * Returns a modifier value from the specified element + * + * @param link - a link to the element + * @param elName - the element name + * @param modName - the modifier name + */ +export function getElMod(this: Block, link: Nullable, elName: string, modName: string): CanUndef { + if (link == null) { + return undefined; + } + + const + MOD_VALUE = 3; + + const + pattern = `(?:^| )(${getFullElName.call(this, elName, modName, '')}[^_ ]*)`, + modRgxp = pattern[pattern] ?? new RegExp(pattern), + el = modRgxp.exec(link.className); + + modRgxpCache[pattern] = modRgxp; + return el != null ? el[1].split(elRxp)[MOD_VALUE] : undefined; +} + +/** + * Sets a modifier to the specified block element. + * The method returns false if the modifier is already set. + * + * @param link - a link to the element + * @param elName - the element name + * @param modName - the modifier name to set + * @param value - the modifier name to set + * @param [reason] - a reason to set the modifier + * + * @example + * ```js + * this.setElMod(node, 'foo', 'focused', true); + * this.setElMod(node, 'foo', 'focused', true, 'initSetMod'); + * ``` + */ +export function setElMod( + this: Block, + link: Nullable, + elName: string, + modName: string, + value: unknown, + reason: ModEventReason = 'setMod' +): boolean { + if (link == null || value == null) { + return false; + } + + elName = elName.camelize(false); + modName = modName.camelize(false); + + const + normalizedValue = String(value).dasherize(), + oldValue = getElMod.call(this, link, elName, modName); + + if (oldValue === normalizedValue) { + return false; + } + + removeElMod.call(this, link, elName, modName, undefined, 'setMod'); + link.classList.add(getFullElName.call(this, elName, modName, normalizedValue)); + + const event: SetElementModEvent = { + type: 'set', + event: 'el.mod.set', + reason, + link, + element: elName, + name: modName, + value: normalizedValue, + oldValue + }; + + this.localEmitter.emit(`el.mod.set.${elName}.${modName}.${normalizedValue}`, event); + return true; +} + +/** + * Removes a modifier from the specified block element. + * The method returns false if the element does not have this modifier. + * + * @param link - a link to the element + * @param elName - the element name + * @param modName - the modifier name to remove + * @param [value] - the modifier value to remove + * @param [reason] - a reason to remove the modifier + * + * @example + * ```js + * this.removeElMod(node, 'foo', 'focused'); + * this.removeElMod(node, 'foo', 'focused', true); + * this.removeElMod(node, 'foo', 'focused', true, 'setMod'); + * ``` + */ +export function removeElMod( + this: Block, + link: Nullable, + elName: string, + modName: string, + value?: unknown, + reason: ModEventReason = 'removeMod' +): boolean { + if (link == null) { + return false; + } + + elName = elName.camelize(false); + modName = modName.camelize(false); + + const + normalizedVal = value != null ? String(value).dasherize() : undefined, + currentVal = getElMod.call(this, link, elName, modName); + + if (currentVal === undefined || normalizedVal !== undefined && currentVal !== normalizedVal) { + return false; + } + + link.classList.remove( + getFullElName.call(this, elName, modName, currentVal) + ); + + const event: ElementModEvent = { + type: 'remove', + event: 'el.mod.remove', + reason, + link, + element: elName, + name: modName, + value: currentVal + }; + + this.localEmitter.emit(`el.mod.remove.${elName}.${modName}.${currentVal}`, event); + return true; +} diff --git a/src/friends/block/index.ts b/src/friends/block/index.ts new file mode 100644 index 0000000000..c2e6ec0c93 --- /dev/null +++ b/src/friends/block/index.ts @@ -0,0 +1,20 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * [[include:friends/block/README.md]] + * @packageDocumentation + */ + +export { default } from 'friends/block/class'; + +export * from 'friends/block/element'; +export * from 'friends/block/traverse'; + +export * from 'friends/block/interface'; + diff --git a/src/super/i-block/modules/block/interface.ts b/src/friends/block/interface.ts similarity index 89% rename from src/super/i-block/modules/block/interface.ts rename to src/friends/block/interface.ts index 9b619534de..f42fadff61 100644 --- a/src/super/i-block/modules/block/interface.ts +++ b/src/friends/block/interface.ts @@ -22,27 +22,27 @@ export type ModEventReason = 'removeMod'; export interface ModEvent { - event: ModEventName; type: ModEventType; + event: ModEventName; reason: ModEventReason; name: string; value: string; } export interface SetModEvent extends ModEvent { - prev: CanUndef; + oldValue: CanUndef; } export interface ElementModEvent { - event: ModEventName; type: ModEventType; + event: ModEventName; reason: ModEventReason; + link: Element; element: string; - link: HTMLElement; - modName: string; + name: string; value: string; } export interface SetElementModEvent extends ElementModEvent { - prev: CanUndef; + oldValue: CanUndef; } diff --git a/src/super/i-block/modules/block/test/index.js b/src/friends/block/test/index.js similarity index 100% rename from src/super/i-block/modules/block/test/index.js rename to src/friends/block/test/index.js diff --git a/src/friends/block/traverse.ts b/src/friends/block/traverse.ts new file mode 100644 index 0000000000..f6df81bb81 --- /dev/null +++ b/src/friends/block/traverse.ts @@ -0,0 +1,210 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import type Block from 'friends/block/class'; + +import { fakeCtx } from 'friends/block/const'; +import { getFullElName } from 'friends/block/element'; +import type { ModsTable } from 'super/i-block/modules/mods'; + +/** + * Returns a CSS selector to the current component block + * + * @param [mods] - additional modifiers + * + * @example + * ```js + * // .b-foo + * console.log(this.getBlockSelector()); + * + * // .b-foo.b-foo_focused_true + * console.log(this.getBlockSelector({focused: true})); + * ``` + */ +export function getBlockSelector(this: Block, mods?: ModsTable): string { + let + res = `.${this.getFullBlockName()}`; + + if (mods != null) { + for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { + const key = keys[i]; + res += `.${this.getFullBlockName(key, mods[key])}`; + } + } + + return res; +} + +/** + * Returns a CSS selector to the specified block element + * + * @param name - the element name + * @param [mods] - additional modifiers + * + * @example + * ```js + * // .$componentId.b-foo__bla + * console.log(this.getElSelector('bla')); + * + * // .$componentId.b-foo__bla.b-foo__bla_focused_true + * console.log(this.getElSelector('bla', {focused: true})); + * ``` + */ +export function getElSelector(this: Block, name: string, mods?: ModsTable): string { + let + res = `.${this.componentId}.${getFullElName.call(this, name)}`; + + if (mods != null) { + for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { + const key = keys[i]; + res += `.${getFullElName.call(this, name, key, mods[key])}`; + } + } + + return res; +} + +/** + * Returns block child elements by the specified selector. + * This overload is used to optimize DOM searching. + * + * @param ctx - a context node to search + * @param name - the element name to search + * @param [mods] - additional modifiers + * + * @example + * ```js + * console.log(this.elements(node, 'foo')); + * console.log(this.elements(node, 'foo', {focused: true})); + * ``` + */ +export function elements( + this: Block, + ctx: Element, + name: string, + mods?: ModsTable +): NodeListOf; + +/** + * Returns block child elements by the specified selector + * + * @param name - the element name to search + * @param [mods] - additional modifiers + * + * @example + * ```js + * console.log(this.elements('foo')); + * console.log(this.elements('foo', {focused: true})); + * ``` + */ +export function elements( + this: Block, + name: string, + mods?: ModsTable +): NodeListOf; + +export function elements( + this: Block, + ctxOrName: Element | string, + name?: string | ModsTable, + mods?: ModsTable +): NodeListOf { + let + ctx = this.node, + elName; + + if (Object.isString(ctxOrName)) { + elName = ctxOrName; + + if (Object.isPlainObject(name)) { + mods = name; + } + + } else { + elName = name; + ctx = ctxOrName; + } + + ctx ??= this.node; + + if (ctx == null) { + return fakeCtx.querySelectorAll('.loopback'); + } + + return ctx.querySelectorAll(getElSelector.call(this, elName, mods)); +} + +/** + * Returns a block child element by the specified selector. + * This overload is used to optimize DOM searching. + * + * @param ctx - a context node to search + * @param name - the element name to search + * @param [mods] - additional modifiers + * + * @example + * ```js + * console.log(this.element(node, 'foo')); + * console.log(this.element(node, 'foo', {focused: true})); + * ``` + */ +export function element( + this: Block, + ctx: Element, + name: string, + mods?: ModsTable +): CanUndef; + +/** + * Returns a block child element by the specified request + * + * @param name - the element name to search + * @param [mods] - additional modifiers + * + * @example + * ```js + * console.log(this.element('foo')); + * console.log(this.element('foo', {focused: true})); + * ``` + */ +export function element( + this: Block, + name: string, + mods?: ModsTable +): CanUndef; + +export function element( + this: Block, + ctxOrName: Element | string, + name?: string | ModsTable, + mods?: ModsTable +): CanUndef { + let + ctx = this.node, + elName; + + if (Object.isString(ctxOrName)) { + elName = ctxOrName; + + if (Object.isPlainObject(name)) { + mods = name; + } + + } else { + elName = name; + ctx = ctxOrName; + } + + ctx ??= this.node; + + if (ctx == null) { + return undefined; + } + + return ctx.querySelector(getElSelector.call(this, elName, mods)) ?? undefined; +} diff --git a/src/super/i-block/modules/block/index.ts b/src/super/i-block/modules/block/index.ts deleted file mode 100644 index e993fc481e..0000000000 --- a/src/super/i-block/modules/block/index.ts +++ /dev/null @@ -1,609 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:super/i-block/modules/block/README.md]] - * @packageDocumentation - */ - -import type iBlock from 'super/i-block/i-block'; -import { fakeCtx, modRgxpCache, elRxp } from 'super/i-block/modules/block/const'; - -import Friend from 'friends/friend'; -import type { ModsTable, ModsNTable } from 'super/i-block/modules/mods'; - -import type { - - ModEvent, - ModEventReason, - SetModEvent, - - ElementModEvent, - SetElementModEvent - -} from 'super/i-block/modules/block/interface'; - -export * from 'super/i-block/modules/block/interface'; - -/** - * Class implements BEM-like API - */ -export default class Block extends Friend { - /** - * Map of applied modifiers - */ - protected readonly mods?: Dictionary>; - - constructor(component: iBlock) { - super(component); - this.mods = Object.createDict(); - - for (let m = component.mods, keys = Object.keys(m), i = 0; i < keys.length; i++) { - const name = keys[i]; - this.setMod(name, m[name], 'initSetMod'); - } - } - - /** - * Returns a full name of the current block - * - * @param [modName] - * @param [modValue] - * - * @example - * ```js - * // b-foo - * this.getFullBlockName(); - * - * // b-foo_focused_true - * this.getFullBlockName('focused', true); - * ``` - */ - getFullBlockName(modName?: string, modValue?: unknown): string { - return this.componentName + (modName != null ? `_${modName.dasherize()}_${String(modValue).dasherize()}` : ''); - } - - /** - * Returns a CSS selector to the current block - * - * @param [mods] - additional modifiers - * - * @example - * ```js - * // .b-foo - * this.getBlockSelector(); - * - * // .b-foo.b-foo_focused_true - * this.getBlockSelector({focused: true}); - * ``` - */ - getBlockSelector(mods?: ModsTable): string { - let - res = `.${this.getFullBlockName()}`; - - if (mods) { - for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { - const key = keys[i]; - res += `.${this.getFullBlockName(key, mods[key])}`; - } - } - - return res; - } - - /** - * Returns a full name of the specified element - * - * @param name - element name - * @param [modName] - * @param [modValue] - * - * @example - * ```js - * // b-foo__bla - * this.getFullElName('bla'); - * - * // b-foo__bla_focused_true - * this.getBlockSelector('bla', 'focused', true); - * ``` - */ - getFullElName(name: string, modName?: string, modValue?: unknown): string { - const modStr = modName != null ? `_${modName.dasherize()}_${String(modValue).dasherize()}` : ''; - return `${this.componentName}__${name.dasherize()}${modStr}`; - } - - /** - * Returns a CSS selector to the specified element - * - * @param name - element name - * @param [mods] - additional modifiers - * - * @example - * ```js - * // .$componentId.b-foo__bla - * this.getElSelector('bla'); - * - * // .$componentId.b-foo__bla.b-foo__bla_focused_true - * this.getElSelector('bla', {focused: true}); - * ``` - */ - getElSelector(name: string, mods?: ModsTable): string { - let - res = `.${this.componentId}.${this.getFullElName(name)}`; - - if (mods) { - for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { - const key = keys[i]; - res += `.${this.getFullElName(name, key, mods[key])}`; - } - } - - return res; - } - - /** - * Returns block child elements by the specified request. - * This overload is used to optimize DOM searching. - * - * @param ctx - context node - * @param name - element name - * @param [mods] - additional modifiers - * - * @example - * ```js - * this.elements(node, 'foo'); - * this.elements(node, 'foo', {focused: true}); - * ``` - */ - elements(ctx: Element, name: string, mods?: ModsTable): NodeListOf; - - /** - * Returns block child elements by the specified request - * - * @param name - element name - * @param [mods] - additional modifiers - * - * @example - * ```js - * this.elements('foo'); - * this.elements('foo', {focused: true}); - * ``` - */ - elements(name: string, mods?: ModsTable): NodeListOf; - elements( - ctxOrName: Element | string, - name?: string | ModsTable, - mods?: ModsTable - ): NodeListOf { - let - ctx = this.node, - elName; - - if (Object.isString(ctxOrName)) { - elName = ctxOrName; - - if (Object.isPlainObject(name)) { - mods = name; - } - - } else { - elName = name; - ctx = ctxOrName; - } - - ctx ??= this.node; - - if (ctx == null) { - return fakeCtx.querySelectorAll('.loopback'); - } - - return ctx.querySelectorAll(this.getElSelector(elName, mods)); - } - - /** - * Returns a block child element by the specified request. - * This overload is used to optimize DOM searching. - * - * @param ctx - context node - * @param name - element name - * @param [mods] - map of additional modifiers - * - * @example - * ```js - * this.element(node, 'foo'); - * this.element(node, 'foo', {focused: true}); - * ``` - */ - element(ctx: Element, name: string, mods?: ModsTable): CanUndef; - - /** - * Returns a block child element by the specified request - * - * @param name - element name - * @param [mods] - additional modifiers - * - * @example - * ```js - * this.element('foo'); - * this.element('foo', {focused: true}); - * ``` - */ - element(name: string, mods?: ModsTable): CanUndef; - element( - ctxOrName: Element | string, - name?: string | ModsTable, - mods?: ModsTable - ): CanUndef { - let - ctx = this.node, - elName; - - if (Object.isString(ctxOrName)) { - elName = ctxOrName; - - if (Object.isPlainObject(name)) { - mods = name; - } - - } else { - elName = name; - ctx = ctxOrName; - } - - ctx ??= this.node; - - if (ctx == null) { - return undefined; - } - - return ctx.querySelector(this.getElSelector(elName, mods)) ?? undefined; - } - - /** - * Sets a modifier to the current block. - * The method returns false if the modifier is already set. - * - * @param name - modifier name - * @param value - * @param [reason] - reason to set a modifier - * - * @example - * ```js - * this.setMod('focused', true); - * this.setMod('focused', true, 'removeMod'); - * ``` - */ - setMod(name: string, value: unknown, reason: ModEventReason = 'setMod'): boolean { - if (value == null) { - return false; - } - - name = name.camelize(false); - - const - {mods, node, ctx} = this; - - const - normalizedVal = String(value).dasherize(), - prevVal = this.getMod(name); - - if (prevVal === normalizedVal) { - return false; - } - - const - isInit = reason === 'initSetMod'; - - let - prevValFromDOM, - needSync = false; - - if (isInit) { - prevValFromDOM = this.getMod(name, true); - needSync = prevValFromDOM !== normalizedVal; - } - - if (needSync) { - this.removeMod(name, prevValFromDOM, 'initSetMod'); - - } else if (!isInit) { - this.removeMod(name, undefined, 'setMod'); - } - - if (node != null && (!isInit || needSync)) { - node.classList.add(this.getFullBlockName(name, normalizedVal)); - } - - if (mods != null) { - mods[name] = normalizedVal; - } - - ctx.mods[name] = normalizedVal; - - if (ctx.isNotRegular === false) { - const - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - watchModsStore = ctx.field?.get('watchModsStore'); - - if (watchModsStore != null && name in watchModsStore && watchModsStore[name] !== normalizedVal) { - delete Object.getPrototypeOf(watchModsStore)[name]; - ctx.field.set(`watchModsStore.${name}`, normalizedVal); - } - } - - if (!isInit || !ctx.isFlyweight) { - const event = { - event: 'block.mod.set', - type: 'set', - name, - value: normalizedVal, - prev: prevVal, - reason - }; - - this.localEmitter - .emit(`block.mod.set.${name}.${normalizedVal}`, event); - - if (!isInit) { - // @deprecated - ctx.emit(`mod-set-${name}-${normalizedVal}`, event); - ctx.emit(`mod:set:${name}:${normalizedVal}`, event); - } - } - - return true; - } - - /** - * Removes a modifier from the current block. - * The method returns false if the block does not have this modifier. - * - * @param name - modifier name - * @param [value] - * @param [reason] - reason to remove a modifier - * - * @example - * ```js - * this.removeMod('focused'); - * this.removeMod('focused', true); - * this.removeMod('focused', true, 'setMod'); - * ``` - */ - removeMod(name: string, value?: unknown, reason: ModEventReason = 'removeMod'): boolean { - name = name.camelize(false); - value = value != null ? String(value).dasherize() : undefined; - - const - {mods, node, ctx} = this; - - const - isInit = reason === 'initSetMod', - currentVal = this.getMod(name, isInit); - - if (currentVal === undefined || value !== undefined && currentVal !== value) { - return false; - } - - if (node != null) { - node.classList.remove(this.getFullBlockName(name, currentVal)); - } - - if (mods != null) { - mods[name] = undefined; - } - - const - needNotify = reason === 'removeMod'; - - if (needNotify) { - ctx.mods[name] = undefined; - - if (ctx.isNotRegular === false) { - const - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - watchModsStore = ctx.field?.get('watchModsStore'); - - if (watchModsStore != null && name in watchModsStore && watchModsStore[name] != null) { - delete Object.getPrototypeOf(watchModsStore)[name]; - ctx.field.set(`watchModsStore.${name}`, undefined); - } - } - } - - if (!isInit || !ctx.isFlyweight) { - const event = { - event: 'block.mod.remove', - type: 'remove', - name, - value: currentVal, - reason - }; - - this.localEmitter - .emit(`block.mod.remove.${name}.${currentVal}`, event); - - if (needNotify) { - // @deprecated - ctx.emit(`mod-remove-${name}-${currentVal}`, event); - ctx.emit(`mod:remove:${name}:${currentVal}`, event); - } - } - - return true; - } - - /** - * Returns a value of the specified block modifier - * - * @param name - modifier name - * @param [fromNode] - if true, then the modifier value will be taken from a DOM node - * - * @example - * ```js - * this.getMod('focused'); - * this.getMod('focused', true); - * ``` - */ - getMod(name: string, fromNode?: boolean): CanUndef { - const - {mods, node} = this; - - if (mods != null && !fromNode) { - return mods[name.camelize(false)]; - } - - if (node == null) { - return undefined; - } - - const - MOD_VALUE = 2; - - const - pattern = `(?:^| )(${this.getFullBlockName(name, '')}[^_ ]*)`, - modRgxp = modRgxpCache[pattern] ?? new RegExp(pattern), - el = modRgxp.exec(node.className); - - modRgxpCache[pattern] = modRgxp; - return el ? el[1].split('_')[MOD_VALUE] : undefined; - } - - /** - * Sets a modifier to the specified element. - * The method returns false if the modifier is already set. - * - * @param link - link to the element - * @param elName - element name - * @param modName - * @param value - * @param [reason] - reason to set a modifier - * - * @example - * ```js - * this.setElMod(node, 'foo', 'focused', true); - * this.setElMod(node, 'foo', 'focused', true, 'initSetMod'); - * ``` - */ - setElMod( - link: Nullable, - elName: string, - modName: string, - value: unknown, - reason: ModEventReason = 'setMod' - ): boolean { - if (!link || value == null) { - return false; - } - - elName = elName.camelize(false); - modName = modName.camelize(false); - - const - normalizedVal = String(value).dasherize(); - - if (this.getElMod(link, elName, modName) === normalizedVal) { - return false; - } - - this.removeElMod(link, elName, modName, undefined, 'setMod'); - link.classList.add(this.getFullElName(elName, modName, normalizedVal)); - - const event = { - element: elName, - event: 'el.mod.set', - type: 'set', - link, - modName, - value: normalizedVal, - reason - }; - - this.localEmitter.emit(`el.mod.set.${elName}.${modName}.${normalizedVal}`, event); - return true; - } - - /** - * Removes a modifier from the specified element. - * The method returns false if the element does not have this modifier. - * - * @param link - link to the element - * @param elName - element name - * @param modName - * @param [value] - * @param [reason] - reason to remove a modifier - * - * @example - * ```js - * this.removeElMod(node, 'foo', 'focused'); - * this.removeElMod(node, 'foo', 'focused', true); - * this.removeElMod(node, 'foo', 'focused', true, 'setMod'); - * ``` - */ - removeElMod( - link: Nullable, - elName: string, - modName: string, - value?: unknown, - reason: ModEventReason = 'removeMod' - ): boolean { - if (!link) { - return false; - } - - elName = elName.camelize(false); - modName = modName.camelize(false); - - const - normalizedVal = value != null ? String(value).dasherize() : undefined, - currentVal = this.getElMod(link, elName, modName); - - if (currentVal === undefined || normalizedVal !== undefined && currentVal !== normalizedVal) { - return false; - } - - link.classList - .remove(this.getFullElName(elName, modName, currentVal)); - - const event = { - element: elName, - event: 'el.mod.remove', - type: 'remove', - link, - modName, - value: currentVal, - reason - }; - - this.localEmitter.emit(`el.mod.remove.${elName}.${modName}.${currentVal}`, event); - return true; - } - - /** - * Returns a value of a modifier from the specified element - * - * @param link - link to the element - * @param elName - element name - * @param modName - modifier name - */ - getElMod(link: Nullable, elName: string, modName: string): CanUndef { - if (!link) { - return undefined; - } - - const - MOD_VALUE = 3; - - const - pattern = `(?:^| )(${this.getFullElName(elName, modName, '')}[^_ ]*)`, - modRgxp = pattern[pattern] ?? new RegExp(pattern), - el = modRgxp.exec(link.className); - - modRgxpCache[pattern] = modRgxp; - return el != null ? el[1].split(elRxp)[MOD_VALUE] : undefined; - } -} From 75761f052c9eea65ee1d8a5ec97077f8e2efe1bf Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 14:31:59 +0300 Subject: [PATCH 0225/2313] docs: improved doc --- src/core/component/accessor/README.md | 50 ++++++++++++++++++++++++++- src/core/component/accessor/index.ts | 39 ++++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/core/component/accessor/README.md b/src/core/component/accessor/README.md index 4aac6efbfc..1530900975 100644 --- a/src/core/component/accessor/README.md +++ b/src/core/component/accessor/README.md @@ -1,3 +1,51 @@ # core/component/accessor -This module provides API to initialize component accessors and computed fields. +This module provides API to initialize component accessors and computed fields to a component instance. + +## What differences between accessors and computed fields? + +A computed field is an accessor that value can be cached or watched. +To enable value caching use the `computed` decorator when define or override your accessor. +After this, the first time you touch the accessor value it will be cached. +To support cache invalidation or watching of changes provide a list of dependencies of your accessor. + +## Methods + +### attachAccessorsFromMeta + +Attaches accessors and computed fields to the specified component instance from its tied meta object. +The function creates cacheable wrappers for computed fields. Also, it creates accessors for deprecated component props. + +```typescript +import iBlock, { component, prop, computed } from 'super/i-block/i-block'; + +@component({ + // Will create an accessor for `name` that refers to `fName` and emits a warning + deprecatedProps: {name: 'fName'} +}) + +export default class bUser extends iBlock { + @prop() + readonly fName: string; + + @prop() + readonly lName: string; + + // This is a cacheable computed field with feature of watching and cache invalidation + @computed({cache: true, dependencies: ['fName', 'lName']}) + get fullName() { + return `${this.fName} ${this.lName}`; + } + + // This is a cacheable computed field without cache invalidation + @computed({cache: true}) + get id() { + return Math.random(); + } + + // This is a simple accessor (a getter) + get element() { + return this.$el; + } +} +``` diff --git a/src/core/component/accessor/index.ts b/src/core/component/accessor/index.ts index 975592da8b..3dea18d3e1 100644 --- a/src/core/component/accessor/index.ts +++ b/src/core/component/accessor/index.ts @@ -17,8 +17,45 @@ import { cacheStatus } from 'core/component/watch'; import type { ComponentInterface } from 'core/component/interface'; /** - * Attaches accessors and computed fields from a meta object to the specified component instance + * Attaches accessors and computed fields to the specified component instance from its tied meta object. + * The function creates cacheable wrappers for computed fields. + * Also, it creates accessors for deprecated component props. + * * @param component + * @example + * ```typescript + * import iBlock, { component, prop, computed } from 'super/i-block/i-block'; + * + * @component({ + * // Will create an accessor for `name` that refers to `fName` and emits a warning + * deprecatedProps: {name: 'fName'} + * }) + * + * export default class bUser extends iBlock { + * @prop() + * readonly fName: string; + * + * @prop() + * readonly lName: string; + * + * // This is a cacheable computed field with feature of watching and cache invalidation + * @computed({cache: true, dependencies: ['fName', 'lName']}) + * get fullName() { + * return `${this.fName} ${this.lName}`; + * } + * + * // This is a cacheable computed field without cache invalidation + * @computed({cache: true}) + * get id() { + * return Math.random(); + * } + * + * // This is a simple accessor (a getter) + * get element() { + * return this.$el; + * } + * } + * ``` */ export function attachAccessorsFromMeta(component: ComponentInterface): void { const { From a1083a0eaaa60e5e717120a574b7720849b5e2d3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 14:32:41 +0300 Subject: [PATCH 0226/2313] :up: --- src/core/component/accessor/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/component/accessor/CHANGELOG.md b/src/core/component/accessor/CHANGELOG.md index 66d7b0b6ee..6af2132b2f 100644 --- a/src/core/component/accessor/CHANGELOG.md +++ b/src/core/component/accessor/CHANGELOG.md @@ -15,6 +15,10 @@ Changelog * Added a new cache type for accessors `auto` +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * Refactoring From 6023f38c91861f7a524362e31f8cee84b91a28b3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 14:35:23 +0300 Subject: [PATCH 0227/2313] doc: better doc --- src/core/component/accessor/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/component/accessor/README.md b/src/core/component/accessor/README.md index 1530900975..ff0a6e8cde 100644 --- a/src/core/component/accessor/README.md +++ b/src/core/component/accessor/README.md @@ -7,7 +7,8 @@ This module provides API to initialize component accessors and computed fields t A computed field is an accessor that value can be cached or watched. To enable value caching use the `computed` decorator when define or override your accessor. After this, the first time you touch the accessor value it will be cached. -To support cache invalidation or watching of changes provide a list of dependencies of your accessor. +To support cache invalidation or watching of changes provide a list of dependencies of your accessor or +use the `cache = 'auto'` option. ## Methods From 6cf5a95919557bfb1dc86faab3320bd743dc094b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 14:43:38 +0300 Subject: [PATCH 0228/2313] chore: fixed grammar --- src/core/component/accessor/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/accessor/README.md b/src/core/component/accessor/README.md index ff0a6e8cde..fba9a51ef5 100644 --- a/src/core/component/accessor/README.md +++ b/src/core/component/accessor/README.md @@ -5,9 +5,9 @@ This module provides API to initialize component accessors and computed fields t ## What differences between accessors and computed fields? A computed field is an accessor that value can be cached or watched. -To enable value caching use the `computed` decorator when define or override your accessor. +To enable value caching, use the `computed` decorator when define or override your accessor. After this, the first time you touch the accessor value it will be cached. -To support cache invalidation or watching of changes provide a list of dependencies of your accessor or +To support cache invalidation or watching of changes, provide a list of dependencies of your accessor or use the `cache = 'auto'` option. ## Methods From 108d726000858e05489b8981a3d52d231a94bee7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 15:16:43 +0300 Subject: [PATCH 0229/2313] doc: added the normal README --- src/core/component/prop/CHANGELOG.md | 4 ++++ src/core/component/prop/README.md | 20 +++++++++++++++++++- src/core/component/prop/index.ts | 17 +++++++++-------- src/core/component/prop/interface.ts | 8 ++++---- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/core/component/prop/CHANGELOG.md b/src/core/component/prop/CHANGELOG.md index 9fc470a2a0..a8181b3748 100644 --- a/src/core/component/prop/CHANGELOG.md +++ b/src/core/component/prop/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * Refactoring diff --git a/src/core/component/prop/README.md b/src/core/component/prop/README.md index 1269537a74..b1f794034a 100644 --- a/src/core/component/prop/README.md +++ b/src/core/component/prop/README.md @@ -1,3 +1,21 @@ # core/component/prop -This module provides API to manage component input properties. +This module provides API to initialize component props to a component instance. + +## Methods + +### initProps + +Initializes input properties (aka "props") of the passed component instance. +While a component prop is being initialized, its name will be stored in the `$activeField` property. +The function returns a dictionary with the initialized props. + +### isTypeCanBeFunc + +Returns true if the specified prop type can be a function. + +```js +console.log(isTypeCanBeFunc(Boolean)); // false +console.log(isTypeCanBeFunc(Function)); // true +console.log(isTypeCanBeFunc([Function, Boolean])); // true +``` diff --git a/src/core/component/prop/index.ts b/src/core/component/prop/index.ts index 0b563e5fa6..e8e093a2b3 100644 --- a/src/core/component/prop/index.ts +++ b/src/core/component/prop/index.ts @@ -19,11 +19,12 @@ import type { InitPropsObjectOptions } from 'core/component/prop/interface'; export * from 'core/component/prop/interface'; /** - * Initializes the specified input properties of a component instance. - * The method returns a dictionary with the initialized properties. + * Initializes input properties (aka "props") of the passed component instance. + * While a component prop is being initialized, its name will be stored in the `$activeField` property. + * The function returns a dictionary with the initialized props. * * @param component - * @param [opts] - additional options + * @param [opts] - additional options of initialization */ export function initProps( component: ComponentInterface, @@ -70,7 +71,7 @@ export function initProps( obj = props[key]; if (obj?.required) { - throw new TypeError(`Missing the required property "${key}" (component "${component.componentName}")`); + throw new TypeError(`Missing the required property "${key}" of the "${component.componentName}" component`); } } @@ -94,14 +95,14 @@ export function initProps( } /** - * Returns true if the specified type can be a function + * Returns true if the specified prop type can be a function * * @param type * @example * ```js - * isTypeCanBeFunc(Boolean); // false - * isTypeCanBeFunc(Function); // true - * isTypeCanBeFunc([Function, Boolean]); // true + * console.log(isTypeCanBeFunc(Boolean)); // false + * console.log(isTypeCanBeFunc(Function)); // true + * console.log(isTypeCanBeFunc([Function, Boolean])); // true * ``` */ export function isTypeCanBeFunc(type: CanUndef>): boolean { diff --git a/src/core/component/prop/interface.ts b/src/core/component/prop/interface.ts index 3254a8e979..fad2d881f8 100644 --- a/src/core/component/prop/interface.ts +++ b/src/core/component/prop/interface.ts @@ -7,22 +7,22 @@ */ /** - * Additional options of component properties initializing + * Additional options of component props initialization */ export interface InitPropsObjectOptions { /** - * Dictionary where is stored the raw modifiers + * A dictionary where is stored the passed component props, like `$props` */ from?: Dictionary; /** - * Store for initialized properties + * A store for the initialized props * @default `{}` */ store?: Dictionary; /** - * If true, then property values will be written to the store object + * If true, then initialized prop values will be written to the passed store object * @default `false` */ saveToStore?: boolean; From 552a917e120fc5ee89fea2ad3d367c3997024b74 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 15:21:54 +0300 Subject: [PATCH 0230/2313] doc: added the normal README --- src/core/component/method/CHANGELOG.md | 4 ++ src/core/component/method/README.md | 17 +++++++- src/core/component/method/index.ts | 60 ++++++++++++++------------ 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/core/component/method/CHANGELOG.md b/src/core/component/method/CHANGELOG.md index b2b53e62cc..860b2b8c3b 100644 --- a/src/core/component/method/CHANGELOG.md +++ b/src/core/component/method/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * Refactoring diff --git a/src/core/component/method/README.md b/src/core/component/method/README.md index 0a82edf7f6..816497e616 100644 --- a/src/core/component/method/README.md +++ b/src/core/component/method/README.md @@ -1,3 +1,18 @@ # core/component/method -This module provides API to manage component methods. +This module provides API to initialize component methods to a component instance. + +## Methods + +### attachMethodsFromMeta + +Attaches methods to the specified component instance from its tied meta object. + +### callMethodFromComponent + +Invokes the given method from the specified component instance. + +```js +// Invoke some method from the passed component +callMethodFromComponent(calculator, 'calc', 1, 2); +``` diff --git a/src/core/component/method/index.ts b/src/core/component/method/index.ts index 16812684dc..4a486fb84f 100644 --- a/src/core/component/method/index.ts +++ b/src/core/component/method/index.ts @@ -14,33 +14,7 @@ import type { ComponentInterface } from 'core/component/interface'; /** - * Invokes a method from the specified component instance - * - * @param component - * @param method - method name - * @param [args] - method arguments - */ -export function callMethodFromComponent(component: ComponentInterface, method: string, ...args: unknown[]): void { - const - obj = component.unsafe.meta.methods[method]; - - if (obj != null) { - try { - const - res = obj.fn.apply(component, args); - - if (Object.isPromise(res)) { - res.catch(stderr); - } - - } catch (err) { - stderr(err); - } - } -} - -/** - * Attaches methods from a meta object to the specified component instance + * Attaches methods to the specified component instance from its tied meta object * @param component */ export function attachMethodsFromMeta(component: ComponentInterface): void { @@ -65,3 +39,35 @@ export function attachMethodsFromMeta(component: ComponentInterface): void { component[key] = method.fn.bind(component); } } + +/** + * Invokes the given method from the specified component instance + * + * @param component + * @param method - the method name + * @param [args] - the method arguments to invoke + * + * @example + * ```js + * // Invoke some method from the passed component + * callMethodFromComponent(calculator, 'calc', 1, 2); + * ``` + */ +export function callMethodFromComponent(component: ComponentInterface, method: string, ...args: unknown[]): void { + const + obj = component.unsafe.meta.methods[method]; + + if (obj != null) { + try { + const + res = obj.fn.apply(component, args); + + if (Object.isPromise(res)) { + res.catch(stderr); + } + + } catch (err) { + stderr(err); + } + } +} From 5617aefa87aeb35c117f29e192525ab6a3710c02 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 15:22:11 +0300 Subject: [PATCH 0231/2313] refactor: removed dead code --- src/core/component/vnode/CHANGELOG.md | 41 ---- src/core/component/vnode/README.md | 3 - src/core/component/vnode/index.ts | 289 -------------------------- src/core/component/vnode/interface.ts | 38 ---- 4 files changed, 371 deletions(-) delete mode 100644 src/core/component/vnode/CHANGELOG.md delete mode 100644 src/core/component/vnode/README.md delete mode 100644 src/core/component/vnode/index.ts delete mode 100644 src/core/component/vnode/interface.ts diff --git a/src/core/component/vnode/CHANGELOG.md b/src/core/component/vnode/CHANGELOG.md deleted file mode 100644 index fa76cfea55..0000000000 --- a/src/core/component/vnode/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -Changelog -========= - -> **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.0.0-rc.121 (2021-01-12) - -#### :bug: Bug Fix - -* Fixed a bug with parsing of styles - -## v3.0.0-rc.108 (2020-12-14) - -#### :bug: Bug Fix - -* Fixed a bug when using `parseStyle` with string trailing `;` ex. `background-color: #2B9FFF; color: #FFFFFF; border: 1px solid #FFFFFF;` - -## v3.0.0-rc.93 (2020-11-03) - -#### :boom: Breaking Change - -* Changed an interface `patchComponentVData` - -## v3.0.0-rc.92 (2020-11-03) - -#### :rocket: New Feature - -* Added `patchComponentVData` -* Added `parseStyle` - -## v3.0.0-rc.37 (2020-07-20) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/core/component/vnode/README.md b/src/core/component/vnode/README.md deleted file mode 100644 index 9c086fc1ff..0000000000 --- a/src/core/component/vnode/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core/component/vnode - -This module provides a bunch of functions to work with a VNode object. diff --git a/src/core/component/vnode/index.ts b/src/core/component/vnode/index.ts deleted file mode 100644 index e25aa162ac..0000000000 --- a/src/core/component/vnode/index.ts +++ /dev/null @@ -1,289 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -/** - * [[include:core/component/vnode/README.md]] - * @packageDocumentation - */ - -import { components } from 'core/component/const'; - -import type { RenderContext, VNode, VNodeData, NormalizedScopedSlot } from 'core/component/engines'; -import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; - -import type { - - ComponentVNodeData, - ComponentModelVNodeData, - PatchComponentVDataOptions - -} from 'core/component/vnode/interface'; - -export * from 'core/component/vnode/interface'; - -/** - * Returns a component render context object from the specified vnode - * - * @param component - component name or meta object - * @param vnode - * @param [parent] - parent component instance - */ -export function getComponentRenderCtxFromVNode( - component: string | ComponentMeta, - vnode: VNode, - parent?: ComponentInterface -): RenderContext { - const - data = getComponentDataFromVNode(component, vnode); - - return { - parent: Object.cast(parent), - children: vnode.children ?? [], - props: data.props, - listeners: >>data.on, - - slots: () => data.slots, - scopedSlots: >data.scopedSlots, - injections: undefined, - - data: { - ref: data.ref, - refInFor: data.refInFor, - on: >>data.on, - nativeOn: >>data.nativeOn, - attrs: data.attrs, - class: data.class, - staticClass: data.staticClass, - style: data.style, - directives: data.directives - } - }; -} - -/** - * Returns a component data object from the specified vnode - * - * @param component - component name or a meta object - * @param vnode - */ -export function getComponentDataFromVNode(component: string | ComponentMeta, vnode: VNode): ComponentVNodeData { - const - vData = vnode.data ?? {}, - {slots, model} = (vData); - - const res = { - ref: vData.ref, - refInFor: vData.refInFor, - - attrs: {}, - props: {}, - - model: (vData).model, - directives: Array.concat([], vData.directives), - - slots: {...slots}, - scopedSlots: {...vData.scopedSlots}, - - on: {...vData.on}, - nativeOn: {...vData.nativeOn}, - - class: [].concat(vData.class ?? []), - staticClass: vData.staticClass, - style: vData.style - }; - - const - meta = Object.isString(component) ? components.get(component) : component; - - if (!meta) { - res.attrs = vData.attrs ?? res.attrs; - return res; - } - - const - componentModel = meta.params.model; - - if (model != null && componentModel) { - const - // eslint-disable-next-line @typescript-eslint/unbound-method - {value, callback} = model, - {prop, event} = componentModel; - - if (prop != null && event != null) { - res.props[prop] = value; - res.on[event] = callback; - } - } - - const - vAttrs = vData.attrs, - propsObj = meta.component.props; - - for (let keys = Object.keys(propsObj), i = 0; i < keys.length; i++) { - res.props[keys[i]] = undefined; - } - - if (vAttrs) { - for (let keys = Object.keys(vAttrs), i = 0; i < keys.length; i++) { - const - key = keys[i], - prop = key.camelize(false), - val = vAttrs[key]; - - if (propsObj[prop]) { - res.props[prop] = val; - - } else { - res.attrs[key] = val; - } - } - } - - if (slots == null && vnode.children) { - const - {children} = vnode; - - let - hasSlots = false; - - for (let i = 0; i < children.length; i++) { - const - node = children[i], - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - data = node?.data ?? {}; - - const - {attrs} = data; - - if (attrs?.slot != null) { - hasSlots = true; - res.slots[attrs.slot] = node; - } - } - - if (!hasSlots) { - res.slots.default = children; - } - } - - return res; -} - -/** - * Patches the specified component VNode data object by using another VNode data object - * - * @param data - VNode data object - * @param anotherData - another VNode data object - * @param [opts] - additional options - */ -export function patchComponentVData( - data: CanUndef, - anotherData: CanUndef, - opts?: PatchComponentVDataOptions -): CanUndef { - if (anotherData == null || data == null) { - return data; - } - - data.staticClass = data.staticClass ?? ''; - - if (Object.isTruly(anotherData.staticClass)) { - data.staticClass += ` ${anotherData.staticClass}`; - } - - if (Object.isTruly(anotherData.class)) { - data.class = Array.concat([], data.class, anotherData.class); - } - - if (Object.isTruly(anotherData.style)) { - data.style = parseStyle(data.style, parseStyle(anotherData.style)); - } - - if (Object.isTruly(anotherData.attrs) && opts?.patchAttrs) { - data.attrs = Object.assign(data.attrs ?? {}, anotherData.attrs); - } - - data.ref = anotherData.ref; - data.refInFor = anotherData.refInFor; - - data.directives = Array.concat([], data.directives, anotherData.directives); - data.on = data.on ?? {}; - - if (anotherData.nativeOn) { - const - {on} = data; - - for (let o = anotherData.nativeOn, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const key = keys[i]; - on[key] = Array.concat([], on[key], o[key]); - } - } - - return data; -} - -/** - * Parses the specified style value and returns a dictionary with styles - * - * @param style - original style - * @param [acc] - accumulator - * - * @example - * ```js - * // {color: 'red', background: 'blue'} - * parseStyle(['color: red', {background: 'blue'}]) - * ``` - */ -export function parseStyle( - style: CanUndef>, - acc: Dictionary = {} -): Dictionary { - if (!Object.isTruly(style)) { - return acc; - } - - if (Object.isDictionary(style)) { - Object.assign(acc, style); - return acc; - } - - if (Object.isString(style)) { - const - styles = style.split(';'); - - for (let i = 0; i < styles.length; i++) { - const - rule = styles[i]; - - if (rule.trim().length === 0) { - continue; - } - - const chunks = rule.split(':'); - acc[chunks[0].trim()] = chunks[1].trim(); - } - - return acc; - } - - if (Object.isArray(style)) { - for (let i = 0; i < style.length; i++) { - const - el = style[i]; - - if (Object.isDictionary(el)) { - Object.assign(acc, el); - - } else { - parseStyle(>el, acc); - } - } - } - - return acc; -} diff --git a/src/core/component/vnode/interface.ts b/src/core/component/vnode/interface.ts deleted file mode 100644 index 2968ae9bfd..0000000000 --- a/src/core/component/vnode/interface.ts +++ /dev/null @@ -1,38 +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 { VNode, VNodeDirective, NormalizedScopedSlot } from 'core/component/engines'; - -export interface ComponentVNodeData { - ref?: string; - refInFor?: boolean; - - attrs: Dictionary; - props: Dictionary; - directives: VNodeDirective[]; - - slots: Dictionary>; - scopedSlots: Dictionary; - - on: Dictionary>; - nativeOn: Dictionary; - - class: string[]; - staticClass: string; - style: CanArray; -} - -export interface ComponentModelVNodeData { - value: unknown; - expression: string; - callback(value: unknown): unknown; -} - -export interface PatchComponentVDataOptions { - patchAttrs: boolean; -} From 0db479c45f36d785d6374e097d19dbb09df237fe Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 16:06:09 +0300 Subject: [PATCH 0232/2313] doc: added the normal README --- src/core/component/field/CHANGELOG.md | 4 ++++ src/core/component/field/README.md | 18 +++++++++++++++++- src/core/component/field/helpers.ts | 21 +++++++++++---------- src/core/component/field/index.ts | 19 ++++++++++--------- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/core/component/field/CHANGELOG.md b/src/core/component/field/CHANGELOG.md index a62bb0c9e6..1abe040653 100644 --- a/src/core/component/field/CHANGELOG.md +++ b/src/core/component/field/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * The module is rewritten to use static sorting of dependencies diff --git a/src/core/component/field/README.md b/src/core/component/field/README.md index f054a41153..4f16788566 100644 --- a/src/core/component/field/README.md +++ b/src/core/component/field/README.md @@ -1,3 +1,19 @@ # core/component/field -This module provides API to initialize component fields. +This module provides API to initialize component fields to a component instance. + +## What differences between fields and system fields? + +The major difference between fields and system fields, that any changes of a component field can force re-rendering of its template. +I.e., if you are totally sure that your component field doesn't need to force rendering, prefer system fields instead of regular. +Mind, changes in any system field still can be watched using built-in API. + +The second difference is that system fields are initialized on the `beforeCreate` hook, +but not on the `created` hook like the regular fields do. + +## Methods + +### initFields + +Initializes all fields of the passed component instance. +The function returns a dictionary with the initialized fields. diff --git a/src/core/component/field/helpers.ts b/src/core/component/field/helpers.ts index db83c2bf26..1ef3e859fe 100644 --- a/src/core/component/field/helpers.ts +++ b/src/core/component/field/helpers.ts @@ -12,15 +12,16 @@ import type { ComponentField } from 'core/component/interface'; import type { SortedFields } from 'core/component/field/interface'; /** - * Returns a weight of the specified field relative to other fields in a scope. - * The weight describes when a field should initialize relative to other fields. - * At start are init all fields with a zero weight. - * After will be init fields with the minimal non-zero weight, etc. + * Returns a weight of the specified field from the given scope. + * The weight describes when a field should initialize relative to other fields: * - * @param field - field to calculate the weight - * @param fields - field scope + * 1. First, we initialize all fields with the zero weight. + * 2. After that, all fields with a minimum non-zero weight will be initialized, and so on. + * + * @param field - the field to calculate the weight + * @param scope - the scope where is stored the passed component field, like `$fields` or `$systemFields` */ -export function getFieldWeight(field: CanUndef, fields: Dictionary): number { +export function getFieldWeight(field: CanUndef, scope: Dictionary): number { if (field == null) { return 0; } @@ -36,13 +37,13 @@ export function getFieldWeight(field: CanUndef, fields: Dictiona for (let o = after.values(), el = o.next(); !el.done; el = o.next()) { const - dep = fields[el.value]; + dep = scope[el.value]; if (dep == null) { - throw new ReferenceError(`The specified dependency "${dep}" is not found in a scope`); + throw new ReferenceError(`The specified dependency "${dep}" is not found in the given scope`); } - weight += getFieldWeight(dep, fields); + weight += getFieldWeight(dep, scope); } } diff --git a/src/core/component/field/index.ts b/src/core/component/field/index.ts index c1b295a834..221ecac242 100644 --- a/src/core/component/field/index.ts +++ b/src/core/component/field/index.ts @@ -17,15 +17,16 @@ import type { ComponentInterface, ComponentField } from 'core/component/interfac export * from 'core/component/field/interface'; /** - * Initializes the specified fields of a component instance. + * Initializes all fields of the passed component instance. + * While a component field is being initialized, its name will be stored in the `$activeField` property. * The function returns a dictionary with the initialized fields. * - * @param fields - fields scope to initialize - * @param component - component instance - * @param [store] - store for initialized fields + * @param from - a dictionary where is stored the passed component fields, like `$fields` or `$systemFields` + * @param component - the component instance + * @param [store] - a store for initialized fields */ export function initFields( - fields: Dictionary, + from: Dictionary, component: ComponentInterface, store: Dictionary = {} ): Dictionary { @@ -40,14 +41,14 @@ export function initFields( ssrMode = component.$renderEngine.supports.ssr, isFunctional = params.functional === true; - for (let sortedFields = sortFields(fields), i = 0; i < sortedFields.length; i++) { + for (let sortedFields = sortFields(from), i = 0; i < sortedFields.length; i++) { const [key, field] = sortedFields[i]; const sourceVal = store[key]; - const dontNeedInit = + const canSkip = field == null || sourceVal !== undefined || @@ -56,7 +57,7 @@ export function initFields( field.init == null && field.default === undefined && instance[key] === undefined; - if (field == null || dontNeedInit) { + if (field == null || canSkip) { store[key] = sourceVal; continue; } @@ -72,7 +73,7 @@ export function initFields( if (val === undefined) { if (store[key] === undefined) { - // We need to clone the default value from a constructor + // We need to clone the default value from the constructor // to prevent linking to the same type component for a non-primitive value val = field.default !== undefined ? field.default : Object.fastClone(instance[key]); store[key] = val; From eb4d11bb4327a80a67d89df2bcd10a14d6802b1a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 16:30:47 +0300 Subject: [PATCH 0233/2313] refactor: removed dead code & better doc --- src/core/component/const/README.md | 3 +++ src/core/component/const/cache.ts | 37 ++++++++------------------ src/core/component/const/emitters.ts | 10 ++++--- src/core/component/const/enums.ts | 15 ++++++----- src/core/component/const/index.ts | 5 ++++ src/core/component/const/symbols.ts | 13 +++++---- src/core/component/const/validators.ts | 12 +++++++-- 7 files changed, 52 insertions(+), 43 deletions(-) create mode 100644 src/core/component/const/README.md diff --git a/src/core/component/const/README.md b/src/core/component/const/README.md new file mode 100644 index 0000000000..9403de70b9 --- /dev/null +++ b/src/core/component/const/README.md @@ -0,0 +1,3 @@ +# core/component/const + +This module provides a bunch of constants to register and manage components. diff --git a/src/core/component/const/cache.ts b/src/core/component/const/cache.ts index 2905868762..d300ea6f95 100644 --- a/src/core/component/const/cache.ts +++ b/src/core/component/const/cache.ts @@ -6,59 +6,44 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { RenderObject } from 'core/component/render'; import type { ComponentEngine, ComponentOptions as ComponentEngineOptions } from 'core/component/engines'; -import type { ComponentInterface, ComponentMeta, ComponentOptions } from 'core/component/interface'; +import type { ComponentInterface, ComponentMeta, ComponentOptions, RenderFactory } from 'core/component/interface'; /** - * Map of component declaration parameters + * A dictionary with component declaration parameters */ export const componentParams = new Map(); /** - * Map of root components + * A dictionary with the registered root components */ export const rootComponents = Object.createDict>>(); /** - * Link to an instance of the global root component + * A link to the root component instance */ export const globalRootComponent = <{link: Nullable}>{ link: null }; /** - * Map of registered components + * A dictionary with the registered components */ export const components = new Map(); /** - * Map of component initializers: - * by default all components don't register automatically, but the first call from a template, - * and this map contains functions to register components. + * A dictionary with the registered component initializers. + * By default, all components don't register automatically, but the first call from some template. + * This structure contains functions to register components. */ export const componentInitializers = Object.createDict(); /** - * Map of component render functions + * A dictionary with the registered component render factories */ -export const componentTemplates = Object.createDict(); +export const componentRenderFactories = Object.createDict(); /** - * Cache of minimal required context objects for component render functions - * - * @example - * ```js - * function bButtonRender() { - * return this.createComponent('div'); - * } - * - * bButtonRender.call(renderCtxCache['b-button']); - * ``` - */ -export const renderCtxCache = Object.createDict(); - -/** - * Map of component pointers for meta tables + * A dictionary with component pointers for meta tables */ export const metaPointers = Object.createDict>(); diff --git a/src/core/component/const/emitters.ts b/src/core/component/const/emitters.ts index 10f8e5a149..6d274e9296 100644 --- a/src/core/component/const/emitters.ts +++ b/src/core/component/const/emitters.ts @@ -10,12 +10,14 @@ import { EventEmitter2 as EventEmitter, ListenerFn, OnOptions } from 'eventemitt import { componentParams } from 'core/component/const/cache'; /** - * Event emitter to broadcast component initialize events + * An event emitter to broadcast component initialize events */ -export const - initEmitter = new EventEmitter({maxListeners: 1e3, newListener: false}); +export const initEmitter = new EventEmitter({ + maxListeners: 1e3, + newListener: false +}); -// We need to wrap an original `once` function of the emitter +// We need to wrap the original `once` function of the emitter // to attach logic of registering smart components ((initEventOnce) => { initEmitter.once = function once( diff --git a/src/core/component/const/enums.ts b/src/core/component/const/enums.ts index 844d575dfe..6d90063caa 100644 --- a/src/core/component/const/enums.ts +++ b/src/core/component/const/enums.ts @@ -6,24 +6,27 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export const mountedHooks = Object.createDict({ - mounted: true, - updated: true, - activated: true -}); - +/** + * A dictionary with names of hooks that occur before a component is created + */ export const beforeHooks = Object.createDict({ beforeRuntime: true, beforeCreate: true, beforeDataCreate: true }); +/** + * A dictionary with names of hooks that occur before a component is mounted + */ export const beforeMountHooks = Object.createDict({ ...beforeHooks, created: true, beforeMount: true }); +/** + * A dictionary with names of hooks that occur before a component is rendered or re-rendered + */ export const beforeRenderHooks = Object.createDict({ ...beforeMountHooks, beforeUpdate: true diff --git a/src/core/component/const/index.ts b/src/core/component/const/index.ts index 4c6b713eb7..9fc6ceca4e 100644 --- a/src/core/component/const/index.ts +++ b/src/core/component/const/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/const/README.md]] + * @packageDocumentation + */ + export * from 'core/component/const/cache'; export * from 'core/component/const/symbols'; export * from 'core/component/const/enums'; diff --git a/src/core/component/const/symbols.ts b/src/core/component/const/symbols.ts index 9f034ae62a..3329d2f4f1 100644 --- a/src/core/component/const/symbols.ts +++ b/src/core/component/const/symbols.ts @@ -6,9 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export const - asyncLabel = Symbol('Component async label'), - defaultWrapper = Symbol('Default wrapper'); +/** + * A flag to mark some function that it’s a generated default wrapper + */ +export const DEFAULT_WRAPPER = Symbol('This function is the generated default wrapper'); -export const - PARENT = {}; +/** + * A value to refer the parent instance + */ +export const PARENT = {}; diff --git a/src/core/component/const/validators.ts b/src/core/component/const/validators.ts index 640eee5bb2..487e690849 100644 --- a/src/core/component/const/validators.ts +++ b/src/core/component/const/validators.ts @@ -6,5 +6,13 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export const - isComponent = /^([bpg]-[^_ ]+)$/; +/** + * A RegExp to check if the given string is the name of a component + * + * @example + * ```js + * console.log(isComponent.test('b-button')); // true + * console.log(isComponent.test('button')); // false + * ``` + */ +export const isComponent = /^([bpg]-[^_ ]+)$/; From 6d7848da26d0abf16303b824bdda74c3815cf13b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 16:53:48 +0300 Subject: [PATCH 0234/2313] doc: added the normal README --- src/core/component/context/README.md | 18 ++++++++++++++++++ src/core/component/context/const.ts | 12 +++++++++--- src/core/component/context/index.ts | 11 +++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/core/component/context/README.md b/src/core/component/context/README.md index 66d2441c19..a02f327c69 100644 --- a/src/core/component/context/README.md +++ b/src/core/component/context/README.md @@ -1,3 +1,21 @@ # core/component/context This module provides a bunch of helpers to work with a component context. + +## What problem is being solved? + +The V4Fire library creates a DSL wrapper over the used component library. +Thanks to this, we can use TS classes to describe our components, regardless of the library used under the hood. +In order for a component library to be used as an engine for V4Fire, it must implement a set of necessary properties and +methods that are described in the [[ComponentInterface]] interface. Also, for the correct working of the entire platform, +V4Fire must redefine some properties and methods from the engine. But since these properties can be marked as read-only, +i.e. we can’t do simple property override. To solve these problems, this module was created. +It provides API to create a new context object whose properties can be overridden. + +## Methods + +### getComponentContext + +Returns a wrapped component context object based on the passed one. +This function is used to allow overwriting component properties and methods without hacking the original object. +Basically, this function returns a new object that contains the original object as a prototype. diff --git a/src/core/component/context/const.ts b/src/core/component/context/const.ts index 2c71f1fed4..ec6db9b370 100644 --- a/src/core/component/context/const.ts +++ b/src/core/component/context/const.ts @@ -6,6 +6,12 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export const - toRaw = Symbol('Link to a RAW component'), - ctxMap = new WeakMap(); +/** + * A symbol to extract the raw component object + */ +export const toRaw = Symbol('A link to the raw component object'); + +/** + * A cache for the wrapped component contexts + */ +export const wrappedContexts = new WeakMap(); diff --git a/src/core/component/context/index.ts b/src/core/component/context/index.ts index 7cede7b57e..f0fe8dee4e 100644 --- a/src/core/component/context/index.ts +++ b/src/core/component/context/index.ts @@ -11,25 +11,28 @@ * @packageDocumentation */ -import { toRaw, ctxMap } from 'core/component/context/const'; +import { toRaw, wrappedContexts } from 'core/component/context/const'; import type { ComponentInterface } from 'core/component/interface'; export * from 'core/component/context/const'; /** - * Returns a component context object by the specified component instance + * Returns a wrapped component context object based on the passed one. + * This function is used to allow overwriting component properties and methods without hacking the original object. + * Basically, this function returns a new object that contains the original object as a prototype. + * * @param component */ export function getComponentContext(component: object): Dictionary & ComponentInterface['unsafe'] { component = component[toRaw] ?? component; let - v = ctxMap.get(component); + v = wrappedContexts.get(component); if (v == null) { v = Object.create(component); Object.defineProperty(v, toRaw, {value: component}); - ctxMap.set(component, v); + wrappedContexts.set(component, v); } return v; From ba83e6b170f417a38de3b5d635c47bba48659fec Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 16:56:34 +0300 Subject: [PATCH 0235/2313] doc: added the normal README --- src/core/component/traverse/README.md | 8 +++++++- src/core/component/traverse/index.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/component/traverse/README.md b/src/core/component/traverse/README.md index f67299ff9a..150fe752ed 100644 --- a/src/core/component/traverse/README.md +++ b/src/core/component/traverse/README.md @@ -1,3 +1,9 @@ # core/component/traverse -This module provides a bunch of functions to iterate over a vnode tree. +This module provides a bunch of functions to iterate over a component vnode tree. + +## Methods + +### getNormalParent + +Returns a link to a "normal" (non-functional) parent component for the passed one. diff --git a/src/core/component/traverse/index.ts b/src/core/component/traverse/index.ts index 5fc5d5e7a1..8e09b93dee 100644 --- a/src/core/component/traverse/index.ts +++ b/src/core/component/traverse/index.ts @@ -14,7 +14,7 @@ import type { ComponentInterface } from 'core/component/interface'; /** - * Returns a link to a "normal" (non-functional and non-flyweight) parent component for the specified component + * Returns a link to a "normal" (non-functional) parent component for the passed one * @param component */ export function getNormalParent(component: ComponentInterface): CanUndef { From 502d34a6f447adce58e87bd59fc8ac0d04673e71 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 17:09:59 +0300 Subject: [PATCH 0236/2313] doc: improved docs --- src/core/component/queue-emitter/README.md | 8 +++-- src/core/component/queue-emitter/index.ts | 34 +++++++++---------- src/core/component/queue-emitter/interface.ts | 2 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/core/component/queue-emitter/README.md b/src/core/component/queue-emitter/README.md index 62fe421056..58501c3cab 100644 --- a/src/core/component/queue-emitter/README.md +++ b/src/core/component/queue-emitter/README.md @@ -1,6 +1,8 @@ # core/component/queue-emitter -This module provides a class to organize event emitter with support of ordering events. +This module provides a class to organize event emitter with support of ordering handlers. + +## Usage ```js import QueueEmitter from 'core/component/queue-emitter'; @@ -8,12 +10,12 @@ import QueueEmitter from 'core/component/queue-emitter'; const eventEmitter = new QueueEmitter(); -// These listeners will be invoked only when all specified events are fired +// This handler will be invoked only when all specified events are fired eventEmitter.on(new Set(['foo', 'bar']), () => { console.log('Crash!'); }); -// This listener does not have any events to listen. +// This handler does not have any events to listen. // It will be invoked after calling the `drain` method. eventEmitter.on(undefined, () => { console.log('Boom!'); diff --git a/src/core/component/queue-emitter/index.ts b/src/core/component/queue-emitter/index.ts index 8d851d0ec1..31751bdbad 100644 --- a/src/core/component/queue-emitter/index.ts +++ b/src/core/component/queue-emitter/index.ts @@ -16,47 +16,47 @@ import type { EventListener } from 'core/component/queue-emitter/interface'; export * from 'core/component/queue-emitter/interface'; /** - * The special kind of event emitter that supports queues of events + * The special kind of event emitter that supports queues of handlers */ export default class QueueEmitter { /** - * Queue of event listeners that are ready to fire + * A queue of event handlers that are ready to invoke */ protected queue: Function[] = []; /** - * Map of tied event listeners that aren't ready to fire + * A dictionary with tied event listeners that aren't ready to invoke */ protected listeners: Dictionary = Object.createDict(); /** - * Attaches a callback for the specified set of events. - * The callback will be invoked only when all specified events are fired. + * Attaches a handler for the specified set of events. + * The handler will be invoked only when all specified events are fired. * - * @param event - set of events (can be undefined) - * @param cb + * @param event - a set of events (can be undefined) + * @param handler */ - on(event: Nullable>, cb: Function): void { + on(event: Nullable>, handler: Function): void { if (event != null && event.size > 0) { for (let o = event.values(), el = o.next(); !el.done; el = o.next()) { const key = el.value, listeners = this.listeners[key] ?? []; - listeners.push({event, cb}); + listeners.push({event, handler}); this.listeners[key] = listeners; } return; } - this.queue.push(cb); + this.queue.push(handler); } /** * Emits the specified event. - * If at least one of listeners returns a promise, - * the method returns promise that will be resolved after all internal promises are resolved. + * If at least one of handlers returns a promise, + * the method returns a promise that will be resolved after all internal promises are resolved. * * @param event */ @@ -69,7 +69,7 @@ export default class QueueEmitter { } const - tasks = >>[]; + tasks: Array> = []; for (let i = 0; i < queue.length; i++) { const @@ -82,7 +82,7 @@ export default class QueueEmitter { if (ev.size === 0) { const - task = el.cb(); + task = el.handler(); if (Object.isPromise(task)) { tasks.push(task); @@ -97,16 +97,16 @@ export default class QueueEmitter { } /** - * Drains the queue of listeners that are ready to fire. + * Drains the queue of handlers that are ready to invoke. * If at least one of listeners returns a promise, - * the method returns promise that will be resolved after all internal promises are resolved. + * the method returns a promise that will be resolved after all internal promises are resolved. */ drain(): CanPromise { const {queue} = this; const - tasks = >>[]; + tasks: Array> = []; for (let i = 0; i < queue.length; i++) { const diff --git a/src/core/component/queue-emitter/interface.ts b/src/core/component/queue-emitter/interface.ts index e3af642744..34d8dc5b7b 100644 --- a/src/core/component/queue-emitter/interface.ts +++ b/src/core/component/queue-emitter/interface.ts @@ -8,5 +8,5 @@ export interface EventListener { event: Set; - cb: Function; + handler: Function; } From 09064e6614f4af4fa5fd0ef2fe80c1dac6eb20a2 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 17:10:17 +0300 Subject: [PATCH 0237/2313] refactor: fixed import --- src/core/component/prop/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/prop/index.ts b/src/core/component/prop/index.ts index e8e093a2b3..cb288c1d71 100644 --- a/src/core/component/prop/index.ts +++ b/src/core/component/prop/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import { defaultWrapper } from 'core/component/const'; +import { DEFAULT_WRAPPER } from 'core/component/const'; import type { ComponentInterface } from 'core/component/interface'; import type { InitPropsObjectOptions } from 'core/component/prop/interface'; @@ -79,7 +79,7 @@ export function initProps( needSaveToStore = opts.saveToStore; if (Object.isFunction(val)) { - if (opts.saveToStore || val[defaultWrapper] !== true) { + if (opts.saveToStore || val[DEFAULT_WRAPPER] !== true) { val = isTypeCanBeFunc(prop.type) ? val.bind(component) : val.call(component); needSaveToStore = true; } From 0d48fcf13301f60da90848fd57b6c65e808fcdd7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 17:52:42 +0300 Subject: [PATCH 0238/2313] feat: exposes methods to `set`/`unset` new set properties and `watch` for them changes & better doc --- src/core/component/state/CHANGELOG.md | 10 ++++++ src/core/component/state/README.md | 47 ++++++++++++++++++++++++++- src/core/component/state/index.ts | 23 ++++++++----- src/core/component/state/interface.ts | 19 +++++++++-- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/core/component/state/CHANGELOG.md b/src/core/component/state/CHANGELOG.md index 3ca3b9d450..2a5e6c8bbe 100644 --- a/src/core/component/state/CHANGELOG.md +++ b/src/core/component/state/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :rocket: New Feature + +* Exposes methods to `set`/`unset` new set properties and `watch` for them changes + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.109 (2020-12-15) #### :bug: Bug Fix diff --git a/src/core/component/state/README.md b/src/core/component/state/README.md index a600fce31a..e97a15d8ce 100644 --- a/src/core/component/state/README.md +++ b/src/core/component/state/README.md @@ -1,3 +1,48 @@ # core/component/state -This module provides an object to declare external state values to components, such as an online status. +The module provides the global watchable store for all components. You could say it’s like Redux, only much simpler. +Use this store to provide data of external modules to components. + +## Usage + +```js +import state, { watch, set, unset } from 'core/component/state'; + +// Online status check +console.log(state.isOnline); + +// Watching the session +watch('isAuth', (value, oldValue) => { + console.log(value, oldValue); +}); + +// Addin a new property to the state +set('newProp', someValue); +``` + +## Embedded state + +V4Fire supports out of the box integration with `core/session`, `core/net` and `core/abt` modules. + +### state.isAuth + +The property indicates whether the session is authorized or not. + +### state.isOnline + +The property indicates whether there is currently an Internet connection or not. + +### state.lastOnlineDate + +The property indicates the date of the last Internet connection. + +### state.experiments + +The property refers to a list of registered AB experiments. + +## API + +As the default export, the module exposes a link to the store object itself. +Besides, the module exports methods to `set`/`unset` new store properties and `watch` for them changes. +To watch the store changes, it uses `core/watch`. Therefore, for detailed information about observing objects, +please refer to the documentation of this module. diff --git a/src/core/component/state/index.ts b/src/core/component/state/index.ts index 81a482f4ee..7aabf99155 100644 --- a/src/core/component/state/index.ts +++ b/src/core/component/state/index.ts @@ -11,7 +11,7 @@ * @packageDocumentation */ -import watch from 'core/object/watch'; +import watchObj from 'core/object/watch'; import { emitter as NetEmitter, NetStatus } from 'core/net'; import { emitter as SessionEmitter, Session } from 'core/session'; @@ -20,24 +20,29 @@ import type { State } from 'core/component/state/interface'; export * from 'core/component/state/interface'; -const state = watch({ +const watcher = watchObj({ isAuth: undefined, isOnline: undefined, lastOnlineDate: undefined, experiments: undefined -}).proxy; +}); + +export default watcher.proxy; + +export const + watch = watchObj.bind(null, watcher.proxy), + set = watcher.set.bind(watcher), + unset = watcher.delete.bind(watcher); SessionEmitter.on('set', (e: Session) => { - state.isAuth = Boolean(e.auth); + set('isAuth', Boolean(e.auth)); }); SessionEmitter.on('clear', () => { - state.isAuth = false; + set('isAuth', false); }); NetEmitter.on('status', (netStatus: NetStatus) => { - state.isOnline = netStatus.status; - state.lastOnlineDate = netStatus.lastOnline; + set('isOnline', netStatus.status); + set('lastOnlineDate', netStatus.lastOnline); }); - -export default state; diff --git a/src/core/component/state/interface.ts b/src/core/component/state/interface.ts index 1172fcfc11..93249a2bea 100644 --- a/src/core/component/state/interface.ts +++ b/src/core/component/state/interface.ts @@ -6,11 +6,26 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ExperimentsSet } from 'core/abt'; +import type { Experiments } from 'core/abt'; export interface State { + /** + * Is session is authorized + */ isAuth?: boolean; + + /** + * Is there an Internet connection + */ isOnline?: boolean; + + /** + * Date of last Internet connection + */ lastOnlineDate?: Date; - experiments?: ExperimentsSet; + + /** + * A list of registered AB experiments + */ + experiments?: Experiments; } From 4c3bbab2d1aef9012817049499ae148012560f5c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 18:05:42 +0300 Subject: [PATCH 0239/2313] refactor: split index into separated files --- src/core/component/state/bindings.ts | 24 +++++++++++++++++++ src/core/component/state/const.ts | 24 +++++++++++++++++++ src/core/component/state/index.ts | 35 +++------------------------- 3 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 src/core/component/state/bindings.ts create mode 100644 src/core/component/state/const.ts diff --git a/src/core/component/state/bindings.ts b/src/core/component/state/bindings.ts new file mode 100644 index 0000000000..5f71e3ee97 --- /dev/null +++ b/src/core/component/state/bindings.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { emitter as NetEmitter, NetStatus } from 'core/net'; +import { emitter as SessionEmitter, Session } from 'core/session'; +import { set } from 'core/component/state/const'; + +SessionEmitter.on('set', (e: Session) => { + set('isAuth', Boolean(e.auth)); +}); + +SessionEmitter.on('clear', () => { + set('isAuth', false); +}); + +NetEmitter.on('status', (netStatus: NetStatus) => { + set('isOnline', netStatus.status); + set('lastOnlineDate', netStatus.lastOnline); +}); diff --git a/src/core/component/state/const.ts b/src/core/component/state/const.ts new file mode 100644 index 0000000000..9417b45026 --- /dev/null +++ b/src/core/component/state/const.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import watchObj from 'core/object/watch'; +import type { State } from 'core/component/state/interface'; + +const watcher = watchObj({ + isAuth: undefined, + isOnline: undefined, + lastOnlineDate: undefined, + experiments: undefined +}); + +export default watcher.proxy; + +export const + watch = watchObj.bind(null, watcher.proxy), + set = watcher.set.bind(watcher), + unset = watcher.delete.bind(watcher); diff --git a/src/core/component/state/index.ts b/src/core/component/state/index.ts index 7aabf99155..8e8431420e 100644 --- a/src/core/component/state/index.ts +++ b/src/core/component/state/index.ts @@ -11,38 +11,9 @@ * @packageDocumentation */ -import watchObj from 'core/object/watch'; +import 'core/component/state/bindings'; -import { emitter as NetEmitter, NetStatus } from 'core/net'; -import { emitter as SessionEmitter, Session } from 'core/session'; - -import type { State } from 'core/component/state/interface'; +export { default } from 'core/component/state/const'; +export * from 'core/component/state/const'; export * from 'core/component/state/interface'; - -const watcher = watchObj({ - isAuth: undefined, - isOnline: undefined, - lastOnlineDate: undefined, - experiments: undefined -}); - -export default watcher.proxy; - -export const - watch = watchObj.bind(null, watcher.proxy), - set = watcher.set.bind(watcher), - unset = watcher.delete.bind(watcher); - -SessionEmitter.on('set', (e: Session) => { - set('isAuth', Boolean(e.auth)); -}); - -SessionEmitter.on('clear', () => { - set('isAuth', false); -}); - -NetEmitter.on('status', (netStatus: NetStatus) => { - set('isOnline', netStatus.status); - set('lastOnlineDate', netStatus.lastOnline); -}); From e585fd6c3eaf509da569b3b3b2a171c426bc388e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 18:07:36 +0300 Subject: [PATCH 0240/2313] :art: --- src/core/component/state/bindings.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/component/state/bindings.ts b/src/core/component/state/bindings.ts index 5f71e3ee97..8e59366432 100644 --- a/src/core/component/state/bindings.ts +++ b/src/core/component/state/bindings.ts @@ -6,19 +6,19 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { emitter as NetEmitter, NetStatus } from 'core/net'; -import { emitter as SessionEmitter, Session } from 'core/session'; +import { emitter as netEmitter, NetStatus } from 'core/net'; +import { emitter as sessionEmitter, Session } from 'core/session'; import { set } from 'core/component/state/const'; -SessionEmitter.on('set', (e: Session) => { +sessionEmitter.on('set', (e: Session) => { set('isAuth', Boolean(e.auth)); }); -SessionEmitter.on('clear', () => { +sessionEmitter.on('clear', () => { set('isAuth', false); }); -NetEmitter.on('status', (netStatus: NetStatus) => { +netEmitter.on('status', (netStatus: NetStatus) => { set('isOnline', netStatus.status); set('lastOnlineDate', netStatus.lastOnline); }); From 81cb45124796e6de26f2ffecd99b54a0e8e4c4eb Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 14 Jun 2022 18:25:20 +0300 Subject: [PATCH 0241/2313] refactor: reorganize the module structure --- .../event/{component-api.ts => component.ts} | 29 +++++++++---- src/core/component/event/emitter.ts | 2 +- src/core/component/event/index.ts | 42 +------------------ src/core/component/event/providers.ts | 29 +++++++++++++ src/core/component/event/providers/index.ts | 7 ---- 5 files changed, 52 insertions(+), 57 deletions(-) rename src/core/component/event/{component-api.ts => component.ts} (61%) create mode 100644 src/core/component/event/providers.ts delete mode 100644 src/core/component/event/providers/index.ts diff --git a/src/core/component/event/component-api.ts b/src/core/component/event/component.ts similarity index 61% rename from src/core/component/event/component-api.ts rename to src/core/component/event/component.ts index d6a42d7fc5..38959ed875 100644 --- a/src/core/component/event/component-api.ts +++ b/src/core/component/event/component.ts @@ -9,15 +9,26 @@ import { EventEmitter2 as EventEmitter } from 'eventemitter2'; import type { UnsafeComponentInterface } from 'core/component/interface'; +import emitter from 'core/component/event/emitter'; +import type { ResetType } from 'core/component/event/interface'; + +/** + * Sends a message to reset all components of the application + * @param [type] - the reset type + */ +export function reset(type?: ResetType): void { + emitter.emit(type != null ? `reset.${type}` : 'reset'); +} + /** - * Implements the base event API to a component instance - * @param obj + * Implements event emitter API for the specified component instance + * @param component */ -export function implementEventAPI(obj: object): void { +export function implementEventEmitterAPI(component: object): void { /* eslint-disable @typescript-eslint/typedef */ const - component = Object.cast(obj); + ctx = Object.cast(component); const $e = new EventEmitter({ maxListeners: 1e3, @@ -26,9 +37,9 @@ export function implementEventAPI(obj: object): void { }); const - nativeEmit = Object.cast>(component.$emit); + nativeEmit = Object.cast>(ctx.$emit); - Object.defineProperty(component, '$emit', { + Object.defineProperty(ctx, '$emit', { configurable: true, enumerable: false, writable: false, @@ -40,21 +51,21 @@ export function implementEventAPI(obj: object): void { } }); - Object.defineProperty(component, '$on', { + Object.defineProperty(ctx, '$on', { configurable: true, enumerable: false, writable: false, value: getMethod('on') }); - Object.defineProperty(component, '$once', { + Object.defineProperty(ctx, '$once', { configurable: true, enumerable: false, writable: false, value: getMethod('once') }); - Object.defineProperty(component, '$off', { + Object.defineProperty(ctx, '$off', { configurable: true, enumerable: false, writable: false, diff --git a/src/core/component/event/emitter.ts b/src/core/component/event/emitter.ts index 4d181599be..f912d940b8 100644 --- a/src/core/component/event/emitter.ts +++ b/src/core/component/event/emitter.ts @@ -10,7 +10,7 @@ import log from 'core/log'; import { EventEmitter2 as EventEmitter } from 'eventemitter2'; /** - * Event emitter to broadcast external events to components + * An event emitter to broadcast external events to components */ const emitter = new EventEmitter({ maxListeners: 1e3, diff --git a/src/core/component/event/index.ts b/src/core/component/event/index.ts index a0babc262e..94ffe137c0 100644 --- a/src/core/component/event/index.ts +++ b/src/core/component/event/index.ts @@ -11,47 +11,9 @@ * @packageDocumentation */ -import * as net from 'core/net'; -import * as i18n from 'core/i18n'; - -//#if runtime has core/session -import * as session from 'core/session'; -//#endif - -import emitter from 'core/component/event/emitter'; import 'core/component/event/providers'; -import type { ResetType } from 'core/component/event/interface'; +export { default as emitter } from 'core/component/event/emitter'; -export * from 'core/component/event/component-api'; +export * from 'core/component/event/component'; export * from 'core/component/event/interface'; - -/** - * Sends a message to reset all components of the application - * @param [type] - reset type - */ -export function reset(type?: ResetType): void { - emitter.emit(type != null ? `reset.${type}` : 'reset'); -} - -net.emitter.on('status', (...args) => { - emitter.emit('net.status', ...args); -}); - -//#if runtime has core/session - -session.emitter.on('set', (...args) => { - emitter.emit('session.set', ...args); -}); - -session.emitter.on('clear', (...args) => { - emitter.emit('session.clear', ...args); -}); - -//#endif - -i18n.emitter.on('setLocale', (...args) => { - emitter.emit('i18n.setLocale', ...args); -}); - -export default emitter; diff --git a/src/core/component/event/providers.ts b/src/core/component/event/providers.ts new file mode 100644 index 0000000000..933fd48bfc --- /dev/null +++ b/src/core/component/event/providers.ts @@ -0,0 +1,29 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { emitter as i18nEmitter } from 'core/i18n'; +import { emitter as netEmitter } from 'core/net'; +import { emitter as sessionEmitter } from 'core/session'; + +import emitter from 'core/component/event/emitter'; + +i18nEmitter.on('setLocale', (...args) => { + emitter.emit('i18n.setLocale', ...args); +}); + +netEmitter.on('status', (...args) => { + emitter.emit('net.status', ...args); +}); + +sessionEmitter.on('set', (...args) => { + emitter.emit('session.set', ...args); +}); + +sessionEmitter.on('clear', (...args) => { + emitter.emit('session.clear', ...args); +}); diff --git a/src/core/component/event/providers/index.ts b/src/core/component/event/providers/index.ts deleted file mode 100644 index e94b2ed4cb..0000000000 --- a/src/core/component/event/providers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ From d7d6da6c4aee1c7061cdc2364d4c02509d1fa196 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 12:14:58 +0300 Subject: [PATCH 0242/2313] :art: --- src/core/component/state/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/state/README.md b/src/core/component/state/README.md index e97a15d8ce..a55fa05bcc 100644 --- a/src/core/component/state/README.md +++ b/src/core/component/state/README.md @@ -20,7 +20,7 @@ watch('isAuth', (value, oldValue) => { set('newProp', someValue); ``` -## Embedded state +## Built-in state V4Fire supports out of the box integration with `core/session`, `core/net` and `core/abt` modules. From 273ba0b47f66efa3d85dd1069ecb76f5903bd793 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 13:23:51 +0300 Subject: [PATCH 0243/2313] chore: Methods -> Functions --- src/core/component/accessor/README.md | 2 +- src/core/component/context/README.md | 2 +- src/core/component/field/README.md | 2 +- src/core/component/method/README.md | 2 +- src/core/component/prop/README.md | 2 +- src/core/component/traverse/README.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/component/accessor/README.md b/src/core/component/accessor/README.md index fba9a51ef5..1d600d665e 100644 --- a/src/core/component/accessor/README.md +++ b/src/core/component/accessor/README.md @@ -10,7 +10,7 @@ After this, the first time you touch the accessor value it will be cached. To support cache invalidation or watching of changes, provide a list of dependencies of your accessor or use the `cache = 'auto'` option. -## Methods +## Functions ### attachAccessorsFromMeta diff --git a/src/core/component/context/README.md b/src/core/component/context/README.md index a02f327c69..6c7d5fc311 100644 --- a/src/core/component/context/README.md +++ b/src/core/component/context/README.md @@ -12,7 +12,7 @@ V4Fire must redefine some properties and methods from the engine. But since thes i.e. we can’t do simple property override. To solve these problems, this module was created. It provides API to create a new context object whose properties can be overridden. -## Methods +## Functions ### getComponentContext diff --git a/src/core/component/field/README.md b/src/core/component/field/README.md index 4f16788566..8f960f6ca5 100644 --- a/src/core/component/field/README.md +++ b/src/core/component/field/README.md @@ -11,7 +11,7 @@ Mind, changes in any system field still can be watched using built-in API. The second difference is that system fields are initialized on the `beforeCreate` hook, but not on the `created` hook like the regular fields do. -## Methods +## Functions ### initFields diff --git a/src/core/component/method/README.md b/src/core/component/method/README.md index 816497e616..190745daf6 100644 --- a/src/core/component/method/README.md +++ b/src/core/component/method/README.md @@ -2,7 +2,7 @@ This module provides API to initialize component methods to a component instance. -## Methods +## Functions ### attachMethodsFromMeta diff --git a/src/core/component/prop/README.md b/src/core/component/prop/README.md index b1f794034a..b5aadb1f15 100644 --- a/src/core/component/prop/README.md +++ b/src/core/component/prop/README.md @@ -2,7 +2,7 @@ This module provides API to initialize component props to a component instance. -## Methods +## Functions ### initProps diff --git a/src/core/component/traverse/README.md b/src/core/component/traverse/README.md index 150fe752ed..f865d9277e 100644 --- a/src/core/component/traverse/README.md +++ b/src/core/component/traverse/README.md @@ -2,7 +2,7 @@ This module provides a bunch of functions to iterate over a component vnode tree. -## Methods +## Functions ### getNormalParent From af5ed35149e3c253fb58d358b8c8a3e3677896bc Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 13:28:22 +0300 Subject: [PATCH 0244/2313] doc: added the normal documentation --- src/core/component/event/CHANGELOG.md | 6 ++ src/core/component/event/README.md | 88 ++++++++++++++++++++++++++- src/core/component/event/component.ts | 20 +++++- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/core/component/event/CHANGELOG.md b/src/core/component/event/CHANGELOG.md index 76418ffae2..f5cddf6d92 100644 --- a/src/core/component/event/CHANGELOG.md +++ b/src/core/component/event/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??)a + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.37 (2020-07-20) #### :house: Internal diff --git a/src/core/component/event/README.md b/src/core/component/event/README.md index 04c0932ea3..886e0584bf 100644 --- a/src/core/component/event/README.md +++ b/src/core/component/event/README.md @@ -1,4 +1,88 @@ # core/component/event -This module provides an event bridge between components and other part of the application. -Also, this module provides a bunch of functions to add base event API to a component instance. +This module provides the global event emitter for all components. Use this emitter to provide events of external modules to components. +Also, this module exposes a bunch of helper functions to implement and work with the component event API. + +## Usage + +```js +import { emitter, reset } from 'core/component/event'; + +// This event can be listen from any component +emitter.emit('reloadAllComponents'); + +// The helper function to fire the special event to reset components storages +reset('storage.silence'); +``` + +## Built-in events + +V4Fire supports out of the box integration with `core/session`, `core/net` and `core/i18n` modules. + +### i18n.setLocale + +This event fires when the application's global locale changes. +See the `core/i18n` module for details. + +### net.status + +This event fires when the Internet connection status changes. +See the `core/net` module for details. + +### session.set + +This event fires when the authorization status of an active user session changes. +See the `core/session` module for details. + +### session.clear + +This event fires when an active user session is reset. +See the `core/session` module for details. + +## API + +### Constants + +#### emitter + +An event emitter to broadcast external events to components. + +```js +import { emitter } from 'core/component/event'; + +emitter.emit('reloadAllComponents', {provider: true, storage: true}); +``` + +### Functions + +#### reset + +Emits the special event for all component to reset the passed component state. +By default, this means a complete reload of all providers and storages bound to components. +Also, you can provide one of several types of component resets: + +1. `'load'` - reloads all data providers bound to components; +2. `'load.silence'` - reloads all data providers bound to components, + but without changing components statuses to `loading`; + +3. `'router'` - resets all component bindings to the application router; +4. `'router.silence'` - resets all component bindings to the application router, + but without changing components statuses to `loading`; + +5. `'storage'` - reloads all storages bound to components; +6. `'storage'` - reloads all storages bound to components, + but without changing components statuses to `loading`; + +7. `'silence'` - reloads all providers and storages bound to components, + but without changing components statuses to `loading`. + +```js +import { reset } from 'core/component/event'; + +reset('load'); +reset('storage.silence'); +``` + +#### implementEventEmitterAPI + +Implements event emitter API for the specified component instance. diff --git a/src/core/component/event/component.ts b/src/core/component/event/component.ts index 38959ed875..ae3ec9be02 100644 --- a/src/core/component/event/component.ts +++ b/src/core/component/event/component.ts @@ -13,8 +13,24 @@ import emitter from 'core/component/event/emitter'; import type { ResetType } from 'core/component/event/interface'; /** - * Sends a message to reset all components of the application - * @param [type] - the reset type + * Emits the special event for all component to reset the passed component state. + * By default, this means a complete reload of all providers and storages bound to components. + * + * @param [type] - the reset type: + * 1. `'load'` - reloads all data providers bound to components; + * 2. `'load.silence'` - reloads all data providers bound to components, + * but without changing components statuses to `loading`; + * + * 3. `'router'` - resets all component bindings to the application router; + * 4. `'router.silence'` - resets all component bindings to the application router, + * but without changing components statuses to `loading`; + * + * 5. `'storage'` - reloads all storages bound to components; + * 6. `'storage'` - reloads all storages bound to components, + * but without changing components statuses to `loading`; + * + * 7. `'silence'` - reloads all providers and storages bound to components, + * but without changing components statuses to `loading`. */ export function reset(type?: ResetType): void { emitter.emit(type != null ? `reset.${type}` : 'reset'); From 2ebe1b457e4af3967bdfb3512bd3afdfebfb8028 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 13:57:06 +0300 Subject: [PATCH 0245/2313] chore: fixed grammar --- src/core/component/accessor/README.md | 2 +- src/core/component/context/README.md | 2 +- src/core/component/field/README.md | 2 +- src/core/component/method/README.md | 2 +- src/core/component/prop/README.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/component/accessor/README.md b/src/core/component/accessor/README.md index 1d600d665e..9c3b09704c 100644 --- a/src/core/component/accessor/README.md +++ b/src/core/component/accessor/README.md @@ -1,6 +1,6 @@ # core/component/accessor -This module provides API to initialize component accessors and computed fields to a component instance. +This module provides an API to initialize component accessors and computed fields to a component instance. ## What differences between accessors and computed fields? diff --git a/src/core/component/context/README.md b/src/core/component/context/README.md index 6c7d5fc311..142b8924ec 100644 --- a/src/core/component/context/README.md +++ b/src/core/component/context/README.md @@ -10,7 +10,7 @@ In order for a component library to be used as an engine for V4Fire, it must imp methods that are described in the [[ComponentInterface]] interface. Also, for the correct working of the entire platform, V4Fire must redefine some properties and methods from the engine. But since these properties can be marked as read-only, i.e. we can’t do simple property override. To solve these problems, this module was created. -It provides API to create a new context object whose properties can be overridden. +It provides an API to create a new context object whose properties can be overridden. ## Functions diff --git a/src/core/component/field/README.md b/src/core/component/field/README.md index 8f960f6ca5..921d8cd893 100644 --- a/src/core/component/field/README.md +++ b/src/core/component/field/README.md @@ -1,6 +1,6 @@ # core/component/field -This module provides API to initialize component fields to a component instance. +This module provides an API to initialize component fields to a component instance. ## What differences between fields and system fields? diff --git a/src/core/component/method/README.md b/src/core/component/method/README.md index 190745daf6..a87efc54a2 100644 --- a/src/core/component/method/README.md +++ b/src/core/component/method/README.md @@ -1,6 +1,6 @@ # core/component/method -This module provides API to initialize component methods to a component instance. +This module provides an API to initialize component methods to a component instance. ## Functions diff --git a/src/core/component/prop/README.md b/src/core/component/prop/README.md index b5aadb1f15..a2fc3d7deb 100644 --- a/src/core/component/prop/README.md +++ b/src/core/component/prop/README.md @@ -1,6 +1,6 @@ # core/component/prop -This module provides API to initialize component props to a component instance. +This module provides an API to initialize component props to a component instance. ## Functions From 6df7b90fc02cdac0a2189488e415cce9ef83dad3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 13:59:21 +0300 Subject: [PATCH 0246/2313] doc: added the normal documentation --- src/core/component/ref/CHANGELOG.md | 6 ++++++ src/core/component/ref/README.md | 18 +++++++++++++++++- src/core/component/ref/index.ts | 6 +++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/core/component/ref/CHANGELOG.md b/src/core/component/ref/CHANGELOG.md index 76418ffae2..4a1e89f8f4 100644 --- a/src/core/component/ref/CHANGELOG.md +++ b/src/core/component/ref/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.37 (2020-07-20) #### :house: Internal diff --git a/src/core/component/ref/README.md b/src/core/component/ref/README.md index ed698713e7..a07d7e11d1 100644 --- a/src/core/component/ref/README.md +++ b/src/core/component/ref/README.md @@ -1,3 +1,19 @@ # core/component/ref -This module provides API to resolve component refs. +This module provides an API to resolve component refs. + +## What problem is being solved? + +V4Fire defines its own component type, functional components. +Components of this type can use states, but any changes to them cannot cause re-render. +From the point of view of the used component library, such components are just some trees of virtual nodes. +So when we add a ref attribute to such a component, it will refer to the component DOM node. +This module provides an API that fixes this, i.e. refs will link to component contexts, just like regular components. + +## Functions + +### resolveRefs + +Resolves ref attributes from the specified component instance. +This function replaces refs from component DOM nodes to the component instance. +Also, this function fires events of refs appearances. diff --git a/src/core/component/ref/index.ts b/src/core/component/ref/index.ts index 07451b0a40..36907bf514 100644 --- a/src/core/component/ref/index.ts +++ b/src/core/component/ref/index.ts @@ -15,10 +15,10 @@ import { getComponentContext } from 'core/component/context'; import type { ComponentElement, ComponentInterface } from 'core/component/interface'; /** - * Resolves references from the specified component instance. + * Resolves `ref` attributes from the specified component instance. * - * This function replaces refs from component DOM nodes to component instances. - * Also, this function fires events of ref appearances. + * This function replaces refs from component DOM nodes to the component instance. + * Also, this function fires events of refs appearances. * * @param component */ From a406dc190cc26cf890587f827c375db2c4d54e46 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 14:02:05 +0300 Subject: [PATCH 0247/2313] doc: improved doc --- src/core/component/queue-emitter/CHANGELOG.md | 6 ++++ src/core/component/queue-emitter/README.md | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/core/component/queue-emitter/CHANGELOG.md b/src/core/component/queue-emitter/CHANGELOG.md index 76418ffae2..4a1e89f8f4 100644 --- a/src/core/component/queue-emitter/CHANGELOG.md +++ b/src/core/component/queue-emitter/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.37 (2020-07-20) #### :house: Internal diff --git a/src/core/component/queue-emitter/README.md b/src/core/component/queue-emitter/README.md index 58501c3cab..241a5baa76 100644 --- a/src/core/component/queue-emitter/README.md +++ b/src/core/component/queue-emitter/README.md @@ -25,3 +25,34 @@ eventEmitter.drain(); eventEmitter.emit('foo'); eventEmitter.emit('bar'); ``` + +## API + +### Properties + +#### queue + +A queue of event handlers that are ready to invoke. + +#### listeners + +A dictionary with tied event listeners that aren't ready to invoke. + +### Methods + +#### on + +Attaches a handler for the specified set of events. +The handler will be invoked only when all specified events are fired. + +#### emit + +Emits the specified event. +If at least one of handlers returns a promise, +the method returns a promise that will be resolved after all internal promises are resolved. + +#### drain + +Drains the queue of handlers that are ready to invoke. +If at least one of listeners returns a promise, +the method returns a promise that will be resolved after all internal promises are resolved. From 6522792425e57179b2a6528ef2194e8b983d77fe Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 14:38:13 +0300 Subject: [PATCH 0248/2313] doc: added the normal documentation --- src/core/component/reflect/CHANGELOG.md | 6 ++ src/core/component/reflect/README.md | 89 +++++++++++++++++++++++ src/core/component/reflect/constructor.ts | 10 +-- src/core/component/reflect/index.ts | 2 +- src/core/component/reflect/interface.ts | 56 +++++++------- src/core/component/reflect/mod.ts | 4 +- src/core/component/reflect/property.ts | 2 +- src/core/component/reflect/types.ts | 11 --- src/core/component/reflect/validators.ts | 17 +++++ 9 files changed, 150 insertions(+), 47 deletions(-) delete mode 100644 src/core/component/reflect/types.ts create mode 100644 src/core/component/reflect/validators.ts diff --git a/src/core/component/reflect/CHANGELOG.md b/src/core/component/reflect/CHANGELOG.md index 5f3e93f756..ecc1832c31 100644 --- a/src/core/component/reflect/CHANGELOG.md +++ b/src/core/component/reflect/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.211 (2021-07-21) #### :rocket: New Feature diff --git a/src/core/component/reflect/README.md b/src/core/component/reflect/README.md index 8fa42d9765..d583efa3cc 100644 --- a/src/core/component/reflect/README.md +++ b/src/core/component/reflect/README.md @@ -2,6 +2,95 @@ This module provides a bunch of functions to reflect component classes. +## Usage + +```js +@component() +class bButton extends iBlock { + static mods = { + 'opened-window': [ + true, + false, + undefined, + [false], + bButton.PARENT + ] + }; +} + +// {openedWindow: ['true', ['false'], bButton.PARENT]} +getComponentMods(getInfoFromConstructor()); +``` + +## API + +### Constants + +#### isSmartComponent + +A RegExp to check if the component name has the "smart" postfix + +#### isAbstractComponent + +A RegExp to check if the component name is abstract. + +### Functions + +#### getInfoFromConstructor + +Returns an object with information from the specified component constructor. + +```js +@component({functional: true}) +class bButton extends iBlock { + +} + +// { +// name: 'b-button', +// componentName: 'b-button', +// parent: iBlock, +// ... +// } +console.log(getInfoFromConstructor(bButton)); +``` + +#### getPropertyInfo + +Returns an information object of a component property by the specified path. + +```js +@component() +class bButton { + @system() + fooStore = {bla: 'bar'}; + + get foo() { + return this.fooStore; + } + + created() { + // { + // name: 'fooStore', + // path: 'fooStore.bar', + // fullPath: '$root.$refs.button.fooStore.bar', + // topPath: '$root.$refs.button.fooStore', + // originalPath: '$root.$refs.button.foo.bar', + // originalTopPath: '$root.$refs.button.foo', + // type: 'system', + // accessor: 'foo', + // accessorType: 'computed' + // } + console.log(getPropertyInfo('$root.$refs.button.foo.bar', this)); + } +} +``` + +#### getComponentMods + +Returns a dictionary with modifiers from the specified component. +This function takes the raw declaration of modifiers, normalizes it, and mixes with the design system modifiers (if there are specified). + ```js @component() class bButton extends iBlock { diff --git a/src/core/component/reflect/constructor.ts b/src/core/component/reflect/constructor.ts index 5e0d28239a..be9d307d17 100644 --- a/src/core/component/reflect/constructor.ts +++ b/src/core/component/reflect/constructor.ts @@ -7,7 +7,7 @@ */ import { componentParams, components } from 'core/component/const'; -import { isAbstractComponent, isSmartComponent } from 'core/component/reflect/types'; +import { isAbstractComponent, isSmartComponent } from 'core/component/reflect/validators'; import type { ComponentOptions, ComponentMeta, ComponentConstructor } from 'core/component/interface'; import type { ComponentConstructorInfo } from 'core/component/reflect/interface'; @@ -42,7 +42,7 @@ export function getComponentName(constructor: Function): string { * Returns an object with information from the specified component constructor * * @param constructor - * @param [declParams] - component declaration parameters + * @param [declParams] - the component declaration parameters * * @example * ```js @@ -57,7 +57,7 @@ export function getComponentName(constructor: Function): string { * // parent: iBlock, * // ... * // } - * getInfoFromConstructor(bButton); + * console.log(getInfoFromConstructor(bButton)); * ``` */ export function getInfoFromConstructor( @@ -71,7 +71,7 @@ export function getInfoFromConstructor( parent = Object.getPrototypeOf(constructor), parentParams = parent != null ? componentParams.get(parent) : undefined; - // Create an object with parameters of a component + // Create an object with the component parameters const params = parentParams != null ? { root: parentParams.root, @@ -88,7 +88,7 @@ export function getInfoFromConstructor( name }; - // Mix the "functional" parameter from a parent @component declaration + // Mix the "functional" parameter from the parent @component declaration if (parentParams) { let functional; diff --git a/src/core/component/reflect/index.ts b/src/core/component/reflect/index.ts index 6bf7b9da47..30d1187b4b 100644 --- a/src/core/component/reflect/index.ts +++ b/src/core/component/reflect/index.ts @@ -12,7 +12,7 @@ */ export * from 'core/component/reflect/const'; -export * from 'core/component/reflect/types'; +export * from 'core/component/reflect/validators'; export * from 'core/component/reflect/constructor'; export * from 'core/component/reflect/mod'; export * from 'core/component/reflect/property'; diff --git a/src/core/component/reflect/interface.ts b/src/core/component/reflect/interface.ts index aacef7e639..47b2ea3196 100644 --- a/src/core/component/reflect/interface.ts +++ b/src/core/component/reflect/interface.ts @@ -16,52 +16,54 @@ import type { } from 'core/component/interface'; /** - * Information of a component that can be taken from a constructor + * Information of a component that can be taken from it constructor */ export interface ComponentConstructorInfo { /** - * The full component name. - * If the component is smart the name can contain a `-functional` postfix. + * Full name of the component. + * If the component is smart, the name can contain a `-functional` postfix. */ name: string; /** - * Name of the component without special postfixes + * Component name without special postfixes */ componentName: string; /** - * True if the component is abstract, i.e., has an abstract `i` prefix within the name + * True if the component is abstract. + * That is, it has an abstract `i` prefix in its name. */ isAbstract: boolean; /** - * True if the component is smart, i.e., it is compiled as a functional component and as regular component + * True if the component is smart. + * That is, it compiles as a functional component and as a regular component. */ isSmart: boolean; /** - * Link to the component constructor + * A link to the component constructor */ constructor: ComponentConstructor; /** - * Map of component parameters that were provided to a `@component` decorator + * A dictionary with the component parameters that were provided to the `@component` decorator */ params: ComponentOptions; /** - * Link to a parent constructor + * A link to the parent component constructor */ parent?: Function; /** - * Map of parent component parameters that were provided to a `@component` decorator + * A dictionary with the parent component parameters that were provided to the `@component` decorator */ parentParams?: ComponentOptions; /** - * Link to a parent component meta object + * A link to the parent component meta object */ parentMeta?: ComponentMeta; } @@ -69,15 +71,15 @@ export interface ComponentConstructorInfo { /** * Available types of a property accessor: * - * 1. computed - the cached type; - * 2. accessor - the non-cached type. + * 1. `computed` - the cached type; + * 2. `accessor` - the non-cached type. */ export type AccessorType = 'computed' | 'accessor'; /** - * Available types of an own component properties + * Available types of own component properties */ export type PropertyType = 'prop' | @@ -87,11 +89,11 @@ export type PropertyType = AccessorType; /** - * Common information of a component property + * The common information of a component property */ export interface CommonPropertyInfo { /** - * The top property name from a component + * Top property name relative to a component that owns the property * * @example * ```js @@ -101,7 +103,7 @@ export interface CommonPropertyInfo { name: string; /** - * Normalized property path relative the component that owns this property + * Normalized property path relative to a component that owns the property * * @example * ```js @@ -121,7 +123,7 @@ export interface CommonPropertyInfo { fullPath: string; /** - * Normalized path to the top property from a component + * Normalized path to a top property relative a component that owns the property * * @example * ```js @@ -131,7 +133,7 @@ export interface CommonPropertyInfo { topPath: string; /** - * Original path to the property + * The original path to the property * * @example * ```js @@ -141,7 +143,7 @@ export interface CommonPropertyInfo { originalPath: string; /** - * Original path to the top property from a component + * The original path to a top property from a component that owns the property * * @example * ```js @@ -151,12 +153,12 @@ export interface CommonPropertyInfo { originalTopPath: string; /** - * Name of accessor that is tied with the property + * An accessor name that is tied with the property */ accessor?: string; /** - * Type of accessor that is tied with the property + * An accessor type that is tied with the property */ accessorType?: AccessorType; } @@ -166,12 +168,12 @@ export interface CommonPropertyInfo { */ export interface ComponentPropertyInfo extends CommonPropertyInfo { /** - * Property type + * The property type */ type: PropertyType; /** - * Link to a context of the property: the component that owns this property + * A link to the component that owns this property * * @example * ```js @@ -183,16 +185,16 @@ export interface ComponentPropertyInfo extends CommonPropertyInfo { /** * Information of a mounted component property. - * The mounted property it's the special kind of component property that refers to another watchable object. + * The mounted properties are a special kind of component properties that refer to other watchable objects. */ export interface MountedPropertyInfo extends CommonPropertyInfo { /** - * Property type + * The property type */ type: 'mounted'; /** - * Link to a context of the property: the raw watchable object that mounted to the property + * A link to the raw watchable object that mounted to the property */ ctx: object; } diff --git a/src/core/component/reflect/mod.ts b/src/core/component/reflect/mod.ts index 30eadb8902..4f7a2607b7 100644 --- a/src/core/component/reflect/mod.ts +++ b/src/core/component/reflect/mod.ts @@ -12,11 +12,11 @@ import type { ModsDecl } from 'core/component/interface'; import type { ComponentConstructorInfo } from 'core/component/reflect/interface'; /** - * Returns a map of component modifiers from the specified component. + * Returns a dictionary with modifiers from the specified component. * This function takes the raw declaration of modifiers, normalizes it, and mixes with the design system modifiers * (if there are specified). * - * @param component - information object of the component + * @param component - the component information object * * @example * ```js diff --git a/src/core/component/reflect/property.ts b/src/core/component/reflect/property.ts index 2f1ec24c76..e05e894c81 100644 --- a/src/core/component/reflect/property.ts +++ b/src/core/component/reflect/property.ts @@ -16,7 +16,7 @@ import type { PropertyInfo } from 'core/component/reflect/interface'; * Returns an information object of a component property by the specified path * * @param path - * @param component - component instance + * @param component - the tied component instance * * @example * ```js diff --git a/src/core/component/reflect/types.ts b/src/core/component/reflect/types.ts deleted file mode 100644 index 445e35e94b..0000000000 --- a/src/core/component/reflect/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -export const - isSmartComponent = /-functional$/, - isAbstractComponent = /^[iv]-/; diff --git a/src/core/component/reflect/validators.ts b/src/core/component/reflect/validators.ts new file mode 100644 index 0000000000..2bfeb06cb3 --- /dev/null +++ b/src/core/component/reflect/validators.ts @@ -0,0 +1,17 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +/** + * A RegExp to check if the component name has the "smart" postfix + */ +export const isSmartComponent = /-functional$/; + +/** + * A RegExp to check if the component name is abstract + */ +export const isAbstractComponent = /^[iv]-/; From ce1371abaac85033e18b302202f33b5f1f77471d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 14:48:45 +0300 Subject: [PATCH 0249/2313] doc: added the normal documentation --- src/core/component/hook/CHANGELOG.md | 4 ++++ src/core/component/hook/README.md | 15 +++++++++++++-- src/core/component/hook/index.ts | 14 ++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/core/component/hook/CHANGELOG.md b/src/core/component/hook/CHANGELOG.md index 9c0ab065e4..3ed9aba033 100644 --- a/src/core/component/hook/CHANGELOG.md +++ b/src/core/component/hook/CHANGELOG.md @@ -11,6 +11,10 @@ Changelog ## v3.??.?? (2022-??-??) +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * Refactoring diff --git a/src/core/component/hook/README.md b/src/core/component/hook/README.md index 39d6095ae1..5d5e70e011 100644 --- a/src/core/component/hook/README.md +++ b/src/core/component/hook/README.md @@ -1,3 +1,14 @@ -# core/component/hooks +# core/component/hook -This module provides API to manage component hooks. +This module provides an API to manage component hooks. + +## Functions + +### runHook + +Runs a hook on the specified component instance. +The function returns a promise that is resolved when all hook handlers are executed. + +```js +runHook('beforeCreate', component).then(() => console.log('Done!')); +``` diff --git a/src/core/component/hook/index.ts b/src/core/component/hook/index.ts index eb56970272..b0f714a885 100644 --- a/src/core/component/hook/index.ts +++ b/src/core/component/hook/index.ts @@ -20,11 +20,17 @@ const resolvedPromise = SyncPromise.resolve(); /** - * Runs a component hook from the specified component instance + * Runs a hook on the specified component instance. + * The function returns a promise that is resolved when all hook handlers are executed. * - * @param hook - hook name - * @param component - component instance - * @param args - hook arguments + * @param hook - the hook name to run + * @param component - the tied component instance + * @param args - the hook arguments + * + * @example + * ```js + * runHook('beforeCreate', component).then(() => console.log('Done!')); + * ``` */ export function runHook(hook: Hook, component: ComponentInterface, ...args: unknown[]): Promise { const unsafe = Object.cast>( From a333975fd32274fb8aa5e2e232df5de6d6817eca Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 15:59:09 +0300 Subject: [PATCH 0250/2313] doc: added the normal documentation --- src/core/component/meta/CHANGELOG.md | 4 ++ src/core/component/meta/README.md | 2 +- src/core/component/meta/create.ts | 6 +- src/core/component/meta/fill.ts | 4 +- src/core/component/meta/inherit.ts | 2 +- src/core/component/meta/interface/index.ts | 73 ++++++++++---------- src/core/component/meta/interface/options.ts | 43 ++++++------ src/core/component/meta/interface/types.ts | 9 --- src/core/component/meta/method.ts | 6 +- src/core/component/meta/tpl.ts | 6 +- 10 files changed, 72 insertions(+), 83 deletions(-) diff --git a/src/core/component/meta/CHANGELOG.md b/src/core/component/meta/CHANGELOG.md index 2e3d8ae542..2ef073d199 100644 --- a/src/core/component/meta/CHANGELOG.md +++ b/src/core/component/meta/CHANGELOG.md @@ -15,6 +15,10 @@ Changelog * Added support for cache delegation of computed fields +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * Refactoring diff --git a/src/core/component/meta/README.md b/src/core/component/meta/README.md index 22ea80e2c8..ee71d36c79 100644 --- a/src/core/component/meta/README.md +++ b/src/core/component/meta/README.md @@ -1,3 +1,3 @@ # core/component/meta -This module provides API to create an abstract representation of a component. +This module provides an API to create an abstract representation of a component. diff --git a/src/core/component/meta/create.ts b/src/core/component/meta/create.ts index ce3865f41b..039bee57e5 100644 --- a/src/core/component/meta/create.ts +++ b/src/core/component/meta/create.ts @@ -14,7 +14,7 @@ import type { ComponentMeta, ComponentConstructorInfo } from 'core/component/int /** * Creates a meta object for the specified component and returns it - * @param component - component constructor info + * @param component - the component constructor info */ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { const meta: ComponentMeta = { @@ -68,9 +68,9 @@ export function createMeta(component: ComponentConstructorInfo): ComponentMeta { props: {}, computed: {}, methods: {}, - render: (() => { + render() { throw new ReferenceError(`A render function for the component "${component.componentName}" is not specified`); - }) + } } }; diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 5099dd9acf..8c8b5ce140 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -8,7 +8,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { defaultWrapper } from 'core/component/const'; +import { DEFAULT_WRAPPER } from 'core/component/const'; import { getComponentContext } from 'core/component/context'; import { isAbstractComponent, bindingRgxp } from 'core/component/reflect'; @@ -82,7 +82,7 @@ export function fillMeta( if (def != null && typeof def === 'object' && (!isTypeCanBeFunc(prop.type) || !Object.isFunction(def))) { defWrapper = () => Object.fastClone(def); - defWrapper[defaultWrapper] = true; + defWrapper[DEFAULT_WRAPPER] = true; } } diff --git a/src/core/component/meta/inherit.ts b/src/core/component/meta/inherit.ts index 4f09a74f1f..d844982ed8 100644 --- a/src/core/component/meta/inherit.ts +++ b/src/core/component/meta/inherit.ts @@ -10,7 +10,7 @@ import { metaPointers, PARENT } from 'core/component/const'; import type { ComponentMeta, ModDeclVal } from 'core/component/interface'; /** - * Inherits the specified meta object from another meta object. + * Inherits the specified meta object from other one. * The function modifies the original object and returns it. * * @param meta diff --git a/src/core/component/meta/interface/index.ts b/src/core/component/meta/interface/index.ts index 12bce05c61..f627f55780 100644 --- a/src/core/component/meta/interface/index.ts +++ b/src/core/component/meta/interface/index.ts @@ -31,148 +31,145 @@ export * from 'core/component/meta/interface/options'; export * from 'core/component/meta/interface/types'; /** - * Abstract representation of a component + * An abstract component representation */ export interface ComponentMeta { /** - * The full component name. - * If the component is smart the name can contain a `-functional` postfix. + * Full name of the component. + * If the component is smart, the name can contain a `-functional` postfix. */ name: string; /** - * Name of the component without special postfixes + * Component name without special postfixes */ componentName: string; /** - * Link to the component constructor + * A link to the component constructor */ constructor: ComponentConstructor; /** - * Link to a component instance + * A link to a component class instance */ instance: Dictionary; /** - * Map of component parameters that were provided to a `@component` decorator + * A dictionary with the component parameters that were provided to the `@component` decorator */ params: ComponentOptions; /** - * Link to a parent component meta object + * A link to the parent component meta object */ parentMeta?: ComponentMeta; /** - * Map of component input properties + * A dictionary with the component input properties, aka "props" */ props: Dictionary; /** - * Map of available component modifiers + * A dictionary with the available component modifiers */ mods: ModsDecl; /** - * Map of component fields that can force re-rendering + * A dictionary with the component fields that can force re-rendering */ fields: Dictionary; /** - * Map of component fields that can't force re-rendering + * A dictionary with the component fields that can't force re-rendering */ systemFields: Dictionary; /** - * Map of component computed fields with support of caching + * A dictionary with the component fields that contains the `Store` postfix */ - computedFields: Dictionary; + tiedFields: Dictionary; /** - * Map of component fields that contains the `Store` postfix + * A dictionary with the component accessors with the support of caching/watching */ - tiedFields: Dictionary; + computedFields: Dictionary; /** - * Map of component accessors + * A dictionary with the simple component accessors */ accessors: Dictionary; /** - * Map of component methods + * A dictionary with the component methods */ methods: Dictionary; /** - * Map of component watchers + * A dictionary with the component watchers */ watchers: Dictionary; /** - * Map of dependencies to watch (to invalidate cache of computed fields) + * A dictionary with the component dependencies to watch + * (to invalidate the cache of computed fields) */ watchDependencies: ComponentWatchDependencies; /** - * Map of prop dependencies to watch (to invalidate cache of computed fields) + * A dictionary with the component prop dependencies to watch + * (to invalidate the cache of computed fields) */ watchPropDependencies: ComponentWatchPropDependencies; /** - * Map of component hook listeners + * A dictionary with the component hook listeners */ hooks: ComponentHooks; /** - * Less abstract representation of the component. - * This representation is more useful to provide to a component library. + * A less abstract representation of the component. + * This structure is more useful for a component library. */ component: { /** - * The full component name. - * If the component is smart the name can contain a `-functional` postfix. + * Full name of the component. + * If the component is smart, the name can contain a `-functional` postfix. */ name: string; /** - * Map of default component modifiers + * A dictionary with the default component modifiers */ mods: Dictionary; /** - * Map of component input properties + * A dictionary with the component input properties, aka "props" */ props: Dictionary; /** - * Map of component computed fields + * A dictionary with the component computed fields */ computed: Dictionary>>; /** - * Map of component methods + * A dictionary with the component methods */ methods: Dictionary; /** - * Map of available component filters - */ - filters?: Dictionary; - - /** - * Map of available component directives + * A dictionary with the available component directives */ directives?: Dictionary; /** - * Map of available local components + * A dictionary with the available local components */ components?: Dictionary; /** - * Component render function + * The component render function */ render: RenderFunction; }; diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts index 1dc9e31d8e..cca5e42c4f 100644 --- a/src/core/component/meta/interface/options.ts +++ b/src/core/component/meta/interface/options.ts @@ -12,8 +12,8 @@ export interface ComponentOptions { /** * The component name. - * If the name isn't specified, it will be taken from a class name by using reflection. - * This parameter can't be inherited from a parent. + * If the name isn't specified, it will be taken from the tied class name by using reflection. + * This parameter can't be inherited from the parent component. * * @example * ```typescript @@ -33,11 +33,11 @@ export interface ComponentOptions { name?: string; /** - * If true, then the component is registered as a root component. - * The root component is the top of components hierarchy, i.e. it contains all components in an application. + * If true, then the component is registered as the root component. + * The root component is the top of components hierarchy, i.e. it contains all components in our application. * * All components, even the root component, have a link to the root component. - * This parameter can be inherited from a parent. + * This parameter can be inherited from the parent component. * * @default `false` * @@ -52,18 +52,15 @@ export interface ComponentOptions { root?: boolean; /** - * If false, then the component won't load an external template. - * It will use the default loopback render function. - * - * It is useful for components without templates. - * This parameter can be inherited from a parent. + * If false, then the component will use the default loopback render function, instead of loading the own template. + * This parameter is useful for components without templates, and it can be inherited from the parent component. * * @default `true` */ tpl?: boolean; /** - * The functional mode: + * The component functional mode: * * 1. If true, the component will be created as a functional component. * 2. If a dictionary, the component can be created as a functional component or regular component, depending on @@ -71,21 +68,21 @@ export interface ComponentOptions { * 1. If an empty dictionary, the component will always created as functional. * 2. If a dictionary with values, the dictionary properties represent component input properties. * If the component invocation takes these properties with the values that - * declared within "functional" parameters, it will be created as a functional. + * declared within "functional" parameters, it will be created as functional. * Also, you can specify multiple values of one input property by using a list of values. * Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, - * but you can directly cast a type by using "v-func" directive. - * 3. If null, all components watchers and listeners that directly specified in a class won't - * be attached to a functional component. It is useful to create superclass behaviour depending + * but you can directly cast a type by using the "v-func" directive. + * 3. If null, all components watchers and listeners that directly specified in the component class won't + * be attached to a functional-kind component. It is useful to create the superclass behaviour depending * on a component type. * * A functional component is a component can be rendered once only from input properties. - * This type of components have a state and lifecycle hooks, but mutation of the state doesn't force rendering. + * This type of components have a state and lifecycle hooks, but mutation of the state doesn't force re-rendering. * Usually, functional components lighter than regular components with the first render, * but avoid their if you have long animations within a component or if you need to frequent re-draws some deep * structure of nested components. * - * This parameter can be inherited from a parent, but the `null` value isn't inherited. + * This parameter can be inherited from the parent component, but the `null` value isn't inherited. * * @default `false` * @@ -119,9 +116,9 @@ export interface ComponentOptions { functional?: Nullable | Dictionary; /** - * A map of deprecated props with specified alternatives. - * The map keys represent deprecated props; the values represent alternatives. - * This parameter can be inherited from a parent. + * A dictionary with the deprecated component props with specified alternatives. + * The keys represent deprecated props; the values represent alternatives. + * This parameter can be inherited from the parent component. * * @example * ```typescript @@ -145,7 +142,7 @@ export interface ComponentOptions { * If true, then the component input properties that aren't registered as props * will be attached to a component node as attributes. * - * This parameter can be inherited from a parent. + * This parameter can be inherited from the parent component. * * @default `true` * @@ -166,7 +163,7 @@ export interface ComponentOptions { /** * If true, then the component is automatically inherited base modifiers from its parent. - * This parameter can be inherited from a parent. + * This parameter can be inherited from the parent component. * * @default `true` */ @@ -174,7 +171,7 @@ export interface ComponentOptions { /** * If false, then all default values of the component input properties are ignored. - * This parameter can be inherited from a parent. + * This parameter can be inherited from the parent component. * * @default `true` */ diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index ad3937824e..89ad0ed78a 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -13,11 +13,6 @@ import type { PropOptions, InitFieldFn, MergeFieldFn, UniqueFieldFn } from 'core import type { WritableComputedOptions, DirectiveBinding } from 'core/component/engines'; import type { ComponentInterface, FieldWatcher, MethodWatcher } from 'core/component/interface'; -import type { ComponentOptions } from 'core/component/meta/interface/options'; - -export type ComponentInfo = ComponentOptions & { - name: string; -}; export interface ComponentProp extends PropOptions { watchers: Map; @@ -44,10 +39,6 @@ export interface ComponentField extends Partial> { - -} - export type ComponentAccessorCacheType = boolean | 'auto'; diff --git a/src/core/component/meta/method.ts b/src/core/component/meta/method.ts index 77520112ef..6616d00816 100644 --- a/src/core/component/meta/method.ts +++ b/src/core/component/meta/method.ts @@ -10,7 +10,7 @@ import { defProp } from 'core/const/props'; import type { ComponentMeta } from 'core/component/interface'; /** - * Iterates over a prototype of a component constructor and adds methods/accessors to the passed meta object + * Iterates over a prototype of the passed component constructor and adds methods/accessors to the specified meta object * * @param meta * @param [constructor] @@ -106,7 +106,7 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me // eslint-disable-next-line @typescript-eslint/unbound-method get = desc.get ?? old?.get; - // To use `super` within a setter we also create a method with a name `${key}Setter` + // To use `super` within the setter we also create a new method with a name `${key}Setter` if (set != null) { const k = `${key}Setter`; @@ -120,7 +120,7 @@ export function addMethodsToMeta(meta: ComponentMeta, constructor: Function = me }; } - // To using `super` within a getter we also create a method with a name `${key}Getter` + // To using `super` within the getter we also create a new method with a name `${key}Getter` if (get != null) { const k = `${key}Getter`; diff --git a/src/core/component/meta/tpl.ts b/src/core/component/meta/tpl.ts index 5005dc3a31..835c83c041 100644 --- a/src/core/component/meta/tpl.ts +++ b/src/core/component/meta/tpl.ts @@ -9,7 +9,7 @@ // @ts-ignore (ss import) import * as defTpls from 'core/block.ss'; -import { componentTemplates } from 'core/component/const'; +import { componentRenderFactories } from 'core/component/const'; import type { ComponentMeta } from 'core/component/interface'; /** @@ -37,8 +37,8 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v return; } - const renderObj = componentTemplates[meta.componentName] ?? tpls.index(); - componentTemplates[meta.componentName] = renderObj; + const renderObj = componentRenderFactories[meta.componentName] ?? tpls.index(); + componentRenderFactories[meta.componentName] = renderObj; methods.render = { wrapper: true, From fdd84df7ea5e82ec1393b926131dd2df300d310e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 16:00:36 +0300 Subject: [PATCH 0251/2313] :art: --- src/core/component/reflect/README.md | 2 +- src/core/component/reflect/mod.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/component/reflect/README.md b/src/core/component/reflect/README.md index d583efa3cc..74ef1131c4 100644 --- a/src/core/component/reflect/README.md +++ b/src/core/component/reflect/README.md @@ -106,5 +106,5 @@ class bButton extends iBlock { } // {openedWindow: ['true', ['false'], bButton.PARENT]} -getComponentMods(getInfoFromConstructor()); +console.log(getComponentMods(getInfoFromConstructor())); ``` diff --git a/src/core/component/reflect/mod.ts b/src/core/component/reflect/mod.ts index 4f7a2607b7..d25dd8e88b 100644 --- a/src/core/component/reflect/mod.ts +++ b/src/core/component/reflect/mod.ts @@ -34,7 +34,7 @@ import type { ComponentConstructorInfo } from 'core/component/reflect/interface' * } * * // {openedWindow: ['true', ['false'], bButton.PARENT]} - * getComponentMods(getInfoFromConstructor()); + * console.log(getComponentMods(getInfoFromConstructor())); * ``` */ export function getComponentMods(component: ComponentConstructorInfo): ModsDecl { From b928bcdaed4914a3448c670b71239bd4ae55c75e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 16:20:03 +0300 Subject: [PATCH 0252/2313] doc: improved doc --- src/core/component/meta/README.md | 30 +++++++++++++++++++++++++++++- src/core/component/meta/tpl.ts | 6 +++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/core/component/meta/README.md b/src/core/component/meta/README.md index ee71d36c79..87ceedb774 100644 --- a/src/core/component/meta/README.md +++ b/src/core/component/meta/README.md @@ -1,3 +1,31 @@ # core/component/meta -This module provides an API to create an abstract representation of a component. +This module provides API to create an abstract representation of the component. +This structure is used by component library adapters to register "real" components such as Vue or React components. + +## Functions + +### createMeta + +Creates a meta object for the specified component and returns it. + +### forkMeta + +Creates a new meta object based on the specified. + +### inheritMeta + +Inherits the specified meta object from other one. +The function modifies the original object and returns it. + +### fillMeta + +Fills the passed meta object with methods and properties from the specified component class constructor. + +### addMethodsToMeta + +Iterates over a prototype of the passed component constructor and adds methods/accessors to the specified meta object. + +### attachTemplatesToMeta + +Attaches templates to the specified meta object. diff --git a/src/core/component/meta/tpl.ts b/src/core/component/meta/tpl.ts index 835c83c041..bcc60d2c49 100644 --- a/src/core/component/meta/tpl.ts +++ b/src/core/component/meta/tpl.ts @@ -15,8 +15,8 @@ import type { ComponentMeta } from 'core/component/interface'; /** * Attaches templates to the specified meta object * - * @param meta - component meta object - * @param [tpls] - dictionary with templates + * @param meta - the component meta object + * @param [tpls] - a dictionary with the registered templates */ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): void { const @@ -29,7 +29,7 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v // In this case, we don't automatically attach a render function if (meta.params.tpl === false) { - // A loopback render function + // The loopback render function return attachTemplatesToMeta(meta, defTpls.block); } From 430908386b175fdc6721d940478ab087cd8336e8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 15 Jun 2022 16:40:52 +0300 Subject: [PATCH 0253/2313] refactor: removed legacy --- src/core/component/interface/README.md | 3 +++ .../component/interface/component/component.ts | 17 ++++++----------- src/core/component/interface/component/types.ts | 12 ------------ .../component/interface/component/unsafe.ts | 12 +++--------- src/core/component/interface/engine.ts | 10 ++++++++++ src/core/component/interface/index.ts | 5 +++++ 6 files changed, 27 insertions(+), 32 deletions(-) create mode 100644 src/core/component/interface/README.md diff --git a/src/core/component/interface/README.md b/src/core/component/interface/README.md new file mode 100644 index 0000000000..3a601460ed --- /dev/null +++ b/src/core/component/interface/README.md @@ -0,0 +1,3 @@ +# core/component/interface + +This module provides a bunch of interfaces to work with components. diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 30bf20ae5c..195e91fd04 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -211,11 +211,6 @@ export abstract class ComponentInterface { */ protected readonly $attrs!: Dictionary; - /** - * A dictionary with external listeners of the component events - */ - protected readonly $listeners!: Dictionary>; - /** * A dictionary with references to component elements that have the "ref" attribute */ @@ -419,10 +414,10 @@ export abstract class ComponentInterface { } /** - * Hook handler: the component has been bound + * Hook handler: the component has been created * (only for functional components) */ - protected onBindHook(): void { + protected onCreatedHook(): void { // Loopback } @@ -430,7 +425,7 @@ export abstract class ComponentInterface { * Hook handler: the component has been mounted * (only for functional components) */ - protected onInsertedHook(): void { + protected onMountedHook(): void { // Loopback } @@ -438,15 +433,15 @@ export abstract class ComponentInterface { * Hook handler: the component has been updated * (only for functional components) */ - protected onUpdateHook(): void { + protected onUpdatedHook(): void { // Loopback } /** - * Hook handler: the component has been unbound + * Hook handler: the component has been unmounted * (only for functional components) */ - protected onUnbindHook(): void { + protected onUnmountedHook(): void { // Loopback } } diff --git a/src/core/component/interface/component/types.ts b/src/core/component/interface/component/types.ts index 50d5629ee0..3275f9051f 100644 --- a/src/core/component/interface/component/types.ts +++ b/src/core/component/interface/component/types.ts @@ -6,8 +6,6 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ComponentMeta } from 'core/component/meta'; - /** * A component constructor function */ @@ -21,13 +19,3 @@ export interface ComponentConstructor { export type ComponentElement = Element & { component?: T; }; - -/** - * The base context of a functional component - */ -export interface FunctionalCtx { - componentName: string; - meta: ComponentMeta; - instance: Dictionary; - $options: Dictionary; -} diff --git a/src/core/component/interface/component/unsafe.ts b/src/core/component/interface/component/unsafe.ts index 713533d188..887e4f188c 100644 --- a/src/core/component/interface/component/unsafe.ts +++ b/src/core/component/interface/component/unsafe.ts @@ -46,9 +46,6 @@ export interface UnsafeComponentInterface { r: RenderAPI; } +export interface RenderFactory { + (ctx: ComponentInterface, cache: unknown[]): () => CanArray; +} + +export interface RenderFn { + (bindings?: Dictionary): CanArray; +} + export interface RenderAPI { render(vnode: VNode, parent?: ComponentInterface): Node; render(vnode: VNode[], parent?: ComponentInterface): Node[]; @@ -103,6 +111,8 @@ export interface RenderAPI { resolveDirective: typeof resolveDirective; withCtx: typeof withCtx; + withAsyncContext(awaitable: T): [Awaited>, Function]; + withKeys: typeof withKeys; withModifiers: typeof withModifiers; withDirectives: typeof withDirectives; diff --git a/src/core/component/interface/index.ts b/src/core/component/interface/index.ts index a2b4af5e55..a2f15e351b 100644 --- a/src/core/component/interface/index.ts +++ b/src/core/component/interface/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/interface/README.md]] + * @packageDocumentation + */ + export * from 'core/component/interface/component'; export * from 'core/component/meta/interface'; export * from 'core/component/reflect/interface'; From ddc78d0492cfab3c037b6cd2c613d8cef2b642e6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 12:48:03 +0300 Subject: [PATCH 0254/2313] refactor: review base component interface --- .../interface/component/component.ts | 70 +++++++++---------- src/core/component/interface/index.ts | 2 +- .../interface/{life-cycle.ts => lc.ts} | 0 src/core/component/interface/mod.ts | 10 +++ src/super/i-block/i-block.ss | 8 +-- src/super/i-block/i-block.ts | 46 +++++------- src/super/i-block/modules/dom/index.ts | 56 --------------- src/super/i-block/modules/mods/interface.ts | 14 +--- src/super/i-block/modules/provide/README.md | 4 +- src/super/i-block/modules/provide/index.ts | 16 ++--- 10 files changed, 76 insertions(+), 150 deletions(-) rename src/core/component/interface/{life-cycle.ts => lc.ts} (100%) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 195e91fd04..1f370be0eb 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -19,22 +19,15 @@ import type Async from 'core/async'; import type { BoundFn, ProxyCb } from 'core/async'; import type { Slots, ComponentOptions } from 'core/component/engines'; +import type { ComponentMeta } from 'core/component/meta'; -import type { - - Hook, - ComponentMeta, - SyncLinkCache, - - WatchPath, - WatchOptions, - RawWatchHandler, - - RenderEngine - -} from 'core/component/interface'; +import type { Hook } from 'core/component/interface/lc'; +import type { ModsProp, ModsDict } from 'core/component/interface/mod'; +import type { SyncLinkCache } from 'core/component/interface/link'; +import type { RenderEngine } from 'core/component/interface/engine'; import type { ComponentElement } from 'core/component/interface/component/types'; +import type { WatchPath, WatchOptions, RawWatchHandler } from 'core/component/interface/watch'; import type { UnsafeGetter, UnsafeComponentInterface } from 'core/component/interface/component/unsafe'; /** @@ -68,6 +61,21 @@ export abstract class ComponentInterface { */ readonly instance!: this; + /** + * Additional modifiers for the component. + * Modifiers allow binding component state properties directly to CSS classes without + * unnecessary re-rendering of a component. + */ + abstract readonly modsProp?: ModsProp; + + /** + * Shareable component modifiers. + * These modifiers are automatically provided to all child components. + * So, for example, you have a component that uses another component within your template, + * and you specify to the outer component some theme modifier. + */ + abstract get shareableMods(): CanUndef>; + /** * Additional classes for component elements. * This option can be useful if you need to attach some extra classes to the internal component elements. @@ -84,7 +92,7 @@ export abstract class ComponentInterface { * } * ``` */ - readonly classes?: Dictionary>; + abstract readonly classes?: Dictionary>; /** * Additional styles for component elements. @@ -103,14 +111,12 @@ export abstract class ComponentInterface { * } * ``` */ - readonly styles?: Dictionary | Dictionary>; + abstract readonly styles?: Dictionary | Dictionary>; /** * The active component hook name */ - get hook(): Hook { - return 'beforeRuntime'; - } + abstract get hook(): Hook; /** * Switches the component to a new hook @@ -119,9 +125,7 @@ export abstract class ComponentInterface { * @emits `componentHook:{$value}(value: Hook, oldValue: Hook)` * @emits `componentHookChange(value: Hook, oldValue: Hook) */ - protected set hook(value: Hook) { - // Loopback - } + protected abstract set hook(value: Hook); /** * An API for unsafely invoking of some internal properties of the component. @@ -182,7 +186,7 @@ export abstract class ComponentInterface { /** * A number that is incremented each time the component is re-rendered */ - protected renderCounter!: number; + protected abstract renderCounter: number; /** * A temporary string identifier of the component @@ -258,7 +262,7 @@ export abstract class ComponentInterface { * @param ctxOrOpts - the logging context or logging options * @param [details] - event details */ - log?(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void; + abstract log?(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void; /** * Activates the component. @@ -269,7 +273,7 @@ export abstract class ComponentInterface { * * @param [force] - if true, then the component will be forced to activate, even if it's already activated */ - activate(force?: boolean): void {} + abstract activate(force?: boolean): void; /** * Deactivates the component. @@ -278,7 +282,7 @@ export abstract class ComponentInterface { * Basically, you don't need to think about the component activation, * because it's automatically synchronized with `keep-alive` or the component prop. */ - deactivate(): void {} + abstract deactivate(): void; /** * Forces the component to re-render @@ -417,31 +421,23 @@ export abstract class ComponentInterface { * Hook handler: the component has been created * (only for functional components) */ - protected onCreatedHook(): void { - // Loopback - } + protected abstract onCreatedHook(): void; /** * Hook handler: the component has been mounted * (only for functional components) */ - protected onMountedHook(): void { - // Loopback - } + protected abstract onMountedHook(): void; /** * Hook handler: the component has been updated * (only for functional components) */ - protected onUpdatedHook(): void { - // Loopback - } + protected abstract onUpdatedHook(): void; /** * Hook handler: the component has been unmounted * (only for functional components) */ - protected onUnmountedHook(): void { - // Loopback - } + protected abstract onUnmountedHook(): void; } diff --git a/src/core/component/interface/index.ts b/src/core/component/interface/index.ts index a2f15e351b..5af196cc93 100644 --- a/src/core/component/interface/index.ts +++ b/src/core/component/interface/index.ts @@ -17,5 +17,5 @@ export * from 'core/component/reflect/interface'; export * from 'core/component/interface/mod'; export * from 'core/component/interface/watch'; export * from 'core/component/interface/link'; -export * from 'core/component/interface/life-cycle'; +export * from 'core/component/interface/lc'; export * from 'core/component/interface/engine'; diff --git a/src/core/component/interface/life-cycle.ts b/src/core/component/interface/lc.ts similarity index 100% rename from src/core/component/interface/life-cycle.ts rename to src/core/component/interface/lc.ts diff --git a/src/core/component/interface/mod.ts b/src/core/component/interface/mod.ts index 5106865f79..3d0e18e4fb 100644 --- a/src/core/component/interface/mod.ts +++ b/src/core/component/interface/mod.ts @@ -25,6 +25,16 @@ export type ModDeclVal = CanArray; */ export type ExpandedModDeclVal = ModDeclVal | typeof PARENT; +/** + * A dictionary with modifiers to pass to the component + */ +export type ModsProp = Dictionary; + +/** + * A dictionary with normalized modifiers + */ +export type ModsDict = Dictionary>; + /** * A dictionary with registered modifiers and their possible values * diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index b5b6be702f..6deb30742f 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -173,10 +173,10 @@ 'v-hook': "!isVirtualTpl && isFunctional ?" + "{" + - "created: createInternalHookListener('bind')," + - "mounted: createInternalHookListener('inserted')," + - "updated: createInternalHookListener('update')," + - "unmounted: createInternalHookListener('unbind')" + + "created: createInternalHookListener('created')," + + "mounted: createInternalHookListener('mounted')," + + "updated: createInternalHookListener('updated')," + + "unmounted: createInternalHookListener('unmounted')" + "} :" + "null" diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index 541f81da04..cc2e63860e 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -87,7 +87,7 @@ import Lfc from 'super/i-block/modules/lfc'; import AsyncRender from 'friends/async-render'; import Sync, { AsyncWatchOptions } from 'super/i-block/modules/sync'; -import Block from 'super/i-block/modules/block'; +import Block from 'friends/block'; import Field from 'super/i-block/modules/field'; import Provide, { classesCache, Classes } from 'super/i-block/modules/provide'; @@ -130,8 +130,8 @@ import { ModVal, ModsDecl, - ModsTable, - ModsNTable + ModsProp, + ModsDict } from 'super/i-block/modules/mods'; @@ -157,7 +157,7 @@ export * from 'core/component'; export * from 'super/i-block/const'; export * from 'super/i-block/interface'; -export * from 'super/i-block/modules/block'; +export * from 'friends/block'; export * from 'super/i-block/modules/field'; export * from 'super/i-block/modules/state'; export * from 'friends/module-loader'; @@ -178,8 +178,8 @@ export { ModVal, ModsDecl, - ModsTable, - ModsNTable + ModsProp, + ModsDict }; @@ -297,13 +297,8 @@ export default abstract class iBlock extends ComponentInterface { return `stage.${this.stage}`; } - /** - * Initial component modifiers. - * The modifiers represent API to bind component state properties directly with CSS classes - * without unnecessary component re-rendering. - */ @prop({type: Object, required: false}) - readonly modsProp?: ModsTable; + override readonly modsProp?: ModsProp; /** * Component modifiers @@ -314,7 +309,7 @@ export default abstract class iBlock extends ComponentInterface { init: initMods }) - readonly mods!: ModsNTable; + readonly mods!: ModsDict; /** * If true, the component is activated. @@ -611,11 +606,11 @@ export default abstract class iBlock extends ComponentInterface { this.emit('componentStatusChange', value, oldValue); } - override get hook(): Hook { + get hook(): Hook { return this.hookStore; } - protected override set hook(value: Hook) { + protected set hook(value: Hook) { const oldValue = this.hook; this.hookStore = value; @@ -724,8 +719,8 @@ export default abstract class iBlock extends ComponentInterface { * and you specify to the outer component some theme modifier. * This modifier will recursively provide to all child components. */ - @computed() - get baseMods(): CanUndef> { + @computed({cache: 'auto'}) + get shareableMods(): CanUndef> { const m = this.mods; @@ -2410,26 +2405,19 @@ export default abstract class iBlock extends ComponentInterface { } protected override onCreatedHook(): void { - if (this.isSSR) { - this.componentStatusStore = 'ready'; - this.isReadyOnce = true; - } - } - - protected override onBindHook(): void { init.beforeMountState(this); } - protected override onInsertedHook(): void { + protected override onMountedHook(): void { init.mountedState(this); } - protected override async onUpdateHook(): Promise { + protected override async onUpdatedHook(): Promise { try { await this.nextTick({label: $$.onUpdateHook}); - this.onBindHook(); - this.onInsertedHook(); + this.onCreatedHook(); + this.onMountedHook(); if (this.$normalParent != null) { resolveRefs(this.$normalParent); @@ -2440,7 +2428,7 @@ export default abstract class iBlock extends ComponentInterface { } } - protected override onUnbindHook(): void { + protected override onUnmountedHook(): void { const parent = this.$normalParent; diff --git a/src/super/i-block/modules/dom/index.ts b/src/super/i-block/modules/dom/index.ts index 3048eeea65..13075683a0 100644 --- a/src/super/i-block/modules/dom/index.ts +++ b/src/super/i-block/modules/dom/index.ts @@ -12,7 +12,6 @@ */ import { memoize } from 'core/promise/sync'; -import { deprecated } from 'core/functools/deprecation'; import { wrapAsDelegateHandler } from 'core/dom'; import type { InViewInitOptions, InViewAdapter } from 'core/dom/in-view'; @@ -22,10 +21,8 @@ import type { AsyncOptions } from 'core/async'; import type { ComponentElement } from 'core/component'; import iBlock from 'super/i-block/i-block'; -import Block from 'super/i-block/modules/block'; import Friend from 'friends/friend'; -import { componentRgxp } from 'super/i-block/modules/dom/const'; import { ElCb, inViewInstanceStore, DOMManipulationOptions } from 'super/i-block/modules/dom/interface'; export * from 'super/i-block/modules/dom/const'; @@ -415,19 +412,6 @@ export default class DOM extends Friend { return destructor; } - /** - * @deprecated - * @see [[DOM.watchForIntersection]] - * - * @param el - * @param inViewOpts - * @param [asyncOpts] - */ - @deprecated({renamedTo: 'watchForIntersection'}) - watchForNodeIntersection(el: Element, inViewOpts: InViewInitOptions, asyncOpts?: AsyncOptions): Function { - return this.watchForIntersection(el, inViewOpts, asyncOpts); - } - /** * Watches for size changes of the specified element by using the `core/dom/resize-observer` module. * The method returns a link to an `Async` worker that wraps the operation. @@ -464,44 +448,4 @@ export default class DOM extends Friend { return destructor; } - - /** - * Creates a [[Block]] instance from the specified node and component instance. - * Basically, you don't need to use this method. - * - * @param node - * @param [component] - component instance, if not specified, the instance is taken from a node - */ - createBlockCtxFromNode(node: CanUndef, component?: iBlock): Dictionary { - const - $el = >>node, - ctxFromNode = component ?? $el?.component; - - const componentName = ctxFromNode ? - ctxFromNode.componentName : - Object.get(componentRgxp.exec($el?.className ?? ''), '1') ?? this.ctx.componentName; - - const resolvedCtx = ctxFromNode ?? { - $el, - componentName, - - mods: {}, - isFlyweight: true, - - localEmitter: { - emit(): void { - // Loopback - } - }, - - emit(): void { - // Loopback - } - }; - - return Object.assign(Object.create(Block.prototype), { - ctx: resolvedCtx, - component: resolvedCtx - }); - } } diff --git a/src/super/i-block/modules/mods/interface.ts b/src/super/i-block/modules/mods/interface.ts index 68fa95e772..f9cec05b72 100644 --- a/src/super/i-block/modules/mods/interface.ts +++ b/src/super/i-block/modules/mods/interface.ts @@ -6,16 +6,4 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { ModVal, ModsDecl } from 'core/component'; - -/** - * Raw modifiers - */ -export type ModsTable = Dictionary; - -/** - * Normalized modifiers - */ -export type ModsNTable = Dictionary>; - -export { ModVal, ModsDecl }; +export type { ModVal, ModsDecl, ModsProp, ModsDict } from 'core/component'; diff --git a/src/super/i-block/modules/provide/README.md b/src/super/i-block/modules/provide/README.md index 11b7855336..3d6414165e 100644 --- a/src/super/i-block/modules/provide/README.md +++ b/src/super/i-block/modules/provide/README.md @@ -50,10 +50,10 @@ console.log(this.provide.fullElName('b-foo', 'foo', 'opened', true)); ## mods Returns a dictionary with the base component modifiers. -The base modifiers are taken from the `baseMods` getter and can be mix in with the specified additional modifiers. +The base modifiers are taken from the `shareableMods` getter and can be mix in with the specified additional modifiers. ```js -this.provide.baseMods === {theme: 'foo'}; +this.provide.shareableMods === {theme: 'foo'}; // {theme: 'foo'} console.log(this.provide.mods()); diff --git a/src/super/i-block/modules/provide/index.ts b/src/super/i-block/modules/provide/index.ts index d4803e51ee..dee2a5812d 100644 --- a/src/super/i-block/modules/provide/index.ts +++ b/src/super/i-block/modules/provide/index.ts @@ -14,7 +14,7 @@ import type iBlock from 'super/i-block/i-block'; import Friend from 'friends/friend'; -import Block from 'super/i-block/modules/block'; +import Block from 'friends/block'; import { classesCache, modsCache } from 'super/i-block/modules/provide/const'; @@ -133,14 +133,14 @@ export default class Provide extends Friend { /** * Returns a dictionary with the base component modifiers. - * The base modifiers are taken from the `baseMods` getter and can be mix in with the specified additional modifiers. + * The base modifiers are taken from the `shareableMods` getter and can be mix in with the specified additional modifiers. * - * @see [[iBlock.baseMods]] + * @see [[iBlock.shareableMods]] * @param [mods] - additional modifiers (`{modifier: value}`) * * @example * ```js - * this.provide.baseMods === {theme: 'foo'}; + * this.provide.shareableMods === {theme: 'foo'}; * * // {theme: 'foo'} * console.log(this.provide.mods()); @@ -151,14 +151,14 @@ export default class Provide extends Friend { */ mods(mods?: ProvideMods): CanUndef> { const - {baseMods} = this.ctx; + {shareableMods} = this.ctx; - if (!baseMods && !mods) { + if (!shareableMods && !mods) { return; } const - key = JSON.stringify(baseMods) + JSON.stringify(mods), + key = JSON.stringify(shareableMods) + JSON.stringify(mods), cacheVal = modsCache[key]; if (cacheVal != null) { @@ -166,7 +166,7 @@ export default class Provide extends Friend { } const - res = {...baseMods}; + res = {...shareableMods}; if (mods) { for (let keys = Object.keys(mods), i = 0; i < keys.length; i++) { From 2edd7d6c015a5377f814eb3d9e9a3f811adc439b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 12:50:05 +0300 Subject: [PATCH 0255/2313] :pen: --- src/core/component/interface/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/interface/mod.ts b/src/core/component/interface/mod.ts index 3d0e18e4fb..2d4e153b24 100644 --- a/src/core/component/interface/mod.ts +++ b/src/core/component/interface/mod.ts @@ -36,7 +36,7 @@ export type ModsProp = Dictionary; export type ModsDict = Dictionary>; /** - * A dictionary with registered modifiers and their possible values + * A dictionary with predefined modifiers and their possible values * * @example * ```typescript From 933212ba50ce9ebce54a42bf5e1195e2397f82b7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 13:41:46 +0300 Subject: [PATCH 0256/2313] refactor: better module structure & code refactoring --- src/core/component/engines/CHANGELOG.md | 10 ++++ src/core/component/engines/README.md | 6 ++- src/core/component/engines/directive.ts | 12 +++-- src/core/component/engines/engine.ts | 13 ------ src/core/component/engines/index.ts | 1 + src/core/component/engines/interface.ts | 43 ++++++++++++++++- src/core/component/engines/vue3/CHANGELOG.md | 48 ++----------------- src/core/component/engines/vue3/README.md | 4 +- src/core/component/engines/vue3/component.ts | 8 +--- src/core/component/engines/vue3/index.ts | 1 - src/core/component/engines/vue3/interface.ts | 49 -------------------- src/core/component/engines/vue3/lib.ts | 14 +++--- src/core/component/engines/vue3/render.ts | 15 ++++-- src/core/component/interface/CHANGELOG.md | 6 +++ src/core/component/interface/engine.ts | 2 +- 15 files changed, 96 insertions(+), 136 deletions(-) delete mode 100644 src/core/component/engines/vue3/interface.ts diff --git a/src/core/component/engines/CHANGELOG.md b/src/core/component/engines/CHANGELOG.md index 08b74bfa10..6dfb0dba47 100644 --- a/src/core/component/engines/CHANGELOG.md +++ b/src/core/component/engines/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Removed the `Vue.js@2` engine + +#### :rocket: New Feature + +* Added a new engine for the [Vue.js@3](https://vuejs.org/) library + ## v3.8.2 (2021-10-26) #### :bug: Bug Fix diff --git a/src/core/component/engines/README.md b/src/core/component/engines/README.md index 4d1e179f1f..f07a50b1e7 100644 --- a/src/core/component/engines/README.md +++ b/src/core/component/engines/README.md @@ -1,3 +1,7 @@ # core/component/engines -This module provides a bunch of adaptors for different MVVM libraries. +This module provides a bunch of adapters for different component libraries to use them as engines for the V4fire platform. + +## Supported libraries + +* [Vue.js@3](https://vuejs.org/) diff --git a/src/core/component/engines/directive.ts b/src/core/component/engines/directive.ts index 1ef488f114..0c717b47cc 100644 --- a/src/core/component/engines/directive.ts +++ b/src/core/component/engines/directive.ts @@ -6,13 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { ComponentEngine, Directive, DirectiveBinding, VNode } from 'core/component/engines/engine'; +import { ComponentEngine } from 'core/component/engines/engine'; +import type { Directive, DirectiveBinding, VNode } from 'core/component/engines/interface'; // eslint-disable-next-line @typescript-eslint/unbound-method const staticDirective = ComponentEngine.directive.length > 0 ? ComponentEngine.directive : null; /** - * A wrapped version of the `ComponentEngine.directive` function with providing of hooks for non-regular components + * A wrapped version of the `ComponentEngine.directive` function, providing hooks for functional components * * @param name * @param [directive] @@ -23,7 +24,7 @@ ComponentEngine.directive = function directive(name: string, directive?: Directi originalDirective = staticDirective ?? ctx.directive; if (originalDirective == null) { - throw new Error("A function to register directives isn't found"); + throw new ReferenceError("A function to register directives isn't found"); } if (directive == null) { @@ -51,9 +52,10 @@ ComponentEngine.directive = function directive(name: string, directive?: Directi ...directive, created(_el: Element, _opts: DirectiveBinding, vnode: VNode) { - const + const args = Object.cast>>( // eslint-disable-next-line prefer-rest-params - args = Array.from(arguments); + Array.from(arguments) + ); if (Object.isFunction(originalCreated)) { originalCreated.apply(this, args); diff --git a/src/core/component/engines/engine.ts b/src/core/component/engines/engine.ts index f730925e96..898a9b6cfe 100644 --- a/src/core/component/engines/engine.ts +++ b/src/core/component/engines/engine.ts @@ -7,16 +7,3 @@ */ export * from 'core/component/engines/vue3'; -export * from 'core/component/engines/interface'; - -export { VNode } from 'core/component/engines/interface'; - -export { - - CreateAppFunction, - - Directive, - DirectiveArguments, - ObjectDirective - -} from 'core/component/engines/vue3'; diff --git a/src/core/component/engines/index.ts b/src/core/component/engines/index.ts index e96e4eb865..6f6e470c31 100644 --- a/src/core/component/engines/index.ts +++ b/src/core/component/engines/index.ts @@ -14,3 +14,4 @@ import 'core/component/engines/directive'; export * from 'core/component/engines/engine'; +export * from 'core/component/engines/interface'; diff --git a/src/core/component/engines/interface.ts b/src/core/component/engines/interface.ts index 692d15e825..15e61e521f 100644 --- a/src/core/component/engines/interface.ts +++ b/src/core/component/engines/interface.ts @@ -6,7 +6,17 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNode as SuperVNode } from 'vue'; +import type { + + CreateAppFunction as SuperCreateAppFunction, + ObjectDirective as SuperObjectDirective, + VNode as SuperVNode, + + DirectiveBinding, + FunctionDirective + +} from 'vue'; + import type { RendererElement, RendererNode } from 'core/component/engines'; import type { ComponentInterface } from 'core/component/interface'; @@ -21,3 +31,34 @@ export interface VNode< > extends SuperVNode { fakeContext?: ComponentInterface; } + +export interface ResolveDirective { + directive(name: string): CanUndef; + directive(name: string, directive: Directive): ReturnType>; +} + +export interface ObjectDirective extends SuperObjectDirective { + beforeCreate?( + this: ResolveDirective & ObjectDirective, + binding: DirectiveBinding, + vnode: VNode + ): CanVoid; +} + +export declare type Directive = + ObjectDirective | + FunctionDirective; + +export declare type DirectiveArguments = Array< + [Directive] | + [Directive, any] | + [Directive, any, string] | + [Directive, any, string, Record] +>; + +export interface CreateAppFunction { + (...args: Parameters>): Overwrite< + ReturnType>, + ResolveDirective + >; +} diff --git a/src/core/component/engines/vue3/CHANGELOG.md b/src/core/component/engines/vue3/CHANGELOG.md index 8440d92f35..b18e802496 100644 --- a/src/core/component/engines/vue3/CHANGELOG.md +++ b/src/core/component/engines/vue3/CHANGELOG.md @@ -9,50 +9,8 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] -## v3.0.0-rc.204 (2021-06-23) +## v3.??.?? (2022-??-??) -#### :bug: Bug Fix +#### :rocket: New Feature -* Fixed async rendering with text elements - -## v3.0.0-rc.137 (2021-02-04) - -#### :bug: Bug fix - -* Fixed redundant listening of events - -## v3.0.0-rc.92 (2020-11-03) - -#### :house: Internal - -* Refactoring - -## v3.0.0-rc.66 (2020-09-22) - -#### :bug: Bug Fix - -* [Fixed mixing of directives within of a component root tag](https://github.com/V4Fire/Client/pull/337) - -## v3.0.0-rc.50 (2020-08-03) - -#### :bug: Bug Fix - -* Fixed `getComponentName` - -## v3.0.0-rc.44 (2020-07-30) - -#### :bug: Bug Fix - -* Fixed setting of `staticClass` - -## v3.0.0-rc.40 (2020-07-27) - -#### :house: Internal - -* Logging Vue errors and warnings via the `core/log` module - -## v3.0.0-rc.37 (2020-07-20) - -#### :house: Internal - -* Fixed ESLint warnings +* Initial release diff --git a/src/core/component/engines/vue3/README.md b/src/core/component/engines/vue3/README.md index 0c91b8488e..c1ca6ca7ef 100644 --- a/src/core/component/engines/vue3/README.md +++ b/src/core/component/engines/vue3/README.md @@ -1,3 +1,3 @@ -# core/component/engines/vue +# core/component/engines/vue3 -This module provides an adaptor for Vue.js. +This module provides an adaptor to use the [Vue.js@3](https://vuejs.org/) component library as an engine for the V4Fire platform. diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index 7babd10e43..3f2ad16c76 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -23,7 +23,7 @@ import { supports, proxyGetters } from 'core/component/engines/vue3/const'; import * as r from 'core/component/engines/vue3/render'; /** - * Returns a component declaration object from the specified component meta object + * Returns a component declaration object from the specified meta object * @param meta */ export function getComponent(meta: ComponentMeta): ComponentOptions { @@ -64,12 +64,6 @@ export function getComponent(meta: ComponentMeta): ComponentOptions { - directive(name: string): CanUndef; - directive(name: string, directive: Directive): ReturnType>; -} - -export interface ObjectDirective extends SuperObjectDirective { - beforeCreate?( - this: ResolveDirective & ObjectDirective, - binding: DirectiveBinding, - vnode: VNode - ): CanVoid; -} - -export declare type Directive = - ObjectDirective | - FunctionDirective; - -export declare type DirectiveArguments = Array< - [Directive] | - [Directive, any] | - [Directive, any, string] | - [Directive, any, string, Record] ->; - -export interface CreateAppFunction { - (...args: Parameters>): Overwrite< - ReturnType>, - ResolveDirective - >; -} diff --git a/src/core/component/engines/vue3/lib.ts b/src/core/component/engines/vue3/lib.ts index 1a3b04682d..58cbda5177 100644 --- a/src/core/component/engines/vue3/lib.ts +++ b/src/core/component/engines/vue3/lib.ts @@ -11,7 +11,7 @@ import makeLazy from 'core/lazy'; import { createApp, Component } from 'vue'; -import type { CreateAppFunction } from 'core/component/engines/vue3/interface'; +import type { CreateAppFunction } from 'core/component/engines/interface'; const App = function App(component: Component & {el?: Element}, rootProps: Nullable) { const @@ -59,38 +59,38 @@ const Vue = makeLazy( component: (contexts, ...args) => { if (args.length === 1) { contexts.forEach((ctx) => { - ctx.component.apply(ctx, args); + ctx.component.apply(ctx, Object.cast(args)); }); return; } const ctx = contexts.at(-1); - return ctx?.component.apply(ctx, args); + return ctx?.component.apply(ctx, Object.cast(args)); }, directive: (contexts, ...args: any[]) => { if (args.length === 1) { contexts.forEach((ctx) => { - ctx.directive.apply(ctx, args); + ctx.directive.apply(ctx, Object.cast(args)); }); return; } const ctx = contexts.at(-1); - return ctx?.directive.apply(ctx, args); + return ctx?.directive.apply(ctx, Object.cast(args)); }, mixin: (contexts, ...args) => { contexts.forEach((ctx) => { - ctx.mixin.apply(ctx, args); + ctx.mixin.apply(ctx, Object.cast(args)); }); }, provide: (contexts, ...args) => { contexts.forEach((ctx) => { - ctx.provide.apply(ctx, args); + 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 227854cc78..7e6f192ba6 100644 --- a/src/core/component/engines/vue3/render.ts +++ b/src/core/component/engines/vue3/render.ts @@ -107,17 +107,24 @@ export const * Renders the specified VNode and returns the result * * @param vnode - * @param [parent] - parent component + * @param [parent] - the parent component */ -export function render(vnode: VNode, parent?: ComponentInterface): Node; +export function render( + vnode: VNode, + parent?: ComponentInterface +): Node; /** * Renders the specified list of VNode-s and returns the result * * @param vnodes - * @param [parent] - parent component + * @param [parent] - the parent component */ -export function render(vnodes: VNode[], parent?: ComponentInterface): Node[]; +export function render( + vnodes: VNode[], + parent?: ComponentInterface +): Node[]; + export function render(vnode: CanArray, parent?: ComponentInterface): CanArray { const vue = new Vue({ render: () => vnode, diff --git a/src/core/component/interface/CHANGELOG.md b/src/core/component/interface/CHANGELOG.md index af4c0b5aee..d6f57b8d47 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] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Migration to Vue3 + ## v3.0.0-rc.206 (2021-06-28) #### :rocket: New Feature diff --git a/src/core/component/interface/engine.ts b/src/core/component/interface/engine.ts index 449807bfd2..42b4cd5ee3 100644 --- a/src/core/component/interface/engine.ts +++ b/src/core/component/interface/engine.ts @@ -52,7 +52,7 @@ import type { vModelRadio, vModelDynamic -} from 'vue'; +} from 'core/component/engines'; import type { ComponentInterface } from 'core/component/interface'; From 55d8548d97ac72bd195a466b1750cdece2248df8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 14:58:36 +0300 Subject: [PATCH 0257/2313] fix: removed infinity warnings while rendering --- src/core/component/context/index.ts | 2 +- src/core/component/engines/vue3/config.ts | 14 +++-- .../interface/component/component.ts | 50 +++--------------- src/core/component/reflect/property.ts | 2 +- src/super/i-block/i-block.ts | 52 ++++++++++++++++--- 5 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/core/component/context/index.ts b/src/core/component/context/index.ts index f0fe8dee4e..c303066431 100644 --- a/src/core/component/context/index.ts +++ b/src/core/component/context/index.ts @@ -24,7 +24,7 @@ export * from 'core/component/context/const'; * @param component */ export function getComponentContext(component: object): Dictionary & ComponentInterface['unsafe'] { - component = component[toRaw] ?? component; + component = toRaw in component ? component[toRaw] : component; let v = wrappedContexts.get(component); diff --git a/src/core/component/engines/vue3/config.ts b/src/core/component/engines/vue3/config.ts index b1c50a02b5..7643e6e6af 100644 --- a/src/core/component/engines/vue3/config.ts +++ b/src/core/component/engines/vue3/config.ts @@ -17,12 +17,11 @@ const logger = log.namespace('vue'); Vue.config.errorHandler = (err, vm, info) => { - console.log(err); - //logger.error('errorHandler', err, info, getComponentInfoLog(vm)); + logger.error('errorHandler', err, info, getComponentInfo(vm)); }; Vue.config.warnHandler = (msg, vm, trace) => { - //logger.warn('warnHandler', msg, trace, getComponentInfoLog(vm)); + logger.warn('warnHandler', msg, trace, getComponentInfo(vm)); }; const @@ -30,10 +29,10 @@ const ROOT_COMPONENT_NAME = 'root-component'; /** - * Returns information of the specified component to log + * Returns a dictionary with information for debugging or logging the component * @param component */ -function getComponentInfoLog(component: Nullable): Dictionary { +function getComponentInfo(component: Nullable): Dictionary { if (component == null) { return { name: UNRECOGNIZED_COMPONENT_NAME @@ -43,8 +42,7 @@ function getComponentInfoLog(component: Nullable(component, '$options.name') ?? UNRECOGNIZED_COMPONENT_NAME; + return Object.get(component, '$options.name') ?? UNRECOGNIZED_COMPONENT_NAME; } diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 1f370be0eb..5c28e925a8 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -13,8 +13,6 @@ eslint-disable * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { LogMessageOptions } from 'core/log'; - import type Async from 'core/async'; import type { BoundFn, ProxyCb } from 'core/async'; @@ -116,16 +114,7 @@ export abstract class ComponentInterface { /** * The active component hook name */ - abstract get hook(): Hook; - - /** - * Switches the component to a new hook - * - * @param value - * @emits `componentHook:{$value}(value: Hook, oldValue: Hook)` - * @emits `componentHookChange(value: Hook, oldValue: Hook) - */ - protected abstract set hook(value: Hook); + abstract hook: Hook; /** * An API for unsafely invoking of some internal properties of the component. @@ -256,14 +245,6 @@ export abstract class ComponentInterface { */ protected $initializer?: Promise; - /** - * Logs an event with the specified context - * - * @param ctxOrOpts - the logging context or logging options - * @param [details] - event details - */ - abstract log?(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void; - /** * Activates the component. * The deactivated component won't load data from its providers during initializing. @@ -284,6 +265,11 @@ export abstract class ComponentInterface { */ abstract deactivate(): void; + /** + * Returns a dictionary with information for debugging or logging the component + */ + abstract getComponentInfo?(): Dictionary; + /** * Forces the component to re-render */ @@ -416,28 +402,4 @@ export abstract class ComponentInterface { protected $emit(event: string, ...args: unknown[]): this { return this; } - - /** - * Hook handler: the component has been created - * (only for functional components) - */ - protected abstract onCreatedHook(): void; - - /** - * Hook handler: the component has been mounted - * (only for functional components) - */ - protected abstract onMountedHook(): void; - - /** - * Hook handler: the component has been updated - * (only for functional components) - */ - protected abstract onUpdatedHook(): void; - - /** - * Hook handler: the component has been unmounted - * (only for functional components) - */ - protected abstract onUnmountedHook(): void; } diff --git a/src/core/component/reflect/property.ts b/src/core/component/reflect/property.ts index e05e894c81..5415a0850f 100644 --- a/src/core/component/reflect/property.ts +++ b/src/core/component/reflect/property.ts @@ -260,7 +260,7 @@ export function getPropertyInfo(path: string, component: ComponentInterface): Pr return info; function resolveCtx(component: object): ComponentInterface { - if (Object.cast(component).$remoteParent != null) { + if ('$remoteParent' in component) { return Object.getPrototypeOf(component); } diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index cc2e63860e..d6ac5a2489 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -606,10 +606,18 @@ export default abstract class iBlock extends ComponentInterface { this.emit('componentStatusChange', value, oldValue); } + /** @inheritDoc */ get hook(): Hook { return this.hookStore; } + /** + * Switches the component to a new hook + * + * @param value + * @emits `componentHook:{$value}(value: Hook, oldValue: Hook)` + * @emits `componentHookChange(value: Hook, oldValue: Hook) + */ protected set hook(value: Hook) { const oldValue = this.hook; this.hookStore = value; @@ -1059,11 +1067,12 @@ export default abstract class iBlock extends ComponentInterface { init: () => Object.create({}) }) - protected watchModsStore!: ModsNTable; + protected watchModsStore!: ModsDict; /** * True if the component context is based on another component via `vdom.bindRenderObject` */ + @system() protected readonly isVirtualTpl: boolean = false; /** @@ -1300,6 +1309,15 @@ export default abstract class iBlock extends ComponentInterface { protected readonly global!: Window; + /** @inheritDoc */ + getComponentInfo(): Dictionary { + return { + name: this.componentName, + hook: this.hook, + componentStatus: this.componentStatus + }; + } + /** * Sets a watcher to a component/object property or event by the specified path. * @@ -2124,11 +2142,13 @@ export default abstract class iBlock extends ComponentInterface { } /** - * @param ctxOrOpts - * @param details + * Logs an event with the specified context + * + * @param ctxOrOpts - the logging context or logging options + * @param [details] - event details */ @p() - override log(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void { + log(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void { let context = ctxOrOpts, logLevel; @@ -2404,15 +2424,27 @@ export default abstract class iBlock extends ComponentInterface { this.emit('mounted', this.$el); } - protected override onCreatedHook(): void { + /** + * Hook handler: the component has been created + * (only for functional components) + */ + protected onCreatedHook(): void { init.beforeMountState(this); } - protected override onMountedHook(): void { + /** + * Hook handler: the component has been mounted + * (only for functional components) + */ + protected onMountedHook(): void { init.mountedState(this); } - protected override async onUpdatedHook(): Promise { + /** + * Hook handler: the component has been updated + * (only for functional components) + */ + protected async onUpdatedHook(): Promise { try { await this.nextTick({label: $$.onUpdateHook}); @@ -2428,7 +2460,11 @@ export default abstract class iBlock extends ComponentInterface { } } - protected override onUnmountedHook(): void { + /** + * Hook handler: the component has been unmounted + * (only for functional components) + */ + protected onUnmountedHook(): void { const parent = this.$normalParent; From 3959c9778370be872c6718e8572746ee8aeb46a7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 15:36:19 +0300 Subject: [PATCH 0258/2313] fix: fixed types --- src/core/component/meta/fill.ts | 6 +++--- src/core/component/meta/interface/types.ts | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/core/component/meta/fill.ts b/src/core/component/meta/fill.ts index 8c8b5ce140..9fb4489c9c 100644 --- a/src/core/component/meta/fill.ts +++ b/src/core/component/meta/fill.ts @@ -16,7 +16,7 @@ import { isAbstractComponent, bindingRgxp } from 'core/component/reflect'; import { isTypeCanBeFunc } from 'core/component/prop'; import { addMethodsToMeta } from 'core/component/meta/method'; -import type { ComponentConstructor, ComponentMeta, ComponentField, WatchObject } from 'core/component/interface'; +import type { ComponentConstructor, ComponentMeta, ComponentField, WatchObject, Hook } from 'core/component/interface'; /** * Fills the passed meta object with methods and properties from the specified component class constructor @@ -237,10 +237,10 @@ export function fillMeta( if (method.hooks) { for (let o = method.hooks, keys = Object.keys(o), i = 0; i < keys.length; i++) { const - key = keys[i], + key = keys[i], hook = o[key]; - if (isFunctional && hook.functional === false) { + if (hook == null || isFunctional && hook.functional === false) { continue; } diff --git a/src/core/component/meta/interface/types.ts b/src/core/component/meta/interface/types.ts index 89ad0ed78a..99922aff59 100644 --- a/src/core/component/meta/interface/types.ts +++ b/src/core/component/meta/interface/types.ts @@ -8,11 +8,9 @@ import type { WatchPath } from 'core/object/watch'; -import type { Hook } from 'core/component/interface/life-cycle'; -import type { PropOptions, InitFieldFn, MergeFieldFn, UniqueFieldFn } from 'core/component/decorators'; import type { WritableComputedOptions, DirectiveBinding } from 'core/component/engines'; - -import type { ComponentInterface, FieldWatcher, MethodWatcher } from 'core/component/interface'; +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 { watchers: Map; @@ -75,7 +73,8 @@ export type ComponentMethodHooks = { [hook in Hook]?: { name: string; hook: string; - after: Set; + functional?: boolean; + after?: Set; }; }; From 0a296fc4b0aba6c448ce547c17d2a921c4767a68 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:03:49 +0300 Subject: [PATCH 0259/2313] refactor: renamed the module to `init` & added new state initializers `render-tracked` and `render-trigered` --- src/core/component/construct/README.md | 3 -- src/core/component/construct/states/index.ts | 20 -------- .../{construct => init}/CHANGELOG.md | 14 ++++++ src/core/component/init/README.md | 49 +++++++++++++++++++ .../component/{construct => init}/index.ts | 6 +-- .../{construct => init}/interface.ts | 0 .../{construct => init}/states/activated.ts | 2 +- .../states/before-create.ts | 8 +-- .../states/before-data-create.ts | 7 +-- .../states/before-destroy.ts | 0 .../states/before-mount.ts | 0 .../states/before-update.ts | 0 .../{construct => init}/states/created.ts | 3 +- .../{construct => init}/states/deactivated.ts | 0 .../{construct => init}/states/destroyed.ts | 0 .../states/error-captured.ts | 0 src/core/component/init/states/index.ts | 22 +++++++++ .../{construct => init}/states/mounted.ts | 2 +- .../component/init/states/render-tracked.ts | 24 +++++++++ .../component/init/states/render-trigered.ts | 24 +++++++++ .../{construct => init}/states/updated.ts | 2 +- 21 files changed, 146 insertions(+), 40 deletions(-) delete mode 100644 src/core/component/construct/README.md delete mode 100644 src/core/component/construct/states/index.ts rename src/core/component/{construct => init}/CHANGELOG.md (82%) create mode 100644 src/core/component/init/README.md rename src/core/component/{construct => init}/index.ts (56%) rename src/core/component/{construct => init}/interface.ts (100%) rename src/core/component/{construct => init}/states/activated.ts (100%) rename src/core/component/{construct => init}/states/before-create.ts (96%) rename src/core/component/{construct => init}/states/before-data-create.ts (91%) rename src/core/component/{construct => init}/states/before-destroy.ts (100%) rename src/core/component/{construct => init}/states/before-mount.ts (100%) rename src/core/component/{construct => init}/states/before-update.ts (100%) rename src/core/component/{construct => init}/states/created.ts (97%) rename src/core/component/{construct => init}/states/deactivated.ts (100%) rename src/core/component/{construct => init}/states/destroyed.ts (100%) rename src/core/component/{construct => init}/states/error-captured.ts (100%) create mode 100644 src/core/component/init/states/index.ts rename src/core/component/{construct => init}/states/mounted.ts (100%) create mode 100644 src/core/component/init/states/render-tracked.ts create mode 100644 src/core/component/init/states/render-trigered.ts rename src/core/component/{construct => init}/states/updated.ts (100%) diff --git a/src/core/component/construct/README.md b/src/core/component/construct/README.md deleted file mode 100644 index 71c7d8641c..0000000000 --- a/src/core/component/construct/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# core/component/construct - -This module provides a bunch of functions to construct or initialize common component handlers. diff --git a/src/core/component/construct/states/index.ts b/src/core/component/construct/states/index.ts deleted file mode 100644 index c3c4a5d905..0000000000 --- a/src/core/component/construct/states/index.ts +++ /dev/null @@ -1,20 +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/construct/states/before-create'; -export * from 'core/component/construct/states/before-data-create'; -export * from 'core/component/construct/states/created'; -export * from 'core/component/construct/states/before-mount'; -export * from 'core/component/construct/states/mounted'; -export * from 'core/component/construct/states/before-update'; -export * from 'core/component/construct/states/updated'; -export * from 'core/component/construct/states/activated'; -export * from 'core/component/construct/states/deactivated'; -export * from 'core/component/construct/states/before-destroy'; -export * from 'core/component/construct/states/destroyed'; -export * from 'core/component/construct/states/error-captured'; diff --git a/src/core/component/construct/CHANGELOG.md b/src/core/component/init/CHANGELOG.md similarity index 82% rename from src/core/component/construct/CHANGELOG.md rename to src/core/component/init/CHANGELOG.md index 3dbe8fd0c1..22df0615d1 100644 --- a/src/core/component/construct/CHANGELOG.md +++ b/src/core/component/init/CHANGELOG.md @@ -9,6 +9,20 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Renamed the module to `init` + +#### :rocket: New Feature + +* Added new state initializers `render-tracked` and `render-trigered` + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.147 (2021-02-18) #### :bug: Bug Fix diff --git a/src/core/component/init/README.md b/src/core/component/init/README.md new file mode 100644 index 0000000000..aebf02d60a --- /dev/null +++ b/src/core/component/init/README.md @@ -0,0 +1,49 @@ +# core/component/init + +This module provides a bunch of functions to initialize common component states. + +## Functions + +### beforeCreateState + +Initializes the "beforeCreate" state to the specified component instance. + +### createdState + +Initializes the "created" state to the specified component instance. + +### beforeMountState + +Initializes the "beforeMount" state to the specified component instance. + +### mountedState + +Initializes the "mounted" state to the specified component instance + +### beforeUpdateState + +Initializes the "beforeUpdate" state to the specified component instance. + +### updatedState + +Initializes the "updated" state to the specified component instance. + +### activatedState + +Initializes the "activated" state to the specified component instance. + +### deactivatedState + +Initializes the "deactivated" state to the specified component instance. + +### beforeDestroyState + +Initializes the "beforeDestroy" state to the specified component instance. + +### destroyedState + +Initializes the "destroyed" state to the specified component instance. + +### errorCapturedState + +Initializes the "errorCaptured" state to the specified component instance. diff --git a/src/core/component/construct/index.ts b/src/core/component/init/index.ts similarity index 56% rename from src/core/component/construct/index.ts rename to src/core/component/init/index.ts index dce4b3a6e0..c76bc7da84 100644 --- a/src/core/component/construct/index.ts +++ b/src/core/component/init/index.ts @@ -7,9 +7,9 @@ */ /** - * [[include:core/component/construct/README.md]] + * [[include:core/component/init/README.md]] * @packageDocumentation */ -export * from 'core/component/construct/states'; -export * from 'core/component/construct/interface'; +export * from 'core/component/init/states'; +export * from 'core/component/init/interface'; diff --git a/src/core/component/construct/interface.ts b/src/core/component/init/interface.ts similarity index 100% rename from src/core/component/construct/interface.ts rename to src/core/component/init/interface.ts diff --git a/src/core/component/construct/states/activated.ts b/src/core/component/init/states/activated.ts similarity index 100% rename from src/core/component/construct/states/activated.ts rename to src/core/component/init/states/activated.ts index 69fa12ade2..4e4bf553e2 100644 --- a/src/core/component/construct/states/activated.ts +++ b/src/core/component/init/states/activated.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { callMethodFromComponent } from 'core/component/method'; import { resolveRefs } from 'core/component/ref'; +import { callMethodFromComponent } from 'core/component/method'; import { runHook } from 'core/component/hook'; import type { ComponentInterface } from 'core/component/interface'; diff --git a/src/core/component/construct/states/before-create.ts b/src/core/component/init/states/before-create.ts similarity index 96% rename from src/core/component/construct/states/before-create.ts rename to src/core/component/init/states/before-create.ts index fb243868ac..97fa04593a 100644 --- a/src/core/component/construct/states/before-create.ts +++ b/src/core/component/init/states/before-create.ts @@ -19,16 +19,16 @@ import { attachAccessorsFromMeta } from 'core/component/accessor'; import { attachMethodsFromMeta, callMethodFromComponent } from 'core/component/method'; import { runHook } from 'core/component/hook'; -import { implementEventAPI } from 'core/component/event'; +import { implementEventEmitterAPI } from 'core/component/event'; import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; -import type { InitBeforeCreateStateOptions } from 'core/component/construct/interface'; +import type { InitBeforeCreateStateOptions } from 'core/component/init/interface'; /** * Initializes the "beforeCreate" state to the specified component instance * * @param component - * @param meta - component meta object + * @param meta - the component meta object * @param [opts] - additional options */ export function beforeCreateState( @@ -98,7 +98,7 @@ export function beforeCreateState( } if (opts?.implementEventAPI) { - implementEventAPI(component); + implementEventEmitterAPI(component); } attachAccessorsFromMeta(component); diff --git a/src/core/component/construct/states/before-data-create.ts b/src/core/component/init/states/before-data-create.ts similarity index 91% rename from src/core/component/construct/states/before-data-create.ts rename to src/core/component/init/states/before-data-create.ts index e6ff19f788..3bf87033b8 100644 --- a/src/core/component/construct/states/before-data-create.ts +++ b/src/core/component/init/states/before-data-create.ts @@ -11,7 +11,7 @@ import { bindRemoteWatchers, implementComponentWatchAPI } from 'core/component/w import { runHook } from 'core/component/hook'; import type { ComponentInterface } from 'core/component/interface'; -import type { InitBeforeDataCreateStateOptions } from 'core/component/construct/interface'; +import type { InitBeforeDataCreateStateOptions } from 'core/component/init/interface'; /** * Initializes the "beforeDataCreate" state to the specified component instance @@ -35,10 +35,7 @@ export function beforeDataCreateState( runHook('beforeDataCreate', component) .catch(stderr); - const hasWatchAPI = - !component.$renderEngine.supports.ssr; - - if (hasWatchAPI) { + if (!component.$renderEngine.supports.ssr) { implementComponentWatchAPI(component, {tieFields: opts?.tieFields}); bindRemoteWatchers(component); } diff --git a/src/core/component/construct/states/before-destroy.ts b/src/core/component/init/states/before-destroy.ts similarity index 100% rename from src/core/component/construct/states/before-destroy.ts rename to src/core/component/init/states/before-destroy.ts diff --git a/src/core/component/construct/states/before-mount.ts b/src/core/component/init/states/before-mount.ts similarity index 100% rename from src/core/component/construct/states/before-mount.ts rename to src/core/component/init/states/before-mount.ts diff --git a/src/core/component/construct/states/before-update.ts b/src/core/component/init/states/before-update.ts similarity index 100% rename from src/core/component/construct/states/before-update.ts rename to src/core/component/init/states/before-update.ts diff --git a/src/core/component/construct/states/created.ts b/src/core/component/init/states/created.ts similarity index 97% rename from src/core/component/construct/states/created.ts rename to src/core/component/init/states/created.ts index e70496d091..3c51456202 100644 --- a/src/core/component/construct/states/created.ts +++ b/src/core/component/init/states/created.ts @@ -34,8 +34,7 @@ export function createdState(component: ComponentInterface): void { unmute(unsafe.$systemFields); const isDynamicallyMountedComponent = - parent != null && - '$remoteParent' in r; + parent != null && '$remoteParent' in r; if (isDynamicallyMountedComponent) { const diff --git a/src/core/component/construct/states/deactivated.ts b/src/core/component/init/states/deactivated.ts similarity index 100% rename from src/core/component/construct/states/deactivated.ts rename to src/core/component/init/states/deactivated.ts diff --git a/src/core/component/construct/states/destroyed.ts b/src/core/component/init/states/destroyed.ts similarity index 100% rename from src/core/component/construct/states/destroyed.ts rename to src/core/component/init/states/destroyed.ts diff --git a/src/core/component/construct/states/error-captured.ts b/src/core/component/init/states/error-captured.ts similarity index 100% rename from src/core/component/construct/states/error-captured.ts rename to src/core/component/init/states/error-captured.ts diff --git a/src/core/component/init/states/index.ts b/src/core/component/init/states/index.ts new file mode 100644 index 0000000000..bced812eb0 --- /dev/null +++ b/src/core/component/init/states/index.ts @@ -0,0 +1,22 @@ +/*! + * 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/init/states/before-create'; +export * from 'core/component/init/states/before-data-create'; +export * from 'core/component/init/states/created'; +export * from 'core/component/init/states/before-mount'; +export * from 'core/component/init/states/mounted'; +export * from 'core/component/init/states/before-update'; +export * from 'core/component/init/states/updated'; +export * from 'core/component/init/states/activated'; +export * from 'core/component/init/states/deactivated'; +export * from 'core/component/init/states/before-destroy'; +export * from 'core/component/init/states/destroyed'; +export * from 'core/component/init/states/error-captured'; +export * from 'core/component/init/states/render-tracked'; +export * from 'core/component/init/states/render-trigered'; diff --git a/src/core/component/construct/states/mounted.ts b/src/core/component/init/states/mounted.ts similarity index 100% rename from src/core/component/construct/states/mounted.ts rename to src/core/component/init/states/mounted.ts index 28e6d04f20..c947644d6b 100644 --- a/src/core/component/construct/states/mounted.ts +++ b/src/core/component/init/states/mounted.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { callMethodFromComponent } from 'core/component/method'; import { resolveRefs } from 'core/component/ref'; +import { callMethodFromComponent } from 'core/component/method'; import { runHook } from 'core/component/hook'; import type { ComponentInterface } from 'core/component/interface'; diff --git a/src/core/component/init/states/render-tracked.ts b/src/core/component/init/states/render-tracked.ts new file mode 100644 index 0000000000..8bd2161577 --- /dev/null +++ b/src/core/component/init/states/render-tracked.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "renderTracked" state to the specified component instance + * + * @param component + * @param args - additional arguments + */ +export function renderTrackedState(component: ComponentInterface, ...args: unknown[]): void { + runHook('renderTracked', component, ...args).then(() => { + callMethodFromComponent(component, 'renderTracked', ...args); + }).catch(stderr); +} diff --git a/src/core/component/init/states/render-trigered.ts b/src/core/component/init/states/render-trigered.ts new file mode 100644 index 0000000000..d82b684f1c --- /dev/null +++ b/src/core/component/init/states/render-trigered.ts @@ -0,0 +1,24 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import { callMethodFromComponent } from 'core/component/method'; +import { runHook } from 'core/component/hook'; + +import type { ComponentInterface } from 'core/component/interface'; + +/** + * Initializes the "renderTriggered" state to the specified component instance + * + * @param component + * @param args - additional arguments + */ +export function renderTriggeredState(component: ComponentInterface, ...args: unknown[]): void { + runHook('renderTriggered', component, ...args).then(() => { + callMethodFromComponent(component, 'renderTriggered', ...args); + }).catch(stderr); +} diff --git a/src/core/component/construct/states/updated.ts b/src/core/component/init/states/updated.ts similarity index 100% rename from src/core/component/construct/states/updated.ts rename to src/core/component/init/states/updated.ts index dff3c75f66..c714ecd01c 100644 --- a/src/core/component/construct/states/updated.ts +++ b/src/core/component/init/states/updated.ts @@ -6,8 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { callMethodFromComponent } from 'core/component/method'; import { resolveRefs } from 'core/component/ref'; +import { callMethodFromComponent } from 'core/component/method'; import { runHook } from 'core/component/hook'; import type { ComponentInterface } from 'core/component/interface'; From ff110d9055868b6bcbc05c784f317662a0d9a57b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:04:28 +0300 Subject: [PATCH 0260/2313] fix: support `isVirtualTpl` --- src/friends/vdom/render.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/friends/vdom/render.ts b/src/friends/vdom/render.ts index cc65d1b738..37b034cf51 100644 --- a/src/friends/vdom/render.ts +++ b/src/friends/vdom/render.ts @@ -131,7 +131,7 @@ export function getRenderFn( const cache = [], - instanceCtx = Object.create(ctx), + instanceCtx = Object.create(ctx, {isVirtualTpl: {value: true}}), render = factory(instanceCtx, cache); return (bindings) => { From 0408840da8e5f45c88c4bae4cd6037021557d615 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:04:58 +0300 Subject: [PATCH 0261/2313] refactor: updated imports --- src/super/i-block/i-block.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index d6ac5a2489..8766288249 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -69,7 +69,7 @@ import { } from 'core/component'; import remoteState from 'core/component/state'; -import * as init from 'core/component/construct'; +import * as init from 'core/component/init'; import 'super/i-block/directives'; import { statuses } from 'super/i-block/const'; From 4c235173e7d694dd920dc08f936df1ba25c05dbd Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:32:55 +0300 Subject: [PATCH 0262/2313] fix: fixed TS warnings --- src/core/component/watch/README.md | 2 +- src/core/component/watch/bind.ts | 10 +++++----- src/core/component/watch/component-api.ts | 4 ++-- src/core/component/watch/const.ts | 6 +++--- src/core/component/watch/create.ts | 4 ++-- src/core/component/watch/helpers.ts | 10 +++++----- src/core/component/watch/interface.ts | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/component/watch/README.md b/src/core/component/watch/README.md index 716aea7bf9..88743c2027 100644 --- a/src/core/component/watch/README.md +++ b/src/core/component/watch/README.md @@ -1,3 +1,3 @@ # core/component/watch -This module provides API to add a feature of object watching to a component. +This module provides an API to add a feature of object watching to a component. diff --git a/src/core/component/watch/bind.ts b/src/core/component/watch/bind.ts index 998c3cd11f..d53660ac40 100644 --- a/src/core/component/watch/bind.ts +++ b/src/core/component/watch/bind.ts @@ -20,7 +20,7 @@ import type { BindRemoteWatchersParams } from 'core/component/watch/interface'; * This method has some "copy-paste" chunks, but it's done for better performance because it's a very hot function. * * Basically, this function takes watchers from a meta property of the component, - * but you can provide custom watchers to initialize by using the second parameter of the function. + * but you can provide the custom watchers to initialize by using the second parameter of the function. * * @param component * @param [params] - additional parameters @@ -36,7 +36,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR // Link to the self component async instance selfAsync = unsafe.$async, - // Link to an async instance + // Link to the resolved async instance $a = p.async ?? selfAsync; const @@ -49,7 +49,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR const watchersMap = p.watchers ?? meta.watchers, - // True if the method has been invoked with passing custom async instance as a property + // True if the method has been invoked with passing the custom async instance as a property customAsync = $a !== unsafe.$async; // Iterate over all registered watchers and listeners and initialize their @@ -86,7 +86,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR } const attachWatcher = () => { - // If we have a custom watcher we need to find a link to the event emitter. + // If we have the 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'` @@ -148,7 +148,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR // Right now, we need to create a wrapper for our handler because there are some conditions for the watcher: // - // 1. It can provide or not provide arguments from an event that it listens. + // 1. It can provide or not provide arguments from the listened event. // 2. The handler can be specified as a function or as a component method name. // // Also, we have two different cases: diff --git a/src/core/component/watch/component-api.ts b/src/core/component/watch/component-api.ts index 408f8608d3..686f2fa222 100644 --- a/src/core/component/watch/component-api.ts +++ b/src/core/component/watch/component-api.ts @@ -272,7 +272,7 @@ export function implementComponentWatchAPI( propsStore = props.value; // We need to attach a watcher for a prop object - // and watchers for each non-primitive value of that object, like arrays or maps. + // and watchers for each non-primitive value of that object, like arrays or maps if (Object.isTruly(propsStore)) { const propWatchOpts = { ...watchOpts, @@ -331,7 +331,7 @@ export function implementComponentWatchAPI( }); if (isFunctional) { - // We need to track all modified fields of a function instance + // We need to track all modified fields of the functional instance // to restore state if a parent has re-created the component const w = watch(watcher.proxy, {deep: true, collapse: true, immediate: true}, (v, o, i) => { unsafe.$modifiedFields[String(i.path[0])] = true; diff --git a/src/core/component/watch/const.ts b/src/core/component/watch/const.ts index 460cc77421..efea56fdbf 100644 --- a/src/core/component/watch/const.ts +++ b/src/core/component/watch/const.ts @@ -13,12 +13,12 @@ export const immediateDynamicHandlers: DynamicHandlers = new WeakMap(); export const - tiedWatchers = Symbol('List of tied watchers'), - watcherInitializer = Symbol('Watcher initializer'); + tiedWatchers = Symbol('A list of tied watchers'), + watcherInitializer = Symbol('The watcher initializer'); export const cacheStatus = Symbol('Cache status'), - toComponentObject = Symbol('Link to a component object'); + toComponentObject = Symbol('A link to the component object'); export const customWatcherRgxp = /^([!?]?)([^!?:]*):(.*)/; diff --git a/src/core/component/watch/create.ts b/src/core/component/watch/create.ts index 64463fd66b..b7b062bd50 100644 --- a/src/core/component/watch/create.ts +++ b/src/core/component/watch/create.ts @@ -26,7 +26,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface watchCache = new Map(); // eslint-disable-next-line @typescript-eslint/typedef,max-lines-per-function - return function watchFn(this: unknown, path, optsOrHandler, rawHandler?) { + return function watchFn(this: ComponentInterface, path, optsOrHandler, rawHandler?) { let info: PropertyInfo, opts: WatchOptions, @@ -516,7 +516,7 @@ export function createWatchFn(component: ComponentInterface): ComponentInterface function wrapDestructor(destructor: T): T { if (Object.isFunction(destructor)) { - // Every worker that passed to async have a counter with number of consumers of this worker, + // Every worker that passed to Async have a counter with a number of consumers of this worker, // but in this case this behaviour is redundant and can produce an error, // that why we wrap original destructor with a new function component.unsafe.$async.worker(() => destructor()); diff --git a/src/core/component/watch/helpers.ts b/src/core/component/watch/helpers.ts index 8d2857d5d0..f21c72c018 100644 --- a/src/core/component/watch/helpers.ts +++ b/src/core/component/watch/helpers.ts @@ -18,11 +18,11 @@ import type { DynamicHandlers } from 'core/component/watch/interface'; * Attaches a dynamic watcher to the specified property. * This function is used to manage a situation when we are watching some accessor. * - * @param component - component that is watched - * @param prop - property to watch + * @param component - the component that is watched + * @param prop - the property to watch * @param watchOpts - options of watching - * @param handler - handler of mutations - * @param [store] - store with dynamic handlers + * @param handler - a function to handle mutations + * @param [store] - store for dynamic handlers */ export function attachDynamicWatcher( component: ComponentInterface, @@ -119,7 +119,7 @@ export function attachDynamicWatcher( }; } - // Every worker that passed to async have a counter with number of consumers of this worker, + // Every worker that passed to Async have a counter with a number of consumers of this worker, // but in this case this behaviour is redundant and can produce an error, // that why we wrap original destructor with a new function component.unsafe.$async.worker(() => destructor()); diff --git a/src/core/component/watch/interface.ts b/src/core/component/watch/interface.ts index 725e6d077d..d380827d55 100644 --- a/src/core/component/watch/interface.ts +++ b/src/core/component/watch/interface.ts @@ -16,12 +16,12 @@ export type DynamicHandlers = WeakMap< export interface BindRemoteWatchersParams { /** - * Link to an instance of Async + * A link to the `Async` instance */ async?: Async; /** - * Dictionary of watchers + * A dictionary with watchers */ watchers?: Dictionary; From 273fe78080ff0a2dd0124813ab2a11309adc6b8e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:35:44 +0300 Subject: [PATCH 0263/2313] doc: improved doc --- src/core/component/watch/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/core/component/watch/README.md b/src/core/component/watch/README.md index 88743c2027..ba3e55fc56 100644 --- a/src/core/component/watch/README.md +++ b/src/core/component/watch/README.md @@ -1,3 +1,19 @@ # core/component/watch This module provides an API to add a feature of object watching to a component. + +## Functions + +### createWatchFn + +Creates a function to watch property changes from the specified component instance and returns it. + +### implementComponentWatchAPI + +Implements watch API to the passed component instance. + +### bindRemoteWatchers + +Binds watchers and event listeners that were registered as remote to the specified component instance. +Basically, this function takes watchers from a meta property of the component, +but you can provide the custom watchers to initialize by using the second parameter of the function. From 038fdde4189ddcdb75ca3661dd6e3fb696ed27b0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:36:32 +0300 Subject: [PATCH 0264/2313] fix: fixed runtime warnings --- src/core/component/render/helpers/attrs.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/component/render/helpers/attrs.ts b/src/core/component/render/helpers/attrs.ts index 6ced2be596..9cadb2b57a 100644 --- a/src/core/component/render/helpers/attrs.ts +++ b/src/core/component/render/helpers/attrs.ts @@ -43,10 +43,9 @@ export function interpolateStaticAttrs(this: Compo { const - key = 'data-cached-class-component-id', - val = props[key]; + key = 'data-cached-class-component-id'; - if (val != null) { + if (key in props && props[key] != null) { Object.assign(props, mergeProps({class: props.class}, {class: this.componentId})); delete props[key]; } @@ -58,11 +57,11 @@ export function interpolateStaticAttrs(this: Compo name = props[key]; if (name != null) { - if (this.classes?.[name] != null) { + if ('classes' in this && this.classes?.[name] != null) { Object.assign(props, mergeProps({class: props.class}, {class: this.classes[name]})); } - if (this.styles?.[name] != null) { + if ('styles' in this && this.styles?.[name] != null) { Object.assign(props, mergeProps({style: props.style}, {style: this.styles[name]})); } From b78861087cb0540ae0ceb66d0bec199e022a18fa Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 16 Jun 2022 17:36:47 +0300 Subject: [PATCH 0265/2313] :pen: --- src/core/component/render/daemon/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/render/daemon/README.md b/src/core/component/render/daemon/README.md index bfd5dd2f09..6e6fafcd2e 100644 --- a/src/core/component/render/daemon/README.md +++ b/src/core/component/render/daemon/README.md @@ -1,3 +1,3 @@ # core/component/render/daemon -This module provides API to register and manage tasks of async rendering. +This module provides an API to register and manage tasks of async rendering. From 9bcc391cbb50db9980a41cabcd0a2c1a750294e8 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 17 Jun 2022 14:14:05 +0300 Subject: [PATCH 0266/2313] doc: added the normal documentation --- src/core/component/register/CHANGELOG.md | 6 + src/core/component/register/README.md | 229 ++++++++++++++++++++++- src/core/component/register/decorator.ts | 22 ++- src/core/component/register/helpers.ts | 26 +-- 4 files changed, 261 insertions(+), 22 deletions(-) diff --git a/src/core/component/register/CHANGELOG.md b/src/core/component/register/CHANGELOG.md index e0678a4f91..1d27e39b4d 100644 --- a/src/core/component/register/CHANGELOG.md +++ b/src/core/component/register/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.131 (2021-01-29) #### :house: Internal diff --git a/src/core/component/register/README.md b/src/core/component/register/README.md index 89d98a12d9..8e6be8cdb1 100644 --- a/src/core/component/register/README.md +++ b/src/core/component/register/README.md @@ -1,10 +1,233 @@ # core/component/register -This module provides API to register components. +This module provides an API to register components. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; -```js @component() -class bFoo { +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; +} +``` + +## How to create a component? + +Also, you can pass additional parameters to this decorator to create a component. +For example, that a component should be created as functional. +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component({functional :true}) +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; } ``` + +### How does it work? + +The decorator aggregates information of the class received from other nested decorators and +with the help of reflection and forms a special structure of the [[ComponentMeta]] type. +Next, the created structure will be passed to the used component library adapter, which will create a "real" component. + +### Additional register options + +#### [name] + +A name of the component we are registering. +If the name isn't specified, it will be taken from the tied class name by using reflection. +This parameter can't be inherited from the parent component. + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; + +// name == 'bExample' +@component({name: 'bExample'}) +class Foo extends iBlock { + +} + +// name == 'bExample' +@component() +class bExample extends iBlock { + +} +``` + +#### [root = `false`] + +If true, then the component is registered as the root component. +The root component is the top of components hierarchy, i.e. it contains all components in our application. + +All components, even the root component, have a link to the root component. +This parameter can be inherited from the parent component. + +```typescript +import iStaticPage, { component } from 'super/i-static-page/i-static-page'; + +@component({root: true}) +class pRoot extends iStaticPage { + +} +``` + +#### [tpl = `true`] + +If false, then the component will use the default loopback render function, instead of loading the own template. +This parameter is useful for components without templates, and it can be inherited from the parent component. + +#### [functional = `false`] + +The component functional mode: + +1. If true, the component will be created as a functional component. +2. If a dictionary, the component can be created as a functional component or regular component, depending on + values of the input properties: + + 1. If an empty dictionary, the component will always created as functional. + 2. If a dictionary with values, the dictionary properties represent component input properties. + If the component invocation takes these properties with the values that + declared within "functional" parameters, it will be created as functional. + Also, you can specify multiple values of one input property by using a list of values. + Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, + but you can directly cast a type by using the "v-func" directive. + +3. If null, all components watchers and listeners that directly specified in the component class won't + be attached to a functional-kind component. It is useful to create the superclass behaviour depending + on a component type. + +A functional component is a component can be rendered once only from input properties. +This type of components have a state and lifecycle hooks, but mutation of the state doesn't force re-rendering. +Usually, functional components lighter than regular components with the first render, +but avoid their if you have long animations within a component or if you need to frequent re-draws some deep +structure of nested components. + +This parameter can be inherited from the parent component, but the `null` value isn't inherited. + +```typescript +import iData, { component } from 'super/i-data/i-data'; + +// `bButton` will be created as a function component +// if its `dataProvider` property is equal to `false` or not specified +@component({functional: {dataProvider: [undefined, false]}}) +class bButton extends iData { + +} + +// `bLink` will always be created as a functional component +@component({functional: true}) +class bLink extends iData { + +} +``` + +``` +// We force `b-button` to create as a regular component +< b-button v-func = false + +// Within `v-func` we can use values from the runtime +< b-button v-func = foo !== bar + +// The direct invoking of a functional version of `bButton` +< b-button-functional +``` + +#### [defaultProps = `true`] + +If false, then all default values of the component input properties are ignored. +This parameter can be inherited from the parent component. + +#### [deprecatedProps] + +A dictionary with the deprecated component props with the specified alternatives. +The keys represent deprecated props; the values represent alternatives. +This parameter can be inherited from the parent component. + +```typescript +import iData, { component, prop } from 'super/i-data/i-data'; + +@component({deprecatedProps: { + value: 'items' +}}) + +class bList extends iData { + @prop() + items: string[]; + + // @deprecated + @prop() + value: string[]; +} +``` + +#### [inheritAttrs = `true`] + +If true, then the component input properties that aren't registered as props +will be attached to a component node as attributes. +This parameter can be inherited from the parent component. + +```typescript +import iData, { component, prop } from 'super/i-data/i-data'; + +@component() +class bInput extends iData { + @prop() + value: string = ''; +} +``` + +``` +< b-input :data-title = 'hello' +``` + +#### [inheritMods = `true`] + +If true, then the component is automatically inherited base modifiers from its parent. +This parameter can be inherited from the parent component. + +## API + +### Decorators + +#### component + +The decorator to register a new component based on the tied class. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; +} +``` + +### Helpers + +#### registerParentComponents + +Registers parent components for the given one. +This function is needed because we have lazy component registration: when we see the "foo" component for +the first time in a template, we need to check the registration of all its parent components. +The function returns false if all parent components are already registered. + +#### registerComponent + +Register a component by the specified name. +This function is needed because we have lazy component registration. +Keep in mind that you must call `registerParentComponents` before calling this function. +The function returns a meta object of the created component, or undefined if the component isn't found. +If the component is already registered, it won't be registered twice. diff --git a/src/core/component/register/decorator.ts b/src/core/component/register/decorator.ts index df62e0cf8c..5f6a597e8a 100644 --- a/src/core/component/register/decorator.ts +++ b/src/core/component/register/decorator.ts @@ -19,16 +19,27 @@ import { registerParentComponents } from 'core/component/register/helpers'; import type { ComponentOptions } from 'core/component/interface'; /** - * Registers a new component + * Registers a new component based on the tied class * * @decorator * @param [opts] - additional options * * @example - * ```js - * @component() - * class Button { + * ```typescript + * import iBlock, { component, prop, computed } from 'super/i-block/i-block'; * + * @component({functional: true}) + * export default class bUser extends iBlock { + * @prop(String) + * readonly fName: string; + * + * @prop(String) + * readonly lName: string; + * + * @computed({cache: true, dependencies: ['fName', 'lName']}) + * get fullName() { + * return `${this.fName} ${this.lName}`; + * } * } * ``` */ @@ -51,7 +62,7 @@ export function component(opts?: ComponentOptions): Function { } // If we have a smart component, - // we need to compile 2 components in the runtime + // we need to compile two components in the runtime if (Object.isPlainObject(componentParams.functional)) { component({ ...opts, @@ -61,7 +72,6 @@ export function component(opts?: ComponentOptions): Function { } function regComponent(): void { - // Lazy initializing of parent components registerParentComponents(componentInfo); const diff --git a/src/core/component/register/helpers.ts b/src/core/component/register/helpers.ts index 9dd0915c36..2fac2cc491 100644 --- a/src/core/component/register/helpers.ts +++ b/src/core/component/register/helpers.ts @@ -12,12 +12,12 @@ import type { ComponentMeta } from 'core/component/interface'; import type { ComponentConstructorInfo } from 'core/component/reflect'; /** - * Registers a parent component of the specified component to a component library (vue, react, etc.). - * This function is needed because we have lazy registering of components: - * when we see a component "foo" in the first time in a template we need to check registration of all - * its parent components. The function returns false if all component parent already registered. + * Registers parent components for the given one. + * This function is needed because we have lazy component registration: when we see the "foo" component for + * the first time in a template, we need to check the registration of all its parent components. + * The function returns false if all parent components are already registered. * - * @param component - information object of the component + * @param component - the component information object */ export function registerParentComponents(component: ComponentConstructorInfo): boolean { const @@ -46,7 +46,7 @@ export function registerParentComponents(component: ComponentConstructorInfo): b const regParentComponent = componentInitializers[parentName]; - if (regParentComponent) { + if (regParentComponent != null) { for (let i = 0; i < regParentComponent.length; i++) { regParentComponent[i](); } @@ -60,13 +60,13 @@ export function registerParentComponents(component: ComponentConstructorInfo): b } /** - * Register a component by the specified name to a component library (vue, react, etc.). - * This function is needed because we have lazy registering of components. - * Mind that you should call registerParentComponents before calling this function. - * The function returns a meta object of the created component or undefined if the component by the specified name - * wasn't found. If the component already registered, it won't be registered twice. + * Register a component by the specified name. + * This function is needed because we have lazy component registration. + * Keep in mind that you must call `registerParentComponents` before calling this function. + * The function returns a meta object of the created component, or undefined if the component isn't found. + * If the component is already registered, it won't be registered twice. * - * @param name - component name + * @param name - the component name */ export function registerComponent(name: CanUndef): CanUndef { if (name == null || !isComponent.test(name)) { @@ -76,7 +76,7 @@ export function registerComponent(name: CanUndef): CanUndef Date: Fri, 17 Jun 2022 14:14:20 +0300 Subject: [PATCH 0267/2313] doc: improved doc --- src/core/component/meta/interface/options.ts | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts index cca5e42c4f..2b5fe77d18 100644 --- a/src/core/component/meta/interface/options.ts +++ b/src/core/component/meta/interface/options.ts @@ -23,9 +23,9 @@ export interface ComponentOptions { * * } * - * // name == 'Bar' + * // name == 'bExample' * @component() - * class Bar extends iBlock { + * class bExample extends iBlock { * * } * ``` @@ -116,7 +116,15 @@ export interface ComponentOptions { functional?: Nullable | Dictionary; /** - * A dictionary with the deprecated component props with specified alternatives. + * If false, then all default values of the component input properties are ignored. + * This parameter can be inherited from the parent component. + * + * @default `true` + */ + defaultProps?: boolean; + + /** + * A dictionary with the deprecated component props with the specified alternatives. * The keys represent deprecated props; the values represent alternatives. * This parameter can be inherited from the parent component. * @@ -124,7 +132,7 @@ export interface ComponentOptions { * ```typescript * @component({deprecatedProps: { * value: 'items' - * }}}}) + * }}) * * class bList extends iData { * @prop() @@ -168,12 +176,4 @@ export interface ComponentOptions { * @default `true` */ inheritMods?: boolean; - - /** - * If false, then all default values of the component input properties are ignored. - * This parameter can be inherited from the parent component. - * - * @default `true` - */ - defaultProps?: boolean; } From b3f6194620f0a6c1df936ad866569e4ad8dc0cba Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 17 Jun 2022 15:44:37 +0300 Subject: [PATCH 0268/2313] refactor: review what exported from `core/component` --- src/core/component/const/emitters.ts | 53 ---------------- src/core/component/const/index.ts | 1 - src/core/component/decorators/factory.ts | 4 +- src/core/component/engines/vue3/component.ts | 14 ++++- src/core/component/event/README.md | 12 ++-- src/core/component/event/component.ts | 8 +-- src/core/component/event/emitter.ts | 63 ++++++++++++++++++-- src/core/component/event/index.ts | 3 +- src/core/component/event/interface.ts | 2 +- src/core/component/event/providers.ts | 10 ++-- src/core/component/index.ts | 41 +++++++------ src/core/component/interface/index.ts | 7 ++- src/core/component/register/decorator.ts | 5 +- src/super/i-block/i-block.ts | 12 ++-- src/super/i-static-page/i-static-page.ts | 6 +- src/super/i-static-page/interface.ts | 2 +- 16 files changed, 129 insertions(+), 114 deletions(-) delete mode 100644 src/core/component/const/emitters.ts diff --git a/src/core/component/const/emitters.ts b/src/core/component/const/emitters.ts deleted file mode 100644 index 6d274e9296..0000000000 --- a/src/core/component/const/emitters.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { EventEmitter2 as EventEmitter, ListenerFn, OnOptions } from 'eventemitter2'; -import { componentParams } from 'core/component/const/cache'; - -/** - * An event emitter to broadcast component initialize events - */ -export const initEmitter = new EventEmitter({ - maxListeners: 1e3, - newListener: false -}); - -// We need to wrap the original `once` function of the emitter -// to attach logic of registering smart components -((initEventOnce) => { - initEmitter.once = function once( - event: CanArray, - listener: ListenerFn, - opts?: true | OnOptions - ): EventEmitter { - const - events = Array.concat([], event); - - for (let i = 0; i < events.length; i++) { - const - el = events[i], - chunks = el.split('.', 2); - - if (chunks[0] === 'constructor') { - initEventOnce(el, listener, opts); - - const - p = componentParams.get(chunks[1]); - - if (p && Object.isPlainObject(p.functional)) { - initEventOnce(`${el}-functional`, listener, opts); - } - - } else { - initEventOnce(el, listener, opts); - } - } - - return this; - }; -})(initEmitter.once.bind(initEmitter)); diff --git a/src/core/component/const/index.ts b/src/core/component/const/index.ts index 9fc6ceca4e..59a7d5b1be 100644 --- a/src/core/component/const/index.ts +++ b/src/core/component/const/index.ts @@ -14,5 +14,4 @@ export * from 'core/component/const/cache'; export * from 'core/component/const/symbols'; export * from 'core/component/const/enums'; -export * from 'core/component/const/emitters'; export * from 'core/component/const/validators'; diff --git a/src/core/component/decorators/factory.ts b/src/core/component/decorators/factory.ts index a5cc390ad0..2fd0d3f8b2 100644 --- a/src/core/component/decorators/factory.ts +++ b/src/core/component/decorators/factory.ts @@ -9,7 +9,9 @@ import { defProp } from 'core/const/props'; import { storeRgxp } from 'core/component/reflect'; -import { initEmitter, metaPointers } from 'core/component/const'; +import { initEmitter } from 'core/component/event'; + +import { metaPointers } from 'core/component/const'; import { inverseFieldMap, tiedFieldMap } from 'core/component/decorators/const'; import type { ComponentMeta } from 'core/component/interface'; diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index 3f2ad16c76..46d2a49c00 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -8,7 +8,7 @@ import watch, { WatchHandlerParams } from 'core/object/watch'; -import * as init from 'core/component/construct'; +import * as init from 'core/component/init'; import { beforeRenderHooks } from 'core/component/const'; import { fillMeta } from 'core/component/meta'; @@ -124,8 +124,16 @@ export function getComponent(meta: ComponentMeta): ComponentOptions { +globalEmitter.emit = (event: string, ...args) => { const res = originalEmit(event, ...args); log(`global:event:${event.replace(/\./g, ':')}`, ...args); return res; }; -export default emitter; +/** + * The event emitter to broadcast component initialization events + */ +export const initEmitter = new EventEmitter({ + maxListeners: 1e3, + newListener: false +}); + +// We need to wrap the original `once` function of the emitter +// to attach logic of registering smart components +((initEventOnce) => { + initEmitter.once = function once( + event: CanArray, + listener: ListenerFn, + opts?: true | OnOptions + ): EventEmitter { + const + events = Array.concat([], event); + + for (let i = 0; i < events.length; i++) { + const + el = events[i], + chunks = el.split('.', 2); + + if (chunks[0] === 'constructor') { + initEventOnce(el, listener, opts); + + const + p = componentParams.get(chunks[1]); + + if (p && Object.isPlainObject(p.functional)) { + initEventOnce(`${el}-functional`, listener, opts); + } + + } else { + initEventOnce(el, listener, opts); + } + } + + return this; + }; +})(initEmitter.once.bind(initEmitter)); + diff --git a/src/core/component/event/index.ts b/src/core/component/event/index.ts index 94ffe137c0..fbc8658fe8 100644 --- a/src/core/component/event/index.ts +++ b/src/core/component/event/index.ts @@ -13,7 +13,6 @@ import 'core/component/event/providers'; -export { default as emitter } from 'core/component/event/emitter'; - +export * from 'core/component/event/emitter'; export * from 'core/component/event/component'; export * from 'core/component/event/interface'; diff --git a/src/core/component/event/interface.ts b/src/core/component/event/interface.ts index ef5282c2e5..859a206ef9 100644 --- a/src/core/component/event/interface.ts +++ b/src/core/component/event/interface.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -export type ResetType = +export type ComponentResetType = 'load' | 'load.silence' | 'router' | diff --git a/src/core/component/event/providers.ts b/src/core/component/event/providers.ts index 933fd48bfc..763156af2e 100644 --- a/src/core/component/event/providers.ts +++ b/src/core/component/event/providers.ts @@ -10,20 +10,20 @@ import { emitter as i18nEmitter } from 'core/i18n'; import { emitter as netEmitter } from 'core/net'; import { emitter as sessionEmitter } from 'core/session'; -import emitter from 'core/component/event/emitter'; +import { globalEmitter } from 'core/component/event/emitter'; i18nEmitter.on('setLocale', (...args) => { - emitter.emit('i18n.setLocale', ...args); + globalEmitter.emit('i18n.setLocale', ...args); }); netEmitter.on('status', (...args) => { - emitter.emit('net.status', ...args); + globalEmitter.emit('net.status', ...args); }); sessionEmitter.on('set', (...args) => { - emitter.emit('session.set', ...args); + globalEmitter.emit('session.set', ...args); }); sessionEmitter.on('clear', (...args) => { - emitter.emit('session.clear', ...args); + globalEmitter.emit('session.clear', ...args); }); diff --git a/src/core/component/index.ts b/src/core/component/index.ts index 96e29547ae..752bd7c588 100644 --- a/src/core/component/index.ts +++ b/src/core/component/index.ts @@ -8,37 +8,40 @@ import 'core/component/directives'; -export * from 'core/component/const'; -export * from 'core/component/functional'; +import * as init from 'core/component/init'; +import * as globalState from 'core/component/state'; -export * from 'core/component/hook'; -export * from 'core/component/field'; -export * from 'core/component/ref'; -export * from 'core/component/watch'; +export { init, globalState }; +export { ComponentEngine as default } from 'core/component/engines'; -export * from 'core/component/register'; -export * from 'core/component/reflect'; -export * from 'core/component/method'; +export { runHook } from 'core/component/hook'; +export { resolveRefs } from 'core/component/ref'; +export { bindRemoteWatchers, customWatcherRgxp } from 'core/component/watch'; -export * from 'core/component/event'; -export * from 'core/component/render'; +export { component } from 'core/component/register'; +export { callMethodFromComponent } from 'core/component/method'; +export { normalizeClass, normalizeStyle } from 'core/component/render'; export { - default as globalEmitter, + isComponent, - /** @deprecated */ - default as globalEvent + rootComponents, + globalRootComponent, -} from 'core/component/event'; + PARENT + +} from 'core/component/const'; export { - cloneVNode, + initEmitter, + globalEmitter, - ComponentEngine as default, - VNode + resetComponents, + ComponentResetType -} from 'core/component/engines'; +} from 'core/component/event'; +export * from 'core/component/reflect'; export * from 'core/component/interface'; diff --git a/src/core/component/interface/index.ts b/src/core/component/interface/index.ts index 5af196cc93..1cd326bc44 100644 --- a/src/core/component/interface/index.ts +++ b/src/core/component/interface/index.ts @@ -11,11 +11,12 @@ * @packageDocumentation */ -export * from 'core/component/interface/component'; export * from 'core/component/meta/interface'; export * from 'core/component/reflect/interface'; + +export * from 'core/component/interface/lc'; export * from 'core/component/interface/mod'; -export * from 'core/component/interface/watch'; export * from 'core/component/interface/link'; -export * from 'core/component/interface/lc'; +export * from 'core/component/interface/watch'; +export * from 'core/component/interface/component'; export * from 'core/component/interface/engine'; diff --git a/src/core/component/register/decorator.ts b/src/core/component/register/decorator.ts index 5f6a597e8a..bd35e4cb97 100644 --- a/src/core/component/register/decorator.ts +++ b/src/core/component/register/decorator.ts @@ -10,6 +10,7 @@ import { identity } from 'core/functools'; import * as c from 'core/component/const'; +import { initEmitter } from 'core/component/event'; import { createMeta, fillMeta, attachTemplatesToMeta } from 'core/component/meta'; import { getInfoFromConstructor } from 'core/component/reflect'; @@ -49,7 +50,7 @@ export function component(opts?: ComponentOptions): Function { componentInfo = getInfoFromConstructor(target, opts), componentParams = componentInfo.params; - c.initEmitter + initEmitter .emit('bindConstructor', componentInfo.name); if (!Object.isTruly(componentInfo.name) || componentParams.root || componentInfo.isAbstract) { @@ -86,7 +87,7 @@ export function component(opts?: ComponentOptions): Function { } c.components.set(componentName, meta); - c.initEmitter.emit(`constructor.${componentName}`, {meta, parentMeta}); + initEmitter.emit(`constructor.${componentName}`, {meta, parentMeta}); if (componentInfo.isAbstract || meta.params.functional === true) { fillMeta(meta, target); diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index 8766288249..a049c1ea2f 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -49,14 +49,17 @@ import type iStaticPage from 'super/i-static-page/i-static-page'; import { + init, component, PARENT, + globalState, globalEmitter, - customWatcherRgxp, resolveRefs, + bindRemoteWatchers, + customWatcherRgxp, WatchPath, RawWatchHandler, @@ -68,9 +71,6 @@ import { } from 'core/component'; -import remoteState from 'core/component/state'; -import * as init from 'core/component/init'; - import 'super/i-block/directives'; import { statuses } from 'super/i-block/const'; @@ -532,8 +532,8 @@ export default abstract class iBlock extends ComponentInterface { * but remember that these mutations may force the re-rendering of all components. */ @computed({watchable: true}) - get remoteState(): typeof remoteState { - return remoteState; + get remoteState(): typeof globalState { + return globalState; } /** diff --git a/src/super/i-static-page/i-static-page.ts b/src/super/i-static-page/i-static-page.ts index b9ae9d8c09..623e16b933 100644 --- a/src/super/i-static-page/i-static-page.ts +++ b/src/super/i-static-page/i-static-page.ts @@ -15,7 +15,7 @@ import symbolGenerator from 'core/symbol'; import { RestrictedCache } from 'core/cache'; import { setLocale, locale } from 'core/i18n'; -import { reset, ResetType, ComponentInterface } from 'core/component'; +import { resetComponents, ComponentResetType, ComponentInterface } from 'core/component'; import type bRouter from 'base/b-router/b-router'; import type { AppliedRoute } from 'core/router'; @@ -235,8 +235,8 @@ export default abstract class iStaticPage extends iPage { * * @param [type] - reset type */ - reset(type?: ResetType): void { - this.nextTick(() => reset(type), { + reset(type?: ComponentResetType): void { + this.nextTick(() => resetComponents(type), { label: $$.reset }); } diff --git a/src/super/i-static-page/interface.ts b/src/super/i-static-page/interface.ts index 04aa18f890..71c8447861 100644 --- a/src/super/i-static-page/interface.ts +++ b/src/super/i-static-page/interface.ts @@ -9,7 +9,7 @@ import type remoteState from 'core/component/state'; import type { ComponentInterface } from 'core/component'; -export { globalEmitter, ResetType } from 'core/component'; +export { globalEmitter, ComponentResetType } from 'core/component'; export type RemoteState = typeof remoteState; export interface RootMod { From 8c6ad709284d4f1652fa4326eb184894d1191433 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 17 Jun 2022 17:45:45 +0300 Subject: [PATCH 0269/2313] doc: improved doc --- src/core/component/decorators/field.ts | 2 +- .../decorators/interface/accessor.ts | 17 ++-- .../component/decorators/interface/field.ts | 90 ++++++++++++++++--- .../component/decorators/interface/hook.ts | 2 +- src/core/component/decorators/system.ts | 2 +- src/core/component/interface/watch.ts | 11 +++ src/core/component/register/README.md | 8 +- 7 files changed, 102 insertions(+), 30 deletions(-) diff --git a/src/core/component/decorators/field.ts b/src/core/component/decorators/field.ts index e312566830..f238190319 100644 --- a/src/core/component/decorators/field.ts +++ b/src/core/component/decorators/field.ts @@ -11,7 +11,7 @@ import type { InitFieldFn, DecoratorField } from 'core/component/decorators/inte /** * Marks a class property as a component field. - * In regular components, mutations of field properties force component re-rendering. + * In non-functional components, field property mutations typically cause the component to re-render. * * @decorator * diff --git a/src/core/component/decorators/interface/accessor.ts b/src/core/component/decorators/interface/accessor.ts index 7d101e6194..bab4e0ac33 100644 --- a/src/core/component/decorators/interface/accessor.ts +++ b/src/core/component/decorators/interface/accessor.ts @@ -11,30 +11,30 @@ import type { DecoratorFunctionalOptions } from 'core/component/decorators/inter export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { /** - * If true, the accessor value will be cached every time it read or changed. - * The option is set to true by default if also provided `dependencies` or the bound accessor matches by a name with - * another prop or field. If the options value is passed as `auto`, caching will be delegated to the used render - * engine. + * If true, the accessor value will be cached after the first touch. + * The option is set to true by default if also provided `dependencies` or the bound accessor matches + * by the name with another prop or field. If the option value is passed as `auto`, caching will be delegated to + * the used component library. */ cache?: ComponentAccessorCacheType; /** - * If true, mutations of the accessor value can be watched + * If true, the accessor returns a link to another watchable object */ watchable?: boolean; /** * A list of dependencies for the accessor. - * The dependencies are needed to watch for changes of the accessor or to invalidate the cache. + * The dependencies are needed to watch for the accessor mutations or to invalidate its cache. * * Also, when the accessor has a logically connected prop/field - * (by using a name convention "${property} -> ${property}Prop | ${property}Store"), + * (by using the name convention "${property} -> ${property}Prop | ${property}Store"), * we don't need to add additional dependencies. * * @example * ```typescript * @component() - * class Foo extends iBlock { + * class bExample extends iBlock { * @field() * blaStore: number = 0; * @@ -43,7 +43,6 @@ export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { * return this.blaStore * 2; * } * - * @computed({cache: true}) * get bla(): number { * return blaStore * 3; * } diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts index 60df2a78cb..1c516e590f 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/interface/field.ts @@ -16,7 +16,7 @@ export interface DecoratorSystem< B = A > extends DecoratorFunctionalOptions { /** - * If true, then the property is unique for each component. + * Marks the field as unique for each component instance. * Also, the parameter can take a function that returns a boolean value. * * @default `false` @@ -24,17 +24,20 @@ export interface DecoratorSystem< unique?: boolean | UniqueFieldFn; /** - * Default value of the property + * Default field value */ default?: unknown; /** - * Initializer of the field value + * A function to initialize the field value. + * The function takes as its first argument a reference to the component context. + * As the second argument, the function takes a reference to a dictionary with other fields of the same type that + * have already been initialized. * * @example - * ``` + * ```typescript * @component() - * class Foo extends iBlock { + * class bExample extends iBlock { * @field({init: () => Math.random()}) * bla!: number; * } @@ -43,30 +46,89 @@ export interface DecoratorSystem< init?: InitFieldFn; /** - * If true, the property will be initialized before all non-atom properties + * Indicates that property should be initialized before all non-atomic properties * @default `false` */ atom?: boolean; /** - * Name or a list of names after which this property should be initialized + * A name or list of names after which this property should be initialized. + * Keep in mind, you can only specify names that are of the same type as the current field (fields or system). + * + * @example + * ```typescript + * @component() + * class bExample extends iBlock { + * @system(() => Math.random()) + * bla!: number; + * + * @system({ + * after: 'bla', + * init: (ctx, data) => data.bla + 10 + * }) + * + * baz!: number; + * } + * ``` */ after?: CanArray; /** - * Watcher for changes of the property + * A watcher or list of watchers for the current field. + * The watcher can be defined as a component method to invoke, callback function, or watch handle. + * + * @example + * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @system({watch: [ + * 'onIncrement', + * + * (ctx, val, oldVal, info) => + * console.log(val, oldVal, info), + * + * // Also, see core/object/watch + * { + * // If false, then a handler that is invoked on the watcher event does not take any arguments from the event + * provideArgs: false, + * + * // How the event handler should be called: + * // + * // 1. `'post'` - the handler will be called on the next tick after the mutation and + * // guaranteed after updating all tied templates; + * // + * // 2. `'pre'` - the handler will be called on the next tick after the mutation and + * // guaranteed before updating all tied templates; + * // + * // 3. `'sync'` - the handler will be invoked immediately after each mutation. + * flush: 'sync', + * + * // Can define as a function too + * handler: 'onIncrement' + * } + * ]}) + * + * i: number = 0; + * + * onIncrement(val, oldVal, info) { + * console.log(val, oldVal, info); + * } + * } + * ``` */ watch?: DecoratorFieldWatcher; /** - * If false, the property can't be watched within a functional component + * If false, the field cannot be watched if created inside a functional component * @default `true` */ functionalWatching?: boolean; /** - * If true, then if a component will restore own state from the old component - * (it occurs when you use a functional component), the actual value will be merged with the previous. + * If true, then if the component will restore its own state from the old component + * (this happens when using a functional component), then the actual value will be merged with the previous one. * Also, this parameter can take a function to merge. * * @default `false` @@ -74,7 +136,7 @@ export interface DecoratorSystem< merge?: MergeFieldFn | boolean; /** - * Non-standard extra information of the field + * A dictionary with some extra information of the field */ meta?: Dictionary; } @@ -85,8 +147,8 @@ export interface DecoratorField< B = A > extends DecoratorSystem { /** - * If false, then changes of the property don't directly force re-rendering of a template. - * Mind, the template still can be re-rendered, but only at the initiative of the engine used. + * If false, then property changes don't directly force re-rendering the template. + * Keep in mind that the template can still be re-rendered, but only at the initiative of the engine being used. * @default `true` */ forceUpdate?: boolean; diff --git a/src/core/component/decorators/interface/hook.ts b/src/core/component/decorators/interface/hook.ts index d44b1ef566..c02852ead7 100644 --- a/src/core/component/decorators/interface/hook.ts +++ b/src/core/component/decorators/interface/hook.ts @@ -18,7 +18,7 @@ export type DecoratorHook = export type DecoratorHookOptions = { [hook in Hook]?: DecoratorFunctionalOptions & { /** - * Method name or a list of names after which this handler should be invoked on a registered hook event + * A method name or list of names after which this handler should be invoked on a registered hook event */ after?: CanArray; } diff --git a/src/core/component/decorators/system.ts b/src/core/component/decorators/system.ts index 7b08ba2550..2a59c9cc64 100644 --- a/src/core/component/decorators/system.ts +++ b/src/core/component/decorators/system.ts @@ -11,7 +11,7 @@ import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/int /** * Marks a class property as a system field. - * Mutations of system properties never force component re-rendering. + * System property mutations never cause components to re-render. * * @decorator * diff --git a/src/core/component/interface/watch.ts b/src/core/component/interface/watch.ts index 58f3b4e1a1..4b216fec48 100644 --- a/src/core/component/interface/watch.ts +++ b/src/core/component/interface/watch.ts @@ -24,6 +24,17 @@ export { WatchHandlerParams }; export type Flush = 'post' | 'pre' | 'sync'; export interface WatchOptions extends RawWatchOptions { + /** + * How the event handler should be called: + * + * 1. `'post'` - the handler will be called on the next tick after the mutation and + * guaranteed after updating all tied templates; + * + * 2. `'pre'` - the handler will be called on the next tick after the mutation and + * guaranteed before updating all tied templates; + * + * 3. `'sync'` - the handler will be invoked immediately after each mutation. + */ flush?: Flush; } diff --git a/src/core/component/register/README.md b/src/core/component/register/README.md index 8e6be8cdb1..2cf29b4938 100644 --- a/src/core/component/register/README.md +++ b/src/core/component/register/README.md @@ -17,8 +17,8 @@ export default class bUser extends iBlock { ## How to create a component? -Also, you can pass additional parameters to this decorator to create a component. -For example, that a component should be created as functional. +To register a new component, you need to create a simple JS class and add the `@component` decorator to it. +Also, you can pass additional parameters to this decorator. For example, in order for a component to be created as functional. ```typescript import iBlock, { component, prop } from 'super/i-block/i-block'; @@ -35,7 +35,7 @@ export default class bUser extends iBlock { ### How does it work? -The decorator aggregates information of the class received from other nested decorators and +The `@component` decorator aggregates information of the class received from other nested decorators and with the help of reflection and forms a special structure of the [[ComponentMeta]] type. Next, the created structure will be passed to the used component library adapter, which will create a "real" component. @@ -198,7 +198,7 @@ This parameter can be inherited from the parent component. ### Decorators -#### component +#### @component The decorator to register a new component based on the tied class. From 9d337f3acb743f99b3d43a6d7a6c2217accc817d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 17 Jun 2022 17:53:13 +0300 Subject: [PATCH 0270/2313] doc: added the normal documentation --- src/core/component/decorators/CHANGELOG.md | 4 + src/core/component/decorators/README.md | 16 ++- .../component/decorators/computed/README.md | 82 +++++++++++ .../{computed.ts => computed/index.ts} | 5 + src/core/component/decorators/field/README.md | 130 ++++++++++++++++++ .../decorators/{field.ts => field/index.ts} | 5 + src/core/component/decorators/prop/README.md | 22 +++ .../decorators/{prop.ts => prop/index.ts} | 5 + .../component/decorators/system/README.md | 130 ++++++++++++++++++ .../decorators/{system.ts => system/index.ts} | 5 + 10 files changed, 397 insertions(+), 7 deletions(-) create mode 100644 src/core/component/decorators/computed/README.md rename src/core/component/decorators/{computed.ts => computed/index.ts} (87%) create mode 100644 src/core/component/decorators/field/README.md rename src/core/component/decorators/{field.ts => field/index.ts} (90%) create mode 100644 src/core/component/decorators/prop/README.md rename src/core/component/decorators/{prop.ts => prop/index.ts} (90%) create mode 100644 src/core/component/decorators/system/README.md rename src/core/component/decorators/{system.ts => system/index.ts} (89%) diff --git a/src/core/component/decorators/CHANGELOG.md b/src/core/component/decorators/CHANGELOG.md index 7480398149..f22195a037 100644 --- a/src/core/component/decorators/CHANGELOG.md +++ b/src/core/component/decorators/CHANGELOG.md @@ -15,6 +15,10 @@ Changelog * Added a new cache type for accessors `auto` +#### :memo: Documentation + +* Added the normal documentation + #### :house: Internal * Refactoring diff --git a/src/core/component/decorators/README.md b/src/core/component/decorators/README.md index c90ae5e4fc..a96434d37e 100644 --- a/src/core/component/decorators/README.md +++ b/src/core/component/decorators/README.md @@ -1,15 +1,17 @@ # core/component/decorators -This module provides a bunch of decorators to annotate a component. +This module provides a bunch of decorators to annotate component properties. +Using these and the `@component` decorator, you can register a component based on your JS class. ```typescript -export default class bExample { +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +export default class bUser extends iBlock { @prop(String) - readonly foo: string; + readonly fName: string; - @watch('foo') - onFoo() { - console.log('`Foo` was changed'); - } + @prop(String) + readonly lName: string; } ``` diff --git a/src/core/component/decorators/computed/README.md b/src/core/component/decorators/computed/README.md new file mode 100644 index 0000000000..79c6a381fc --- /dev/null +++ b/src/core/component/decorators/computed/README.md @@ -0,0 +1,82 @@ +# core/component/decorators/computed + +The decorator attaches meta information to a component computed field or accessor. + +```typescript +import iBlock, { component, prop, computed } from 'super/i-block/i-block'; + +@component() +export default class bUser extends iBlock { + @prop() + readonly fName: string; + + @prop() + readonly lName: string; + + @prop({required: false}) + stageProp?: 'basic' | 'advanced'; + + // This is a cacheable computed field with feature of watching and cache invalidation + @computed({cache: true, dependencies: ['fName', 'lName']}) + get fullName() { + return `${this.fName} ${this.lName}`; + } + + // This is a cacheable computed field without cache invalidation + @computed({cache: true}) + get id() { + return Math.random(); + } + + // This is a cacheable computed field tied with the `stageProp` prop + get stage() { + return Math.random(); + } + + // This is a simple accessor (a getter) + get element() { + return this.$el; + } +} +``` + +#### Additional options + +##### [cache] + +If true, the accessor value will be cached after the first touch. +The option is set to true by default if also provided `dependencies` or the bound accessor matches +by the name with another prop or field. If the option value is passed as `auto`, caching will be delegated to +the used component library. + +##### [watchable] + +If true, the accessor returns a link to another watchable object. + +##### [dependencies] + +A list of dependencies for the accessor. +The dependencies are needed to watch for the accessor mutations or to invalidate its cache. + +Also, when the accessor has a logically connected prop/field +(by using the name convention "${property} -> ${property}Prop | ${property}Store"), +we don't need to add additional dependencies. + +```typescript +import iBlock, { component, field, computed } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field() + blaStore: number = 0; + + @computed({cache: true, dependencies: ['blaStore']}) + get bar(): number { + return this.blaStore * 2; + } + + get bla(): number { + return blaStore * 3; + } +} +``` diff --git a/src/core/component/decorators/computed.ts b/src/core/component/decorators/computed/index.ts similarity index 87% rename from src/core/component/decorators/computed.ts rename to src/core/component/decorators/computed/index.ts index 0a03f346fc..0b76a3ebac 100644 --- a/src/core/component/decorators/computed.ts +++ b/src/core/component/decorators/computed/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/computed/README.md]] + * @packageDocumentation + */ + import { paramsFactory } from 'core/component/decorators/factory'; import type { DecoratorComponentAccessor } from 'core/component/decorators/interface'; diff --git a/src/core/component/decorators/field/README.md b/src/core/component/decorators/field/README.md new file mode 100644 index 0000000000..f26893793e --- /dev/null +++ b/src/core/component/decorators/field/README.md @@ -0,0 +1,130 @@ +# core/component/decorators/field + +The decorator marks a class property as a component field. +In non-functional components, field property mutations typically cause the component to re-render. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field() + bla: number = 0; + + @field(() => Math.random()) + baz?: number; +} +``` + +#### Additional options + +##### [unique = `false`] + +Marks the field as unique for each component instance. +Also, the parameter can take a function that returns a boolean value. + +##### [default] + +Default field value. + +##### [init] + +A function to initialize the field value. +The function takes as its first argument a reference to the component context. +As the second argument, the function takes a reference to a dictionary with other fields of the same type that +have already been initialized. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field({init: () => Math.random()}) + bla!: number; +} +``` + +##### [atom = `false`] + +Indicates that property should be initialized before all non-atomic properties. + +##### [after] + +A name or list of names after which this property should be initialized. +Keep in mind, you can only specify names that are of the same type as the current field (fields or field). + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field(() => Math.random()) + bla!: number; + + @field({ + after: 'bla', + init: (ctx, data) => data.bla + 10 + }) + + baz!: number; +} +``` + +##### [watch] + +A watcher or list of watchers for the current field. +The watcher can be defined as a component method to invoke, callback function, or watch handle. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field({watch: [ + 'onIncrement', + + (ctx, val, oldVal, info) => + console.log(val, oldVal, info), + + // Also, see core/object/watch + { + // If false, then a handler that is invoked on the watcher event does not take any arguments from the event + provideArgs: false, + + // How the event handler should be called: + // + // 1. `'post'` - the handler will be called on the next tick after the mutation and + // guaranteed after updating all tied templates; + // + // 2. `'pre'` - the handler will be called on the next tick after the mutation and + // guaranteed before updating all tied templates; + // + // 3. `'sync'` - the handler will be invoked immediately after each mutation. + flush: 'sync', + + // Can define as a function too + handler: 'onIncrement' + } + ]}) + + i: number = 0; + + onIncrement(val, oldVal, info) { + console.log(val, oldVal, info); + } +} +``` + +##### [functionalWatching = `false`] + +If false, the field cannot be watched if created inside a functional component + +##### [merge = `false`] + +If true, then if the component will restore its own state from the old component +(this happens when using a functional component), then the actual value will be merged with the previous one. +Also, this parameter can take a function to merge. + +##### [meta] + +A dictionary with some extra information of the field. diff --git a/src/core/component/decorators/field.ts b/src/core/component/decorators/field/index.ts similarity index 90% rename from src/core/component/decorators/field.ts rename to src/core/component/decorators/field/index.ts index f238190319..29b100c49e 100644 --- a/src/core/component/decorators/field.ts +++ b/src/core/component/decorators/field/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/field/README.md]] + * @packageDocumentation + */ + import { paramsFactory } from 'core/component/decorators/factory'; import type { InitFieldFn, DecoratorField } from 'core/component/decorators/interface'; diff --git a/src/core/component/decorators/prop/README.md b/src/core/component/decorators/prop/README.md new file mode 100644 index 0000000000..23a47464bf --- /dev/null +++ b/src/core/component/decorators/prop/README.md @@ -0,0 +1,22 @@ +# core/component/decorators/prop + +The decorator marks a class property as a component prop. + +```typescript +import iBlock, { component, prop } from '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; +} +``` + +#### Additional options + diff --git a/src/core/component/decorators/prop.ts b/src/core/component/decorators/prop/index.ts similarity index 90% rename from src/core/component/decorators/prop.ts rename to src/core/component/decorators/prop/index.ts index 90d4b49e97..158fad4542 100644 --- a/src/core/component/decorators/prop.ts +++ b/src/core/component/decorators/prop/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/prop/README.md]] + * @packageDocumentation + */ + import { paramsFactory } from 'core/component/decorators/factory'; import type { DecoratorProp } from 'core/component/decorators/interface'; diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md new file mode 100644 index 0000000000..e959501aa6 --- /dev/null +++ b/src/core/component/decorators/system/README.md @@ -0,0 +1,130 @@ +# core/component/decorators/system + +The decorator marks a class property as a system field. +System property mutations never cause components to re-render. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system() + bla: number = 0; + + @system(() => Math.random()) + baz?: number; +} +``` + +#### Additional options + +##### [unique = `false`] + +Marks the field as unique for each component instance. +Also, the parameter can take a function that returns a boolean value. + +##### [default] + +Default field value. + +##### [init] + +A function to initialize the field value. +The function takes as its first argument a reference to the component context. +As the second argument, the function takes a reference to a dictionary with other fields of the same type that +have already been initialized. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system({init: () => Math.random()}) + bla!: number; +} +``` + +##### [atom = `false`] + +Indicates that property should be initialized before all non-atomic properties. + +##### [after] + +A name or list of names after which this property should be initialized. +Keep in mind, you can only specify names that are of the same type as the current field (fields or system). + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system(() => Math.random()) + bla!: number; + + @system({ + after: 'bla', + init: (ctx, data) => data.bla + 10 + }) + + baz!: number; +} +``` + +##### [watch] + +A watcher or list of watchers for the current field. +The watcher can be defined as a component method to invoke, callback function, or watch handle. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system({watch: [ + 'onIncrement', + + (ctx, val, oldVal, info) => + console.log(val, oldVal, info), + + // Also, see core/object/watch + { + // If false, then a handler that is invoked on the watcher event does not take any arguments from the event + provideArgs: false, + + // How the event handler should be called: + // + // 1. `'post'` - the handler will be called on the next tick after the mutation and + // guaranteed after updating all tied templates; + // + // 2. `'pre'` - the handler will be called on the next tick after the mutation and + // guaranteed before updating all tied templates; + // + // 3. `'sync'` - the handler will be invoked immediately after each mutation. + flush: 'sync', + + // Can define as a function too + handler: 'onIncrement' + } + ]}) + + i: number = 0; + + onIncrement(val, oldVal, info) { + console.log(val, oldVal, info); + } +} +``` + +##### [functionalWatching = `false`] + +If false, the field cannot be watched if created inside a functional component + +##### [merge = `false`] + +If true, then if the component will restore its own state from the old component +(this happens when using a functional component), then the actual value will be merged with the previous one. +Also, this parameter can take a function to merge. + +##### [meta] + +A dictionary with some extra information of the field. diff --git a/src/core/component/decorators/system.ts b/src/core/component/decorators/system/index.ts similarity index 89% rename from src/core/component/decorators/system.ts rename to src/core/component/decorators/system/index.ts index 2a59c9cc64..2f2f496be3 100644 --- a/src/core/component/decorators/system.ts +++ b/src/core/component/decorators/system/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/system/README.md]] + * @packageDocumentation + */ + import { paramsFactory } from 'core/component/decorators/factory'; import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/interface'; From 447f1121f2c848eb44ace1542677a2c4b059a091 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Fri, 17 Jun 2022 18:23:15 +0300 Subject: [PATCH 0271/2313] doc: improved doc --- src/core/component/decorators/field/README.md | 53 +++++++++++++++++-- .../component/decorators/interface/field.ts | 53 +++++++++++++++---- .../component/decorators/system/README.md | 52 +++++++++++++++--- src/core/component/decorators/system/index.ts | 2 +- 4 files changed, 138 insertions(+), 22 deletions(-) diff --git a/src/core/component/decorators/field/README.md b/src/core/component/decorators/field/README.md index f26893793e..65651820e1 100644 --- a/src/core/component/decorators/field/README.md +++ b/src/core/component/decorators/field/README.md @@ -8,11 +8,17 @@ import iBlock, { component, field } from 'super/i-block/i-block'; @component() class bExample extends iBlock { + // The decorator can be called either without parameters @field() bla: number = 0; + // Or by passing a value initializer function @field(() => Math.random()) - baz?: number; + baz!: number; + + // Or a dictionary with additional options + @field({unique: true, init: () => Math.random()}) + ban!: number; } ``` @@ -25,7 +31,18 @@ Also, the parameter can take a function that returns a boolean value. ##### [default] -Default field value. +This option allows you to set a default value for the field. +But using it, as a rule, is not explicitly required, since a default value can be passed through the native syntax of class properties. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field() + bla: number = 0; +} +``` ##### [init] @@ -41,12 +58,16 @@ import iBlock, { component, field } from 'super/i-block/i-block'; class bExample extends iBlock { @field({init: () => Math.random()}) bla!: number; + + @field(() => Math.random()) + bar!: number; } ``` -##### [atom = `false`] +##### [forceUpdate = `true`] -Indicates that property should be initialized before all non-atomic properties. +If false, then property changes don't directly force re-rendering the template. +Keep in mind that the template can still be re-rendered, but only at the initiative of the engine being used. ##### [after] @@ -70,11 +91,33 @@ class bExample extends iBlock { } ``` +##### [atom = `false`] + +Indicates that property should be initialized before all non-atom properties. +This option is needed when you have a field that must be guaranteed to be initialized before other fields, +and you don't want to use `after` everywhere. But you can still use `after` along with other atomic fields. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field({atom: true, init: () => Math.random()}) + bla!: number; + + @field((ctx, data) => data.bla + 10) + baz!: number; +} +``` + ##### [watch] A watcher or list of watchers for the current field. The watcher can be defined as a component method to invoke, callback function, or watch handle. +The `core/watch` module is used to make objects watchable. +Therefore, for more information, please refer to its documentation. + ```typescript import iBlock, { component, field } from 'super/i-block/i-block'; @@ -117,7 +160,7 @@ class bExample extends iBlock { ##### [functionalWatching = `false`] -If false, the field cannot be watched if created inside a functional component +If false, the field can't be watched if created inside a functional component. ##### [merge = `false`] diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts index 1c516e590f..be1c315b30 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/interface/field.ts @@ -24,7 +24,20 @@ export interface DecoratorSystem< unique?: boolean | UniqueFieldFn; /** - * Default field value + * This option allows you to set a default value for the field. + * But using it, as a rule, is not explicitly required, since a default value can be passed through the + * native syntax of class properties. + * + * @example + * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @system() + * bla: number = 0; + * } + * ``` */ default?: unknown; @@ -38,19 +51,16 @@ export interface DecoratorSystem< * ```typescript * @component() * class bExample extends iBlock { - * @field({init: () => Math.random()}) + * @system({init: () => Math.random()}) * bla!: number; + * + * @system(() => Math.random()) + * bar!: number; * } * ``` */ init?: InitFieldFn; - /** - * Indicates that property should be initialized before all non-atomic properties - * @default `false` - */ - atom?: boolean; - /** * A name or list of names after which this property should be initialized. * Keep in mind, you can only specify names that are of the same type as the current field (fields or system). @@ -73,10 +83,35 @@ export interface DecoratorSystem< */ after?: CanArray; + /** + * Indicates that property should be initialized before all non-atom properties. + * This option is needed when you have a field that must be guaranteed to be initialized before other fields, + * and you don't want to use `after` everywhere. But you can still use `after` along with other atomic fields. + * + * @default `false` + * @example + * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @system({atom: true, init: () => Math.random()}) + * bla!: number; + * + * @system((ctx, data) => data.bla + 10) + * baz!: number; + * } + * ``` + */ + atom?: boolean; + /** * A watcher or list of watchers for the current field. * The watcher can be defined as a component method to invoke, callback function, or watch handle. * + * The `core/watch` module is used to make objects watchable. + * Therefore, for more information, please refer to its documentation. + * * @example * ```typescript * import iBlock, { component, system } from 'super/i-block/i-block'; @@ -121,7 +156,7 @@ export interface DecoratorSystem< watch?: DecoratorFieldWatcher; /** - * If false, the field cannot be watched if created inside a functional component + * If false, the field can't be watched if created inside a functional component * @default `true` */ functionalWatching?: boolean; diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md index e959501aa6..fc7a181b01 100644 --- a/src/core/component/decorators/system/README.md +++ b/src/core/component/decorators/system/README.md @@ -8,11 +8,17 @@ import iBlock, { component, system } from 'super/i-block/i-block'; @component() class bExample extends iBlock { + // The decorator can be called either without parameters @system() bla: number = 0; + // Or by passing a value initializer function @system(() => Math.random()) - baz?: number; + baz!: number; + + // Or a dictionary with additional options + @system({unique: true, init: () => Math.random()}) + ban!: number; } ``` @@ -25,7 +31,18 @@ Also, the parameter can take a function that returns a boolean value. ##### [default] -Default field value. +This option allows you to set a default value for the field. +But using it, as a rule, is not explicitly required, since a default value can be passed through the native syntax of class properties. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system() + bla: number = 0; +} +``` ##### [init] @@ -41,13 +58,12 @@ import iBlock, { component, system } from 'super/i-block/i-block'; class bExample extends iBlock { @system({init: () => Math.random()}) bla!: number; + + @system(() => Math.random()) + bar!: number; } ``` -##### [atom = `false`] - -Indicates that property should be initialized before all non-atomic properties. - ##### [after] A name or list of names after which this property should be initialized. @@ -70,11 +86,33 @@ class bExample extends iBlock { } ``` +##### [atom = `false`] + +Indicates that property should be initialized before all non-atom properties. +This option is needed when you have a field that must be guaranteed to be initialized before other fields, +and you don't want to use `after` everywhere. But you can still use `after` along with other atomic fields. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system({atom: true, init: () => Math.random()}) + bla!: number; + + @system((ctx, data) => data.bla + 10) + baz!: number; +} +``` + ##### [watch] A watcher or list of watchers for the current field. The watcher can be defined as a component method to invoke, callback function, or watch handle. +The `core/watch` module is used to make objects watchable. +Therefore, for more information, please refer to its documentation. + ```typescript import iBlock, { component, system } from 'super/i-block/i-block'; @@ -117,7 +155,7 @@ class bExample extends iBlock { ##### [functionalWatching = `false`] -If false, the field cannot be watched if created inside a functional component +If false, the field can't be watched if created inside a functional component. ##### [merge = `false`] diff --git a/src/core/component/decorators/system/index.ts b/src/core/component/decorators/system/index.ts index 2f2f496be3..461bb64508 100644 --- a/src/core/component/decorators/system/index.ts +++ b/src/core/component/decorators/system/index.ts @@ -28,7 +28,7 @@ import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/int * bla: number = 0; * * @system(() => Math.random()) - * baz?: number; + * baz!: number; * } * ``` */ From c887f0a81a6e486615f1dca14a145b93dd60e4d7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 20 Jun 2022 15:35:38 +0300 Subject: [PATCH 0272/2313] refactor: moved the `component` decorator to `core/component/decorators` --- .../component/decorators/component/README.md | 233 ++++++++++++++++++ .../component/index.ts} | 0 2 files changed, 233 insertions(+) create mode 100644 src/core/component/decorators/component/README.md rename src/core/component/{register/decorator.ts => decorators/component/index.ts} (100%) diff --git a/src/core/component/decorators/component/README.md b/src/core/component/decorators/component/README.md new file mode 100644 index 0000000000..1068027f88 --- /dev/null +++ b/src/core/component/decorators/component/README.md @@ -0,0 +1,233 @@ +# core/component/decorators/component + +This module provides an API to register components. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; +} +``` + +## How to create a component? + +To register a new component, you need to create a simple JS class and add the `@component` decorator to it. +Also, you can pass additional parameters to this decorator. For example, in order for a component to be created as functional. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component({functional :true}) +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; +} +``` + +### How does it work? + +The `@component` decorator aggregates information of the class received from other nested decorators and +with the help of reflection and forms a special structure of the [[ComponentMeta]] type. +Next, the created structure will be passed to the used component library adapter, which will create a "real" component. + +### Additional register options + +#### [name] + +A name of the component we are registering. +If the name isn't specified, it will be taken from the tied class name by using reflection. +This parameter can't be inherited from the parent component. + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; + +// name == 'bExample' +@component({name: 'bExample'}) +class Foo extends iBlock { + +} + +// name == 'bExample' +@component() +class bExample extends iBlock { + +} +``` + +#### [root = `false`] + +If true, then the component is registered as the root component. +The root component is the top of components hierarchy, i.e. it contains all components in our application. + +All components, even the root component, have a link to the root component. +This parameter can be inherited from the parent component. + +```typescript +import iStaticPage, { component } from 'super/i-static-page/i-static-page'; + +@component({root: true}) +class pRoot extends iStaticPage { + +} +``` + +#### [tpl = `true`] + +If false, then the component will use the default loopback render function, instead of loading the own template. +This parameter is useful for components without templates, and it can be inherited from the parent component. + +#### [functional = `false`] + +The component functional mode: + +1. If true, the component will be created as a functional component. +2. If a dictionary, the component can be created as a functional component or regular component, depending on + values of the input properties: + + 1. If an empty dictionary, the component will always created as functional. + 2. If a dictionary with values, the dictionary properties represent component input properties. + If the component invocation takes these properties with the values that + declared within "functional" parameters, it will be created as functional. + Also, you can specify multiple values of one input property by using a list of values. + Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, + but you can directly cast a type by using the "v-func" directive. + +3. If null, all components watchers and listeners that directly specified in the component class won't + be attached to a functional-kind component. It is useful to create the superclass behaviour depending + on a component type. + +A functional component is a component can be rendered once only from input properties. +This type of components have a state and lifecycle hooks, but mutation of the state doesn't force re-rendering. +Usually, functional components lighter than regular components with the first render, +but avoid their if you have long animations within a component or if you need to frequent re-draws some deep +structure of nested components. + +This parameter can be inherited from the parent component, but the `null` value isn't inherited. + +```typescript +import iData, { component } from 'super/i-data/i-data'; + +// `bButton` will be created as a function component +// if its `dataProvider` property is equal to `false` or not specified +@component({functional: {dataProvider: [undefined, false]}}) +class bButton extends iData { + +} + +// `bLink` will always be created as a functional component +@component({functional: true}) +class bLink extends iData { + +} +``` + +``` +// We force `b-button` to create as a regular component +< b-button v-func = false + +// Within `v-func` we can use values from the runtime +< b-button v-func = foo !== bar + +// The direct invoking of a functional version of `bButton` +< b-button-functional +``` + +#### [defaultProps = `true`] + +If false, then all default values of the component input properties are ignored. +This parameter can be inherited from the parent component. + +#### [deprecatedProps] + +A dictionary with the deprecated component props with the specified alternatives. +The keys represent deprecated props; the values represent alternatives. +This parameter can be inherited from the parent component. + +```typescript +import iData, { component, prop } from 'super/i-data/i-data'; + +@component({deprecatedProps: { + value: 'items' +}}) + +class bList extends iData { + @prop() + items: string[]; + + // @deprecated + @prop() + value: string[]; +} +``` + +#### [inheritAttrs = `true`] + +If true, then the component input properties that aren't registered as props +will be attached to a component node as attributes. +This parameter can be inherited from the parent component. + +```typescript +import iData, { component, prop } from 'super/i-data/i-data'; + +@component() +class bInput extends iData { + @prop() + value: string = ''; +} +``` + +``` +< b-input :data-title = 'hello' +``` + +#### [inheritMods = `true`] + +If true, then the component is automatically inherited base modifiers from its parent. +This parameter can be inherited from the parent component. + +## API + +### Decorators + +#### @component + +The decorator to register a new component based on the tied class. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; +} +``` + +### Helpers + +#### registerParentComponents + +Registers parent components for the given one. +This function is needed because we have lazy component registration: when we see the "foo" component for +the first time in a template, we need to check the registration of all its parent components. +The function returns false if all parent components are already registered. + +#### registerComponent + +Register a component by the specified name. +This function is needed because we have lazy component registration. +Keep in mind that you must call `registerParentComponents` before calling this function. +The function returns a meta object of the created component, or undefined if the component isn't found. +If the component is already registered, it won't be registered twice. diff --git a/src/core/component/register/decorator.ts b/src/core/component/decorators/component/index.ts similarity index 100% rename from src/core/component/register/decorator.ts rename to src/core/component/decorators/component/index.ts From c1d5beeb629766caa15cb361e5f4e765ac7de854 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 20 Jun 2022 17:35:54 +0300 Subject: [PATCH 0273/2313] refactor: moved the `register` module to `init` & improved doc --- src/core/component/init/README.md | 99 ++++++-- .../helpers.ts => init/component.ts} | 32 +-- src/core/component/init/index.ts | 1 + src/core/component/register/CHANGELOG.md | 34 --- src/core/component/register/README.md | 233 ------------------ src/core/component/register/index.ts | 15 -- 6 files changed, 89 insertions(+), 325 deletions(-) rename src/core/component/{register/helpers.ts => init/component.ts} (68%) delete mode 100644 src/core/component/register/CHANGELOG.md delete mode 100644 src/core/component/register/README.md delete mode 100644 src/core/component/register/index.ts diff --git a/src/core/component/init/README.md b/src/core/component/init/README.md index aebf02d60a..dd52bd382f 100644 --- a/src/core/component/init/README.md +++ b/src/core/component/init/README.md @@ -1,49 +1,100 @@ # core/component/init -This module provides a bunch of functions to initialize common component states. +This module provides a bunch of functions to register components and initialize their states. +Basically, this API is used by adaptors of component libraries, and you don't need to use it manually. -## Functions +## Registering a component -### beforeCreateState +V4Fire provides the ability to describe components using native JavaScript classes and decorators, +instead of using the API of the component library being used. But in order to convert a component from a class form +to a form understood by the used component library, it is necessary to register the component. -Initializes the "beforeCreate" state to the specified component instance. +Registering a component in V4Fire is a lazy and one-time operation. That is, the component is only registered when +it is actually used in the template. Once a component has been registered once, it can already be used +by the component library as if it had been explicitly declared using the library's API. -### createdState +#### registerParentComponents -Initializes the "created" state to the specified component instance. +Registers parent components for the given one. +The function returns false if all parent components are already registered. -### beforeMountState +#### registerComponent -Initializes the "beforeMount" state to the specified component instance. +Register a component by the specified name. +The function returns the meta object of the created component, or undefined if the component isn't found. +If the component is already registered, it won't be registered twice. +Keep in mind that you must call `registerParentComponents` before calling this function. -### mountedState +## State initializers -Initializes the "mounted" state to the specified component instance +All lifecycle hooks have special initialization methods. These methods must be called within the hook handlers +they are associated with. Typically, this is done by the adapter of the used component library. -### beforeUpdateState +```typescript +import * as init from 'core/component/init'; -Initializes the "beforeUpdate" state to the specified component instance. +import { implementComponentForceUpdateAPI } from 'core/component/render'; +import { getComponentContext } from 'core/component/context'; -### updatedState +import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; +import type { ComponentMeta } from 'core/component/interface'; -Initializes the "updated" state to the specified component instance. +export function getComponent(meta: ComponentMeta): ComponentOptions { + return { + // ... other component properties -### activatedState + beforeCreate(): void { + init.beforeCreateState(this, meta, {implementEventAPI: true}); + implementComponentForceUpdateAPI(this, this.$forceUpdate.bind(this)); + }, -Initializes the "activated" state to the specified component instance. + created(): void { + init.createdState(getComponentContext(this)); + }, -### deactivatedState + beforeMount(): void { + init.beforeMountState(getComponentContext(this)); + }, -Initializes the "deactivated" state to the specified component instance. + mounted(): void { + init.mountedState(getComponentContext(this)); + }, -### beforeDestroyState + beforeUpdate(): void { + init.beforeUpdateState(getComponentContext(this)); + }, -Initializes the "beforeDestroy" state to the specified component instance. + updated(): void { + init.updatedState(getComponentContext(this)); + }, -### destroyedState + activated(): void { + init.activatedState(getComponentContext(this)); + }, -Initializes the "destroyed" state to the specified component instance. + deactivated(): void { + init.deactivatedState(getComponentContext(this)); + }, -### errorCapturedState + beforeUnmount(): void { + init.beforeDestroyState(getComponentContext(this)); + }, -Initializes the "errorCaptured" state to the specified component instance. + unmounted(): void { + init.destroyedState(getComponentContext(this)); + }, + + errorCaptured(...args: unknown[]): void { + init.errorCapturedState(getComponentContext(this), ...args); + }, + + renderTracked(...args: unknown[]): void { + init.renderTrackedState(getComponentContext(this), ...args); + }, + + renderTriggered(...args: unknown[]): void { + init.errorCapturedState(getComponentContext(this), ...args); + } + }; +} +``` diff --git a/src/core/component/register/helpers.ts b/src/core/component/init/component.ts similarity index 68% rename from src/core/component/register/helpers.ts rename to src/core/component/init/component.ts index 2fac2cc491..5e5d781d54 100644 --- a/src/core/component/register/helpers.ts +++ b/src/core/component/init/component.ts @@ -1,22 +1,15 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { isComponent, componentInitializers, componentParams, components } from 'core/component/const'; +import { isComponent, componentRegInitializers, componentParams, components } from 'core/component/const'; import type { ComponentMeta } from 'core/component/interface'; import type { ComponentConstructorInfo } from 'core/component/reflect'; /** * Registers parent components for the given one. - * This function is needed because we have lazy component registration: when we see the "foo" component for - * the first time in a template, we need to check the registration of all its parent components. * The function returns false if all parent components are already registered. * + * This function is needed because we have lazy component registration: when we see the "foo" component for + * the first time in the template, we need to check the registration of all its parent components. + * * @param component - the component information object */ export function registerParentComponents(component: ComponentConstructorInfo): boolean { @@ -27,7 +20,7 @@ export function registerParentComponents(component: ComponentConstructorInfo): b parentName = component.parentParams?.name, parentComponent = component.parent; - if (!Object.isTruly(parentName) || !componentInitializers[parentName]) { + if (!Object.isTruly(parentName) || !componentRegInitializers[parentName]) { return false; } @@ -44,14 +37,14 @@ export function registerParentComponents(component: ComponentConstructorInfo): b parentName = parentName; const - regParentComponent = componentInitializers[parentName]; + regParentComponent = componentRegInitializers[parentName]; if (regParentComponent != null) { for (let i = 0; i < regParentComponent.length; i++) { regParentComponent[i](); } - delete componentInitializers[parentName]; + delete componentRegInitializers[parentName]; return true; } } @@ -60,11 +53,12 @@ export function registerParentComponents(component: ComponentConstructorInfo): b } /** - * Register a component by the specified name. + * Registers a component by the specified name. + * The function returns the meta object of the created component, or undefined if the component isn't found. + * If the component is already registered, it won't be registered twice. + * * This function is needed because we have lazy component registration. * Keep in mind that you must call `registerParentComponents` before calling this function. - * The function returns a meta object of the created component, or undefined if the component isn't found. - * If the component is already registered, it won't be registered twice. * * @param name - the component name */ @@ -74,14 +68,14 @@ export function registerComponent(name: CanUndef): CanUndef **Tags:** -> - :boom: [Breaking Change] -> - :rocket: [New Feature] -> - :bug: [Bug Fix] -> - :memo: [Documentation] -> - :house: [Internal] -> - :nail_care: [Polish] - -## v3.??.?? (2022-??-??) - -#### :memo: Documentation - -* Added the normal documentation - -## v3.0.0-rc.131 (2021-01-29) - -#### :house: Internal - -* Now using `requestIdleCallback` instead of `setTimeout` - -## v3.0.0-rc.99 (2020-11-17) - -#### :bug: Bug Fix - -* [Fixed dynamic creation of flyweight components](https://github.com/V4Fire/Client/issues/434) - -## v3.0.0-rc.48 (2020-08-02) - -#### :house: Internal - -* Fixed ESLint warnings diff --git a/src/core/component/register/README.md b/src/core/component/register/README.md deleted file mode 100644 index 2cf29b4938..0000000000 --- a/src/core/component/register/README.md +++ /dev/null @@ -1,233 +0,0 @@ -# core/component/register - -This module provides an API to register components. - -```typescript -import iBlock, { component, prop } from 'super/i-block/i-block'; - -@component() -export default class bUser extends iBlock { - @prop(String) - readonly fName: string; - - @prop(String) - readonly lName: string; -} -``` - -## How to create a component? - -To register a new component, you need to create a simple JS class and add the `@component` decorator to it. -Also, you can pass additional parameters to this decorator. For example, in order for a component to be created as functional. - -```typescript -import iBlock, { component, prop } from 'super/i-block/i-block'; - -@component({functional :true}) -export default class bUser extends iBlock { - @prop(String) - readonly fName: string; - - @prop(String) - readonly lName: string; -} -``` - -### How does it work? - -The `@component` decorator aggregates information of the class received from other nested decorators and -with the help of reflection and forms a special structure of the [[ComponentMeta]] type. -Next, the created structure will be passed to the used component library adapter, which will create a "real" component. - -### Additional register options - -#### [name] - -A name of the component we are registering. -If the name isn't specified, it will be taken from the tied class name by using reflection. -This parameter can't be inherited from the parent component. - -```typescript -import iBlock, { component } from 'super/i-block/i-block'; - -// name == 'bExample' -@component({name: 'bExample'}) -class Foo extends iBlock { - -} - -// name == 'bExample' -@component() -class bExample extends iBlock { - -} -``` - -#### [root = `false`] - -If true, then the component is registered as the root component. -The root component is the top of components hierarchy, i.e. it contains all components in our application. - -All components, even the root component, have a link to the root component. -This parameter can be inherited from the parent component. - -```typescript -import iStaticPage, { component } from 'super/i-static-page/i-static-page'; - -@component({root: true}) -class pRoot extends iStaticPage { - -} -``` - -#### [tpl = `true`] - -If false, then the component will use the default loopback render function, instead of loading the own template. -This parameter is useful for components without templates, and it can be inherited from the parent component. - -#### [functional = `false`] - -The component functional mode: - -1. If true, the component will be created as a functional component. -2. If a dictionary, the component can be created as a functional component or regular component, depending on - values of the input properties: - - 1. If an empty dictionary, the component will always created as functional. - 2. If a dictionary with values, the dictionary properties represent component input properties. - If the component invocation takes these properties with the values that - declared within "functional" parameters, it will be created as functional. - Also, you can specify multiple values of one input property by using a list of values. - Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, - but you can directly cast a type by using the "v-func" directive. - -3. If null, all components watchers and listeners that directly specified in the component class won't - be attached to a functional-kind component. It is useful to create the superclass behaviour depending - on a component type. - -A functional component is a component can be rendered once only from input properties. -This type of components have a state and lifecycle hooks, but mutation of the state doesn't force re-rendering. -Usually, functional components lighter than regular components with the first render, -but avoid their if you have long animations within a component or if you need to frequent re-draws some deep -structure of nested components. - -This parameter can be inherited from the parent component, but the `null` value isn't inherited. - -```typescript -import iData, { component } from 'super/i-data/i-data'; - -// `bButton` will be created as a function component -// if its `dataProvider` property is equal to `false` or not specified -@component({functional: {dataProvider: [undefined, false]}}) -class bButton extends iData { - -} - -// `bLink` will always be created as a functional component -@component({functional: true}) -class bLink extends iData { - -} -``` - -``` -// We force `b-button` to create as a regular component -< b-button v-func = false - -// Within `v-func` we can use values from the runtime -< b-button v-func = foo !== bar - -// The direct invoking of a functional version of `bButton` -< b-button-functional -``` - -#### [defaultProps = `true`] - -If false, then all default values of the component input properties are ignored. -This parameter can be inherited from the parent component. - -#### [deprecatedProps] - -A dictionary with the deprecated component props with the specified alternatives. -The keys represent deprecated props; the values represent alternatives. -This parameter can be inherited from the parent component. - -```typescript -import iData, { component, prop } from 'super/i-data/i-data'; - -@component({deprecatedProps: { - value: 'items' -}}) - -class bList extends iData { - @prop() - items: string[]; - - // @deprecated - @prop() - value: string[]; -} -``` - -#### [inheritAttrs = `true`] - -If true, then the component input properties that aren't registered as props -will be attached to a component node as attributes. -This parameter can be inherited from the parent component. - -```typescript -import iData, { component, prop } from 'super/i-data/i-data'; - -@component() -class bInput extends iData { - @prop() - value: string = ''; -} -``` - -``` -< b-input :data-title = 'hello' -``` - -#### [inheritMods = `true`] - -If true, then the component is automatically inherited base modifiers from its parent. -This parameter can be inherited from the parent component. - -## API - -### Decorators - -#### @component - -The decorator to register a new component based on the tied class. - -```typescript -import iBlock, { component, prop } from 'super/i-block/i-block'; - -@component() -export default class bUser extends iBlock { - @prop(String) - readonly fName: string; - - @prop(String) - readonly lName: string; -} -``` - -### Helpers - -#### registerParentComponents - -Registers parent components for the given one. -This function is needed because we have lazy component registration: when we see the "foo" component for -the first time in a template, we need to check the registration of all its parent components. -The function returns false if all parent components are already registered. - -#### registerComponent - -Register a component by the specified name. -This function is needed because we have lazy component registration. -Keep in mind that you must call `registerParentComponents` before calling this function. -The function returns a meta object of the created component, or undefined if the component isn't found. -If the component is already registered, it won't be registered twice. diff --git a/src/core/component/register/index.ts b/src/core/component/register/index.ts deleted file mode 100644 index f0c60a015c..0000000000 --- a/src/core/component/register/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 - */ - -/** - * [[include:core/component/register/README.md]] - * @packageDocumentation - */ - -export * from 'core/component/register/helpers'; -export * from 'core/component/register/decorator'; From 0caa01e2e7b98770855ead18bd1c71cf3b237a91 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 20 Jun 2022 18:02:29 +0300 Subject: [PATCH 0274/2313] doc: improved doc --- src/core/component/accessor/README.md | 7 +- .../component/decorators/computed/README.md | 120 +++++++++++++++++- .../component/decorators/computed/index.ts | 4 +- 3 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/core/component/accessor/README.md b/src/core/component/accessor/README.md index 9c3b09704c..17d4d76a48 100644 --- a/src/core/component/accessor/README.md +++ b/src/core/component/accessor/README.md @@ -5,10 +5,9 @@ This module provides an API to initialize component accessors and computed field ## What differences between accessors and computed fields? A computed field is an accessor that value can be cached or watched. -To enable value caching, use the `computed` decorator when define or override your accessor. -After this, the first time you touch the accessor value it will be cached. -To support cache invalidation or watching of changes, provide a list of dependencies of your accessor or -use the `cache = 'auto'` option. +To enable value caching, use the `@computed` decorator when define or override your accessor. +After that, the first time the getter value is touched, it will be cached. To support cache invalidation or +adding change watching capabilities, provide a list of your accessor dependencies or use the `cache = 'auto'` option. ## Functions diff --git a/src/core/component/decorators/computed/README.md b/src/core/component/decorators/computed/README.md index 79c6a381fc..5ec1cba461 100644 --- a/src/core/component/decorators/computed/README.md +++ b/src/core/component/decorators/computed/README.md @@ -2,8 +2,17 @@ The decorator attaches meta information to a component computed field or accessor. +## What differences between accessors and computed fields? + +A computed field is an accessor that value can be cached or watched. +To enable value caching, use the `@computed` decorator when define or override your accessor. +After that, the first time the getter value is touched, it will be cached. To support cache invalidation or +adding change watching capabilities, provide a list of your accessor dependencies or use the `cache = 'auto'` option. + +## Usage + ```typescript -import iBlock, { component, prop, computed } from 'super/i-block/i-block'; +import iBlock, {component, prop, computed} from 'super/i-block/i-block'; @component() export default class bUser extends iBlock { @@ -49,21 +58,96 @@ The option is set to true by default if also provided `dependencies` or the boun by the name with another prop or field. If the option value is passed as `auto`, caching will be delegated to the used component library. +```typescript +import iBlock, { component, field, computed } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + // The value is cached after the first touch and will never be reset + @computed({cache: true}) + get hashCode(): number { + return Math.random(); + } + + @field() + i: number = 0; + + // The value is cached after the first touch, but the cache can be reset if the fields used internally change + @computed({cache: 'auto'}) + get iWrapper(): number { + return this.i; + } +} +``` + +Also, when an accessor has a logically related prop/field +(using the naming convention "${property} -> ${property}Prop | ${property}Store") we don't need to add additional dependencies. + +```typescript +import iBlock, { component, prop, field, computed } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop(Number) + readonly fooProp: number = 0; + + // The getter use caching and can be watched + get foo(): number { + return this.fooProp * 3; + } + + @field() + blaStore: number = 0; + + // The getter use caching and can be watched + get bla(): number { + return this.blaStore * 3; + } +} +``` + ##### [watchable] If true, the accessor returns a link to another watchable object. +This option allows you to mount external watchable objects to the component. + +```typescript +import watch from 'core/object/watch'; +import iBlock, { component, computed } from 'super/i-block/i-block'; + +const {proxy: state} = watch({ + a: 1, + b: { + c: 2 + } +}); + +setTimeout(() => { + state.b.c++; +}, 500); + +@component() +class bExample extends iBlock { + @computed({watchable: true}) + get state(): typeof state { + return state; + } + + mounted() { + this.watch('state', {deep: true}, (value, oldValue) => { + console.log(value, oldValue); + }); + } +} +``` ##### [dependencies] A list of dependencies for the accessor. The dependencies are needed to watch for the accessor mutations or to invalidate its cache. -Also, when the accessor has a logically connected prop/field -(by using the name convention "${property} -> ${property}Prop | ${property}Store"), -we don't need to add additional dependencies. - ```typescript -import iBlock, { component, field, computed } from 'super/i-block/i-block'; +import iBlock, {component, field, computed} from 'super/i-block/i-block'; @component() class bExample extends iBlock { @@ -74,9 +158,31 @@ class bExample extends iBlock { get bar(): number { return this.blaStore * 2; } +} +``` + +Also, when an accessor has a logically related prop/field +(using the naming convention "${property} -> ${property}Prop | ${property}Store") we don't need to add additional dependencies. + +```typescript +import iBlock, { component, prop, field, computed } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop(Number) + readonly fooProp: number = 0; + + // The getter use caching and can be watched + get foo(): number { + return this.fooProp * 3; + } + + @field() + blaStore: number = 0; + // The getter use caching and can be watched get bla(): number { - return blaStore * 3; + return this.blaStore * 3; } } ``` diff --git a/src/core/component/decorators/computed/index.ts b/src/core/component/decorators/computed/index.ts index 0b76a3ebac..f3707eec03 100644 --- a/src/core/component/decorators/computed/index.ts +++ b/src/core/component/decorators/computed/index.ts @@ -24,8 +24,8 @@ import type { DecoratorComponentAccessor } from 'core/component/decorators/inter * @component() * class bExample extends iBlock { * @computed({cache: true}) - * get foo() { - * return 42; + * get hashCode(): number { + * return Math.random(); * } * } * ``` From 65081890fb6685a6d50a7a01465eff2f8596ee62 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 20 Jun 2022 20:05:08 +0300 Subject: [PATCH 0275/2313] doc: improved doc --- src/core/component/decorators/field/README.md | 118 ++++++++++++++---- .../decorators/interface/accessor.ts | 71 +++++++++-- .../component/decorators/interface/field.ts | 94 ++++++++++---- .../component/decorators/system/README.md | 116 +++++++++++++---- 4 files changed, 324 insertions(+), 75 deletions(-) diff --git a/src/core/component/decorators/field/README.md b/src/core/component/decorators/field/README.md index 65651820e1..67160fd601 100644 --- a/src/core/component/decorators/field/README.md +++ b/src/core/component/decorators/field/README.md @@ -3,6 +3,17 @@ The decorator marks a class property as a component field. In non-functional components, field property mutations typically cause the component to re-render. +## What differences between fields and system fields? + +The major difference between fields and system fields, that any changes of a component field can force re-rendering of its template. +I.e., if you are totally sure that your component field doesn't need to force rendering, prefer system fields instead of regular. +Mind, changes in any system field still can be watched using built-in API. + +The second difference is that system fields are initialized on the `beforeCreate` hook, +but not on the `created` hook like the regular fields do. + +## Usage + ```typescript import iBlock, { component, field } from 'super/i-block/i-block'; @@ -13,12 +24,12 @@ class bExample extends iBlock { bla: number = 0; // Or by passing a value initializer function - @field(() => Math.random()) + @field(Math.random) baz!: number; // Or a dictionary with additional options - @field({unique: true, init: () => Math.random()}) - ban!: number; + @field({unique: true, init: Math.random}) + hashCode!: number; } ``` @@ -28,11 +39,26 @@ class bExample extends iBlock { Marks the field as unique for each component instance. Also, the parameter can take a function that returns a boolean value. +If this value is true, then the parameter is considered unique. + +Please note that the uniqueness guarantee must be provided by the "external" code, +because V4Fire does not perform special checks for uniqueness. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field({unique: true, init: Math.random}) + hashCode!: number; +} +``` ##### [default] -This option allows you to set a default value for the field. -But using it, as a rule, is not explicitly required, since a default value can be passed through the native syntax of class properties. +This option allows you to set the default value of the field. +But using it, as a rule, is not explicitly required, since the default value can be passed through +the native syntax of class properties. ```typescript import iBlock, { component, field } from 'super/i-block/i-block'; @@ -41,6 +67,28 @@ import iBlock, { component, field } from 'super/i-block/i-block'; class bExample extends iBlock { @field() bla: number = 0; + + @field({default: 0}) + bar!: number; +} +``` + +Note that the default value provided is a prototype, not a real value. That is, when set to each new instance, +it will be cloned using `Object.fastClone`. If this behavior does not suit you, then use the init option and pass the +initializer function. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + // There will be a trouble here when cloning the value + @field() + body: Element = document.body; + + // All fine + @field(() => document.body) + validBody!: Element; } ``` @@ -52,15 +100,15 @@ As the second argument, the function takes a reference to a dictionary with othe have already been initialized. ```typescript -import iBlock, { component, field } from 'super/i-block/i-block'; +import iBlock, { component, prop, field } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @field({init: () => Math.random()}) - bla!: number; + @field({init: Math.random}) + hashCode!: number; - @field(() => Math.random()) - bar!: number; + @field((ctx, {hashCode}) => String(hashCode)) + normalizedHashCode!: string; } ``` @@ -72,22 +120,22 @@ Keep in mind that the template can still be re-rendered, but only at the initiat ##### [after] A name or list of names after which this property should be initialized. -Keep in mind, you can only specify names that are of the same type as the current field (fields or field). +Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). ```typescript import iBlock, { component, field } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @field(() => Math.random()) - bla!: number; + @field(Math.random) + hashCode!: number; @field({ - after: 'bla', - init: (ctx, data) => data.bla + 10 + after: 'hashCode', + init: (ctx, {hashCode}) => String(hashCode) }) - baz!: number; + normalizedHashCode!: string; } ``` @@ -98,15 +146,16 @@ This option is needed when you have a field that must be guaranteed to be initia and you don't want to use `after` everywhere. But you can still use `after` along with other atomic fields. ```typescript +import Async from 'core/async'; import iBlock, { component, field } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @field({atom: true, init: () => Math.random()}) - bla!: number; + @field({atom: true, init: (ctx) => new Async(ctx)}) + async!: Async; - @field((ctx, data) => data.bla + 10) - baz!: number; + @field((ctx, data) => data.async.proxy(() => { /* ... */ })) + handler!: Function; } ``` @@ -161,13 +210,36 @@ class bExample extends iBlock { ##### [functionalWatching = `false`] If false, the field can't be watched if created inside a functional component. +This option is useful when you are writing a superclass or a smart component that can be created as regular or functional. ##### [merge = `false`] -If true, then if the component will restore its own state from the old component -(this happens when using a functional component), then the actual value will be merged with the previous one. -Also, this parameter can take a function to merge. +This option is only relevant for functional components. +The fact is that when a component state changes, all its child functional components are recreated from scratch. +But we need to restore the state of such components. By default, properties are simply copied from old instances to +new ones, but sometimes this strategy does not suit us. This option helps here - it allows you to declare that +a certain property should be mixed based on the old and new values. + +Set this property to true to enable the strategy of merging old and new values. +Or specify a function that will perform the merge. This function takes contexts of the old and new components, +the name of the field to restore, and optionally a path to a property to which the given is bound. ##### [meta] A dictionary with some extra information of the field. +You can access this information using `meta.fields`. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system({init: Math.random, meta: {debug: true}}) + hashCode!: number; + + created() { + // {debug: true} + console.log(this.meta.systemFields.hashCode.meta); + } +} +``` diff --git a/src/core/component/decorators/interface/accessor.ts b/src/core/component/decorators/interface/accessor.ts index bab4e0ac33..c6af80d86a 100644 --- a/src/core/component/decorators/interface/accessor.ts +++ b/src/core/component/decorators/interface/accessor.ts @@ -15,11 +15,69 @@ export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { * The option is set to true by default if also provided `dependencies` or the bound accessor matches * by the name with another prop or field. If the option value is passed as `auto`, caching will be delegated to * the used component library. + * + * Also, when an accessor has a logically related prop/field (using the naming convention + * "${property} -> ${property}Prop | ${property}Store") we don't need to add additional dependencies. + * + * @example + * ```typescript + * import iBlock, { component, field, computed } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * // The value is cached after the first touch and will never be reset + * @computed({cache: true}) + * get hashCode(): number { + * return Math.random(); + * } + * + * @field() + * i: number = 0; + * + * // The value is cached after the first touch, but the cache can be reset if the fields used internally change + * @computed({cache: 'auto'}) + * get iWrapper(): number { + * return this.i; + * } + * } + * ``` */ cache?: ComponentAccessorCacheType; /** - * If true, the accessor returns a link to another watchable object + * If true, the accessor returns a link to another watchable object. + * This option allows you to mount external watchable objects to the component. + * + * @example + * ```typescript + * import watch from 'core/object/watch'; + * import iBlock, { component, computed } from 'super/i-block/i-block'; + * + * const {proxy: state} = watch({ + * a: 1, + * b: { + * c: 2 + * } + * }); + * + * setTimeout(() => { + * state.b.c++; + * }, 500); + * + * @component() + * class bExample extends iBlock { + * @computed({watchable: true}) + * get state(): typeof state { + * return state; + * } + * + * mounted() { + * this.watch('state', {deep: true}, (value, oldValue) => { + * console.log(value, oldValue); + * }); + * } + * } + * ``` */ watchable?: boolean; @@ -27,12 +85,13 @@ export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { * A list of dependencies for the accessor. * The dependencies are needed to watch for the accessor mutations or to invalidate its cache. * - * Also, when the accessor has a logically connected prop/field - * (by using the name convention "${property} -> ${property}Prop | ${property}Store"), - * we don't need to add additional dependencies. + * Also, when an accessor has a logically related prop/field (using the naming convention + * "${property} -> ${property}Prop | ${property}Store") we don't need to add additional dependencies. * * @example * ```typescript + * import iBlock, {component, field, computed} from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { * @field() @@ -42,10 +101,6 @@ export interface DecoratorComponentAccessor extends DecoratorFunctionalOptions { * get bar(): number { * return this.blaStore * 2; * } - * - * get bla(): number { - * return blaStore * 3; - * } * } * ``` */ diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts index be1c315b30..7b0023a262 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/interface/field.ts @@ -18,15 +18,23 @@ export interface DecoratorSystem< /** * Marks the field as unique for each component instance. * Also, the parameter can take a function that returns a boolean value. + * If this value is true, then the parameter is considered unique. + * + * Please note that the uniqueness guarantee must be provided by the "external" code, + * because V4Fire does not perform special checks for uniqueness. * * @default `false` */ unique?: boolean | UniqueFieldFn; /** - * This option allows you to set a default value for the field. - * But using it, as a rule, is not explicitly required, since a default value can be passed through the - * native syntax of class properties. + * This option allows you to set the default value of the field. + * But using it, as a rule, is not explicitly required, since the default value can be passed through + * the native syntax of class properties. + * + * Note that the default value provided is a prototype, not a real value. That is, when set to each new instance, + * it will be cloned using `Object.fastClone`. If this behavior does not suit you, then use the init option and + * pass the initializer function. * * @example * ```typescript @@ -36,6 +44,17 @@ export interface DecoratorSystem< * class bExample extends iBlock { * @system() * bla: number = 0; + * + * @field({default: 0}) + * bar!: number; + * + * // There will be a trouble here when cloning the value + * @field() + * body: Element = document.body; + * + * // All fine + * @field(() => document.body) + * validBody!: Element; * } * ``` */ @@ -49,13 +68,15 @@ export interface DecoratorSystem< * * @example * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { - * @system({init: () => Math.random()}) - * bla!: number; + * @system({init: Math.random}) + * hashCode!: number; * - * @system(() => Math.random()) - * bar!: number; + * @system((ctx, {hashCode}) => String(hashCode)) + * normalizedHashCode!: string; * } * ``` */ @@ -63,21 +84,23 @@ export interface DecoratorSystem< /** * A name or list of names after which this property should be initialized. - * Keep in mind, you can only specify names that are of the same type as the current field (fields or system). + * Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). * * @example * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { - * @system(() => Math.random()) - * bla!: number; + * @system(Math.random) + * hashCode!: number; * * @system({ - * after: 'bla', - * init: (ctx, data) => data.bla + 10 + * after: 'hashCode', + * init: (ctx, {hashCode}) => String(hashCode) * }) * - * baz!: number; + * normalizedHashCode!: string; * } * ``` */ @@ -91,15 +114,16 @@ export interface DecoratorSystem< * @default `false` * @example * ```typescript + * import Async from 'core/async'; * import iBlock, { component, system } from 'super/i-block/i-block'; * * @component() * class bExample extends iBlock { - * @system({atom: true, init: () => Math.random()}) - * bla!: number; + * @system({atom: true, init: (ctx) => new Async(ctx)}) + * async!: Async; * - * @system((ctx, data) => data.bla + 10) - * baz!: number; + * @system((ctx, data) => data.async.proxy(() => { /* ... *\/ })) + * handler!: Function; * } * ``` */ @@ -156,22 +180,48 @@ export interface DecoratorSystem< watch?: DecoratorFieldWatcher; /** - * If false, the field can't be watched if created inside a functional component + * If false, the field can't be watched if created inside a functional component. + * This option is useful when you are writing a superclass or a smart component that can be created + * as regular or functional. + * * @default `true` */ functionalWatching?: boolean; /** - * If true, then if the component will restore its own state from the old component - * (this happens when using a functional component), then the actual value will be merged with the previous one. - * Also, this parameter can take a function to merge. + * This option is only relevant for functional components. + * The fact is that when a component state changes, all its child functional components are recreated from scratch. + * But we need to restore the state of such components. By default, properties are simply copied from old instances to + * new ones, but sometimes this strategy does not suit us. This option helps here - it allows you to declare that + * a certain property should be mixed based on the old and new values. + * + * Set this property to true to enable the strategy of merging old and new values. + * Or specify a function that will perform the merge. This function takes contexts of the old and new components, + * the name of the field to restore, and optionally a path to a property to which the given is bound. * * @default `false` */ merge?: MergeFieldFn | boolean; /** - * A dictionary with some extra information of the field + * A dictionary with some extra information of the field. + * You can access this information using `meta.fields` or `meta.systemFields`. + * + * @example + * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @system({init: Math.random, meta: {debug: true}}) + * hashCode!: number; + * + * created() { + * // {debug: true}} + * console.log(this.meta.systemFields.hashCode.meta); + * } + * } + * ``` */ meta?: Dictionary; } diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md index fc7a181b01..90cf9aba1f 100644 --- a/src/core/component/decorators/system/README.md +++ b/src/core/component/decorators/system/README.md @@ -3,6 +3,17 @@ The decorator marks a class property as a system field. System property mutations never cause components to re-render. +## What differences between fields and system fields? + +The major difference between fields and system fields, that any changes of a component field can force re-rendering of its template. +I.e., if you are totally sure that your component field doesn't need to force rendering, prefer system fields instead of regular. +Mind, changes in any system field still can be watched using built-in API. + +The second difference is that system fields are initialized on the `beforeCreate` hook, +but not on the `created` hook like the regular fields do. + +## Usage + ```typescript import iBlock, { component, system } from 'super/i-block/i-block'; @@ -28,11 +39,26 @@ class bExample extends iBlock { Marks the field as unique for each component instance. Also, the parameter can take a function that returns a boolean value. +If this value is true, then the parameter is considered unique. + +Please note that the uniqueness guarantee must be provided by the "external" code, +because V4Fire does not perform special checks for uniqueness. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system({unique: true, init: Math.random}) + hashCode!: number; +} +``` ##### [default] -This option allows you to set a default value for the field. -But using it, as a rule, is not explicitly required, since a default value can be passed through the native syntax of class properties. +This option allows you to set the default value of the field. +But using it, as a rule, is not explicitly required, since the default value can be passed through +the native syntax of class properties. ```typescript import iBlock, { component, system } from 'super/i-block/i-block'; @@ -41,6 +67,28 @@ import iBlock, { component, system } from 'super/i-block/i-block'; class bExample extends iBlock { @system() bla: number = 0; + + @system({default: 0}) + bar!: number; +} +``` + +Note that the default value provided is a prototype, not a real value. That is, when set to each new instance, +it will be cloned using `Object.fastClone`. If this behavior does not suit you, then use the init option and pass the +initializer function. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + // There will be a trouble here when cloning the value + @system() + body: Element = document.body; + + // All fine + @system(() => document.body) + validBody!: Element; } ``` @@ -52,37 +100,37 @@ As the second argument, the function takes a reference to a dictionary with othe have already been initialized. ```typescript -import iBlock, { component, system } from 'super/i-block/i-block'; +import iBlock, { component, prop, system } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @system({init: () => Math.random()}) - bla!: number; + @system({init: Math.random}) + hashCode!: number; - @system(() => Math.random()) - bar!: number; + @system((ctx, {hashCode}) => String(hashCode)) + normalizedHashCode!: string; } ``` ##### [after] A name or list of names after which this property should be initialized. -Keep in mind, you can only specify names that are of the same type as the current field (fields or system). +Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). ```typescript import iBlock, { component, system } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @system(() => Math.random()) - bla!: number; + @system(Math.random) + hashCode!: number; @system({ - after: 'bla', - init: (ctx, data) => data.bla + 10 + after: 'hashCode', + init: (ctx, {hashCode}) => String(hashCode) }) - baz!: number; + normalizedHashCode!: string; } ``` @@ -93,15 +141,16 @@ This option is needed when you have a field that must be guaranteed to be initia and you don't want to use `after` everywhere. But you can still use `after` along with other atomic fields. ```typescript +import Async from 'core/async'; import iBlock, { component, system } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @system({atom: true, init: () => Math.random()}) - bla!: number; + @system({atom: true, init: (ctx) => new Async(ctx)}) + async!: Async; - @system((ctx, data) => data.bla + 10) - baz!: number; + @system((ctx, data) => data.async.proxy(() => { /* ... */ })) + handler!: Function; } ``` @@ -114,11 +163,11 @@ The `core/watch` module is used to make objects watchable. Therefore, for more information, please refer to its documentation. ```typescript -import iBlock, { component, system } from 'super/i-block/i-block'; +import iBlock, { component, field } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @system({watch: [ + @field({watch: [ 'onIncrement', (ctx, val, oldVal, info) => @@ -156,13 +205,36 @@ class bExample extends iBlock { ##### [functionalWatching = `false`] If false, the field can't be watched if created inside a functional component. +This option is useful when you are writing a superclass or a smart component that can be created as regular or functional. ##### [merge = `false`] -If true, then if the component will restore its own state from the old component -(this happens when using a functional component), then the actual value will be merged with the previous one. -Also, this parameter can take a function to merge. +This option is only relevant for functional components. +The fact is that when a component state changes, all its child functional components are recreated from scratch. +But we need to restore the state of such components. By default, properties are simply copied from old instances to +new ones, but sometimes this strategy does not suit us. This option helps here - it allows you to declare that +a certain property should be mixed based on the old and new values. + +Set this property to true to enable the strategy of merging old and new values. +Or specify a function that will perform the merge. This function takes contexts of the old and new components, +the name of the field to restore, and optionally a path to a property to which the given is bound. ##### [meta] A dictionary with some extra information of the field. +You can access this information using `meta.fields`. + +```typescript +import iBlock, { component, system } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @system({init: Math.random, meta: {debug: true}}) + hashCode!: number; + + created() { + // {debug: true} + console.log(this.meta.systemFields.hashCode.meta); + } +} +``` From 2fa06ac46bbef12099e69718e8d8935292d35777 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 20 Jun 2022 21:35:24 +0300 Subject: [PATCH 0276/2313] doc: improved doc --- .../component/decorators/component/README.md | 64 +++++-------------- .../component/decorators/component/index.ts | 11 +++- .../component/decorators/computed/README.md | 8 +-- src/core/component/decorators/field/README.md | 22 +++---- .../component/decorators/interface/hook.ts | 6 +- .../component/decorators/system/README.md | 16 ++--- 6 files changed, 48 insertions(+), 79 deletions(-) diff --git a/src/core/component/decorators/component/README.md b/src/core/component/decorators/component/README.md index 1068027f88..3c199268a6 100644 --- a/src/core/component/decorators/component/README.md +++ b/src/core/component/decorators/component/README.md @@ -1,6 +1,6 @@ # core/component/decorators/component -This module provides an API to register components. +The decorator creates a component based on the specified class and its properties. ```typescript import iBlock, { component, prop } from 'super/i-block/i-block'; @@ -39,11 +39,11 @@ The `@component` decorator aggregates information of the class received from oth with the help of reflection and forms a special structure of the [[ComponentMeta]] type. Next, the created structure will be passed to the used component library adapter, which will create a "real" component. -### Additional register options +## Additional options -#### [name] +### [name] -A name of the component we are registering. +The name of the component we are registering. If the name isn't specified, it will be taken from the tied class name by using reflection. This parameter can't be inherited from the parent component. @@ -63,7 +63,7 @@ class bExample extends iBlock { } ``` -#### [root = `false`] +### [root = `false`] If true, then the component is registered as the root component. The root component is the top of components hierarchy, i.e. it contains all components in our application. @@ -80,12 +80,12 @@ class pRoot extends iStaticPage { } ``` -#### [tpl = `true`] +### [tpl = `true`] If false, then the component will use the default loopback render function, instead of loading the own template. This parameter is useful for components without templates, and it can be inherited from the parent component. -#### [functional = `false`] +### [functional = `false`] The component functional mode: @@ -141,12 +141,12 @@ class bLink extends iData { < b-button-functional ``` -#### [defaultProps = `true`] +### [defaultProps = `true`] If false, then all default values of the component input properties are ignored. This parameter can be inherited from the parent component. -#### [deprecatedProps] +### [deprecatedProps] A dictionary with the deprecated component props with the specified alternatives. The keys represent deprecated props; the values represent alternatives. @@ -169,7 +169,7 @@ class bList extends iData { } ``` -#### [inheritAttrs = `true`] +### [inheritAttrs = `true`] If true, then the component input properties that aren't registered as props will be attached to a component node as attributes. @@ -182,6 +182,10 @@ import iData, { component, prop } from 'super/i-data/i-data'; class bInput extends iData { @prop() value: string = ''; + + mounted() { + console.log(this.$attrs['data-title']); + } } ``` @@ -189,45 +193,7 @@ class bInput extends iData { < b-input :data-title = 'hello' ``` -#### [inheritMods = `true`] +### [inheritMods = `true`] If true, then the component is automatically inherited base modifiers from its parent. This parameter can be inherited from the parent component. - -## API - -### Decorators - -#### @component - -The decorator to register a new component based on the tied class. - -```typescript -import iBlock, { component, prop } from 'super/i-block/i-block'; - -@component() -export default class bUser extends iBlock { - @prop(String) - readonly fName: string; - - @prop(String) - readonly lName: string; -} -``` - -### Helpers - -#### registerParentComponents - -Registers parent components for the given one. -This function is needed because we have lazy component registration: when we see the "foo" component for -the first time in a template, we need to check the registration of all its parent components. -The function returns false if all parent components are already registered. - -#### registerComponent - -Register a component by the specified name. -This function is needed because we have lazy component registration. -Keep in mind that you must call `registerParentComponents` before calling this function. -The function returns a meta object of the created component, or undefined if the component isn't found. -If the component is already registered, it won't be registered twice. diff --git a/src/core/component/decorators/component/index.ts b/src/core/component/decorators/component/index.ts index bd35e4cb97..be06ad5932 100644 --- a/src/core/component/decorators/component/index.ts +++ b/src/core/component/decorators/component/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/component/README.md]] + * @packageDocumentation + */ + import { identity } from 'core/functools'; import * as c from 'core/component/const'; @@ -15,7 +20,7 @@ import { createMeta, fillMeta, attachTemplatesToMeta } from 'core/component/meta import { getInfoFromConstructor } from 'core/component/reflect'; import { getComponent, ComponentEngine } from 'core/component/engines'; -import { registerParentComponents } from 'core/component/register/helpers'; +import { registerParentComponents } from 'core/component/init'; import type { ComponentOptions } from 'core/component/interface'; @@ -57,8 +62,8 @@ export function component(opts?: ComponentOptions): Function { regComponent(); } else { - const initList = c.componentInitializers[componentInfo.name] ?? []; - c.componentInitializers[componentInfo.name] = initList; + const initList = c.componentRegInitializers[componentInfo.name] ?? []; + c.componentRegInitializers[componentInfo.name] = initList; initList.push(regComponent); } diff --git a/src/core/component/decorators/computed/README.md b/src/core/component/decorators/computed/README.md index 5ec1cba461..32a6c20161 100644 --- a/src/core/component/decorators/computed/README.md +++ b/src/core/component/decorators/computed/README.md @@ -49,9 +49,9 @@ export default class bUser extends iBlock { } ``` -#### Additional options +## Additional options -##### [cache] +### [cache] If true, the accessor value will be cached after the first touch. The option is set to true by default if also provided `dependencies` or the bound accessor matches @@ -106,7 +106,7 @@ class bExample extends iBlock { } ``` -##### [watchable] +### [watchable] If true, the accessor returns a link to another watchable object. This option allows you to mount external watchable objects to the component. @@ -141,7 +141,7 @@ class bExample extends iBlock { } ``` -##### [dependencies] +### [dependencies] A list of dependencies for the accessor. The dependencies are needed to watch for the accessor mutations or to invalidate its cache. diff --git a/src/core/component/decorators/field/README.md b/src/core/component/decorators/field/README.md index 67160fd601..9bc7b54f73 100644 --- a/src/core/component/decorators/field/README.md +++ b/src/core/component/decorators/field/README.md @@ -33,9 +33,9 @@ class bExample extends iBlock { } ``` -#### Additional options +## Additional options -##### [unique = `false`] +### [unique = `false`] Marks the field as unique for each component instance. Also, the parameter can take a function that returns a boolean value. @@ -54,7 +54,7 @@ class bExample extends iBlock { } ``` -##### [default] +### [default] This option allows you to set the default value of the field. But using it, as a rule, is not explicitly required, since the default value can be passed through @@ -92,7 +92,7 @@ class bExample extends iBlock { } ``` -##### [init] +### [init] A function to initialize the field value. The function takes as its first argument a reference to the component context. @@ -112,12 +112,12 @@ class bExample extends iBlock { } ``` -##### [forceUpdate = `true`] +### [forceUpdate = `true`] If false, then property changes don't directly force re-rendering the template. Keep in mind that the template can still be re-rendered, but only at the initiative of the engine being used. -##### [after] +### [after] A name or list of names after which this property should be initialized. Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). @@ -139,7 +139,7 @@ class bExample extends iBlock { } ``` -##### [atom = `false`] +### [atom = `false`] Indicates that property should be initialized before all non-atom properties. This option is needed when you have a field that must be guaranteed to be initialized before other fields, @@ -159,7 +159,7 @@ class bExample extends iBlock { } ``` -##### [watch] +### [watch] A watcher or list of watchers for the current field. The watcher can be defined as a component method to invoke, callback function, or watch handle. @@ -207,12 +207,12 @@ class bExample extends iBlock { } ``` -##### [functionalWatching = `false`] +### [functionalWatching = `false`] If false, the field can't be watched if created inside a functional component. This option is useful when you are writing a superclass or a smart component that can be created as regular or functional. -##### [merge = `false`] +### [merge = `false`] This option is only relevant for functional components. The fact is that when a component state changes, all its child functional components are recreated from scratch. @@ -224,7 +224,7 @@ Set this property to true to enable the strategy of merging old and new values. Or specify a function that will perform the merge. This function takes contexts of the old and new components, the name of the field to restore, and optionally a path to a property to which the given is bound. -##### [meta] +### [meta] A dictionary with some extra information of the field. You can access this information using `meta.fields`. diff --git a/src/core/component/decorators/interface/hook.ts b/src/core/component/decorators/interface/hook.ts index c02852ead7..26f4d2ecfa 100644 --- a/src/core/component/decorators/interface/hook.ts +++ b/src/core/component/decorators/interface/hook.ts @@ -10,10 +10,8 @@ import type { Hook } from 'core/component/interface'; import type { DecoratorFunctionalOptions } from 'core/component/decorators/interface/types'; export type DecoratorHook = - Hook | - Hook[] | - DecoratorHookOptions | - DecoratorHookOptions[]; + CanArray | + CanArray; export type DecoratorHookOptions = { [hook in Hook]?: DecoratorFunctionalOptions & { diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md index 90cf9aba1f..35eeb02c7f 100644 --- a/src/core/component/decorators/system/README.md +++ b/src/core/component/decorators/system/README.md @@ -54,7 +54,7 @@ class bExample extends iBlock { } ``` -##### [default] +### [default] This option allows you to set the default value of the field. But using it, as a rule, is not explicitly required, since the default value can be passed through @@ -92,7 +92,7 @@ class bExample extends iBlock { } ``` -##### [init] +### [init] A function to initialize the field value. The function takes as its first argument a reference to the component context. @@ -112,7 +112,7 @@ class bExample extends iBlock { } ``` -##### [after] +### [after] A name or list of names after which this property should be initialized. Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). @@ -134,7 +134,7 @@ class bExample extends iBlock { } ``` -##### [atom = `false`] +### [atom = `false`] Indicates that property should be initialized before all non-atom properties. This option is needed when you have a field that must be guaranteed to be initialized before other fields, @@ -154,7 +154,7 @@ class bExample extends iBlock { } ``` -##### [watch] +### [watch] A watcher or list of watchers for the current field. The watcher can be defined as a component method to invoke, callback function, or watch handle. @@ -202,12 +202,12 @@ class bExample extends iBlock { } ``` -##### [functionalWatching = `false`] +### [functionalWatching = `false`] If false, the field can't be watched if created inside a functional component. This option is useful when you are writing a superclass or a smart component that can be created as regular or functional. -##### [merge = `false`] +### [merge = `false`] This option is only relevant for functional components. The fact is that when a component state changes, all its child functional components are recreated from scratch. @@ -219,7 +219,7 @@ Set this property to true to enable the strategy of merging old and new values. Or specify a function that will perform the merge. This function takes contexts of the old and new components, the name of the field to restore, and optionally a path to a property to which the given is bound. -##### [meta] +### [meta] A dictionary with some extra information of the field. You can access this information using `meta.fields`. From cae5543cf1b3332860f63120ad72b8b3fb3e682a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 20 Jun 2022 21:35:51 +0300 Subject: [PATCH 0277/2313] refactor: renamed `componentInitializers` to `componentRegInitializers` --- src/core/component/const/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/component/const/cache.ts b/src/core/component/const/cache.ts index d300ea6f95..d06ee40115 100644 --- a/src/core/component/const/cache.ts +++ b/src/core/component/const/cache.ts @@ -36,7 +36,7 @@ export const components = new Map(); * By default, all components don't register automatically, but the first call from some template. * This structure contains functions to register components. */ -export const componentInitializers = Object.createDict(); +export const componentRegInitializers = Object.createDict(); /** * A dictionary with the registered component render factories From 735a6948a07e56cb6001b650fa92fd528b95e2d3 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 10:41:10 +0300 Subject: [PATCH 0278/2313] :art: --- src/core/component/decorators/field/README.md | 6 +++--- src/core/component/decorators/interface/field.ts | 6 +++--- src/core/component/decorators/interface/hook.ts | 2 +- src/core/component/decorators/interface/prop.ts | 2 +- src/core/component/decorators/system/README.md | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/component/decorators/field/README.md b/src/core/component/decorators/field/README.md index 9bc7b54f73..698f869a81 100644 --- a/src/core/component/decorators/field/README.md +++ b/src/core/component/decorators/field/README.md @@ -119,7 +119,7 @@ Keep in mind that the template can still be re-rendered, but only at the initiat ### [after] -A name or list of names after which this property should be initialized. +A name or a list of names after which this property should be initialized. Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). ```typescript @@ -161,7 +161,7 @@ class bExample extends iBlock { ### [watch] -A watcher or list of watchers for the current field. +A watcher or a list of watchers for the current field. The watcher can be defined as a component method to invoke, callback function, or watch handle. The `core/watch` module is used to make objects watchable. @@ -222,7 +222,7 @@ a certain property should be mixed based on the old and new values. Set this property to true to enable the strategy of merging old and new values. Or specify a function that will perform the merge. This function takes contexts of the old and new components, -the name of the field to restore, and optionally a path to a property to which the given is bound. +the name of the field to restore, and optionally, a path to a property to which the given is bound. ### [meta] diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts index 7b0023a262..e8e1033705 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/interface/field.ts @@ -83,7 +83,7 @@ export interface DecoratorSystem< init?: InitFieldFn; /** - * A name or list of names after which this property should be initialized. + * A name or a list of names after which this property should be initialized. * Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). * * @example @@ -130,7 +130,7 @@ export interface DecoratorSystem< atom?: boolean; /** - * A watcher or list of watchers for the current field. + * A watcher or a list of watchers for the current field. * The watcher can be defined as a component method to invoke, callback function, or watch handle. * * The `core/watch` module is used to make objects watchable. @@ -197,7 +197,7 @@ export interface DecoratorSystem< * * Set this property to true to enable the strategy of merging old and new values. * Or specify a function that will perform the merge. This function takes contexts of the old and new components, - * the name of the field to restore, and optionally a path to a property to which the given is bound. + * the name of the field to restore, and optionally, a path to a property to which the given is bound. * * @default `false` */ diff --git a/src/core/component/decorators/interface/hook.ts b/src/core/component/decorators/interface/hook.ts index 26f4d2ecfa..a93b90b919 100644 --- a/src/core/component/decorators/interface/hook.ts +++ b/src/core/component/decorators/interface/hook.ts @@ -16,7 +16,7 @@ export type DecoratorHook = export type DecoratorHookOptions = { [hook in Hook]?: DecoratorFunctionalOptions & { /** - * A method name or list of names after which this handler should be invoked on a registered hook event + * A method name or a list of names after which this handler should be invoked on a registered hook event */ after?: CanArray; } diff --git a/src/core/component/decorators/interface/prop.ts b/src/core/component/decorators/interface/prop.ts index 25e28e3b93..d4f98bbd26 100644 --- a/src/core/component/decorators/interface/prop.ts +++ b/src/core/component/decorators/interface/prop.ts @@ -14,7 +14,7 @@ import type { DecoratorFieldWatcher } from 'core/component/decorators/interface/ */ export interface PropOptions { /** - * Property type constructor or a list of constructors (if the property can have several types) + * A prop type constructor or a list of constructors (if the property can have multiple types) * * @example * ```typescript diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md index 35eeb02c7f..2e0103b751 100644 --- a/src/core/component/decorators/system/README.md +++ b/src/core/component/decorators/system/README.md @@ -114,7 +114,7 @@ class bExample extends iBlock { ### [after] -A name or list of names after which this property should be initialized. +A name or a list of names after which this property should be initialized. Keep in mind, you can only specify names that are of the same type as the current field (`@system` or `@field`). ```typescript @@ -156,7 +156,7 @@ class bExample extends iBlock { ### [watch] -A watcher or list of watchers for the current field. +A watcher or a list of watchers for the current field. The watcher can be defined as a component method to invoke, callback function, or watch handle. The `core/watch` module is used to make objects watchable. @@ -217,7 +217,7 @@ a certain property should be mixed based on the old and new values. Set this property to true to enable the strategy of merging old and new values. Or specify a function that will perform the merge. This function takes contexts of the old and new components, -the name of the field to restore, and optionally a path to a property to which the given is bound. +the name of the field to restore, and optionally, a path to a property to which the given is bound. ### [meta] From 4bce41e0b82ddd20e770e84ee172ab70d49075d9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 12:29:22 +0300 Subject: [PATCH 0279/2313] doc: improved doc --- src/core/component/decorators/field/README.md | 13 +++++++++---- .../component/decorators/interface/field.ts | 13 +++++++++---- .../component/decorators/system/README.md | 19 ++++++++++++------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/core/component/decorators/field/README.md b/src/core/component/decorators/field/README.md index 698f869a81..fc9c7ed68b 100644 --- a/src/core/component/decorators/field/README.md +++ b/src/core/component/decorators/field/README.md @@ -73,9 +73,10 @@ class bExample extends iBlock { } ``` -Note that the default value provided is a prototype, not a real value. That is, when set to each new instance, -it will be cloned using `Object.fastClone`. If this behavior does not suit you, then use the init option and pass the -initializer function. +Note that if the default value is set using class property syntax, then it is a prototype, not a real value. +That is, when set to each new instance, it will be cloned using `Object.fastClone`. +If this behavior does not suit you, then pass the value explicitly via `default` or using the `init` option and +an initializer function. ```typescript import iBlock, { component, field } from 'super/i-block/i-block'; @@ -87,8 +88,12 @@ class bExample extends iBlock { body: Element = document.body; // All fine - @field(() => document.body) + @field({default: document.body}) validBody!: Element; + + // All fine + @field(() => document.body) + validBody2!: Element; } ``` diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts index e8e1033705..8b74f7b12f 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/interface/field.ts @@ -32,9 +32,10 @@ export interface DecoratorSystem< * But using it, as a rule, is not explicitly required, since the default value can be passed through * the native syntax of class properties. * - * Note that the default value provided is a prototype, not a real value. That is, when set to each new instance, - * it will be cloned using `Object.fastClone`. If this behavior does not suit you, then use the init option and - * pass the initializer function. + * Note that if the default value is set using class property syntax, then it is a prototype, not a real value. + * That is, when set to each new instance, it will be cloned using `Object.fastClone`. + * If this behavior does not suit you, then pass the value explicitly via `default` or using the `init` option and + * an initializer function. * * @example * ```typescript @@ -53,8 +54,12 @@ export interface DecoratorSystem< * body: Element = document.body; * * // All fine - * @field(() => document.body) + * @field({default: document.body}) * validBody!: Element; + * + * // All fine + * @field(() => document.body) + * validBody2!: Element; * } * ``` */ diff --git a/src/core/component/decorators/system/README.md b/src/core/component/decorators/system/README.md index 2e0103b751..7cc474837c 100644 --- a/src/core/component/decorators/system/README.md +++ b/src/core/component/decorators/system/README.md @@ -61,21 +61,22 @@ But using it, as a rule, is not explicitly required, since the default value can the native syntax of class properties. ```typescript -import iBlock, { component, system } from 'super/i-block/i-block'; +import iBlock, { component, field } from 'super/i-block/i-block'; @component() class bExample extends iBlock { - @system() + @field() bla: number = 0; - @system({default: 0}) + @field({default: 0}) bar!: number; } ``` -Note that the default value provided is a prototype, not a real value. That is, when set to each new instance, -it will be cloned using `Object.fastClone`. If this behavior does not suit you, then use the init option and pass the -initializer function. +Note that if the default value is set using class property syntax, then it is a prototype, not a real value. +That is, when set to each new instance, it will be cloned using `Object.fastClone`. +If this behavior does not suit you, then pass the value explicitly via `default` or using the `init` option and +an initializer function. ```typescript import iBlock, { component, system } from 'super/i-block/i-block'; @@ -87,8 +88,12 @@ class bExample extends iBlock { body: Element = document.body; // All fine - @system(() => document.body) + @system({default: document.body}) validBody!: Element; + + // All fine + @system(() => document.body) + validBody2!: Element; } ``` From f7f781bf0b8af466b45c6c63e99871ff10da2ea9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 14:54:25 +0300 Subject: [PATCH 0280/2313] doc: improved doc --- .../component/decorators/interface/field.ts | 1 + .../component/decorators/interface/method.ts | 33 +++- .../component/decorators/interface/prop.ts | 138 +++++++++++--- src/core/component/decorators/prop/README.md | 180 +++++++++++++++++- 4 files changed, 318 insertions(+), 34 deletions(-) diff --git a/src/core/component/decorators/interface/field.ts b/src/core/component/decorators/interface/field.ts index 8b74f7b12f..6f8a509046 100644 --- a/src/core/component/decorators/interface/field.ts +++ b/src/core/component/decorators/interface/field.ts @@ -239,6 +239,7 @@ export interface DecoratorField< /** * If false, then property changes don't directly force re-rendering the template. * Keep in mind that the template can still be re-rendered, but only at the initiative of the engine being used. + * * @default `true` */ forceUpdate?: boolean; diff --git a/src/core/component/decorators/interface/method.ts b/src/core/component/decorators/interface/method.ts index 73917aceb2..c343e4a44d 100644 --- a/src/core/component/decorators/interface/method.ts +++ b/src/core/component/decorators/interface/method.ts @@ -11,17 +11,44 @@ import type { DecoratorHook } from 'core/component/decorators/interface/hook'; export interface DecoratorMethod { /** - * Watcher for changes of some properties + * A path to a property to watch or a list of such paths. + * Each time at least one of the specified properties is mutated, this method will be called. + * In addition to specifying the watching paths, you can also set other watching parameters. + * + * 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 '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; /** - * Parameters for watcher + * 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; /** - * Hook or a list of hooks after which the method should be invoked + * A component lifecycle hook or a list of such hooks on which this method should be called */ hook?: DecoratorHook; } diff --git a/src/core/component/decorators/interface/prop.ts b/src/core/component/decorators/interface/prop.ts index d4f98bbd26..432f8144f5 100644 --- a/src/core/component/decorators/interface/prop.ts +++ b/src/core/component/decorators/interface/prop.ts @@ -14,12 +14,15 @@ import type { DecoratorFieldWatcher } from 'core/component/decorators/interface/ */ export interface PropOptions { /** - * A prop type constructor or a list of constructors (if the property can have multiple types) + * A constructor function of the prop type. + * If the prop can be of different types, then you need to specify a list of constructors. * * @example * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * * @component() - * class Foo extends iBlock { + * class bExample extends iBlock { * @prop({type: Number}) * bla!: number; * @@ -31,13 +34,17 @@ export interface PropOptions { type?: PropType; /** - * Should or not the property has always a value - * @default `true` + * By default, all component props must be value-initialized. + * The values are either passed explicitly when a component is called, or are taken from the default values. + * If you set the `required` option to false, then the prop can be non-initialized. * + * @default `true` * @example * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * * @component() - * class Foo extends iBlock { + * class bExample extends iBlock { * @prop({required: false}) * bla?: number; * @@ -49,63 +56,144 @@ export interface PropOptions { required?: boolean; /** - * Default value for the property + * This option allows you to set the default value of the prop. + * But using it, as a rule, is not explicitly required, since the default value can be passed through + * the native syntax of class properties. + * + * Note that if the default value is set using class property syntax, then it is a prototype, not a real value. + * That is, when set to each new instance, it will be cloned using `Object.fastClone`. + * If this behavior does not suit you, then pass the value explicitly via `default`. + * + * Also, you can pass the default value as a function. + * It will be called, and its result will become the default value. + * Note that if your prop type is `Function`, then the default value will be treated "as is". * * @example * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * * @component() - * class Foo extends iBlock { + * class bExample extends iBlock { + * @prop() + * bla: number = 0; + * * @prop({default: 1}) - * bla!: number; + * baz!: number; * - * @prop() - * baz: number = 0; + * @prop({default: Math.random}) + * hashCode!: number; * } * ``` */ - default?: T | null | undefined | (() => T | null | undefined); + default?: Nullable | (() => Nullable); /** - * If false, the property can't work within functional or flyweight components - * @default `true` - */ - functional?: boolean; - - /** - * Property validator + * A function to check the passed value for compliance with the requirements. + * Use it if you want to impose additional checks besides checking the prop type. * * @param value - * * @example * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * * @component() - * class Foo extends iBlock { - * @prop({type: Number, validator: (v) => v > 0}}) + * class bExample extends iBlock { + * @prop({type: Number, validator: Number.isPositive}) * bla!: number; * } * ``` */ validator?(value: T): boolean; + + /** + * If false, the prop can't be passed to a functional component + * @default `true` + */ + functional?: boolean; } export interface DecoratorProp< CTX extends ComponentInterface = ComponentInterface, A = unknown, B = A - > extends PropOptions { +> extends PropOptions { /** - * If true, then the property always uses own default property when it is necessary + * If true, the prop always uses its own default value when needed. + * In fact, this option is used when the `defaultProps` property is set to false on the class being described, + * and we want to cancel this behaviour for a particular prop. + * * @default `false` */ forceDefault?: boolean; /** - * Watcher for changes of the property + * 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. + * + * The `core/watch` module is used to make objects watchable. + * Therefore, for more information, please refer to its documentation. + * + * @example + * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @prop({watch: [ + * 'onIncrement', + * + * (ctx, val, oldVal, info) => + * console.log(val, oldVal, info), + * + * // Also, see core/object/watch + * { + * // If false, then a handler that is invoked on the watcher event does not take any arguments from the event + * provideArgs: false, + * + * // How the event handler should be called: + * // + * // 1. `'post'` - the handler will be called on the next tick after the mutation and + * // guaranteed after updating all tied templates; + * // + * // 2. `'pre'` - the handler will be called on the next tick after the mutation and + * // guaranteed before updating all tied templates; + * // + * // 3. `'sync'` - the handler will be invoked immediately after each mutation. + * flush: 'sync', + * + * // Can define as a function too + * handler: 'onIncrement' + * } + * ]}) + * + * i: number = 0; + * + * onIncrement(val, oldVal, info) { + * console.log(val, oldVal, info); + * } + * } + * ``` */ watch?: DecoratorFieldWatcher; /** - * Additional information about the property + * A dictionary with some extra information of the prop. + * You can access this information using `meta.props`. + * + * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @prop({default: Math.random, meta: {debug: true}}) + * hashCode!: number; + * + * created() { + * // {debug: true} + * console.log(this.meta.props.hashCode.meta); + * } + * } + * ``` */ meta?: Dictionary; } diff --git a/src/core/component/decorators/prop/README.md b/src/core/component/decorators/prop/README.md index 23a47464bf..9916644a24 100644 --- a/src/core/component/decorators/prop/README.md +++ b/src/core/component/decorators/prop/README.md @@ -1,22 +1,190 @@ # core/component/decorators/prop -The decorator marks a class property as a component prop. +The decorator marks a class property as a component input property (aka "prop"). ```typescript import iBlock, { component, prop } from 'super/i-block/i-block'; @component() class bExample extends iBlock { + // The decorator can be called either without parameters + @prop() + foo: number = 0; + + // Or by passing a constructor function of the prop @prop(Number) bla: number = 0; - @prop({type: Number, required: false}) - baz?: number; - - @prop({type: Number, default: () => Math.random()}) + // Or a dictionary with additional options + @prop({type: Number, default: Math.random}) bar!: number; + + // If the prop can be of different types, then pass an array of constructors + @prop({type: [Number, String], required: false}) + baz?: number | string; +} +``` + +## Additional options + +### [type] + +A constructor function of the prop type. +If the prop can be of different types, then you need to specify a list of constructors. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop({type: Number}) + bla!: number; + + @prop({type: [Number, String]}) + baz!: number | string; } ``` -#### Additional options +### [required = `true`] + +By default, all component props must be value-initialized. +The values are either passed explicitly when a component is called, or are taken from the default values. +If you set the `required` option to false, then the prop can be non-initialized. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop({required: false}) + bla?: number; + + @prop() + baz: number = 0; +} +``` + +### [default] + +This option allows you to set the default value of the prop. +But using it, as a rule, is not explicitly required, since the default value can be passed through the native syntax of +class properties. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + // There will be a trouble here when cloning the value + @prop(Element) + body: Element = document.body; + + // All fine + @prop(() => document.body) + validBody!: Element; +} +``` + +Note that if the default value is set using class property syntax, then it is a prototype, not a real value. +That is, when set to each new instance, it will be cloned using `Object.fastClone`. +If this behavior does not suit you, then pass the value explicitly via `default`. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + // There will be a trouble here when cloning the value + @field() + body: Element = document.body; + + // All fine + @prop({default: document.body}) + validBody!: Element; +} +``` + +Also, you can pass the default value as a function. It will be called, and its result will become the default value. +Note that if your prop type is `Function`, then the default value will be treated "as is". + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop({default: Math.random}) + hashCode!: number; +} +``` + +### [validator] + +A function to check the passed value for compliance with the requirements. +Use it if you want to impose additional checks besides checking the prop type. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop({type: Number, validator: Number.isPositive}) + bla!: number; +} +``` + +### [watch] + +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. + +The `core/watch` module is used to make objects watchable. +Therefore, for more information, please refer to its documentation. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @prop({watch: [ + 'onIncrement', + + (ctx, val, oldVal, info) => + console.log(val, oldVal, info), + + // Also, see core/object/watch + { + // If false, then a handler that is invoked on the watcher event does not take any arguments from the event + provideArgs: false, + + // How the event handler should be called: + // + // 1. `'post'` - the handler will be called on the next tick after the mutation and + // guaranteed after updating all tied templates; + // + // 2. `'pre'` - the handler will be called on the next tick after the mutation and + // guaranteed before updating all tied templates; + // + // 3. `'sync'` - the handler will be invoked immediately after each mutation. + flush: 'sync', + + // Can define as a function too + handler: 'onIncrement' + } + ]}) + + i: number = 0; + + onIncrement(val, oldVal, info) { + console.log(val, oldVal, info); + } +} +``` + +### [functional = `true`] + +If false, the prop can't be passed to a functional component. + +### [forceDefault = `false`] +If true, the prop always uses its own default value when needed. In fact, this option is used when the `defaultProps` +property is set to false on the class being described, and we want to cancel this behaviour for a particular prop. From ce199344f8ad9d66b1d3dacd9e2728a187c1a565 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 15:11:57 +0300 Subject: [PATCH 0281/2313] doc: improved doc --- src/core/component/decorators/hook/README.md | 64 +++++++++++++++++++ .../decorators/{hook.ts => hook/index.ts} | 12 +++- .../component/decorators/interface/hook.ts | 23 +++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/core/component/decorators/hook/README.md rename src/core/component/decorators/{hook.ts => hook/index.ts} (65%) diff --git a/src/core/component/decorators/hook/README.md b/src/core/component/decorators/hook/README.md new file mode 100644 index 0000000000..4b49c785e4 --- /dev/null +++ b/src/core/component/decorators/hook/README.md @@ -0,0 +1,64 @@ +# core/component/decorators/hook + +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. + +## Usage + +```typescript +import iBlock, { component, hook } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + // Adding a handler for one hook + @hook('mounted') + onMounted() { + + } + + // Adding a handler for several hooks + @hook(['mounted', 'activated']) + onMountedOrActivated() { + + } + + // Adding a handler for several hooks + @hook('mounted') + @hook('activated') + onMountedOrActivated2() { + + } +} +``` + +## Additional options + +### [after] + +A method name or a list of names after which this handler should be invoked on a registered hook event. + +```typescript +import iBlock, { component, hook } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @hook('mounted') + initializeComponent() { + + } + + @hook({mounted: {after: 'initializeComponent'}}) + addedListeners() { + + } + + @hook({mounted: {after: ['initializeComponent', 'addedListeners']}}) + sendData() { + + } +} +``` + +### [functional = `true`] + +If false, the registered hook handler won't work inside a functional component. diff --git a/src/core/component/decorators/hook.ts b/src/core/component/decorators/hook/index.ts similarity index 65% rename from src/core/component/decorators/hook.ts rename to src/core/component/decorators/hook/index.ts index f9ffa58aba..d7ef3a25df 100644 --- a/src/core/component/decorators/hook.ts +++ b/src/core/component/decorators/hook/index.ts @@ -6,18 +6,26 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/hook/README.md]] + * @packageDocumentation + */ + import { paramsFactory } from 'core/component/decorators/factory'; import type { DecoratorHook } from 'core/component/decorators/interface'; /** * Attaches a hook listener to a component method. - * It means, that when a component is switched to the specified hook/s, the method will be invoked. + * 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 'super/i-block/i-block'; + * * @component() - * class Foo extends iBlock { + * class bExample extends iBlock { * @hook('mounted') * onMounted() { * diff --git a/src/core/component/decorators/interface/hook.ts b/src/core/component/decorators/interface/hook.ts index a93b90b919..b730f076fa 100644 --- a/src/core/component/decorators/interface/hook.ts +++ b/src/core/component/decorators/interface/hook.ts @@ -17,6 +17,29 @@ 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 + * + * @example + * ```typescript + * import iBlock, { component, hook } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @hook('mounted') + * initializeComponent() { + * + * } + * + * @hook({mounted: {after: 'initializeComponent'}}) + * addedListeners() { + * + * } + * + * @hook({mounted: {after: ['initializeComponent', 'addedListeners']}}) + * sendData() { + * + * } + * } + * ``` */ after?: CanArray; } From d7cfe8a81f0ce14ac8037a9320c317d282fbbc5d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 16:01:48 +0300 Subject: [PATCH 0282/2313] doc: improved doc --- .../component/decorators/computed/index.ts | 2 + src/core/component/decorators/field/index.ts | 2 + .../component/decorators/interface/watcher.ts | 33 +++- src/core/component/decorators/prop/index.ts | 2 + src/core/component/decorators/system/index.ts | 2 + src/core/component/decorators/watch/README.md | 166 ++++++++++++++++++ .../decorators/{watch.ts => watch/index.ts} | 40 +++-- 7 files changed, 230 insertions(+), 17 deletions(-) create mode 100644 src/core/component/decorators/watch/README.md rename src/core/component/decorators/{watch.ts => watch/index.ts} (66%) diff --git a/src/core/component/decorators/computed/index.ts b/src/core/component/decorators/computed/index.ts index f3707eec03..12453cc623 100644 --- a/src/core/component/decorators/computed/index.ts +++ b/src/core/component/decorators/computed/index.ts @@ -21,6 +21,8 @@ import type { DecoratorComponentAccessor } from 'core/component/decorators/inter * * @example * ```typescript + * import iBlock, { component, computed } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { * @computed({cache: true}) diff --git a/src/core/component/decorators/field/index.ts b/src/core/component/decorators/field/index.ts index 29b100c49e..af653627d4 100644 --- a/src/core/component/decorators/field/index.ts +++ b/src/core/component/decorators/field/index.ts @@ -22,6 +22,8 @@ import type { InitFieldFn, DecoratorField } from 'core/component/decorators/inte * * @example * ```typescript + * import iBlock, { component, field } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { * @field() diff --git a/src/core/component/decorators/interface/watcher.ts b/src/core/component/decorators/interface/watcher.ts index 1d1697392f..70c6403efb 100644 --- a/src/core/component/decorators/interface/watcher.ts +++ b/src/core/component/decorators/interface/watcher.ts @@ -14,13 +14,44 @@ export interface DecoratorFieldWatcherObject< B = A > extends WatchOptions { /** - * Handler (or a name of a component method) that is invoked on watcher events + * A handler (or a name of the component method) that is invoked on watcher events + * + * @example + * ```typescript + * import iBlock, { component, field, watch } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @watch({handler: 'onIncrement'}) + * @field() + * i: number = 0; + * + * onIncrement(val, oldVal, info) { + * console.log(val, oldVal, info); + * } + * } + * ``` */ handler: string | DecoratorWatchHandler; /** * If false, then a handler that is invoked on the watcher event does not take any arguments from the event + * * @default `true` + * @example + * ```typescript + * import iBlock, { component, field } from 'super/i-block/i-block'; + * + * @component() + * class bExample extends iBlock { + * @field({watch: {handler: 'onIncrement', provideArgs: false}}) + * i: number = 0; + * + * onIncrement(val) { + * console.log(val === undefined); + * } + * } + * ``` */ provideArgs?: boolean; } diff --git a/src/core/component/decorators/prop/index.ts b/src/core/component/decorators/prop/index.ts index 158fad4542..21284e194b 100644 --- a/src/core/component/decorators/prop/index.ts +++ b/src/core/component/decorators/prop/index.ts @@ -21,6 +21,8 @@ import type { DecoratorProp } from 'core/component/decorators/interface'; * * @example * ```typescript + * import iBlock, { component, prop } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { * @prop(Number) diff --git a/src/core/component/decorators/system/index.ts b/src/core/component/decorators/system/index.ts index 461bb64508..a7a9f9dc4a 100644 --- a/src/core/component/decorators/system/index.ts +++ b/src/core/component/decorators/system/index.ts @@ -22,6 +22,8 @@ import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/int * * @example * ```typescript + * import iBlock, { component, system } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { * @system() diff --git a/src/core/component/decorators/watch/README.md b/src/core/component/decorators/watch/README.md new file mode 100644 index 0000000000..1a46794c02 --- /dev/null +++ b/src/core/component/decorators/watch/README.md @@ -0,0 +1,166 @@ +# core/component/decorators/watch + +Attaches a watcher of a component property/event to a component method or property. + +The `core/watch` module is used to make objects watchable. +Therefore, for more information, please refer to its documentation. + +## Usage + +When you watch some property change, the handler function can take a second argument that refers to +the old value of the property. If the object being watched is non-primitive, the old value will be cloned from +the original old value to avoid the problem when we have two references to the one object. + +```typescript +import iBlock, { component, field, watch } from '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]); + } + + // When you don't declare 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]); + } + + // When you deep-watch a property and declare a second argument in a watcher, + // the property's old value 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 an event you need to use the special delimiter ":" within a watch path. +Also, you can specify an event emitter to listen by writing a link before ":". +For instance: + +1. `':onChange'` - the component will listen its own event `onChange`; +2. `'localEmitter:onChange'` - the component will listen an event `onChange` from `localEmitter`; +3. `'$parent.localEmitter:onChange'` - the component will listen an event `onChange` from `$parent.localEmitter`; +4. `'document:scroll'` - the component will listen an event `scroll` from `window.document`. + +A link to the event emitter is taken from component properties or from the global object. +The empty link '' is a link to a component itself. + +Also, if you listen an event, you can manage when start to listen the event by using special characters at the +beginning of a watch path: + +1. `'!'` - start to listen an event on the "beforeCreate" hook, for example: `'!rootEmitter:reset'`; +2. `'?'` - start to listen an event on the "mounted" hook, for example: `'?$el:click'`. + +By default, all events start to listen on the "created" hook. + +```typescript +import iBlock, { component, field, watch } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field() + foo: Dictionary = {bla: 0}; + + // Watch for changes of "foo" + @watch('foo') + watcher1() { + + } + + // Deep watch for changes of "foo" + @watch({path: 'foo', deep: true}) + watcher2() { + + } + + // Watch for changes of "foo.bla" + @watch('foo.bla') + watcher3() { + + } + + // Listen "onChange" event of a component + @watch(':onChange') + watcher3() { + + } + + // Listen "onChange" event of a component `parentEmitter` + @watch('parentEmitter:onChange') + watcher4() { + + } +} +``` + +## Additional options + +The `@watch` decorator can accept any options that the `watch` function from the `core/object/watch` module can accept, +so please refer to the documentation for that module. In addition, the decorator can accept some extra parameters. + +### [handler] + +A handler (or a name of the component method) that is invoked on watcher events. + +```typescript +import iBlock, { component, field, watch } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @watch({handler: 'onIncrement'}) + @field() + i: number = 0; + + onIncrement(val, oldVal, info) { + console.log(val, oldVal, info); + } +} +``` + +### [provideArgs = `true`] + +If false, then a handler that is invoked on the watcher event does not take any arguments from the event. + +```typescript +import iBlock, { component, field } from 'super/i-block/i-block'; + +@component() +class bExample extends iBlock { + @field({watch: {handler: 'onIncrement', provideArgs: false}}) + i: number = 0; + + onIncrement(val) { + console.log(val === undefined); + } +} +``` + +### [flush = `'sync'`] + +How the event handler should be called: + +1. `'post'` - the handler will be called on the next tick after the mutation and + guaranteed after updating all tied templates; + +2. `'pre'` - the handler will be called on the next tick after the mutation and + guaranteed before updating all tied templates; + +3. `'sync'` - the handler will be invoked immediately after each mutation. diff --git a/src/core/component/decorators/watch.ts b/src/core/component/decorators/watch/index.ts similarity index 66% rename from src/core/component/decorators/watch.ts rename to src/core/component/decorators/watch/index.ts index 144cc82bea..87fdd60062 100644 --- a/src/core/component/decorators/watch.ts +++ b/src/core/component/decorators/watch/index.ts @@ -6,18 +6,24 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/decorators/watch/README.md]] + * @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 watch for some property changes, the handler function can take the second argument - * that refers to the old value of a property. If the object that watching is non-primitive, - * the old value will be cloned from the original old value to avoid the problem when we have two - * links to the one object. + * When you watch some property change, the handler function can take a second argument that refers to + * the old value of the property. If the object being watched is non-primitive, the old value will be cloned from + * the original old value to avoid the problem when we have two references to the one object. * * ```typescript + * import iBlock, { component, field, watch } from 'super/i-block/i-block'; + * * @component() * class Foo extends iBlock { * @field() @@ -30,8 +36,8 @@ import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/compone * console.log(value[0] !== oldValue[0]); * } * - * // When you don't declare the second argument in a watcher, - * // the previous value isn't cloned + * // When you don't declare a second argument in a watcher, + * // the property's old value won't be cloned * @watch('list') * onListChangeWithoutCloning(value: Dictionary[]): void { * // true @@ -39,8 +45,8 @@ import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/compone * console.log(value[0] === oldValue[0]); * } * - * // When you watch a property in deep and declare the second argument - * // in a watcher, the previous value is cloned deeply + * // When you deep-watch a property and declare a second argument in a watcher, + * // the property's old value will be deep-cloned * @watch({path: 'list', deep: true}) * onListChangeWithDeepCloning(value: Dictionary[], oldValue: Dictionary[]): void { * // true @@ -55,20 +61,20 @@ import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/compone * } * ``` * - * To listen an event you need to use the special delimiter ":" within a path. + * To listen an event you need to use the special delimiter ":" within a watch path. * Also, you can specify an event emitter to listen by writing a link before ":". * For instance: * - * 1. `':onChange'` - a component will listen its own event `onChange`; - * 2. `'localEmitter:onChange'` - a component will listen an event `onChange` from `localEmitter`; - * 3. `'$parent.localEmitter:onChange'` - a component will listen an event `onChange` from `$parent.localEmitter`; - * 4. `'document:scroll'` - a component will listen an event `scroll` from `window.document`. + * 1. `':onChange'` - the component will listen its own event `onChange`; + * 2. `'localEmitter:onChange'` - the component will listen an event `onChange` from `localEmitter`; + * 3. `'$parent.localEmitter:onChange'` - the component will listen an event `onChange` from `$parent.localEmitter`; + * 4. `'document:scroll'` - the component will listen an event `scroll` from `window.document`. * * A link to the event emitter is taken from component properties or from the global object. * The empty link '' is a link to a component itself. * * Also, if you listen an event, you can manage when start to listen the event by using special characters at the - * beginning of a path string: + * beginning of a watch path: * * 1. `'!'` - start to listen an event on the "beforeCreate" hook, for example: `'!rootEmitter:reset'`; * 2. `'?'` - start to listen an event on the "mounted" hook, for example: `'?$el:click'`. @@ -79,6 +85,8 @@ import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/compone * * @example * ```typescript + * import iBlock, { component, field, watch } from 'super/i-block/i-block'; + * * @component() * class bExample extends iBlock { * @field() @@ -91,7 +99,7 @@ import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/compone * } * * // Deep watch for changes of "foo" - * @watch({path: 'foo', deep: true}}) + * @watch({path: 'foo', deep: true}) * watcher2() { * * } @@ -108,7 +116,7 @@ import type { DecoratorFieldWatcher, DecoratorMethodWatcher } from 'core/compone * * } * - * // Listen "onChange" event of a component parentEmitter + * // Listen "onChange" event of a component `parentEmitter` * @watch('parentEmitter:onChange') * watcher4() { * From ba18f318882bc6f16456fbc6779454ffc933f162 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 16:09:55 +0300 Subject: [PATCH 0283/2313] refactor: removed legacy --- src/core/component/decorators/index.ts | 2 +- src/core/component/decorators/p.ts | 34 ------------------- src/core/component/index.ts | 2 +- .../interface/component/component.ts | 4 +-- src/core/component/meta/interface/options.ts | 6 ++++ src/core/component/watch/bind.ts | 2 +- src/super/i-block/i-block.ts | 18 +--------- src/super/i-block/modules/decorators/index.ts | 11 ------ 8 files changed, 12 insertions(+), 67 deletions(-) delete mode 100644 src/core/component/decorators/p.ts diff --git a/src/core/component/decorators/index.ts b/src/core/component/decorators/index.ts index e4c5c2a6b1..c8f0e2d071 100644 --- a/src/core/component/decorators/index.ts +++ b/src/core/component/decorators/index.ts @@ -11,8 +11,8 @@ * @packageDocumentation */ +export * from 'core/component/decorators/component'; export * from 'core/component/decorators/factory'; -export * from 'core/component/decorators/p'; export * from 'core/component/decorators/prop'; export * from 'core/component/decorators/field'; diff --git a/src/core/component/decorators/p.ts b/src/core/component/decorators/p.ts deleted file mode 100644 index 66ed3fd8e2..0000000000 --- a/src/core/component/decorators/p.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import { paramsFactory } from 'core/component/decorators/factory'; -import type { DecoratorProp, DecoratorField, DecoratorMethod, DecoratorComponentAccessor } from 'core/component/decorators/interface'; - -/** - * The universal decorator for a component property/accessor/method - * - * @decorator - * - * @example - * ```typescript - * @component() - * class bExample extends iBlock { - * @p({cache: true}) - * get foo() { - * return 42; - * } - * } - * ``` - */ -export const p = paramsFactory< - DecoratorProp | - DecoratorField | - DecoratorMethod | - DecoratorComponentAccessor ->(null); - diff --git a/src/core/component/index.ts b/src/core/component/index.ts index 752bd7c588..ec519e724a 100644 --- a/src/core/component/index.ts +++ b/src/core/component/index.ts @@ -18,7 +18,6 @@ export { runHook } from 'core/component/hook'; export { resolveRefs } from 'core/component/ref'; export { bindRemoteWatchers, customWatcherRgxp } from 'core/component/watch'; -export { component } from 'core/component/register'; export { callMethodFromComponent } from 'core/component/method'; export { normalizeClass, normalizeStyle } from 'core/component/render'; @@ -44,4 +43,5 @@ export { } from 'core/component/event'; export * from 'core/component/reflect'; +export * from 'core/component/decorators'; export * from 'core/component/interface'; diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 5c28e925a8..46c82aff16 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -82,7 +82,7 @@ export abstract class ComponentInterface { * @example * ```js * // Key names are tied with the component elements - * // Values contain a CSS class or list of classes we want to add + * // Values contain a CSS class or a list of classes we want to add * * { * foo: 'bla', @@ -100,7 +100,7 @@ export abstract class ComponentInterface { * @example * ```js * // Key names are tied with component elements, - * // Values contains a CSS style string, a style object or list of style strings + * // Values contains a CSS style string, a style object or a list of style strings * * { * foo: 'color: red', diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts index 2b5fe77d18..9e76b4eec3 100644 --- a/src/core/component/meta/interface/options.ts +++ b/src/core/component/meta/interface/options.ts @@ -156,10 +156,16 @@ export interface ComponentOptions { * * @example * ```typescript + * import iData, { component, prop } from 'super/i-data/i-data'; + * * @component() * class bInput extends iData { * @prop() * value: string = ''; + * + * mounted() { + * console.log(this.$attrs['data-title']); + * } * } * ``` * diff --git a/src/core/component/watch/bind.ts b/src/core/component/watch/bind.ts index d53660ac40..401faef46b 100644 --- a/src/core/component/watch/bind.ts +++ b/src/core/component/watch/bind.ts @@ -158,7 +158,7 @@ export function bindRemoteWatchers(component: ComponentInterface, params?: BindR // 2. We watch for some component property, and we need to analyze the old property value. // // These cases are based on one problem: if we watch for some property that isn't primitive, - // like a hash table or list, and we add a new item to this structure but don't change the original object, + // like a hash table or a list, and we add a new item to this structure but don't change the original object, // the new and old values will be equal. // // class bButton { diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index a049c1ea2f..912dd33e6f 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -137,8 +137,6 @@ import { import { - p, - prop, field, system, @@ -167,7 +165,7 @@ export * from 'super/i-block/modules/event-emitter'; export * from 'super/i-block/modules/sync'; export * from 'friends/async-render'; -export * from 'super/i-block/modules/decorators'; +export { prop, field, system, computed, hook, watch, wait } from 'super/i-block/modules/decorators'; export { default as Friend } from 'friends/friend'; @@ -1488,7 +1486,6 @@ export default abstract class iBlock extends ComponentInterface { opts?: AsyncWatchOptions ): void; - @p() watch( path: WatchPath | object, optsOrHandler: AsyncWatchOptions | RawWatchHandler, @@ -1572,7 +1569,6 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param args */ - @p() emit(event: string | ComponentEvent, ...args: unknown[]): void { const eventDecl = Object.isString(event) ? {event} : event, @@ -1611,7 +1607,6 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param args */ - @p() emitError(event: string, ...args: unknown[]): void { this.emit({event, type: 'error'}, ...args); } @@ -1622,7 +1617,6 @@ export default abstract class iBlock extends ComponentInterface { * @param event * @param args */ - @p() dispatch(event: string | ComponentEvent, ...args: unknown[]): void { const eventDecl = Object.isString(event) ? {event} : event, @@ -1746,7 +1740,6 @@ export default abstract class iBlock extends ComponentInterface { opts?: WaitDecoratorOptions ): CanPromise>; - @p() waitStatus>( status: ComponentStatus, cbOrOpts?: F | WaitDecoratorOptions, @@ -2021,7 +2014,6 @@ export default abstract class iBlock extends ComponentInterface { */ setMod(name: string, value: unknown): CanPromise; - @p() setMod(nodeOrName: Element | string, name: string | unknown, value?: unknown): CanPromise { if (Object.isString(nodeOrName)) { const res = this.lfc.execCbAfterBlockReady(() => this.block!.setMod(nodeOrName, name)); @@ -2049,7 +2041,6 @@ export default abstract class iBlock extends ComponentInterface { */ removeMod(name: string, value?: unknown): CanPromise; - @p() removeMod(nodeOrName: Element | string, name?: string | unknown, value?: unknown): CanPromise { if (Object.isString(nodeOrName)) { const res = this.lfc.execCbAfterBlockReady(() => this.block!.removeMod(nodeOrName, name)); @@ -2082,7 +2073,6 @@ export default abstract class iBlock extends ComponentInterface { * console.log(document.documentElement.classList.contains('b-az-foo-bla')); * ``` */ - @p() setRootMod(name: string, value: unknown): boolean { return this.r.setRootMod(name, value, this); } @@ -2106,7 +2096,6 @@ export default abstract class iBlock extends ComponentInterface { * console.log(document.documentElement.classList.contains('b-az-foo-bla') === false); * ``` */ - @p() removeRootMod(name: string, value?: unknown): boolean { return this.r.removeRootMod(name, value, this); } @@ -2123,7 +2112,6 @@ export default abstract class iBlock extends ComponentInterface { * console.log(this.getRootMod('foo') === 'bla-bar'); * ``` */ - @p() getRootMod(name: string): CanUndef { return this.r.getRootMod(name, this); } @@ -2147,7 +2135,6 @@ export default abstract class iBlock extends ComponentInterface { * @param ctxOrOpts - the logging context or logging options * @param [details] - event details */ - @p() log(ctxOrOpts: string | LogMessageOptions, ...details: unknown[]): void { let context = ctxOrOpts, @@ -2283,7 +2270,6 @@ export default abstract class iBlock extends ComponentInterface { * @param ref - ref name * @param [opts] - additional options */ - @p() protected waitRef>(ref: string, opts?: AsyncOptions): Promise { let that = this; @@ -2331,7 +2317,6 @@ export default abstract class iBlock extends ComponentInterface { * Initializes an instance of the `Block` class for the current component */ @hook('mounted') - @p() protected initBlockInstance(): void { if (this.block != null) { const @@ -2489,7 +2474,6 @@ export default abstract class iBlock extends ComponentInterface { /** * Hook handler: component will be destroyed */ - @p() protected beforeDestroy(): void { this.componentStatus = 'destroyed'; this.async.clearAll().locked = true; diff --git a/src/super/i-block/modules/decorators/index.ts b/src/super/i-block/modules/decorators/index.ts index d29a2e8cf4..54940f6291 100644 --- a/src/super/i-block/modules/decorators/index.ts +++ b/src/super/i-block/modules/decorators/index.ts @@ -18,7 +18,6 @@ import { initEmitter, ModVal } from 'core/component'; import { - p as pDecorator, prop as propDecorator, field as fieldDecorator, system as systemDecorator, @@ -54,16 +53,6 @@ import type { export { hook, computed } from 'core/component/decorators'; export * from 'super/i-block/modules/decorators/interface'; -/** - * @see core/component/decorators/base.ts - * @inheritDoc - */ -export const p = pDecorator as ( - params?: - // @ts-ignore (unsafe cast) - DecoratorProp | DecoratorField | DecoratorMethod | DecoratorComponentAccessor -) => Function; - /** * @see core/component/decorators/base.ts * @inheritDoc From 7de7f1f5eeebf18ff744d054ecb82f3b590cdd93 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 21 Jun 2022 16:16:09 +0300 Subject: [PATCH 0284/2313] doc: improved doc --- src/core/component/decorators/README.md | 10 ++++++++++ src/core/component/decorators/system/index.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/component/decorators/README.md b/src/core/component/decorators/README.md index a96434d37e..bcd6e21ae7 100644 --- a/src/core/component/decorators/README.md +++ b/src/core/component/decorators/README.md @@ -15,3 +15,13 @@ export default class bUser extends iBlock { readonly lName: string; } ``` + +## Built-in decorators + +* `@component` to register a new component; +* `@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; +* `@hook` to attach a hook listener; +* `@watch` to attach a watcher. diff --git a/src/core/component/decorators/system/index.ts b/src/core/component/decorators/system/index.ts index a7a9f9dc4a..55789f474d 100644 --- a/src/core/component/decorators/system/index.ts +++ b/src/core/component/decorators/system/index.ts @@ -16,7 +16,7 @@ import type { InitFieldFn, DecoratorSystem } from 'core/component/decorators/int /** * Marks a class property as a system field. - * System property mutations never cause components to re-render. + * System field mutations never cause components to re-render. * * @decorator * From 16b304c926949b1c26d5311b8854c5ebae83f1f6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:39:08 +0300 Subject: [PATCH 0285/2313] refactor: better naming --- src/core/component/meta/tpl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/meta/tpl.ts b/src/core/component/meta/tpl.ts index bcc60d2c49..8fc4fdcb2d 100644 --- a/src/core/component/meta/tpl.ts +++ b/src/core/component/meta/tpl.ts @@ -37,13 +37,13 @@ export function attachTemplatesToMeta(meta: ComponentMeta, tpls?: Dictionary): v return; } - const renderObj = componentRenderFactories[meta.componentName] ?? tpls.index(); - componentRenderFactories[meta.componentName] = renderObj; + const renderFactory = componentRenderFactories[meta.componentName] ?? tpls.index(); + componentRenderFactories[meta.componentName] = renderFactory; methods.render = { wrapper: true, watchers: {}, hooks: {}, - fn: renderObj + fn: renderFactory }; } From 505f1f63f410262b7a0479f44a2a2817ad8c14db Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:39:55 +0300 Subject: [PATCH 0286/2313] refactor: removed dead code --- src/core/component/engines/vue3/component.ts | 2 -- src/core/component/render/api.ts | 25 -------------------- src/core/component/render/index.ts | 1 - 3 files changed, 28 deletions(-) delete mode 100644 src/core/component/render/api.ts diff --git a/src/core/component/engines/vue3/component.ts b/src/core/component/engines/vue3/component.ts index 46d2a49c00..486064d28c 100644 --- a/src/core/component/engines/vue3/component.ts +++ b/src/core/component/engines/vue3/component.ts @@ -12,7 +12,6 @@ import * as init from 'core/component/init'; import { beforeRenderHooks } from 'core/component/const'; import { fillMeta } from 'core/component/meta'; -import { implementComponentForceUpdateAPI } from 'core/component/render'; import { getComponentContext } from 'core/component/context'; import type { ComponentEngine, ComponentOptions } from 'core/component/engines'; @@ -85,7 +84,6 @@ export function getComponent(meta: ComponentMeta): ComponentOptions { - if (!('renderCounter' in component)) { - return; - } - - forceUpdate.call(component); - }; -} diff --git a/src/core/component/render/index.ts b/src/core/component/render/index.ts index 66161b2163..90ce001c7a 100644 --- a/src/core/component/render/index.ts +++ b/src/core/component/render/index.ts @@ -11,6 +11,5 @@ * @packageDocumentation */ -export * from 'core/component/render/api'; export * from 'core/component/render/wrappers'; export * from 'core/component/render/helpers'; From ff11957450813d9c0e82411b7ba861394407423e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:41:01 +0300 Subject: [PATCH 0287/2313] fix: set component render functions manually for function components --- src/core/component/method/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/component/method/index.ts b/src/core/component/method/index.ts index 4a486fb84f..96185bdbde 100644 --- a/src/core/component/method/index.ts +++ b/src/core/component/method/index.ts @@ -38,6 +38,10 @@ export function attachMethodsFromMeta(component: ComponentInterface): void { component[key] = method.fn.bind(component); } + + if (isFunctional) { + component.render = Object.cast(meta.component.render); + } } /** From 59f148ba8f017fbb3b27b4e2ff5f1bb551eff100 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:42:30 +0300 Subject: [PATCH 0288/2313] refactor: renamed the `fakeContext` property to `virtualContext` --- src/core/component/directives/image/index.ts | 6 +++--- src/core/component/directives/resize-observer/index.ts | 4 ++-- src/core/component/directives/update-on/index.ts | 4 ++-- src/core/component/engines/CHANGELOG.md | 1 + src/core/component/engines/directive.ts | 4 ++-- src/core/component/engines/interface.ts | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/core/component/directives/image/index.ts b/src/core/component/directives/image/index.ts index 824fee3178..8e4179686b 100644 --- a/src/core/component/directives/image/index.ts +++ b/src/core/component/directives/image/index.ts @@ -24,14 +24,14 @@ ComponentEngine.directive('image', { return; } - if (vnode.fakeContext != null) { + if (vnode.virtualContext != null) { if (Object.isPlainObject(value)) { - value.ctx = value.ctx ?? vnode.fakeContext; + value.ctx = value.ctx ?? vnode.virtualContext; } else { value = { src: value, - ctx: vnode.fakeContext + ctx: vnode.virtualContext }; } } diff --git a/src/core/component/directives/resize-observer/index.ts b/src/core/component/directives/resize-observer/index.ts index b13e6722a3..456b6061c7 100644 --- a/src/core/component/directives/resize-observer/index.ts +++ b/src/core/component/directives/resize-observer/index.ts @@ -28,7 +28,7 @@ ComponentEngine.directive('resize-observer', { } Array.concat([], val).forEach((options) => { - options = normalizeOptions(options, vnode.fakeContext); + options = normalizeOptions(options, vnode.virtualContext); setCreatedViaDirectiveFlag(ResizeWatcher.observe(el, options)); }); }, @@ -53,7 +53,7 @@ ComponentEngine.directive('resize-observer', { return; } - opts = normalizeOptions(opts, vnode.fakeContext); + opts = normalizeOptions(opts, vnode.virtualContext); setCreatedViaDirectiveFlag(ResizeWatcher.observe(el, opts)); }); }, diff --git a/src/core/component/directives/update-on/index.ts b/src/core/component/directives/update-on/index.ts index 1f70f1f063..eab192f594 100644 --- a/src/core/component/directives/update-on/index.ts +++ b/src/core/component/directives/update-on/index.ts @@ -29,7 +29,7 @@ ComponentEngine.directive('update-on', { unmounted(el: Element, opts: DirectiveOptions, vnode: VNode): void { const - ctx = vnode.fakeContext; + ctx = vnode.virtualContext; if (ctx != null) { engine.remove(el, ctx); @@ -39,7 +39,7 @@ ComponentEngine.directive('update-on', { function add(el: Element, value: Nullable>, vnode: VNode): void { const - ctx = vnode.fakeContext; + ctx = vnode.virtualContext; if (ctx == null) { return; diff --git a/src/core/component/engines/CHANGELOG.md b/src/core/component/engines/CHANGELOG.md index 6dfb0dba47..94adeb3783 100644 --- a/src/core/component/engines/CHANGELOG.md +++ b/src/core/component/engines/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog #### :boom: Breaking Change * Removed the `Vue.js@2` engine +* Renamed the `fakeContext` property to `virtualContext` #### :rocket: New Feature diff --git a/src/core/component/engines/directive.ts b/src/core/component/engines/directive.ts index 0c717b47cc..749787ac67 100644 --- a/src/core/component/engines/directive.ts +++ b/src/core/component/engines/directive.ts @@ -61,8 +61,8 @@ ComponentEngine.directive = function directive(name: string, directive?: Directi originalCreated.apply(this, args); } - if (vnode.fakeContext != null) { - vnode.fakeContext.unsafe.$on('component-hook:before-destroy', () => { + if (vnode.virtualContext != null) { + vnode.virtualContext.unsafe.$on('component-hook:before-destroy', () => { originalUnmounted.apply(this, args); }); } diff --git a/src/core/component/engines/interface.ts b/src/core/component/engines/interface.ts index 15e61e521f..b5f1682c60 100644 --- a/src/core/component/engines/interface.ts +++ b/src/core/component/engines/interface.ts @@ -29,7 +29,7 @@ export interface VNode< HostElement = RendererElement, ExtraProps = {[key: string]: any} > extends SuperVNode { - fakeContext?: ComponentInterface; + virtualContext?: ComponentInterface; } export interface ResolveDirective { From d323b3400130860df9d667080ba8b53b9287527e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:43:32 +0300 Subject: [PATCH 0289/2313] fix: borrow the render context from the parent if the component is functional --- src/friends/vdom/class.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/friends/vdom/class.ts b/src/friends/vdom/class.ts index dea194de32..9649d7da3a 100644 --- a/src/friends/vdom/class.ts +++ b/src/friends/vdom/class.ts @@ -41,11 +41,22 @@ class VDOM extends Friend { constructor(component: iBlock) { super(component); - this.meta.hooks.mounted.push({ - fn: () => { - this.setInstance = this.ctx.$renderEngine.r.withAsyncContext.call(this.ctx, Promise.resolve.bind(Promise))[1]; - } - }); + if (this.ctx.isFunctional) { + Object.defineProperty(this, 'setInstance', { + configurable: true, + enumerable: true, + get() { + return this.ctx.$normalParent?.unsafe.vdom.setInstance; + } + }); + + } else { + this.meta.hooks.mounted.push({ + fn: () => { + this.setInstance = this.ctx.$renderEngine.r.withAsyncContext.call(this.ctx, Promise.resolve.bind(Promise))[1]; + } + }); + } } } From d98dff367ea9b1707445d1f7fd3eadaae3988af1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:44:10 +0300 Subject: [PATCH 0290/2313] refactor: refactoring & remove dead code --- .../interface/component/component.ts | 40 ++++++++++++++----- .../component/interface/component/unsafe.ts | 12 ------ 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 46c82aff16..4636c68757 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -16,7 +16,7 @@ eslint-disable import type Async from 'core/async'; import type { BoundFn, ProxyCb } from 'core/async'; -import type { Slots, ComponentOptions } from 'core/component/engines'; +import type { VNode, Slots, ComponentOptions } from 'core/component/engines'; import type { ComponentMeta } from 'core/component/meta'; import type { Hook } from 'core/component/interface/lc'; @@ -270,10 +270,22 @@ export abstract class ComponentInterface { */ abstract getComponentInfo?(): Dictionary; + /** + * The component render function + * + * @param ctx + * @param cache + */ + render(ctx: ComponentInterface, cache: unknown[]): VNode { + return Object.throw(); + } + /** * Forces the component to re-render */ - $forceUpdate(): void {} + $forceUpdate(): void { + return Object.throw(); + } /** * Executes the specified function on the next render tick @@ -285,12 +297,16 @@ export abstract class ComponentInterface { * Returns a promise that will be resolved on the next render tick */ $nextTick(): Promise; - $nextTick(): CanPromise {} + $nextTick(): CanPromise { + return Object.throw(); + } /** * Destroys the component */ - protected $destroy(): void {} + protected $destroy(): void { + return Object.throw(); + } /** * Sets a new reactive value to the specified property of the passed object @@ -300,7 +316,7 @@ export abstract class ComponentInterface { * @param value */ protected $set(object: object, key: unknown, value: T): T { - return value; + return Object.throw(); } /** @@ -309,7 +325,9 @@ export abstract class ComponentInterface { * @param object * @param key */ - protected $delete(object: object, key: unknown): void {} + protected $delete(object: object, key: unknown): void { + Object.throw(); + } /** * Sets a watcher to a component/object property by the specified path @@ -360,7 +378,7 @@ export abstract class ComponentInterface { ): Nullable; protected $watch(): Nullable { - return null; + return Object.throw(); } /** @@ -370,7 +388,7 @@ export abstract class ComponentInterface { * @param handler */ protected $on(event: CanArray, handler: ProxyCb): this { - return this; + return Object.throw(); } /** @@ -380,7 +398,7 @@ export abstract class ComponentInterface { * @param handler */ protected $once(event: string, handler: ProxyCb): this { - return this; + return Object.throw(); } /** @@ -390,7 +408,7 @@ export abstract class ComponentInterface { * @param [handler] */ protected $off(event?: CanArray, handler?: Function): this { - return this; + return Object.throw(); } /** @@ -400,6 +418,6 @@ export abstract class ComponentInterface { * @param args */ protected $emit(event: string, ...args: unknown[]): this { - return this; + return Object.throw(); } } diff --git a/src/core/component/interface/component/unsafe.ts b/src/core/component/interface/component/unsafe.ts index 887e4f188c..81eff27704 100644 --- a/src/core/component/interface/component/unsafe.ts +++ b/src/core/component/interface/component/unsafe.ts @@ -107,16 +107,4 @@ export interface UnsafeComponentInterface Date: Wed, 22 Jun 2022 16:44:32 +0300 Subject: [PATCH 0291/2313] feat: added the support of private events --- src/core/component/event/component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/component/event/component.ts b/src/core/component/event/component.ts index 839f4b4e3c..8adb6b29c0 100644 --- a/src/core/component/event/component.ts +++ b/src/core/component/event/component.ts @@ -60,8 +60,11 @@ export function implementEventEmitterAPI(component: object): void { enumerable: false, writable: false, - value(event, ...args) { - nativeEmit?.(event, ...args); + value(event: string, ...args) { + if (!event.startsWith('[[')) { + nativeEmit?.(event, ...args); + } + $e.emit(event, ...args); return this; } From 35eb2d60c4b44178e33df74c2a11154d3d239fe1 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:45:17 +0300 Subject: [PATCH 0292/2313] refactor: :art: --- src/core/component/init/states/before-create.ts | 9 ++++++--- src/core/component/init/states/created.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/component/init/states/before-create.ts b/src/core/component/init/states/before-create.ts index 97fa04593a..543e5d1fef 100644 --- a/src/core/component/init/states/before-create.ts +++ b/src/core/component/init/states/before-create.ts @@ -56,7 +56,10 @@ export function beforeCreateState( unsafe.$async = new Async(component); const - parent = unsafe.$parent, + root = unsafe.$root, + parent = unsafe.$parent; + + const isFunctional = meta.params.functional === true; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -65,14 +68,14 @@ export function beforeCreateState( configurable: true, enumerable: true, writable: true, - value: unsafe.$root.unsafe + value: root.unsafe }); Object.defineProperty(unsafe, '$parent', { configurable: true, enumerable: true, writable: true, - value: unsafe.$root.$remoteParent + value: root.$remoteParent }); } diff --git a/src/core/component/init/states/created.ts b/src/core/component/init/states/created.ts index 3c51456202..d40ab20451 100644 --- a/src/core/component/init/states/created.ts +++ b/src/core/component/init/states/created.ts @@ -14,7 +14,7 @@ import { runHook } from 'core/component/hook'; import type { ComponentInterface, Hook } from 'core/component/interface'; const - remoteActivationLabel = Symbol('Remote activation label'); + remoteActivationLabel = Symbol('The remote activation label'); /** * Initializes the "created" state to the specified component instance From 69d1acea02bffeae13d706cc70b0406d1db9b6b0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:46:08 +0300 Subject: [PATCH 0293/2313] refactor: work on supporting functional components --- src/core/component/functional/README.md | 2 +- src/core/component/functional/const.ts | 6 - src/core/component/functional/context.ts | 216 +++++++++++++++++++++ src/core/component/functional/fake-ctx.ts | 157 --------------- src/core/component/functional/helpers.ts | 7 +- src/core/component/functional/index.ts | 2 +- src/core/component/functional/interface.ts | 15 +- src/core/component/functional/vnode.ts | 33 +--- src/core/component/render/wrappers.ts | 54 +++++- 9 files changed, 278 insertions(+), 214 deletions(-) create mode 100644 src/core/component/functional/context.ts delete mode 100644 src/core/component/functional/fake-ctx.ts diff --git a/src/core/component/functional/README.md b/src/core/component/functional/README.md index 3ed64db616..89598b55b7 100644 --- a/src/core/component/functional/README.md +++ b/src/core/component/functional/README.md @@ -1,4 +1,4 @@ # core/component/functional -This module provides API to create a functional component. +This module provides an API to create a functional component. Mind that V4Fire has own cross-platform implementation of functional components. diff --git a/src/core/component/functional/const.ts b/src/core/component/functional/const.ts index 0daf20ad3e..555390e5cf 100644 --- a/src/core/component/functional/const.ts +++ b/src/core/component/functional/const.ts @@ -10,9 +10,3 @@ import symbolGenerator from 'core/symbol'; export const $$ = symbolGenerator(); - -export const componentOpts = [ - 'filters', - 'directives', - 'components' -]; diff --git a/src/core/component/functional/context.ts b/src/core/component/functional/context.ts new file mode 100644 index 0000000000..85ad3a7110 --- /dev/null +++ b/src/core/component/functional/context.ts @@ -0,0 +1,216 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +import * as init from 'core/component/init'; + +import { resolveRefs } from 'core/component/ref'; +import { forkMeta, ComponentMeta } from 'core/component/meta'; +import { initProps } from 'core/component/prop'; + +import type { ComponentInterface } from 'core/component/interface'; +import type { VirtualContextOptions } from 'core/component/functional/interface'; + +const + componentInitLabel = Symbol('The component initialization label'); + +/** + * Creates a virtual context for the passed functional component + * + * @param component - the component meta object + * @param parent - the component parent + * @param [props] - the component props + * @param [slots] - the component slots + */ +export function createVirtualContext( + component: ComponentMeta, + {parent, props = {}, slots = {}}: VirtualContextOptions +): ComponentInterface { + const meta = forkMeta(component); + meta.params.functional = true; + + const + $props = {}, + $attrs = {}; + + const + handlers: Array<[string, boolean, Function]> = []; + + if (props != null) { + const + isOnceEvent = /.Once(.|$)/, + isDOMEvent = /.(?:Passive|Capture)(.|$)/; + + const isComponentEventHandler = (event: string, handler: unknown): handler is Function => { + if (!event.startsWith('on') || isDOMEvent.test(event) || !Object.isFunction(handler)) { + return false; + } + + return handler.name !== 'withModifiers' && handler.name !== 'withKeys'; + }; + + for (let keys = Object.keys(props), i = 0; i < keys.length; i++) { + const + key = keys[i], + normalizedKey = key.camelize(false), + prop = props[key]; + + if (normalizedKey in meta.props) { + $props[normalizedKey] = prop; + + } else { + if (isComponentEventHandler(key, prop)) { + let + event = key.slice('on'.length).camelize(false); + + const + once = isOnceEvent.test(key); + + if (once) { + event = event.replace(/Once$/, ''); + } + + handlers.push([event, once, prop]); + } + + $attrs[key] = prop; + } + } + } + + let + $options; + + if ('$options' in parent) { + const { + directives = {}, + components = {} + } = parent.$options; + + $options = { + directives: Object.create(directives), + components: Object.create(components) + }; + + } else { + $options = { + directives: {}, + components: {} + }; + } + + const virtualCtx = Object.cast({ + componentName: meta.componentName, + + meta, + instance: Object.cast(meta.instance), + + $parent: parent, + $root: parent.$root, + + $options, + $renderEngine: parent.$renderEngine, + + $refs: {}, + $slots: slots ?? {}, + + $props, + $attrs, + + $nextTick(cb?: AnyFunction): CanVoid> { + if (cb != null) { + setImmediate(cb); + return; + } + + return Promise.resolve(); + }, + + $forceUpdate(): void { + return undefined; + } + }); + + initProps(virtualCtx, { + from: $props, + store: virtualCtx, + saveToStore: true + }); + + init.beforeCreateState(virtualCtx, meta, { + addMethods: true, + implementEventAPI: true + }); + + for (let i = 0; i < handlers.length; i++) { + const + [event, once, handler] = handlers[i]; + + if (once) { + virtualCtx.$once(event, handler); + + } else { + virtualCtx.$on(event, handler); + } + } + + init.beforeDataCreateState(virtualCtx, {tieFields: true}); + + virtualCtx.$on('[[COMPONENT_HOOK]]', async (hook) => { + switch (hook) { + case 'beforeUpdate': + init.createdState(virtualCtx); + break; + + case 'updated': { + await virtualCtx.$async.promise(virtualCtx.$nextTick(), { + label: componentInitLabel + }); + + init.mountedState(virtualCtx); + + const + parent = virtualCtx.$normalParent; + + if (parent != null) { + resolveRefs(parent); + } + + break; + } + + case 'beforeDestroy': { + // const + // parent = virtualCtx.$normalParent; + // + // const needImmediateDestroy = + // parent == null || + // parent.componentStatus === 'destroyed' || + // parent.$root === parent; + // + // if (needImmediateDestroy) { + // this.$destroy(); + // + // } else { + // virtualCtx.async.on(parent, 'on-component-hook:before-destroy', virtualCtx.$destroy.bind(virtualCtx), { + // label: $$.onUnbindHook, + // group: ':zombie' + // }); + // + // virtualCtx.async.clearAll().locked = true; + // } + + break; + } + + default: + init[`${hook}State`](virtualCtx); + } + }); + + return virtualCtx; +} diff --git a/src/core/component/functional/fake-ctx.ts b/src/core/component/functional/fake-ctx.ts deleted file mode 100644 index 9767cf5317..0000000000 --- a/src/core/component/functional/fake-ctx.ts +++ /dev/null @@ -1,157 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import * as init from 'core/component/construct'; - -import { forkMeta } from 'core/component/meta'; -import { initProps } from 'core/component/prop'; - -import type { RenderContext } from 'core/component/render'; -import type { CreateElement } from 'core/component/engines'; - -import { $$, componentOpts } from 'core/component/functional/const'; -import { destroyComponent } from 'core/component/functional/helpers'; - -import type { FunctionalCtx } from 'core/component/interface'; -import type { CreateFakeCtxOptions } from 'core/component/functional/interface'; - -export * from 'core/component/functional/interface'; - -/** - * Creates the fake context for a functional component is based on the specified parameters - * - * @param createElement - function to create VNode element - * @param renderCtx - render context from VNode - * @param baseCtx - component context that provided the core functionality - * @param [opts] - additional options - */ -export function createFakeCtx( - createElement: CreateElement, - renderCtx: Partial, - baseCtx: FunctionalCtx, - opts: CreateFakeCtxOptions -): T { - const - fakeCtx = Object.create(baseCtx), - meta = forkMeta(fakeCtx.meta); - - const - {component} = meta, - {parent, children, data: dataOpts} = renderCtx; - - let - $options; - - if (parent?.$options) { - const { - filters = {}, - directives = {}, - components = {} - } = parent.$options; - - $options = { - filters: Object.create(filters), - directives: Object.create(directives), - components: Object.create(components) - }; - - } else { - $options = { - filters: {}, - directives: {}, - components: {} - }; - } - - if (Object.isDictionary(component)) { - Object.assign($options, Object.reject(component, componentOpts)); - Object.assign($options.filters, component.filters); - Object.assign($options.directives, component.directives); - Object.assign($options.components, component.components); - } - - if (renderCtx.$options) { - const o = renderCtx.$options; - Object.assign($options, Object.reject(o, componentOpts)); - Object.assign($options.filters, o.filters); - Object.assign($options.directives, o.directives); - Object.assign($options.components, o.components); - } - - fakeCtx.unsafe = fakeCtx; - fakeCtx.children = Object.isArray(children) ? children : []; - - fakeCtx.$parent = parent; - fakeCtx.$root = renderCtx.$root ?? parent?.$root ?? fakeCtx; - fakeCtx.$renderEngine = fakeCtx.$root.$renderEngine; - - fakeCtx.$options = $options; - fakeCtx.$props = renderCtx.props ?? {}; - fakeCtx.$attrs = dataOpts?.attrs ?? {}; - fakeCtx.$listeners = renderCtx.listeners ?? dataOpts?.on ?? {}; - fakeCtx.$refs = {}; - - fakeCtx.$slots = { - default: Object.size(children) > 0 ? children : undefined, - ...renderCtx.slots?.() - }; - - fakeCtx.$scopedSlots = { - ...Object.isFunction(renderCtx.scopedSlots) ? renderCtx.scopedSlots() : renderCtx.scopedSlots - }; - - fakeCtx.$createElement = createElement.bind(fakeCtx); - fakeCtx.$destroy = () => destroyComponent(fakeCtx); - - fakeCtx._self = fakeCtx; - fakeCtx._renderProxy = fakeCtx; - fakeCtx._c = fakeCtx.$createElement; - fakeCtx._staticTrees = []; - - fakeCtx.$nextTick = (cb?: Function) => { - const - {$async: $a} = fakeCtx; - - if (cb) { - $a.setImmediate(cb); - return; - } - - return $a.nextTick(); - }; - - fakeCtx.$forceUpdate = () => { - // eslint-disable-next-line @typescript-eslint/unbound-method - if (!Object.isFunction(parent?.$forceUpdate)) { - return; - } - - fakeCtx.$async.setImmediate(() => parent!.$forceUpdate(), { - label: $$.forceUpdate - }); - }; - - if (fakeCtx.$root == null) { - fakeCtx.$root = fakeCtx; - } - - initProps(fakeCtx, { - from: renderCtx.props, - store: fakeCtx, - saveToStore: opts.initProps - }); - - init.beforeCreateState(fakeCtx, meta, { - addMethods: true, - implementEventAPI: true - }); - - init.beforeDataCreateState(fakeCtx, {tieFields: true}); - - return fakeCtx; -} diff --git a/src/core/component/functional/helpers.ts b/src/core/component/functional/helpers.ts index 7c7c26ff38..bc6a7d70f7 100644 --- a/src/core/component/functional/helpers.ts +++ b/src/core/component/functional/helpers.ts @@ -8,7 +8,7 @@ import { $$ } from 'core/component/functional/const'; -import * as init from 'core/component/construct'; +import * as init from 'core/component/init'; import type { ComponentInterface } from 'core/component/interface'; /** @@ -22,8 +22,5 @@ export function destroyComponent(component: ComponentInterface): void { component[$$.destroyed] = true; init.beforeDestroyState(component); - - if (!component.isFlyweight) { - init.destroyedState(component); - } + init.destroyedState(component); } diff --git a/src/core/component/functional/index.ts b/src/core/component/functional/index.ts index b9f77dcbf6..c209e197ae 100644 --- a/src/core/component/functional/index.ts +++ b/src/core/component/functional/index.ts @@ -12,7 +12,7 @@ */ export * from 'core/component/functional/const'; -export * from 'core/component/functional/fake-ctx'; +export * from 'core/component/functional/context'; export * from 'core/component/functional/vnode'; export * from 'core/component/functional/helpers'; export * from 'core/component/functional/interface'; diff --git a/src/core/component/functional/interface.ts b/src/core/component/functional/interface.ts index ad456ebac4..9662633222 100644 --- a/src/core/component/functional/interface.ts +++ b/src/core/component/functional/interface.ts @@ -6,16 +6,15 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import type { VNode } from 'core/component/engines'; +import type { VNode, Slots } from 'core/component/engines'; import type { ComponentInterface } from 'core/component/interface'; -export interface CreateFakeCtxOptions { - /** - * If true, then component prop values will be forced to initialize - */ - initProps?: boolean; +export interface VirtualContextOptions { + parent: ComponentInterface; + props?: Nullable; + slots?: Nullable; } -export interface FlyweightVNode extends VNode { - fakeInstance: ComponentInterface; +export interface FunctionalVNode extends VNode { + virtualComponent: ComponentInterface; } diff --git a/src/core/component/functional/vnode.ts b/src/core/component/functional/vnode.ts index ec7b8c44f2..70b7b37654 100644 --- a/src/core/component/functional/vnode.ts +++ b/src/core/component/functional/vnode.ts @@ -6,15 +6,14 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import * as init from 'core/component/construct'; +import * as init from 'core/component/init'; import type { VNode } from 'core/component/engines'; -import type { RenderContext } from 'core/component/render'; import { $$ } from 'core/component/functional/const'; import type { ComponentField, ComponentInterface } from 'core/component/interface'; -import type { FlyweightVNode } from 'core/component/functional/interface'; +import type { FunctionalVNode } from 'core/component/functional/interface'; /** * Initializes a component from the specified VNode. @@ -22,37 +21,17 @@ import type { FlyweightVNode } from 'core/component/functional/interface'; * * @param vnode * @param component - component instance - * @param renderCtx - render context */ export function initComponentVNode( vnode: VNode, - component: ComponentInterface, - renderCtx: RenderContext -): FlyweightVNode { + component: ComponentInterface +): FunctionalVNode { const - {unsafe} = component, - {data} = renderCtx; + {unsafe} = component; const flyweightVNode = Object.assign(vnode, {fakeInstance: component}); - component.$renderEngine.patchVNode(flyweightVNode, component, renderCtx); - // Attach component event listeners - if (data.on != null) { - for (let o = data.on, keys = Object.keys(o), i = 0; i < keys.length; i++) { - const - key = keys[i], - fns = Array.concat([], o[key]); - - for (let i = 0; i < fns.length; i++) { - const - fn = fns[i]; - - if (Object.isFunction(fn)) { - unsafe.$on(key, fn); - } - } - } - } + return flyweightVNode; const originalOnBindHook = unsafe.onBindHook; unsafe.onBindHook = onBindHook; diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index 36dcfcb941..f9a6b592fd 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -8,6 +8,10 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import * as c from 'core/component/const'; +import { attachTemplatesToMeta } from 'core/component/meta'; +import { createVirtualContext, initComponentVNode } from 'core/component/functional'; + import type { resolveComponent, @@ -27,32 +31,64 @@ import type { } from 'core/component/engines'; -import { registerComponent } from 'core/component/register'; +import { registerComponent } from 'core/component/init'; import { interpolateStaticAttrs } from 'core/component/render/helpers'; import type { ComponentInterface } from 'core/component/interface'; export function wrapCreateVNode(original: T): T { - return Object.cast(function createVNode(this: ComponentInterface) { - return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); + return Object.cast(function createVNode(this: ComponentInterface, ...args: Parameters) { + return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } export function wrapCreateElementVNode(original: T): T { - return Object.cast(function createElementVNode(this: ComponentInterface) { - return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); + return Object.cast(function createElementVNode(this: ComponentInterface, ...args: Parameters) { + return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } export function wrapCreateBlock(original: T): T { - return Object.cast(function wrapCreateBlock(this: ComponentInterface) { - return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); + return Object.cast(function wrapCreateBlock(this: ComponentInterface, ...args: Parameters) { + const + [name] = args, + {supports, r} = this.$renderEngine; + + const + supportFunctionalComponents = !supports.regular || supports.functional; + + if (Object.isString(name) && supportFunctionalComponents) { + const + component = registerComponent(name); + + if (component?.params.functional === true) { + // const + // {componentName} = component; + // + // if (c.componentRenderFactories[componentName] == null) { + // attachTemplatesToMeta(component, TPLS[componentName]); + // } + // + // const virtualCtx = createVirtualContext(component, { + // parent: this, + // props: args[1], + // slots: args[2] + // }); + // + // const vnode = virtualCtx.render(virtualCtx, []); + + console.log(args); + return original.apply(null, args); + } + } + + return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } export function wrapCreateElementBlock(original: T): T { - return Object.cast(function createElementBlock(this: ComponentInterface) { - return interpolateStaticAttrs.call(this, original.apply(null, Object.cast(arguments))); + return Object.cast(function createElementBlock(this: ComponentInterface, ...args: Parameters) { + return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } From 5d814dfa5d4f7ab7bbff1f6ca35f4d265b3c19d7 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 16:46:51 +0300 Subject: [PATCH 0294/2313] refactor: re-export decorators & new API for functional hooks --- src/super/i-block/i-block.ss | 13 ++- src/super/i-block/i-block.ts | 89 ++----------------- src/super/i-block/modules/decorators/index.ts | 5 +- 3 files changed, 18 insertions(+), 89 deletions(-) diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 6deb30742f..675cd2d1e3 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -173,10 +173,15 @@ 'v-hook': "!isVirtualTpl && isFunctional ?" + "{" + - "created: createInternalHookListener('created')," + - "mounted: createInternalHookListener('mounted')," + - "updated: createInternalHookListener('updated')," + - "unmounted: createInternalHookListener('unmounted')" + + "created : $emit.bind(self, '[[COMPONENT_HOOK]]', 'created' )," + + "beforeMount : $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeMount' )," + + "mounted : $emit.bind(self, '[[COMPONENT_HOOK]]', 'mounted' )," + + "beforeUpdate : $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeUpdate' )," + + "updated : $emit.bind(self, '[[COMPONENT_HOOK]]', 'updated' )," + + "beforeUpdate : $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeUpdate' )," + + "updated : $emit.bind(self, '[[COMPONENT_HOOK]]', 'updated' )," + + "beforeUnmount: $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeDestroy')," + + "unmounted : $emit.bind(self, '[[COMPONENT_HOOK]]', 'destroyed' )" + "} :" + "null" diff --git a/src/super/i-block/i-block.ts b/src/super/i-block/i-block.ts index 912dd33e6f..72dfd17b3a 100644 --- a/src/super/i-block/i-block.ts +++ b/src/super/i-block/i-block.ts @@ -667,8 +667,14 @@ export default abstract class iBlock extends ComponentInterface { * Link to the component root */ get r(): this['$root'] { - const r = this.$root; - return r.unsafe.$remoteParent?.$root ?? r; + const + r = this.$root; + + if ('$remoteParent' in r.unsafe) { + return r.unsafe.$remoteParent!.$root; + } + + return r; } /** @@ -2379,14 +2385,6 @@ export default abstract class iBlock extends ComponentInterface { this.parentEmitter.on('onCallChild', this.onCallChild.bind(this)); } - /** - * Factory to create listeners from internal hook events - * @param hook - hook name to listen - */ - protected createInternalHookListener(hook: string): Function { - return (...args) => (this[`on-${hook}-hook`.camelize(false)]).call(this, ...args); - } - /** * Handler: `callChild` event * @param e @@ -2400,77 +2398,6 @@ export default abstract class iBlock extends ComponentInterface { } } - /** - * Hook handler: the component has been mounted - * @emits `mounted(el: Element)` - */ - @hook('mounted') - protected onMounted(): void { - this.emit('mounted', this.$el); - } - - /** - * Hook handler: the component has been created - * (only for functional components) - */ - protected onCreatedHook(): void { - init.beforeMountState(this); - } - - /** - * Hook handler: the component has been mounted - * (only for functional components) - */ - protected onMountedHook(): void { - init.mountedState(this); - } - - /** - * Hook handler: the component has been updated - * (only for functional components) - */ - protected async onUpdatedHook(): Promise { - try { - await this.nextTick({label: $$.onUpdateHook}); - - this.onCreatedHook(); - this.onMountedHook(); - - if (this.$normalParent != null) { - resolveRefs(this.$normalParent); - } - - } catch (err) { - stderr(err); - } - } - - /** - * Hook handler: the component has been unmounted - * (only for functional components) - */ - protected onUnmountedHook(): void { - const - parent = this.$normalParent; - - const needImmediateDestroy = - parent == null || - parent.componentStatus === 'destroyed' || - parent.r === parent; - - if (needImmediateDestroy) { - this.$destroy(); - - } else { - this.async.on(parent, 'on-component-hook:before-destroy', this.$destroy.bind(this), { - label: $$.onUnbindHook, - group: ':zombie' - }); - - this.async.clearAll().locked = true; - } - } - /** * Hook handler: component will be destroyed */ diff --git a/src/super/i-block/modules/decorators/index.ts b/src/super/i-block/modules/decorators/index.ts index 54940f6291..310b00455a 100644 --- a/src/super/i-block/modules/decorators/index.ts +++ b/src/super/i-block/modules/decorators/index.ts @@ -21,10 +21,7 @@ import { prop as propDecorator, field as fieldDecorator, system as systemDecorator, - watch as watchDecorator, - - DecoratorMethod, - DecoratorComponentAccessor + watch as watchDecorator } from 'core/component/decorators'; From f566d359a40396249775cb268e2fbfe037b14e05 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 19:05:01 +0300 Subject: [PATCH 0295/2313] fix: fixed rendering of functional components --- src/core/component/render/wrappers.ts | 45 ++++++++++++++++----------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index f9a6b592fd..ee1c1de298 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -9,8 +9,9 @@ */ import * as c from 'core/component/const'; + import { attachTemplatesToMeta } from 'core/component/meta'; -import { createVirtualContext, initComponentVNode } from 'core/component/functional'; +import { createVirtualContext } from 'core/component/functional'; import type { @@ -52,7 +53,7 @@ export function wrapCreateBlock(original: T): T { return Object.cast(function wrapCreateBlock(this: ComponentInterface, ...args: Parameters) { const [name] = args, - {supports, r} = this.$renderEngine; + {supports} = this.$renderEngine; const supportFunctionalComponents = !supports.regular || supports.functional; @@ -62,23 +63,29 @@ export function wrapCreateBlock(original: T): T { component = registerComponent(name); if (component?.params.functional === true) { - // const - // {componentName} = component; - // - // if (c.componentRenderFactories[componentName] == null) { - // attachTemplatesToMeta(component, TPLS[componentName]); - // } - // - // const virtualCtx = createVirtualContext(component, { - // parent: this, - // props: args[1], - // slots: args[2] - // }); - // - // const vnode = virtualCtx.render(virtualCtx, []); - - console.log(args); - return original.apply(null, args); + const + {componentName} = component; + + if (c.componentRenderFactories[componentName] == null) { + attachTemplatesToMeta(component, TPLS[componentName]); + } + + const virtualCtx = createVirtualContext(component, { + parent: this, + props: args[1], + slots: args[2] + }); + + const + vnode: VNode = original.apply(null, args), + functionalVNode = virtualCtx.render(virtualCtx, []); + + vnode.type = functionalVNode.type; + vnode.props = functionalVNode.props; + vnode.children = functionalVNode.children; + vnode.dynamicChildren = functionalVNode.dynamicChildren; + + return vnode; } } From 6b6b3736820c17b08c4cbd74062b3ee1e9ae05cc Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 22 Jun 2022 19:05:54 +0300 Subject: [PATCH 0296/2313] feat: added a new property `dynamicChildren` --- src/core/component/engines/interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/component/engines/interface.ts b/src/core/component/engines/interface.ts index b5f1682c60..472f1ab97f 100644 --- a/src/core/component/engines/interface.ts +++ b/src/core/component/engines/interface.ts @@ -29,6 +29,7 @@ export interface VNode< HostElement = RendererElement, ExtraProps = {[key: string]: any} > extends SuperVNode { + dynamicChildren?: VNode[]; virtualContext?: ComponentInterface; } From 265b7578b67099cea4f6596f1836b119a53e3f57 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 27 Jun 2022 18:17:12 +0300 Subject: [PATCH 0297/2313] refactor: removed `$componentId` & renamed `renderCounter` to `$renderCounter` --- src/core/component/interface/component/component.ts | 8 +------- src/core/component/interface/component/unsafe.ts | 5 +---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index 4636c68757..e1d8254205 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -175,13 +175,7 @@ export abstract class ComponentInterface { /** * A number that is incremented each time the component is re-rendered */ - protected abstract renderCounter: number; - - /** - * A temporary string identifier of the component - * (only for functional components) - */ - protected readonly $componentId?: string; + protected readonly $renderCounter!: number; /** * A dictionary with the watchable component properties that can force re-rendering diff --git a/src/core/component/interface/component/unsafe.ts b/src/core/component/interface/component/unsafe.ts index 81eff27704..ee6792257f 100644 --- a/src/core/component/interface/component/unsafe.ts +++ b/src/core/component/interface/component/unsafe.ts @@ -29,10 +29,7 @@ export interface UnsafeComponentInterface Date: Mon, 27 Jun 2022 18:17:55 +0300 Subject: [PATCH 0298/2313] refactor: updated support of function components --- src/core/component/functional/const.ts | 12 -- src/core/component/functional/context.ts | 73 ++------- src/core/component/functional/helpers.ts | 188 +++++++++++++++++++++-- src/core/component/functional/index.ts | 3 - src/core/component/functional/vnode.ts | 172 --------------------- 5 files changed, 186 insertions(+), 262 deletions(-) delete mode 100644 src/core/component/functional/const.ts delete mode 100644 src/core/component/functional/vnode.ts diff --git a/src/core/component/functional/const.ts b/src/core/component/functional/const.ts deleted file mode 100644 index 555390e5cf..0000000000 --- a/src/core/component/functional/const.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import symbolGenerator from 'core/symbol'; - -export const - $$ = symbolGenerator(); diff --git a/src/core/component/functional/context.ts b/src/core/component/functional/context.ts index 85ad3a7110..20aee1439d 100644 --- a/src/core/component/functional/context.ts +++ b/src/core/component/functional/context.ts @@ -8,16 +8,14 @@ import * as init from 'core/component/init'; -import { resolveRefs } from 'core/component/ref'; import { forkMeta, ComponentMeta } from 'core/component/meta'; import { initProps } from 'core/component/prop'; +import { initDynamicComponentLifeCycle } from 'core/component/functional/helpers'; + import type { ComponentInterface } from 'core/component/interface'; import type { VirtualContextOptions } from 'core/component/functional/interface'; -const - componentInitLabel = Symbol('The component initialization label'); - /** * Creates a virtual context for the passed functional component * @@ -109,18 +107,18 @@ export function createVirtualContext( meta, instance: Object.cast(meta.instance), + $props, + $attrs, + + $refs: {}, + $slots: slots ?? {}, + $parent: parent, $root: parent.$root, $options, $renderEngine: parent.$renderEngine, - $refs: {}, - $slots: slots ?? {}, - - $props, - $attrs, - $nextTick(cb?: AnyFunction): CanVoid> { if (cb != null) { setImmediate(cb); @@ -159,58 +157,5 @@ export function createVirtualContext( } init.beforeDataCreateState(virtualCtx, {tieFields: true}); - - virtualCtx.$on('[[COMPONENT_HOOK]]', async (hook) => { - switch (hook) { - case 'beforeUpdate': - init.createdState(virtualCtx); - break; - - case 'updated': { - await virtualCtx.$async.promise(virtualCtx.$nextTick(), { - label: componentInitLabel - }); - - init.mountedState(virtualCtx); - - const - parent = virtualCtx.$normalParent; - - if (parent != null) { - resolveRefs(parent); - } - - break; - } - - case 'beforeDestroy': { - // const - // parent = virtualCtx.$normalParent; - // - // const needImmediateDestroy = - // parent == null || - // parent.componentStatus === 'destroyed' || - // parent.$root === parent; - // - // if (needImmediateDestroy) { - // this.$destroy(); - // - // } else { - // virtualCtx.async.on(parent, 'on-component-hook:before-destroy', virtualCtx.$destroy.bind(virtualCtx), { - // label: $$.onUnbindHook, - // group: ':zombie' - // }); - // - // virtualCtx.async.clearAll().locked = true; - // } - - break; - } - - default: - init[`${hook}State`](virtualCtx); - } - }); - - return virtualCtx; + return initDynamicComponentLifeCycle(virtualCtx); } diff --git a/src/core/component/functional/helpers.ts b/src/core/component/functional/helpers.ts index bc6a7d70f7..5dc188d616 100644 --- a/src/core/component/functional/helpers.ts +++ b/src/core/component/functional/helpers.ts @@ -6,21 +6,187 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -import { $$ } from 'core/component/functional/const'; - import * as init from 'core/component/init'; + +import { resolveRefs } from 'core/component/ref'; import type { ComponentInterface } from 'core/component/interface'; -/** - * Emits destroying of the specified component - * @param component - */ -export function destroyComponent(component: ComponentInterface): void { - if (component[$$.destroyed] === true) { +const + componentInitLabel = Symbol('The component initialization label'); + +export function initDynamicComponentLifeCycle(component: ComponentInterface): ComponentInterface { + const + {unsafe} = component; + + unsafe.$on('[[COMPONENT_HOOK]]', async (hook, node) => { + switch (hook) { + case 'mounted': + unsafe.unsafe.$el = node; + init.mountedState(unsafe); + break; + + case 'beforeUpdate': + break; + + case 'updated': { + inheritContext(unsafe, node.component); + + await unsafe.$async.promise(unsafe.$nextTick(), { + label: componentInitLabel + }); + + init.createdState(unsafe); + init.mountedState(unsafe); + + const + parent = unsafe.$normalParent; + + if (parent != null) { + resolveRefs(parent); + } + + break; + } + + case 'beforeDestroy': { + const + parent = unsafe.$normalParent; + + const needImmediateDestroy = + parent == null || + parent.hook === 'beforeDestroy' || + parent.hook === 'destroyed' || + parent.$root === parent; + + if (needImmediateDestroy) { + unsafe.$destroy(); + + } else { + const eventHandle = ['on-component-hook:before-destroy', unsafe.$destroy.bind(unsafe)] as const; + parent.unsafe.$once(...eventHandle); + + unsafe.async.worker(() => parent.unsafe.$off(...eventHandle), { + group: ':zombie' + }); + + unsafe.async.clearAll().locked = true; + } + + break; + } + + default: + init[`${hook}State`](unsafe); + } + }); + + return component; +} + +export function inheritContext( + ctx: ComponentInterface['unsafe'], + parentCtx: CanUndef +): void { + if ( + parentCtx == null || + parentCtx === ctx || + ctx.componentName !== parentCtx.componentName + ) { return; } - component[$$.destroyed] = true; - init.beforeDestroyState(component); - init.destroyedState(component); + parentCtx.unsafe.$destroy(); + + const + props = ctx.$props, + parentProps = parentCtx.$props; + + const + linkedFields = {}; + + for (let keys = Object.keys(parentProps), i = 0; i < keys.length; i++) { + const + prop = keys[i], + linked = parentCtx.$syncLinkCache.get(prop); + + if (linked != null) { + for (let keys = Object.keys(linked), i = 0; i < keys.length; i++) { + const + link = linked[keys[i]]; + + if (link != null) { + linkedFields[link.path] = prop; + } + } + } + } + + const fields = [ + parentCtx.meta.systemFields, + parentCtx.meta.fields + ]; + + for (let i = 0; i < fields.length; i++) { + const + cluster = fields[i], + keys = Object.keys(cluster); + + for (let j = 0; j < keys.length; j++) { + const + key = keys[j], + field = cluster[key]; + + if (field == null) { + continue; + } + + const + link = linkedFields[key]; + + const + val = ctx[key], + oldVal = parentCtx[key]; + + const needMerge = + ctx.$modifiedFields[key] !== true && + + ( + Object.isFunction(field.unique) ? + !Object.isTruly(field.unique(ctx, parentCtx)) : + !field.unique + ) && + + !Object.fastCompare(val, oldVal) && + + ( + link == null || + Object.fastCompare(props[link], parentProps[link]) + ); + + if (needMerge) { + if (Object.isTruly(field.merge)) { + if (field.merge === true) { + let + newVal = oldVal; + + if (Object.isPlainObject(val) || Object.isPlainObject(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); + } + + ctx[key] = newVal; + + } else if (Object.isFunction(field.merge)) { + field.merge(ctx, parentCtx, key, link); + } + + } else { + ctx[key] = parentCtx[key]; + } + } + } + } } diff --git a/src/core/component/functional/index.ts b/src/core/component/functional/index.ts index c209e197ae..808e356f65 100644 --- a/src/core/component/functional/index.ts +++ b/src/core/component/functional/index.ts @@ -11,8 +11,5 @@ * @packageDocumentation */ -export * from 'core/component/functional/const'; export * from 'core/component/functional/context'; -export * from 'core/component/functional/vnode'; -export * from 'core/component/functional/helpers'; export * from 'core/component/functional/interface'; diff --git a/src/core/component/functional/vnode.ts b/src/core/component/functional/vnode.ts deleted file mode 100644 index 70b7b37654..0000000000 --- a/src/core/component/functional/vnode.ts +++ /dev/null @@ -1,172 +0,0 @@ -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -import * as init from 'core/component/init'; - -import type { VNode } from 'core/component/engines'; - -import { $$ } from 'core/component/functional/const'; - -import type { ComponentField, ComponentInterface } from 'core/component/interface'; -import type { FunctionalVNode } from 'core/component/functional/interface'; - -/** - * Initializes a component from the specified VNode. - * This function provides life-cycle hooks, adds classes and event listeners, etc. - * - * @param vnode - * @param component - component instance - */ -export function initComponentVNode( - vnode: VNode, - component: ComponentInterface -): FunctionalVNode { - const - {unsafe} = component; - - const flyweightVNode = Object.assign(vnode, {fakeInstance: component}); - - return flyweightVNode; - - const originalOnBindHook = unsafe.onBindHook; - unsafe.onBindHook = onBindHook; - - init.createdState(component); - return flyweightVNode; - - function onBindHook(): void { - const - el = component.$el; - - if (el == null) { - unsafe.onUnbindHook(); - return; - } - - let - oldCtx = el[$$.component]; - - // The situation when we have an old context of the same component on the same node: - // we need to merge the old state with a new - if (oldCtx != null) { - if (oldCtx === component) { - return; - } - - if (component.componentName !== oldCtx.componentName) { - oldCtx = undefined; - delete el[$$.component]; - } - } - - if (oldCtx != null) { - oldCtx.$componentId = component.componentId; - - // Destroy the old component - oldCtx.onUnbindHook(); - - const - props = component.$props, - oldProps = oldCtx.$props, - linkedFields = >{}; - - // Merge prop values - for (let keys = Object.keys(oldProps), i = 0; i < keys.length; i++) { - const - key = keys[i], - linked = oldCtx.$syncLinkCache.get(key); - - if (linked != null) { - for (let keys = Object.keys(linked), i = 0; i < keys.length; i++) { - const - link = linked[keys[i]]; - - if (link != null) { - linkedFields[link.path] = key; - } - } - } - } - - // Merge field/system field values - - { - const list = [ - oldCtx.meta.systemFields, - oldCtx.meta.fields - ]; - - for (let i = 0; i < list.length; i++) { - const - obj = list[i], - keys = Object.keys(obj); - - for (let j = 0; j < keys.length; j++) { - const - key = keys[j], - field = >obj[key]; - - if (field == null) { - continue; - } - - const - link = linkedFields[key]; - - const - val = component[key], - oldVal = oldCtx[key]; - - const needMerge = - unsafe.$modifiedFields[key] !== true && - - ( - Object.isFunction(field.unique) ? - !Object.isTruly(field.unique(component.unsafe, oldCtx)) : - !field.unique - ) && - - !Object.fastCompare(val, oldVal) && - - ( - link == null || - Object.fastCompare(props[link], oldProps[link]) - ); - - if (needMerge) { - if (Object.isTruly(field.merge)) { - if (field.merge === true) { - let - newVal = oldVal; - - if (Object.isPlainObject(val) || Object.isPlainObject(oldVal)) { - newVal = {...val, ...oldVal}; - - } else if (Object.isArray(val) || Object.isArray(oldVal)) { - newVal = Object.assign([], val, oldVal); - } - - component[key] = newVal; - - } else if (Object.isFunction(field.merge)) { - field.merge(component.unsafe, oldCtx, key, link); - } - - } else { - component[key] = oldCtx[key]; - } - } - } - } - } - } - - el[$$.component] = unsafe; - originalOnBindHook.call(unsafe); - } -} From 28cc35ac03dbfea355d99968337bbbb55c3f210b Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 27 Jun 2022 18:19:29 +0300 Subject: [PATCH 0299/2313] refactor: moved `v-hook` to driver --- src/core/component/render/wrappers.ts | 28 ++++++++++++++++++++++++++- src/super/i-block/i-block.ss | 18 +---------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index ee1c1de298..cc28a3771e 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -53,7 +53,7 @@ export function wrapCreateBlock(original: T): T { return Object.cast(function wrapCreateBlock(this: ComponentInterface, ...args: Parameters) { const [name] = args, - {supports} = this.$renderEngine; + {supports, r} = this.$renderEngine; const supportFunctionalComponents = !supports.regular || supports.functional; @@ -85,6 +85,32 @@ export function wrapCreateBlock(original: T): T { vnode.children = functionalVNode.children; vnode.dynamicChildren = functionalVNode.dynamicChildren; + vnode.dirs = functionalVNode.dirs ?? []; + vnode.dirs.push({ + dir: Object.cast(r.resolveDirective.call(virtualCtx, 'hook')), + + modifiers: {}, + arg: undefined, + + value: { + created: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'created', n), + beforeMount: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeMount', n), + mounted: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'mounted', n), + beforeUpdate: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeUpdate', n), + updated: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'updated', n), + beforeUnmount: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeDestroy', n), + unmounted: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'destroyed', n) + }, + + oldValue: undefined, + instance: Object.cast(virtualCtx) + }); + + functionalVNode.props = {}; + functionalVNode.dirs = null; + functionalVNode.children = []; + functionalVNode.dynamicChildren = []; + return vnode; } } diff --git a/src/super/i-block/i-block.ss b/src/super/i-block/i-block.ss index 675cd2d1e3..ed1f32f7c2 100644 --- a/src/super/i-block/i-block.ss +++ b/src/super/i-block/i-block.ss @@ -167,24 +167,8 @@ - rootAttrs = { & 'class': 'i-block-helper', - 'data-cached-class-component-id': '', - 'data-cached-dynamic-class': 'self.provide.componentClasses("' + self.name() + '", self.mods)', - - 'v-hook': "!isVirtualTpl && isFunctional ?" + - "{" + - "created : $emit.bind(self, '[[COMPONENT_HOOK]]', 'created' )," + - "beforeMount : $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeMount' )," + - "mounted : $emit.bind(self, '[[COMPONENT_HOOK]]', 'mounted' )," + - "beforeUpdate : $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeUpdate' )," + - "updated : $emit.bind(self, '[[COMPONENT_HOOK]]', 'updated' )," + - "beforeUpdate : $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeUpdate' )," + - "updated : $emit.bind(self, '[[COMPONENT_HOOK]]', 'updated' )," + - "beforeUnmount: $emit.bind(self, '[[COMPONENT_HOOK]]', 'beforeDestroy')," + - "unmounted : $emit.bind(self, '[[COMPONENT_HOOK]]', 'destroyed' )" + - "} :" + - - "null" + 'data-cached-dynamic-class': 'self.provide.componentClasses("' + self.name() + '", self.mods)' } . - if skeletonMarker From b0136a255978e7776085b898ccd862ea5184e061 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 27 Jun 2022 18:20:22 +0300 Subject: [PATCH 0300/2313] fix: implement $destroy --- .../component/init/states/before-create.ts | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/core/component/init/states/before-create.ts b/src/core/component/init/states/before-create.ts index 543e5d1fef..fb6c7376af 100644 --- a/src/core/component/init/states/before-create.ts +++ b/src/core/component/init/states/before-create.ts @@ -21,6 +21,9 @@ import { attachMethodsFromMeta, callMethodFromComponent } from 'core/component/m import { runHook } from 'core/component/hook'; import { implementEventEmitterAPI } from 'core/component/event'; +import { beforeDestroyState } from 'core/component/init/states/before-destroy'; +import { destroyedState } from 'core/component/init/states/destroyed'; + import type { ComponentInterface, ComponentMeta } from 'core/component/interface'; import type { InitBeforeCreateStateOptions } from 'core/component/init/interface'; @@ -42,26 +45,39 @@ export function beforeCreateState( const unsafe = Object.cast>(component); unsafe.unsafe = unsafe; - unsafe.meta = meta; - unsafe.componentName = meta.componentName; + + unsafe.meta = meta; unsafe.instance = Object.cast(meta.instance); unsafe.$fields = {}; unsafe.$systemFields = {}; unsafe.$modifiedFields = {}; unsafe.$refHandlers = {}; + unsafe.$renderCounter = 0; unsafe.async = new Async(component); unsafe.$async = new Async(component); + Object.defineProperty(unsafe, '$destroy', { + configurable: true, + enumerable: false, + writable: true, + value: () => { + if (component.hook !== 'beforeDestroy' && component.hook !== 'destroyed') { + beforeDestroyState(component); + } + + if (component.hook !== 'destroyed') { + destroyedState(component); + } + } + }); + const root = unsafe.$root, parent = unsafe.$parent; - const - isFunctional = meta.params.functional === true; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (parent != null && parent.componentName == null) { Object.defineProperty(unsafe, '$root', { @@ -124,6 +140,9 @@ export function beforeCreateState( fakeHandler = () => undefined; if (watchDependencies.size > 0) { + const + isFunctional = meta.params.functional === true; + const watchSet = new Set(); From 435fceff5132933ca85f8bc9a5c97b23a9d90f02 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 27 Jun 2022 18:21:36 +0300 Subject: [PATCH 0301/2313] fix: clear all async handlers --- src/core/component/init/states/before-destroy.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/component/init/states/before-destroy.ts b/src/core/component/init/states/before-destroy.ts index 80ec5c988c..f5d5011c9b 100644 --- a/src/core/component/init/states/before-destroy.ts +++ b/src/core/component/init/states/before-destroy.ts @@ -18,5 +18,11 @@ import type { ComponentInterface } from 'core/component/interface'; export function beforeDestroyState(component: ComponentInterface): void { runHook('beforeDestroy', component).catch(stderr); callMethodFromComponent(component, 'beforeDestroy'); - component.unsafe.$async.clearAll().locked = true; + + const + {unsafe} = component; + + unsafe.async.clearAll().locked = true; + unsafe.$async.clearAll().locked = true; + unsafe.$off(); } From 2e53e1cfec7f23cd62dc107526e1f43450c1cc7e Mon Sep 17 00:00:00 2001 From: kobezzza Date: Mon, 27 Jun 2022 18:22:25 +0300 Subject: [PATCH 0302/2313] feat: update $renderCounter --- src/core/component/init/states/render-trigered.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/component/init/states/render-trigered.ts b/src/core/component/init/states/render-trigered.ts index d82b684f1c..3244d58a0b 100644 --- a/src/core/component/init/states/render-trigered.ts +++ b/src/core/component/init/states/render-trigered.ts @@ -19,6 +19,9 @@ import type { ComponentInterface } from 'core/component/interface'; */ export function renderTriggeredState(component: ComponentInterface, ...args: unknown[]): void { runHook('renderTriggered', component, ...args).then(() => { + const unsafe = Object.cast>(component); + unsafe.$renderCounter++; + callMethodFromComponent(component, 'renderTriggered', ...args); }).catch(stderr); } From 363fd92451abb5cf39191f4e2680f1090af2490d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 28 Jun 2022 12:01:10 +0300 Subject: [PATCH 0303/2313] doc: better doc --- src/core/component/interface/component/component.ts | 4 ++-- src/core/component/interface/lc.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/component/interface/component/component.ts b/src/core/component/interface/component/component.ts index e1d8254205..ae963e03f1 100644 --- a/src/core/component/interface/component/component.ts +++ b/src/core/component/interface/component/component.ts @@ -229,8 +229,8 @@ export abstract class ComponentInterface { protected readonly async!: Async; /** - * An API to tie and control async operations - * (this parameter is used by a render engine) + * An API to tie and control async operations. + * This API is used for protected/private consumers, such as directives or component engines. */ protected readonly $async!: Async; diff --git a/src/core/component/interface/lc.ts b/src/core/component/interface/lc.ts index 91ab80d649..c5e04c6327 100644 --- a/src/core/component/interface/lc.ts +++ b/src/core/component/interface/lc.ts @@ -7,7 +7,7 @@ */ /** - * A list of supported life-cycle component hooks + * A list of supported lifecycle component hooks */ export type Hook = 'beforeRuntime' | From d3d3577f3c28b57e4ced639e8c173415bfcb1f95 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 28 Jun 2022 12:37:44 +0300 Subject: [PATCH 0304/2313] refactor: removed `virtualContext` & added `virtualComponent` --- src/core/component/engines/interface.ts | 2 +- src/core/component/functional/helpers.ts | 37 +++++++++--------------- src/core/component/render/wrappers.ts | 29 +++++++++++++++++-- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/core/component/engines/interface.ts b/src/core/component/engines/interface.ts index 472f1ab97f..c91b822974 100644 --- a/src/core/component/engines/interface.ts +++ b/src/core/component/engines/interface.ts @@ -30,7 +30,7 @@ export interface VNode< ExtraProps = {[key: string]: any} > extends SuperVNode { dynamicChildren?: VNode[]; - virtualContext?: ComponentInterface; + virtualComponent?: ComponentInterface; } export interface ResolveDirective { diff --git a/src/core/component/functional/helpers.ts b/src/core/component/functional/helpers.ts index 5dc188d616..8c314c651f 100644 --- a/src/core/component/functional/helpers.ts +++ b/src/core/component/functional/helpers.ts @@ -14,6 +14,13 @@ import type { ComponentInterface } from 'core/component/interface'; const componentInitLabel = Symbol('The component initialization label'); +/** + * Initializes the default component dynamic lifecycle handlers for the passed functional component. + * Also, the function adds the ability for the component to emit lifecycle events, + * such as `mounted` or `destroyed` hooks. + * + * @param component + */ export function initDynamicComponentLifeCycle(component: ComponentInterface): ComponentInterface { const {unsafe} = component; @@ -49,29 +56,7 @@ export function initDynamicComponentLifeCycle(component: ComponentInterface): Co } case 'beforeDestroy': { - const - parent = unsafe.$normalParent; - - const needImmediateDestroy = - parent == null || - parent.hook === 'beforeDestroy' || - parent.hook === 'destroyed' || - parent.$root === parent; - - if (needImmediateDestroy) { - unsafe.$destroy(); - - } else { - const eventHandle = ['on-component-hook:before-destroy', unsafe.$destroy.bind(unsafe)] as const; - parent.unsafe.$once(...eventHandle); - - unsafe.async.worker(() => parent.unsafe.$off(...eventHandle), { - group: ':zombie' - }); - - unsafe.async.clearAll().locked = true; - } - + unsafe.$destroy(); break; } @@ -83,6 +68,12 @@ export function initDynamicComponentLifeCycle(component: ComponentInterface): Co return component; } +/** + * Overrides inheritable parameters to the given functional component context from the parent + * + * @param ctx + * @param parentCtx + */ export function inheritContext( ctx: ComponentInterface['unsafe'], parentCtx: CanUndef diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index cc28a3771e..5c2a411ed9 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -81,6 +81,8 @@ export function wrapCreateBlock(original: T): T { functionalVNode = virtualCtx.render(virtualCtx, []); vnode.type = functionalVNode.type; + vnode.virtualComponent = virtualCtx; + vnode.props = functionalVNode.props; vnode.children = functionalVNode.children; vnode.dynamicChildren = functionalVNode.dynamicChildren; @@ -115,7 +117,18 @@ export function wrapCreateBlock(original: T): T { } } - return interpolateStaticAttrs.call(this, original.apply(null, args)); + const vnode = interpolateStaticAttrs.call( + this, + original.apply(null, args) + ); + + Object.defineProperty(vnode, 'virtualComponent', { + configurable: true, + enumerable: true, + get: () => vnode.component?.['ctx'] + }); + + return vnode; }); } @@ -146,10 +159,22 @@ export function wrapRenderList(original: T): T { } export function wrapWithDirectives(original: T): T { - return Object.cast(function withDirectives(this: ComponentInterface, vnode: VNode, dirs: DirectiveArguments) { + return Object.cast(function withDirectives( + this: CanUndef, + vnode: VNode, + dirs: DirectiveArguments + ) { const resolvedDirs: DirectiveArguments = []; + if (this == null) { + Object.defineProperty(vnode, 'virtualComponent', { + configurable: true, + enumerable: true, + get: () => vnode.el?.component + }); + } + for (let i = 0; i < dirs.length; i++) { const decl = dirs[i], From 442914a733909f1c931dfcc1fff4b242b18043a5 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 28 Jun 2022 15:08:53 +0300 Subject: [PATCH 0305/2313] doc: added the normal documentation --- src/core/component/functional/CHANGELOG.md | 10 + src/core/component/functional/README.md | 252 ++++++++++++++++++++- 2 files changed, 260 insertions(+), 2 deletions(-) diff --git a/src/core/component/functional/CHANGELOG.md b/src/core/component/functional/CHANGELOG.md index eb3948f62c..994e515da3 100644 --- a/src/core/component/functional/CHANGELOG.md +++ b/src/core/component/functional/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :memo: Documentation + +* Added the normal documentation + +#### :house: Internal + +* Migration to Vue3 + ## v3.0.0-rc.177 (2021-04-14) #### :bug: Bug Fix diff --git a/src/core/component/functional/README.md b/src/core/component/functional/README.md index 89598b55b7..d78d02dbc4 100644 --- a/src/core/component/functional/README.md +++ b/src/core/component/functional/README.md @@ -1,4 +1,252 @@ # core/component/functional -This module provides an API to create a functional component. -Mind that V4Fire has own cross-platform implementation of functional components. +This module provides an API to create functional components. +Please note that V4Fire has its own cross-platform implementation of functional components. + +## What differences between regular and functional components? + +The main difference between functional components and regular components is that the template of a functional component +is never automatically updated when the state of this component changes. In fact, such components are rendered once and +can only be re-rendered if any of their non-functional parent components update the state. + +Another difference is that we can optionally disable watching for some component properties or events +if this component is created as a functional one. Typically, this is done by passing the `functional: false` option to +one of the component property or method decorators. + +### Why are functional components called like that? + +Technically, such components are represented as ordinary functions that accept input parameters (component props, etc.), +have their own context (a state), but are executed only once during rendering. + +## Why are functional components needed? + +Such components are faster to initialize and render than regular ones. +If there are many such components on the page, then the performance gain can be quite noticeable. + +## When should functional components be used? + +When a component template does not physically change its markup depending on the state of the component. +However, you can still use component modifiers to affect how the template is shown. +For example, you can hide it or show only a certain part. This works because modifiers are always set as +regular CSS classes to the root component node. + +## When should you not use functional components? + +There is a simple rule: if your component changes its markup depending on the state, for example, +it receives data from the network and displays it, then a functional component is not suitable for you. + +Also, functional components don't play well with long CSS animations. +The problem is that if any of the non-functional parents of such a component changes the state, +then all of its child functional components will be re-created, and animations in progress will be reset. + +## How is a component updated if its non-functional parent is updated? + +In such situations, a simple strategy is used - the component is destroyed and re-created. +When creating a new component, it usually reuses the state of the past. +In simple terms, if you change any property in the old component, then it will be the same in the new one. +But we can decorate this behavior using the special options of decorators. + +## How to create a functional component? + +There are two ways to declare a functional component. + +1. Pass the `functional: true` option to the `@component` decorator when registering the component. + Note that this option is inherited from parent component to child component, unless you explicitly override it. + + ```typescript + import iData, { component } from 'super/i-data/i-data'; + + // `bLink` will always be created as a functional component + @component({functional: true}) + class bLink extends iData { + + } + + // `bCustomLink` will always be created as a functional component + @component() + class bCustomLink extends bLink { + + } + + // `bSmartLink` will always be created as a regular component + @component({functional: false}) + class bSmartLink extends bLink { + + } + ``` + + If you pass the `functional: null` option, then all property and event observers of the component, + as well as its input parameters, will not work if declared inside a functional component. + But you can override this rule for a specific property or observer by explicitly passing `functional: true` in + the associated decorator. Keep in mind that we are talking about those entities that are declared within a particular class. + This option cannot be inherited. + + ```typescript + import iBlock, { component, prop, watch, hook } from 'super/i-block/i-block'; + + @component({functional: null}) + class iData extends iBlock { + // This prop available only if used with a regular component + @prop({type: String, required: false}) + readonly dataProvider?: string; + + // This watcher only works if used with a regular component + @watch({path: 'dataProvider', immediate: true}) + onProviderChange(value, oldValue) { + console.log(value, oldValue); + } + + // This hook works in all cases + @hook({mounted: {functional: true}}) + onMounted() { + console.log('Mounted!'); + } + } + + @component({functional: true}) + class bLink extends iData { + + } + ``` + +2. If you pass the `functional` option as an object, then you can specify under which output parameters a functional + component should be used, and under which a regular one. This kind of components is called "smart". + Note that this option is inherited from parent component to child component, unless you explicitly override it. + + ```typescript + import iData, { component } from 'super/i-data/i-data'; + + // `bLink` will be created as a functional component if its `dataProvider` prop is not passed or is undefined or null + @component({functional: {dataProvider: [undefined, null]}}) + class bLink extends iData { + + } + ``` + + If you pass a dictionary with no values, then such a component will be created as a functional one by default. + But you can still explicitly specify that a component should be non-functional at a particular place with the `v-func` directive. + + ```typescript + import iData, { component } from 'super/i-data/i-data'; + + @component({functional: {}}) + class bLink extends iData { + + } + ``` + + ``` + < bLink v-func = false + ``` + +## How to update a functional component template? + +It is better to avoid such situations, because if you need to update a template, then you need a regular component. +However, you can do this using one of the suggested techniques. + +### Using modifiers + +This technique will not allow you to physically change the template, but only add or remove one or +another CSS class that is associated with the component root node. However, this is an extremely powerful API, +which is enough for 80% of the cases. + +__b-link/b-link.ts__ + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; + +@component({functional: true}) +class bLink extends iBlock { + onClick() { + this.block.setMod('active', true); + } +} +``` + +__b-link/b-link.ss__ + +``` +- namespace [%fileName%] + +- include 'super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + - rootTag = 'a' + - rootAttrs = {'@click': 'onClick'} +``` + +### Using node references and the DOM API + +Using this technique, you can simply add or remove attributes for DOM nodes, as well as change their properties. +But be extremely careful when adding new DOM nodes to the component tree. + +__b-link/b-link.ts__ + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; + +@component({functional: true}) +class bLink extends iBlock { + mounted() { + this.$refs.link.setAttribute('data-foo', 'bar'); + } +} +``` + +__b-link/b-link.ss__ + +``` +- namespace [%fileName%] + +- include 'super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + - block body + < a ref = link +``` + +### Using the `v-update-on` directive + +This directive, together with the modifiers API and the DOM API, gives you more flexibility in updating nodes. +See the directive documentation for more information. + +### Using the `AsyncRender` API + +This is the most powerful technique that allows you to manually cause parts of a component template to be re-rendered. +See the [[AsyncRender]] module documentation for more information. + +__b-link/b-link.ts__ + +```typescript +import iBlock, { component } from 'super/i-block/i-block'; + +@component({functional: true}) +class bLink extends iBlock { + updateTemplate() { + this.asyncRender.forceRender(); + } +} +``` + +__b-link/b-link.ss__ + +``` +- namespace [%fileName%] + +- include 'super/i-block'|b as placeholder + +- template index() extends ['i-block'].index + - block body + < .&__content v-async-target + < template v-for = el in asyncRender.iterate(true, { & + filter: asyncRender.waitForceRender('content') + }) . + < .&__content + += self.slot() +``` + +## Functions + +### createVirtualContext + +Creates a virtual context for the passed functional component. From 366c2be7f630dc7927213780916dfabe7b751fd0 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Tue, 28 Jun 2022 16:49:28 +0300 Subject: [PATCH 0306/2313] feat: moved props normalizing into runtime --- .../component/render/helpers/normalizers.ts | 39 ++++++ src/core/component/render/wrappers.ts | 126 ++++++++++-------- 2 files changed, 106 insertions(+), 59 deletions(-) diff --git a/src/core/component/render/helpers/normalizers.ts b/src/core/component/render/helpers/normalizers.ts index 239c225397..08b4d390f6 100644 --- a/src/core/component/render/helpers/normalizers.ts +++ b/src/core/component/render/helpers/normalizers.ts @@ -6,6 +6,8 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +import type { ComponentMeta } from 'core/component/meta'; + /** * Normalizes the passed class attribute and returns the result * @param classValue @@ -104,3 +106,40 @@ export function parseStringStyle(style: string): Dictionary { return res; } + +/** + * Normalizes the passed attributes using the specified component meta object + * + * @param attrs + * @param component + */ +export function normalizeComponentAttrs(attrs: Nullable, component: ComponentMeta): void { + const + {props, params: {deprecatedProps}} = component; + + if (attrs == null) { + return; + } + + for (let keys = Object.keys(attrs), i = 0; i < keys.length; i++) { + let + key = keys[i], + propKey = `${key}Prop`; + + if (deprecatedProps != null) { + const + alternativeKey = deprecatedProps[key] ?? deprecatedProps[propKey] ?? key; + + attrs[alternativeKey] = attrs[key]; + delete attrs[key]; + + key = alternativeKey; + propKey = `${alternativeKey}Prop`; + } + + if (propKey in props) { + attrs[propKey] = attrs[key]; + delete attrs[key]; + } + } +} diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index 5c2a411ed9..52c152e6e9 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -10,7 +10,7 @@ import * as c from 'core/component/const'; -import { attachTemplatesToMeta } from 'core/component/meta'; +import { attachTemplatesToMeta, ComponentMeta } from 'core/component/meta'; import { createVirtualContext } from 'core/component/functional'; import type { @@ -33,7 +33,7 @@ import type { } from 'core/component/engines'; import { registerComponent } from 'core/component/init'; -import { interpolateStaticAttrs } from 'core/component/render/helpers'; +import { interpolateStaticAttrs, normalizeComponentAttrs } from 'core/component/render/helpers'; import type { ComponentInterface } from 'core/component/interface'; @@ -52,69 +52,77 @@ export function wrapCreateElementVNode(orig export function wrapCreateBlock(original: T): T { return Object.cast(function wrapCreateBlock(this: ComponentInterface, ...args: Parameters) { const - [name] = args, - {supports, r} = this.$renderEngine; + [name, attrs] = args; - const - supportFunctionalComponents = !supports.regular || supports.functional; + let + component: CanUndef; - if (Object.isString(name) && supportFunctionalComponents) { - const - component = registerComponent(name); + if (Object.isString(name)) { + component = registerComponent(name); - if (component?.params.functional === true) { - const - {componentName} = component; + } else if (!Object.isPrimitive(name) && 'name' in name) { + component = registerComponent(name.name); + } - if (c.componentRenderFactories[componentName] == null) { - attachTemplatesToMeta(component, TPLS[componentName]); - } + if (component == null) { + return original.apply(null, args); + } + + normalizeComponentAttrs(attrs, component); - const virtualCtx = createVirtualContext(component, { - parent: this, - props: args[1], - slots: args[2] - }); - - const - vnode: VNode = original.apply(null, args), - functionalVNode = virtualCtx.render(virtualCtx, []); - - vnode.type = functionalVNode.type; - vnode.virtualComponent = virtualCtx; - - vnode.props = functionalVNode.props; - vnode.children = functionalVNode.children; - vnode.dynamicChildren = functionalVNode.dynamicChildren; - - vnode.dirs = functionalVNode.dirs ?? []; - vnode.dirs.push({ - dir: Object.cast(r.resolveDirective.call(virtualCtx, 'hook')), - - modifiers: {}, - arg: undefined, - - value: { - created: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'created', n), - beforeMount: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeMount', n), - mounted: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'mounted', n), - beforeUpdate: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeUpdate', n), - updated: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'updated', n), - beforeUnmount: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeDestroy', n), - unmounted: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'destroyed', n) - }, - - oldValue: undefined, - instance: Object.cast(virtualCtx) - }); - - functionalVNode.props = {}; - functionalVNode.dirs = null; - functionalVNode.children = []; - functionalVNode.dynamicChildren = []; - - return vnode; + const + {componentName, params} = component, + {supports, r} = this.$renderEngine; + + if ((!supports.regular || supports.functional) && params.functional === true) { + if (c.componentRenderFactories[componentName] == null) { + attachTemplatesToMeta(component, TPLS[componentName]); } + + const virtualCtx = createVirtualContext(component, { + parent: this, + props: args[1], + slots: args[2] + }); + + const + vnode: VNode = original.apply(null, args), + functionalVNode = virtualCtx.render(virtualCtx, []); + + vnode.type = functionalVNode.type; + vnode.virtualComponent = virtualCtx; + + vnode.props = functionalVNode.props; + vnode.children = functionalVNode.children; + vnode.dynamicChildren = functionalVNode.dynamicChildren; + + vnode.dirs = functionalVNode.dirs ?? []; + vnode.dirs.push({ + dir: Object.cast(r.resolveDirective.call(virtualCtx, 'hook')), + + modifiers: {}, + arg: undefined, + + value: { + created: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'created', n), + beforeMount: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeMount', n), + mounted: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'mounted', n), + beforeUpdate: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeUpdate', n), + updated: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'updated', n), + beforeUnmount: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'beforeDestroy', n), + unmounted: (n) => virtualCtx.$emit('[[COMPONENT_HOOK]]', 'destroyed', n) + }, + + oldValue: undefined, + instance: Object.cast(virtualCtx) + }); + + functionalVNode.props = {}; + functionalVNode.dirs = null; + functionalVNode.children = []; + functionalVNode.dynamicChildren = []; + + return vnode; } const vnode = interpolateStaticAttrs.call( From c3c808ce20f6fc8d629f5a0c7d4cd24282c73d43 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 29 Jun 2022 15:56:19 +0300 Subject: [PATCH 0307/2313] refactor: removed statical prop casting & refactoring --- build/graph/component-params.js | 8 -- build/graph/const.js | 6 -- build/snakeskin/default-filters.js | 129 ++++++++++++---------------- build/snakeskin/filters/bem.js | 24 +++--- build/snakeskin/filters/const.js | 44 +--------- build/snakeskin/filters/element.js | 40 --------- build/snakeskin/filters/include.js | 4 +- build/snakeskin/filters/index.js | 1 - build/snakeskin/filters/tag-name.js | 2 +- build/snakeskin/filters/tag.js | 13 ++- build/snakeskin/filters/typograf.js | 8 +- build/snakeskin/vars.js | 30 +++---- 12 files changed, 97 insertions(+), 212 deletions(-) delete mode 100644 build/snakeskin/filters/element.js diff --git a/build/graph/component-params.js b/build/graph/component-params.js index 03328092aa..51b733414b 100644 --- a/build/graph/component-params.js +++ b/build/graph/component-params.js @@ -19,7 +19,6 @@ const { componentRgxp, componentClassRgxp, - propRgxp, genericRgxp, extendsRgxp, @@ -106,13 +105,6 @@ $C(componentFiles).forEach((el) => { if (p.inheritMods != null) { obj.inheritMods = p.inheritMods; } - - let s; - - // eslint-disable-next-line no-cond-assign - while (s = propRgxp.exec(file)) { - obj.props[s[2].split(' ').slice(-1)[0]] = true; - } }); /** diff --git a/build/graph/const.js b/build/graph/const.js index bc91d977f2..f373d5b6d0 100644 --- a/build/graph/const.js +++ b/build/graph/const.js @@ -30,12 +30,6 @@ exports.componentRgxp = /@component\(([^@]*?)\)[\s\S]+?class\s+/; */ exports.componentClassRgxp = /^\s*(?:export\s+default\s+)?(?:abstract\s+)?class\s+(([\s\S]*?)\s+extends\s+[\s\S]*?)(?:\s+implements\s+[^{]*|\s*){/m; -/** - * RegExp to extract a name of a component prop - * @type {!RegExp} - */ -exports.propRgxp = /^(\t+)@prop[^(]*\([^@]+?\)+\n+\1([ \w$]+)(?:[?!]?:\s*[ \w|&$?()[\]{}<>'"`:.]+?)?\s*(?:=|;$)/gm; - /** * RegExp to match the generic syntax * @type {!RegExp} diff --git a/build/snakeskin/default-filters.js b/build/snakeskin/default-filters.js index c4f74d76ca..10534c3dd0 100644 --- a/build/snakeskin/default-filters.js +++ b/build/snakeskin/default-filters.js @@ -24,6 +24,9 @@ const tagNameFilters = include('build/snakeskin/filters/tag-name'), bemFilters = include('build/snakeskin/filters/bem'); +const + SMART_PROPS = Symbol('The component smart props'); + const bind = { bind: [ (o) => o.getVar('$attrs'), @@ -38,11 +41,14 @@ Snakeskin.importFilters({ }); function tagFilter({name, attrs = {}}) { - $C(tagFilters).forEach((filter) => { - filter({name, attrs}); - }); + Object.forEach(tagFilters, (filter) => filter({name, attrs})); - if (name !== 'component' && !attrs[':instance-of'] && !validators.blockName(name)) { + const isSimpleTag = + name !== 'component' && + !attrs[':instance-of'] && + !validators.blockName(name); + + if (isSimpleTag) { return; } @@ -59,11 +65,10 @@ function tagFilter({name, attrs = {}}) { const component = componentParams[componentName], - props = component ? component.props : Object.create(null); + smartProps = attrs[SMART_PROPS]; - const - smartProps = [attrs['v-func-placeholder'], delete attrs['v-func-placeholder']][0] && component && component.functional, - funcMode = [attrs['v-func'], delete attrs['v-func']][0]; + const funcDir = attrs['v-func']?.[0]; + delete attrs['v-func']; let isFunctional = false; @@ -71,16 +76,16 @@ function tagFilter({name, attrs = {}}) { if (component && component.functional === true) { isFunctional = true; - } else if (!funcMode) { - isFunctional = $C(smartProps).every((el, key) => { - key = key.dasherize(true); + } else if (!funcDir) { + isFunctional = $C(smartProps).every((propVal, prop) => { + prop = prop.dasherize(true); - if (!isV4Prop.test(key)) { - key = `:${key}`; + if (!isV4Prop.test(prop)) { + prop = `:${prop}`; } let - attr = attrs[key] && attrs[key][0]; + attr = attrs[prop]?.[0]; try { // eslint-disable-next-line no-new-func @@ -88,89 +93,60 @@ function tagFilter({name, attrs = {}}) { } catch {} - if (Object.isArray(el)) { - if (!Object.isArray(el[0])) { - return $C(el).includes(attr); + if (Object.isArray(propVal)) { + if (!Object.isArray(propVal[0])) { + return $C(propVal).includes(attr); } - return Object.fastCompare(el[0], attr); + return Object.fastCompare(propVal[0], attr); } - if (Object.isRegExp(el)) { - return el.test(attr); + if (Object.isRegExp(propVal)) { + return propVal.test(attr); } - if (Object.isFunction(el)) { - return el(attr); + if (Object.isFunction(propVal)) { + return propVal(attr); } - return Object.fastCompare(el, attr); + return Object.fastCompare(propVal, attr); }); } - if ((isFunctional || funcMode) && smartProps) { - if (funcMode) { - attrs[':is'] = [`'${attrs['is'][0]}' + (${funcMode[0]} ? '-functional' : '')`]; - delete attrs['is']; + const + isSmartFunctional = smartProps && (isFunctional || funcDir); - } else { + if (isSmartFunctional) { + if (funcDir == null || funcDir === 'true') { attrs['is'] = [`${attrs['is'][0]}-functional`]; - } - } - - if (component) { - $C(attrs).forEach((el, key) => { - if (key[0] !== ':') { - return; - } - - const - basePropName = key.slice(1).camelize(false), - directPropName = `${basePropName}Prop`; - - let - resolvedPropName = basePropName, - alternative = component.deprecatedProps[resolvedPropName]; - - if (!props[basePropName] && props[directPropName]) { - resolvedPropName = directPropName; - alternative = component.deprecatedProps[resolvedPropName]; - if (!alternative) { - attrs[`:${directPropName.dasherize(true)}`] = el; - } - - delete attrs[key]; - } - - if (alternative) { - attrs[`:${alternative.dasherize(true)}`] = el; - delete attrs[key]; - } - }); - - if (component.inheritMods !== false && !attrs[':mods-prop']) { - attrs[':mods-prop'] = ['provide.mods()']; + } else if (funcDir !== 'false') { + attrs[':is'] = [`'${attrs['is'][0]}' + (${funcDir} ? '-functional' : '')`]; + delete attrs['is']; } } } -// eslint-disable-next-line default-param-last -function tagNameFilter(tag, attrs = {}, rootTag) { +function tagNameFilter(tag, attrs, rootTag) { + attrs ??= {}; + tag = $C(tagNameFilters) .to(tag) .reduce((tag, filter) => filter(tag, attrs, rootTag)); const - nm = tag.camelize(false), - component = componentParams[nm]; - - if (component != null && !Object.isBoolean(component.functional)) { - Object.assign(attrs, { - ':instance-of': [nm], - 'v-func-placeholder': [true], - is: [tag] - }); + componentName = tag.camelize(false), + component = componentParams[componentName]; + + const isSmartComponent = + component != null && + !Object.isBoolean(component.functional); + + if (isSmartComponent) { + attrs.is = [tag]; + + attrs[':instance-of'] = [componentName]; + attrs[SMART_PROPS] = component.functional; return 'component'; } @@ -178,8 +154,9 @@ function tagNameFilter(tag, attrs = {}, rootTag) { return tag; } -// eslint-disable-next-line default-param-last -function bemFilter(block, attrs = {}, rootTag, value) { +function bemFilter(block, attrs, rootTag, value) { + attrs ??= {}; + return $C(bemFilters) .to('') .reduce((res, filter) => res + filter(block, attrs, rootTag, value)); diff --git a/build/snakeskin/filters/bem.js b/build/snakeskin/filters/bem.js index bdd6a99ffd..e897cf815f 100644 --- a/build/snakeskin/filters/bem.js +++ b/build/snakeskin/filters/bem.js @@ -11,22 +11,24 @@ const {wrapAttrArray} = include('build/snakeskin/filters/helpers'); -const - elSeparatorRgxp = /^_+/; - module.exports = [ /** - * Integrates BEM classes to a component: attaches identifiers, provides runtime transformers, etc. + * Integrates BEM classes to components: attaches identifiers, provides runtime transformers, etc. * - * @param {string} block - * @param {!Object} attrs - * @param {string} rootTag - * @param {string} value + * @param {string} block - a name of the active BEM block + * @param {!Object} attrs - a dictionary with attributes of the node to which the filter is applied + * @param {string} rootTag - a type of the component root tag within which the directive is applied + * @param {string} element - a name of the BEM element to create, with a prefix * @returns {string} */ - function bem2Component(block, attrs, rootTag, value) { + function bem2Component(block, attrs, rootTag, element) { attrs['data-cached-class-component-id'] = wrapAttrArray([true]); - attrs['data-cached-class-provided-classes-styles'] = wrapAttrArray([value.replace(elSeparatorRgxp, '')]); - return block + value; + + attrs['data-cached-class-provided-classes-styles'] = wrapAttrArray( + [element.replace(/^_+/, '')] + ); + + return block + element; + } ]; diff --git a/build/snakeskin/filters/const.js b/build/snakeskin/filters/const.js index 2c7f9bbb32..e5d8e217b5 100644 --- a/build/snakeskin/filters/const.js +++ b/build/snakeskin/filters/const.js @@ -8,62 +8,26 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -const - {validators} = require('@pzlr/build-core'); - -/** - * RegExp to match a tag declaration body - * - * @type {!RegExp} - * @example - * ``` - *
- * ``` - */ -exports.tagRgxp = /<[^>]+>/; - -/** - * RegExp to match a component element class - * - * @type {!RegExp} - * @example - * ``` - * b-foo__bla-bar - * ``` - */ -exports.componentElRgxp = new RegExp(`\\b${validators.baseBlockName}__[a-z0-9][a-z0-9-_]*\\b`); - -/** - * RegExp to match requiring of svg images - * - * @type {!RegExp} - * @example - * ``` - * require('foo.svg') - * ``` - */ -exports.isSvgRequire = /require\(.*?\.svg[\\"']+\)/; - /** - * RegExp to detect V4Fire specific attributes + * The RegExp to detect V4Fire specific attributes * @type {!RegExp} */ exports.isV4Prop = /^(:|@|v-)/; /** - * RegExp to detect V4Fire specific static attributes + * The RegExp to detect V4Fire specific static attributes * @type {!RegExp} */ exports.isStaticV4Prop = /^[^[]+$/; /** - * RegExp to detect commas + * The RegExp to detect commas * @type {!RegExp} */ exports.commaRgxp = /\s*,\s*/; /** - * RegExp to detect Snakeskin file extensions + * The RegExp to detect Snakeskin file extensions * @type {!RegExp} */ exports.ssExtRgxp = /\.e?ss$/; diff --git a/build/snakeskin/filters/element.js b/build/snakeskin/filters/element.js deleted file mode 100644 index a557559f56..0000000000 --- a/build/snakeskin/filters/element.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -/*! - * V4Fire Client Core - * https://github.com/V4Fire/Client - * - * Released under the MIT license - * https://github.com/V4Fire/Client/blob/master/LICENSE - */ - -const - Snakeskin = require('snakeskin'), - Escaper = require('escaper'); - -const - {tagRgxp, componentElRgxp} = include('build/snakeskin/filters/const'); - -Snakeskin.importFilters({ - /** - * Returns the first name of an element - * - * @param {string} decl - * @returns {?string} - */ - getFirstTagElementName(decl) { - const - escapedFragments = [], - escapedStr = Escaper.replace(decl, escapedFragments); - - const - tagMatch = tagRgxp.exec(escapedStr); - - if (!tagMatch) { - return null; - } - - const search = componentElRgxp.exec(Escaper.paste(tagMatch[0], escapedFragments)); - return search ? search[0] : null; - } -}); diff --git a/build/snakeskin/filters/include.js b/build/snakeskin/filters/include.js index dd1be239b9..bff0c6d289 100644 --- a/build/snakeskin/filters/include.js +++ b/build/snakeskin/filters/include.js @@ -34,7 +34,7 @@ Snakeskin.importFilters({ * The filter adds the support of layers. * * @param {string} filePath - * @param {string} sourceFilePath - original file source path + * @param {string} sourceFilePath - the original source file path * @returns {(string|!Array)} * * @example @@ -48,7 +48,7 @@ Snakeskin.importFilters({ let start = 0; - if (superRgxp.removeFlags('g').test(filePath)) { + if (RegExp.test(superRgxp, filePath)) { filePath = filePath.replace(superRgxp, ''); for (let i = 0; i < resources.length; i++) { diff --git a/build/snakeskin/filters/index.js b/build/snakeskin/filters/index.js index e579da00a2..be7f2c3886 100644 --- a/build/snakeskin/filters/index.js +++ b/build/snakeskin/filters/index.js @@ -10,4 +10,3 @@ include('build/snakeskin/filters/include'); include('build/snakeskin/filters/typograf'); -include('build/snakeskin/filters/element'); diff --git a/build/snakeskin/filters/tag-name.js b/build/snakeskin/filters/tag-name.js index 7939e8e4a4..b6a92ead10 100644 --- a/build/snakeskin/filters/tag-name.js +++ b/build/snakeskin/filters/tag-name.js @@ -16,7 +16,7 @@ const module.exports = [ /** - * Expands the `_` snippet as a `<${rootTag}>` tag + * Expands the `_` snippet as a `
` tag * * @param {string} tag * @param {!Object} attrs diff --git a/build/snakeskin/filters/tag.js b/build/snakeskin/filters/tag.js index 1719eaab98..02c6bd7768 100644 --- a/build/snakeskin/filters/tag.js +++ b/build/snakeskin/filters/tag.js @@ -9,14 +9,11 @@ */ const - $C = require('collection.js'); - -const - {isSvgRequire, isV4Prop, isStaticV4Prop} = include('build/snakeskin/filters/const'); + {isV4Prop, isStaticV4Prop} = include('build/snakeskin/filters/const'); module.exports = [ /** - * Normalizes webpack SVG require functions + * Normalizes Webpack SVG `require` attributes * * @param {string} name * @param {!Object} attrs @@ -29,17 +26,17 @@ module.exports = [ const src = attrs[':src']; - if (src && isSvgRequire.test(src[0])) { + if (src && /require\(.*?\.svg[\\"']+\)/.test(src[0])) { src[0] += '.replace(/^"|"$/g, \'\')'; } }, /** - * Normalizes component attributes: adds memoization, expands aliases, etc. + * Normalizes component attributes * @param {!Object} attrs */ function normalizeComponentAttrs({attrs}) { - $C(attrs).forEach((el, key) => { + Object.forEach(attrs, (el, key) => { if (!isV4Prop.test(key)) { return; } diff --git a/build/snakeskin/filters/typograf.js b/build/snakeskin/filters/typograf.js index 828ba31f50..9fd9b70971 100644 --- a/build/snakeskin/filters/typograf.js +++ b/build/snakeskin/filters/typograf.js @@ -9,18 +9,18 @@ */ const - Snakeskin = require('snakeskin'), - Typograf = require('typograf'); + config = require('@config/config'); const - config = require('@config/config'); + Snakeskin = require('snakeskin'), + Typograf = require('typograf'); const tp = new Typograf(config.typograf()); Snakeskin.importFilters({ /** - * Applies Typograf to the specified string and returns it + * Applies Typograf to the specified string and returns the result * * @param {string} str * @returns {string} diff --git a/build/snakeskin/vars.js b/build/snakeskin/vars.js index 4088c1e3b9..b912f76520 100644 --- a/build/snakeskin/vars.js +++ b/build/snakeskin/vars.js @@ -8,18 +8,18 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ -const - $C = require('collection.js'), - Snakeskin = require('snakeskin'), - path = require('upath'); +const path = require('upath'); + +/** @type {!Object} */ +const {Vars} = require('snakeskin'); module.exports = { /** - * Saves a basename of the specified directory by the passed name in the global namespace. + * Saves a basename of the specified dirname to the global namespace by the passed aliases. * The function should be used via the `eval` directive. * - * @param dirName - * @param names + * @param dirname + * @param aliases * * @example * ``` @@ -33,19 +33,19 @@ module.exports = { * Hello world! * ``` */ - saveTplDir(dirName, ...names) { + saveTplDir(dirname, ...aliases) { const - dir = path.basename(dirName); + dir = path.basename(dirname); - if (!Snakeskin.Vars['globalTplDirs']) { - Snakeskin.Vars['globalTplDirs'] = {}; + if (!Vars['globalTplDirs']) { + Vars['globalTplDirs'] = {}; } - $C(names).forEach((name) => { - if (!Snakeskin.Vars['globalTplDirs'][name]) { - Snakeskin.Vars['globalTplDirs'][name] = dir; + Object.forEach(aliases, (name) => { + if (!Vars['globalTplDirs'][name]) { + Vars['globalTplDirs'][name] = dir; - } else if (Snakeskin.Vars['globalTplDirs'][name] !== dir) { + } else if (Vars['globalTplDirs'][name] !== dir) { throw new Error(`The name "${name}" is already exist in the global namespace`); } }); From 227b2abe489ba95b869acce97032d9a257d458f6 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 29 Jun 2022 15:59:49 +0300 Subject: [PATCH 0308/2313] refactor: removed dead code --- build/graph/component-params.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/graph/component-params.js b/build/graph/component-params.js index 51b733414b..5c0ebdd751 100644 --- a/build/graph/component-params.js +++ b/build/graph/component-params.js @@ -95,7 +95,6 @@ $C(componentFiles).forEach((el) => { const obj = componentParams[component]; - obj.model = p.model; obj.deprecatedProps = p.deprecatedProps ?? {}; if (p.functional != null) { @@ -134,7 +133,6 @@ function getParentParameters(component) { } const fields = [ - ['model'], ['deprecatedProps'], ['functional', false] ]; From 21a4709439615fa20a4423a38e82b7ff559cc6da Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 29 Jun 2022 16:41:33 +0300 Subject: [PATCH 0309/2313] refactor: removed legacy & updated docs --- build/snakeskin/default-filters.js | 14 +----- .../component/decorators/component/README.md | 45 ++++++++++--------- src/core/component/functional/README.md | 4 +- src/core/component/meta/interface/options.ts | 45 ++++++++++--------- 4 files changed, 52 insertions(+), 56 deletions(-) diff --git a/build/snakeskin/default-filters.js b/build/snakeskin/default-filters.js index 10534c3dd0..b34309e313 100644 --- a/build/snakeskin/default-filters.js +++ b/build/snakeskin/default-filters.js @@ -94,19 +94,7 @@ function tagFilter({name, attrs = {}}) { } catch {} if (Object.isArray(propVal)) { - if (!Object.isArray(propVal[0])) { - return $C(propVal).includes(attr); - } - - return Object.fastCompare(propVal[0], attr); - } - - if (Object.isRegExp(propVal)) { - return propVal.test(attr); - } - - if (Object.isFunction(propVal)) { - return propVal(attr); + return $C(propVal).some((propVal) => Object.fastCompare(propVal, attr)); } return Object.fastCompare(propVal, attr); diff --git a/src/core/component/decorators/component/README.md b/src/core/component/decorators/component/README.md index 3c199268a6..4262969cf1 100644 --- a/src/core/component/decorators/component/README.md +++ b/src/core/component/decorators/component/README.md @@ -87,31 +87,34 @@ This parameter is useful for components without templates, and it can be inherit ### [functional = `false`] -The component functional mode: +The component functional mode. +This parameter can be inherited from the parent component, but the `null` value isn’t inherited. 1. If true, the component will be created as a functional component. 2. If a dictionary, the component can be created as a functional component or regular component, depending on - values of the input properties: - - 1. If an empty dictionary, the component will always created as functional. - 2. If a dictionary with values, the dictionary properties represent component input properties. - If the component invocation takes these properties with the values that - declared within "functional" parameters, it will be created as functional. - Also, you can specify multiple values of one input property by using a list of values. - Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, - but you can directly cast a type by using the "v-func" directive. - -3. If null, all components watchers and listeners that directly specified in the component class won't - be attached to a functional-kind component. It is useful to create the superclass behaviour depending - on a component type. - -A functional component is a component can be rendered once only from input properties. -This type of components have a state and lifecycle hooks, but mutation of the state doesn't force re-rendering. -Usually, functional components lighter than regular components with the first render, -but avoid their if you have long animations within a component or if you need to frequent re-draws some deep -structure of nested components. + values of its props: + 1. If passed an empty dictionary is passed, the component will always be created as a functional one. + But you still have the option to create it like regular using the `v-func` directive. + + 2. If passed a dictionary with values, the dictionary properties represent component props. + If the component invocation takes these props with the values that declared within the `functional` + dictionary, it will be created as a functional one. The values themselves can be represented as any value that + can be encoded in JSON. + + In addition, you can specify multiple values for the same prop using a list of values + Keep in mind that component type inference is a compile-time operation, meaning you cannot depend on values + from the runtime. If you need this feature, then use the `v-for` directive. -This parameter can be inherited from the parent component, but the `null` value isn't inherited. +3. If null, all components watchers and listeners that directly specified in the component class + won't be attached in the case of a functional component kind. It is useful to create superclass behavior + depending on a component type. + +A functional component is a component that can only be rendered once from input properties. +Components of this type have state and lifecycle hooks, but changing the state does not cause re-render. + +Usually functional components are lighter than regular components on first render, +but avoid them if you have long animations inside a component or if you need to frequently redraw some deep ones +structure of nested components. ```typescript import iData, { component } from 'super/i-data/i-data'; diff --git a/src/core/component/functional/README.md b/src/core/component/functional/README.md index d78d02dbc4..a579664dde 100644 --- a/src/core/component/functional/README.md +++ b/src/core/component/functional/README.md @@ -116,8 +116,8 @@ There are two ways to declare a functional component. ```typescript import iData, { component } from 'super/i-data/i-data'; - // `bLink` will be created as a functional component if its `dataProvider` prop is not passed or is undefined or null - @component({functional: {dataProvider: [undefined, null]}}) + // `bLink` will be created as a functional component if its `dataProvider` prop is not passed or undefined + @component({functional: {dataProvider: undefined}}) class bLink extends iData { } diff --git a/src/core/component/meta/interface/options.ts b/src/core/component/meta/interface/options.ts index 9e76b4eec3..a8a32abe60 100644 --- a/src/core/component/meta/interface/options.ts +++ b/src/core/component/meta/interface/options.ts @@ -60,29 +60,34 @@ export interface ComponentOptions { tpl?: boolean; /** - * The component functional mode: + * The component functional mode. + * This parameter can be inherited from the parent component, but the `null` value isn’t inherited. * * 1. If true, the component will be created as a functional component. * 2. If a dictionary, the component can be created as a functional component or regular component, depending on - * values of the input properties: - * 1. If an empty dictionary, the component will always created as functional. - * 2. If a dictionary with values, the dictionary properties represent component input properties. - * If the component invocation takes these properties with the values that - * declared within "functional" parameters, it will be created as functional. - * Also, you can specify multiple values of one input property by using a list of values. - * Mind that inferring of a component type is compile-based, i.e. you can't depend on values from the runtime, - * but you can directly cast a type by using the "v-func" directive. - * 3. If null, all components watchers and listeners that directly specified in the component class won't - * be attached to a functional-kind component. It is useful to create the superclass behaviour depending - * on a component type. - * - * A functional component is a component can be rendered once only from input properties. - * This type of components have a state and lifecycle hooks, but mutation of the state doesn't force re-rendering. - * Usually, functional components lighter than regular components with the first render, - * but avoid their if you have long animations within a component or if you need to frequent re-draws some deep - * structure of nested components. + * values of its props: + * 1. If passed an empty dictionary is passed, the component will always be created as a functional one. + * But you still have the option to create it like regular using the `v-func` directive. + * + * 2. If passed a dictionary with values, the dictionary properties represent component props. + * If the component invocation takes these props with the values that declared within the `functional` + * dictionary, it will be created as a functional one. The values themselves can be represented as any value that + * can be encoded in JSON. + * + * In addition, you can specify multiple values for the same prop using a list of values + * Keep in mind that component type inference is a compile-time operation, meaning you cannot depend on values + * from the runtime. If you need this feature, then use the `v-for` directive. * - * This parameter can be inherited from the parent component, but the `null` value isn't inherited. + * 3. If null, all components watchers and listeners that directly specified in the component class + * won't be attached in the case of a functional component kind. It is useful to create superclass behavior + * depending on a component type. + * + * A functional component is a component that can only be rendered once from input properties. + * Components of this type have state and lifecycle hooks, but changing the state does not cause re-render. + * + * Usually functional components are lighter than regular components on first render, + * but avoid them if you have long animations inside a component or if you need to frequently redraw some deep ones + * structure of nested components. * * @default `false` * @@ -113,7 +118,7 @@ export interface ComponentOptions { * < b-button-functional * ``` */ - functional?: Nullable | Dictionary; + functional?: Nullable | Dictionary>; /** * If false, then all default values of the component input properties are ignored. From 2f6c17cdeb1e1be67f97eb645a80523108e0a33c Mon Sep 17 00:00:00 2001 From: kobezzza Date: Wed, 29 Jun 2022 16:53:22 +0300 Subject: [PATCH 0310/2313] doc: added typedoc --- src/core/component/render/wrappers.ts | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/core/component/render/wrappers.ts b/src/core/component/render/wrappers.ts index 52c152e6e9..0ba6d95964 100644 --- a/src/core/component/render/wrappers.ts +++ b/src/core/component/render/wrappers.ts @@ -37,18 +37,30 @@ import { interpolateStaticAttrs, normalizeComponentAttrs } from 'core/component/ import type { ComponentInterface } from 'core/component/interface'; +/** + * Wrapper for the component library `createVNode` function + * @param original + */ export function wrapCreateVNode(original: T): T { return Object.cast(function createVNode(this: ComponentInterface, ...args: Parameters) { return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } +/** + * Wrapper for the component library `createElementVNode` function + * @param original + */ export function wrapCreateElementVNode(original: T): T { return Object.cast(function createElementVNode(this: ComponentInterface, ...args: Parameters) { return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } +/** + * Wrapper for the component library `createBlock` function + * @param original + */ export function wrapCreateBlock(original: T): T { return Object.cast(function wrapCreateBlock(this: ComponentInterface, ...args: Parameters) { const @@ -140,12 +152,20 @@ export function wrapCreateBlock(original: T): T { }); } +/** + * Wrapper for the component library `createElementBlock` function + * @param original + */ export function wrapCreateElementBlock(original: T): T { return Object.cast(function createElementBlock(this: ComponentInterface, ...args: Parameters) { return interpolateStaticAttrs.call(this, original.apply(null, args)); }); } +/** + * Wrapper for the component library `resolveComponent` or `resolveDynamicComponent` functions + * @param original + */ export function wrapResolveComponent( original: T ): T { @@ -155,6 +175,10 @@ export function wrapResolveComponent(original: T): T { return Object.cast(function renderList( this: ComponentInterface, @@ -166,6 +190,10 @@ export function wrapRenderList(original: T): T { }); } +/** + * Wrapper for the component library `withDirectives` function + * @param original + */ export function wrapWithDirectives(original: T): T { return Object.cast(function withDirectives( this: CanUndef, From ba0041f3c2fde9ba37be4f230bdf13cb48d93c4a Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 30 Jun 2022 13:44:28 +0300 Subject: [PATCH 0311/2313] doc: added normal documentation --- src/core/component/README.md | 101 +++++++++++++++++++++++++++++++++++ src/core/component/index.ts | 5 ++ 2 files changed, 106 insertions(+) create mode 100644 src/core/component/README.md diff --git a/src/core/component/README.md b/src/core/component/README.md new file mode 100644 index 0000000000..d991054b33 --- /dev/null +++ b/src/core/component/README.md @@ -0,0 +1,101 @@ +# core/component + +This module is the entry point for the V4Fire DSL system to define the UI components. + +## Some submodules + +This module contains many other submodules, which are mainly used for internal purposes. +But there are several submodules that you should familiarize yourself with. + +* `core/component/decorators` - this module provides the necessary set of decorators to define components; +* `core/component/directives` - this module provides a bunch of useful built-in directives to use with components or tags; +* `core/component/state` - this module provides an object that can be shared between any component and used as a global state; +* `core/component/event` - this module provides a global event emitter that any component in the application can listen; +* `core/component/functional` - this module contains the implementation of the V4Fire functional components; +* `core/component/engines` - this module contains adaptors for component libraries and the V4Fire DSL. + +## How it works? + +Any user interface components in V4Fire are described as regular TypeScript classes: class methods are component methods; +properties are either input parameters to components, or properties of their states, and so on. We can use any +OOP techniques and patterns to describe a component: composition, strategy, visitor and other patterns. + +But how to declare that some class properties are component props? And how is a component based on a class generally created? +To do this, V4Fire provides a set of decorators with which class properties and the classes themselves must be annotated. + +```typescript +import iBlock, { component, prop } from 'super/i-block/i-block'; + +@component() +export default class bUser extends iBlock { + @prop(String) + readonly fName: string; + + @prop(String) + readonly lName: string; +} +``` + +For detailed information on all existing decorators, you can refer to the `core/component/decorators` module. + +## Why do we need DSL for UI components? + +Short answer: we like the OOP approach to describe components. And, of course, we wanted to be able to easily apply +any OO patterns we might need. But the problem is that many popular component libraries offer either a procedural or +functional API and don't allow us to write code the way we would like to. + +Therefore, instead of creating our own component library, we simply created a simple DSL based on classes and decorators. +Another advantage of this approach is the ability to more easily migrate between the used component libraries, +or even share them within micro-frontends. + +## What library is used to create components? + +As mentioned earlier, we are not tied to using any particular library. Simply add an adapter for your favorite library +to the `core/component/engines` module, and it will work. But, by default, V4Fire works with the Vue library, +and moreover, we reuse the approaches of this library. + +### Implicit state management + +No `setState` calls - just set a value of any component property and its template will update automatically. + +### Unidirectional flow of changes + +Changes in a child component cannot cause the parent to rerender. All component props are immutable. + +### Vue declarative templates to describe component markup + +In our opinion, the idea of mixing imperative approach and templates, as is done in JSX, is fundamentally wrong. +Let me give you a few examples to support my point of view: + +1. If the markup of a component is created as plain HTML with special attributes, then even a non-programmer can write such code. + +2. HTML/XML templates seamlessly integrate with many other utilities: templating or preprocessors, validators, etc. + For example, we can use Pug to reuse markup code, or PostHTML to add necessary "boilerplate" attributes to html tags + or integrate linters. + +3. A huge advantage of templates is the support for custom directives. + In simple terms, we can define our own attributes for tags with additional logic. + These directives can include modules that either implement end-to-end functionality, such as logging or analytics. + + ```html + + ``` + + Or, provide the ability to integrate third-party APIs, such as `IntersectionObserver`. + + ```html + + ``` + +4. Slots. While this feature is more of a Vue feature than a template feature, + it allows for incredible flexibility in component architecture design. + + ```html + + + + ``` + +5. Declarative templates can be better optimized by the compiler because the source code is much simpler than the imperative one. diff --git a/src/core/component/index.ts b/src/core/component/index.ts index ec519e724a..f0e3f39345 100644 --- a/src/core/component/index.ts +++ b/src/core/component/index.ts @@ -6,6 +6,11 @@ * https://github.com/V4Fire/Client/blob/master/LICENSE */ +/** + * [[include:core/component/README.md]] + * @packageDocumentation + */ + import 'core/component/directives'; import * as init from 'core/component/init'; From ee6d312ed024fbb12b01854374292dabb0dd0a4d Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 30 Jun 2022 14:15:14 +0300 Subject: [PATCH 0312/2313] doc: improve doc --- src/core/component/render/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/core/component/render/README.md b/src/core/component/render/README.md index b89fb16a74..e0ca758534 100644 --- a/src/core/component/render/README.md +++ b/src/core/component/render/README.md @@ -1,3 +1,15 @@ # core/component/render -This module provides a bunch of functions to render vnode-s. +This module provides a bunch of functions to create and work with VNode-s. + +## Wrappers + +These wrappers should be used to wrap the original component library functions. + +* `wrapCreateVNode` - a wrapper for the component library `createVNode` function; +* `wrapCreateElementVNode` - a wrapper for the component library `createElementVNode` function; +* `wrapCreateBlock` - a wrapper for the component library `createBlock` function; +* `wrapCreateElementBlock` - a wrapper for the component library `createElementBlock` function; +* `wrapResolveComponent` - a wrapper for the component library `resolveComponent` or `resolveDynamicComponent` functions; +* `wrapRenderList` - a wrapper for the component library `renderList` function; +* `wrapWithDirectives` - a wrapper for the component library `withDirectives` function. From 41be580aceca130f9f73476822afec6e717331a9 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 30 Jun 2022 14:37:19 +0300 Subject: [PATCH 0313/2313] feat: added options for `asyncRender` --- src/config/index.ts | 9 ++++++++- src/config/interface.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/config/index.ts b/src/config/index.ts index c4a327aa76..9548971952 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -13,10 +13,17 @@ import { extend } from '@v4fire/core/config'; -export * from '@v4fire/core/config'; export { default } from '@v4fire/core/config'; +export * from '@v4fire/core/config'; +export * from 'config/interface'; + extend({ + asyncRender: { + weightPerTick: 5, + delay: 40 + }, + components: COMPONENTS, componentStaticDependencies: {} }); diff --git a/src/config/interface.ts b/src/config/interface.ts index 5d24335f1c..8473a2aedb 100644 --- a/src/config/interface.ts +++ b/src/config/interface.ts @@ -9,6 +9,22 @@ import type { Config as SuperConfig } from '@v4fire/core/config/interface'; export interface Config extends SuperConfig { + /** + * Options of the asynchronous component renderer. + * See "core/components/render/daemon" for the more information. + */ + asyncRender: { + /** + * The maximum weight of tasks per one render iteration + */ + weightPerTick: number; + + /** + * The delay in milliseconds between render iterations + */ + delay: number; + }; + components: typeof COMPONENTS; componentStaticDependencies: Dictionary Promise>>; } From f1b71b32d81e8e3039c98b436902db8886c27313 Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 30 Jun 2022 14:38:52 +0300 Subject: [PATCH 0314/2313] refactor: added support for custom configuration & improved doc --- src/core/component/render/daemon/CHANGELOG.md | 8 +++ src/core/component/render/daemon/README.md | 51 +++++++++++++++++++ src/core/component/render/daemon/const.ts | 18 ++----- src/core/component/render/daemon/index.ts | 38 +++++++------- 4 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/core/component/render/daemon/CHANGELOG.md b/src/core/component/render/daemon/CHANGELOG.md index 7077a6f51c..05c0e638c7 100644 --- a/src/core/component/render/daemon/CHANGELOG.md +++ b/src/core/component/render/daemon/CHANGELOG.md @@ -15,6 +15,14 @@ Changelog * Renamed the module to `component/render/daemon` +#### :rocket: New Feature + +* Added support for custom configuration + +#### :memo: Documentation + +* Added the normal documentation + ## v3.0.0-rc.131 (2021-01-29) #### :house: Internal diff --git a/src/core/component/render/daemon/README.md b/src/core/component/render/daemon/README.md index 6e6fafcd2e..369b113f8a 100644 --- a/src/core/component/render/daemon/README.md +++ b/src/core/component/render/daemon/README.md @@ -1,3 +1,54 @@ # core/component/render/daemon This module provides an API to register and manage tasks of async rendering. + +## Why is this module needed? + +Using the [[AsyncRender]] API allows you to split the rendering of a component template into multiple chunks and execute them deferred. +This approach allows you to significantly optimize the rendering of components. But here lies another problem - what if there are several +such components using asynchronous rendering on the page? With a small amount, we are unlikely to notice any deterioration. +But if there are many such components, then over time they will clog the browser event-loop and this will lead to page freezes. + +To avoid this issue, all asynchronous rendering tasks should not be performed directly, but through a special scheduler. +This scheduler calculates the "weight" of each task before it is executed. When the total weight of tasks exceeds the specified threshold, +the scheduler suspends the execution of other browser tasks. + +## How is task weight calculated? + +The weight of any task can be set by a consumer. Otherwise, it will be equal to one. + +``` +< .container v-async-target + /// The first ten elements are rendered synchronously. + /// After that, the rest elements will be split into chunks by ten elements and rendered asynchronously. + /// The rendering of async fragments does not force re-rendering of the main component template. + < .&__item v-for = el in asyncRender.iterate(myData, 10, {weight: 0.5}) + {{ el }} +``` + +## How to set the maximum weight of tasks and the delay between their executions? + +The necessary options are placed in the config module of your project. + +__your-app/src/config/index.ts__ + +```js +import { extend } from '@v4fire/client/config'; + +export { default } from '@v4fire/client/config'; +export * from '@v4fire/client/config'; + +extend({ + asyncRender: { + /** + * The maximum weight of tasks per one render iteration + */ + weightPerTick: 5, + + /** + * The delay in milliseconds between render iterations + */ + delay: 40 + } +}); +``` diff --git a/src/core/component/render/daemon/const.ts b/src/core/component/render/daemon/const.ts index 3c8bee5fdc..ac78cfb8f1 100644 --- a/src/core/component/render/daemon/const.ts +++ b/src/core/component/render/daemon/const.ts @@ -10,28 +10,16 @@ import Async from 'core/async'; import type { Task } from 'core/component/render/daemon/interface'; /** - * The maximum number of tasks per one render iteration - */ -export const - TASKS_PER_TICK = 5; - -/** - * Delay in milliseconds between render iterations - */ -export const - DELAY = 40; - -/** - * Render queue + * The rendering queue */ export const queue = new Set(); /** - * Render daemon + * The rendering daemon */ export const daemon = new Async(); /** - * Adds a task to the queue + * Adds a task to the rendering queue */ export const add = queue.add.bind(queue); diff --git a/src/core/component/render/daemon/index.ts b/src/core/component/render/daemon/index.ts index d974202c41..c4560a564e 100644 --- a/src/core/component/render/daemon/index.ts +++ b/src/core/component/render/daemon/index.ts @@ -11,23 +11,17 @@ * @packageDocumentation */ -import { - - daemon, - - queue, - add as addToQueue, - - TASKS_PER_TICK, - DELAY - -} from 'core/component/render/daemon/const'; +import config from 'config'; +import { daemon, queue, add as addToQueue } from 'core/component/render/daemon/const'; import type { Task } from 'core/component/render/daemon/interface'; export * from 'core/component/render/daemon/const'; export * from 'core/component/render/daemon/interface'; +const + opts = config.asyncRender; + let inProgress = false, isStarted = false; @@ -43,7 +37,7 @@ queue.add = function add(task: Task): typeof queue { }; /** - * Restarts the render daemon + * Restarts the rendering daemon */ export function restart(): void { isStarted = false; @@ -52,7 +46,7 @@ export function restart(): void { } /** - * Creates a task to restart the render daemon on the next tick + * Creates a task to restart the rendering daemon on the next tick */ export function deferRestart(): void { isStarted = false; @@ -70,22 +64,24 @@ function run(): void { let time = Date.now(), - done = TASKS_PER_TICK; + done = opts.weightPerTick; for (let w = queue.values(), el = w.next(); !el.done; el = w.next()) { const val = el.value; - if (done <= 0 || Date.now() - time > DELAY) { - await daemon.idle({timeout: DELAY}); + if (done <= 0 || Date.now() - time > opts.delay) { + await daemon.idle({timeout: opts.delay}); time = Date.now(); - done = TASKS_PER_TICK; + + // eslint-disable-next-line require-atomic-updates + done = opts.weightPerTick; } const w = val.weight ?? 1; - if (done - w < 0 && done !== TASKS_PER_TICK) { + if (done - w < 0 && done !== opts.weightPerTick) { continue; } @@ -103,7 +99,7 @@ function run(): void { const now = Date.now(); await canRender.then(exec); - if (now - time > DELAY) { + if (now - time > opts.delay) { time = now; done += val.weight ?? 1; } @@ -125,7 +121,7 @@ function run(): void { } }; - if (inProgress || queue.size >= TASKS_PER_TICK) { + if (inProgress || queue.size >= opts.weightPerTick) { exec().catch(stderr); } else { @@ -139,7 +135,7 @@ function canProcessing(): boolean { function runOnNextTick(): boolean { if (canProcessing()) { - daemon.requestIdleCallback(run, {timeout: DELAY}); + daemon.requestIdleCallback(run, {timeout: opts.delay}); return true; } From b83f2949a9aad317ee4048472d7cf4986bac73bf Mon Sep 17 00:00:00 2001 From: kobezzza Date: Thu, 30 Jun 2022 15:07:14 +0300 Subject: [PATCH 0315/2313] doc: added the normal documentation --- build/snakeskin/CHANGELOG.md | 10 +++ build/snakeskin/README.md | 124 +++++++++++++++++++++++++++- build/snakeskin/default-filters.js | 25 ++++-- build/snakeskin/filters/README.md | 3 - build/snakeskin/filters/bem.js | 1 - build/snakeskin/filters/tag-name.js | 10 +-- build/snakeskin/filters/tag.js | 4 +- 7 files changed, 156 insertions(+), 21 deletions(-) delete mode 100644 build/snakeskin/filters/README.md diff --git a/build/snakeskin/CHANGELOG.md b/build/snakeskin/CHANGELOG.md index 3b53ce8618..94be1dc077 100644 --- a/build/snakeskin/CHANGELOG.md +++ b/build/snakeskin/CHANGELOG.md @@ -9,6 +9,16 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v3.??.?? (2022-??-??) + +#### :boom: Breaking Change + +* Removed the `getFirstTagElementName` filter + +#### :memo: Documentation + +* Added the normal documentation + ## v3.15.0 (2021-12-16) #### :bug: Bug Fix diff --git a/build/snakeskin/README.md b/build/snakeskin/README.md index ead7ef62b4..b04b1b3e56 100644 --- a/build/snakeskin/README.md +++ b/build/snakeskin/README.md @@ -1,3 +1,125 @@ # build/snakeskin -This module provides an initializer of Snakeskin to the build config. +This module provides a bunch of built-in filters and variables for Snakeskin templates. +Also, the module sets some default filters for component tags. + +## Filters + +### b + +Resolves the specified file path to use with the Snakeskin include directive. +The filter adds the support of layers. + +``` +- include 'super/i-data'|b as placeholder + +/// The filter also supports Glob patterns +- include '**/*.window.ss'|b + +- template index() extends ['i-data'].index +``` + +### typograf + +Applies [Typograf](https://www.npmjs.com/package/typograf) to the specified string and returns the result. +To customize Typograf options, use the `typograf` method from your application config. + +__your-app/config/default.js__ + +```js +const + config = require('@v4fire/core/config/default'); + +module.exports = config.createConfig({dirs: [__dirname, 'client']}, { + __proto__: config, + + /** + * Returns parameters for `typograf` + * @returns {!Object} + */ + typograf() { + return { + locale: 'en-us' + }; + } +}); +``` + +``` +- namespace [%fileName%] + +- template example() + /// Hello ‘world’ + {Hello "world"!|typograf} +``` + +## Tag snippets + +### _ + +Expands the `_` snippet as `
`. + +``` +- rootTag = 'span' + +/// `. + +``` +/// +< a:void.bar +``` + +### button:link + +Expands the `button:link` snippet as ` +< button:link.bar +``` + +### `:section` and `:/section` + +These snippets help to use semantics HTML tags, like `article` or `section` and don't care about the `h` levels. + +``` +///

Foo

+< article:section + < h1 + Foo +< :/section +``` + +### :-attr + +Expands the `:-attr=value` attribute snippet as `:data-attr=value`. + +``` +/// +< a :-hint = hintText +``` + +## Super-global variables + +### saveTplDir + +Saves a basename of the specified dirname to the global namespace by the passed aliases. +The function should be used via the `eval` directive. + +``` +- namespace b-window + +- eval + ? @@saveTplDir(__dirname, 'windowSlotEmptyTransactions') + +- block index->windowSlotEmptyTransactions(nms) + < ?.${nms} + Hello world! +``` diff --git a/build/snakeskin/default-filters.js b/build/snakeskin/default-filters.js index b34309e313..43c0da372e 100644 --- a/build/snakeskin/default-filters.js +++ b/build/snakeskin/default-filters.js @@ -25,6 +25,7 @@ const bemFilters = include('build/snakeskin/filters/bem'); const + TYPE_OF = Symbol('A type of the component to create'), SMART_PROPS = Symbol('The component smart props'); const bind = { @@ -45,7 +46,7 @@ function tagFilter({name, attrs = {}}) { const isSimpleTag = name !== 'component' && - !attrs[':instance-of'] && + !attrs[TYPE_OF] && !validators.blockName(name); if (isSimpleTag) { @@ -55,17 +56,23 @@ function tagFilter({name, attrs = {}}) { let componentName; - if (attrs[':instance-of']) { - componentName = attrs[':instance-of'][0].camelize(false); - delete attrs[':instance-of']; + if (attrs[TYPE_OF]) { + componentName = attrs[TYPE_OF]; } else { componentName = name === 'component' ? 'iBlock' : name.camelize(false); } const - component = componentParams[componentName], - smartProps = attrs[SMART_PROPS]; + component = componentParams[componentName]; + + if (!component) { + return; + } + + if (component.inheritMods !== false && !attrs[':mods-prop']) { + attrs[':mods-prop'] = ['shareableMods']; + } const funcDir = attrs['v-func']?.[0]; delete attrs['v-func']; @@ -77,7 +84,7 @@ function tagFilter({name, attrs = {}}) { isFunctional = true; } else if (!funcDir) { - isFunctional = $C(smartProps).every((propVal, prop) => { + isFunctional = $C(attrs[SMART_PROPS]).every((propVal, prop) => { prop = prop.dasherize(true); if (!isV4Prop.test(prop)) { @@ -102,7 +109,7 @@ function tagFilter({name, attrs = {}}) { } const - isSmartFunctional = smartProps && (isFunctional || funcDir); + isSmartFunctional = attrs[SMART_PROPS] && (isFunctional || funcDir); if (isSmartFunctional) { if (funcDir == null || funcDir === 'true') { @@ -133,7 +140,7 @@ function tagNameFilter(tag, attrs, rootTag) { if (isSmartComponent) { attrs.is = [tag]; - attrs[':instance-of'] = [componentName]; + attrs[TYPE_OF] = componentName.camelize(false); attrs[SMART_PROPS] = component.functional; return 'component'; diff --git a/build/snakeskin/filters/README.md b/build/snakeskin/filters/README.md deleted file mode 100644 index 30656373e3..0000000000 --- a/build/snakeskin/filters/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# build/snakeskin/filters - -This module provides a bunch of Snakeskin filters. diff --git a/build/snakeskin/filters/bem.js b/build/snakeskin/filters/bem.js index e897cf815f..920261418c 100644 --- a/build/snakeskin/filters/bem.js +++ b/build/snakeskin/filters/bem.js @@ -29,6 +29,5 @@ module.exports = [ ); return block + element; - } ]; diff --git a/build/snakeskin/filters/tag-name.js b/build/snakeskin/filters/tag-name.js index b6a92ead10..92e26fe33d 100644 --- a/build/snakeskin/filters/tag-name.js +++ b/build/snakeskin/filters/tag-name.js @@ -16,7 +16,7 @@ const module.exports = [ /** - * Expands the `_` snippet as a `
` tag + * Expands the `_` snippet as `
` * * @param {string} tag * @param {!Object} attrs @@ -47,7 +47,7 @@ module.exports = [ /** * Expands the `:section` and `:/section` snippets. - * These snippets help to use semantics HTML tags, like `article` or `section` and don't care about `h` types. + * These snippets help to use semantics HTML tags, like `article` or `section` and don't care about the `h` levels. * * @param {string} tag * @returns {string} @@ -89,7 +89,7 @@ module.exports = [ }, /** - * Expands the `a:void` snippet as a `` tag + * Expands the `a:void` snippet as `` * * @param {string} tag * @param {!Object} attrs @@ -111,7 +111,7 @@ module.exports = [ }, /** - * Expands the `button:link` snippet as a `