UNPKG

@v4fire/client

Version:

V4Fire client core library

483 lines (386 loc) • 10.4 kB
/*! * 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/field/README.md]] * @packageDocumentation */ 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 type { KeyGetter, ValueGetter } from 'super/i-block/modules/field/interface'; export * from 'super/i-block/modules/field/interface'; /** * Class provides helper methods to safety access to a component property */ export default class Field extends Friend { /** * Returns a property from a component by the specified path * * @param path - path to the property (`bla.baz.foo`) * @param getter - function that returns a value from the passed object * * @example * ```js * this.field.get('bla.foo'); * this.field.get('bla.fooBla', (prop, obj) => Object.get(obj, prop.underscore())); * ``` */ get<T = unknown>(path: ObjectPropertyPath, getter: ValueGetter): CanUndef<T>; /** * Returns a property from an object by the specified path * * @param path - path to the property (`bla.baz.foo`) * @param [obj] - source object * @param [getter] - function that returns a value from the passed object * * @example * ```js * this.field.get('bla.foo', obj); * this.field.get('bla.fooBla', obj, (prop, obj) => Object.get(obj, prop.underscore())); * ``` */ get<T = unknown>( path: string, obj?: Nullable<object>, getter?: ValueGetter ): CanUndef<T>; get<T = unknown>( path: string, obj: Nullable<object | ValueGetter> = this.ctx, getter?: ValueGetter ): CanUndef<T> { if (Object.isFunction(obj)) { getter = obj; obj = this.ctx; } if (obj == null) { return; } let {ctx} = this; let isComponent = false; if ((<Dictionary>obj).instance instanceof iBlock) { ctx = (<iBlock>obj).unsafe; isComponent = true; } let res: unknown = obj, chunks; if (isComponent) { const info = getPropertyInfo(path, ctx); ctx = Object.cast(info.ctx); res = ctx; chunks = info.path.split('.'); if (info.accessor != null) { chunks[0] = info.accessor; } else if (!ctx.isFlyweight) { switch (info.type) { case 'prop': if (ctx.lfc.isBeforeCreate('beforeDataCreate')) { return undefined; } break; case 'field': res = ctx.$fields; break; default: // Do nothing } } } else { chunks = path.split('.'); } if (getter == null) { res = Object.get<T>(res, chunks); } else { for (let i = 0; i < chunks.length; i++) { if (res == null) { return undefined; } const key = chunks[i]; if (Object.isPromiseLike(res) && !(key in res)) { res = res.then((res) => getter!(key, res)); } else { res = getter(key, res); } } } if (Object.isPromiseLike(res)) { return Object.cast(this.async.promise(res)); } return Object.cast(res); } /** * Sets a new property to an object by the specified path * * @param path - path to the property (`bla.baz.foo`) * @param value - value to set * @param keyGetter - function that returns a key name from the passed object * * @example * ```js * this.field.set('bla.foo', 1); * this.field.get('bla.fooBla', 1, String.underscore); * ``` */ set<T = unknown>(path: string, value: T, keyGetter: KeyGetter): T; /** * Sets a new property to an object by the specified path * * @param path - path to the property (`bla.baz.foo`) * @param value - value to set * @param [obj] - source object * @param [keyGetter] - function that returns a key name from the passed object * * @example * ```js * this.field.set('bla.foo', 1); * this.field.set('bla.foo', 1, obj); * this.field.get('bla.fooBla', 1, obj, String.underscore); * ``` */ set<T = unknown>( path: string, value: T, obj?: Nullable<object>, keyGetter?: KeyGetter ): T; set<T = unknown>( path: string, value: T, obj: Nullable<object> = this.ctx, keyGetter?: ValueGetter ): T { if (Object.isFunction(obj)) { keyGetter = obj; obj = this.ctx; } if (obj == null) { return value; } let {ctx} = this; let isComponent = false; if ((<Dictionary>obj).instance instanceof iBlock) { ctx = (<iBlock>obj).unsafe; isComponent = true; } let sync, needSetToWatch = isComponent; let ref = obj, chunks; if (isComponent) { const info = getPropertyInfo(path, ctx); ctx = Object.cast(info.ctx); ref = ctx; chunks = info.path.split('.'); if (info.accessor != null) { needSetToWatch = false; chunks[0] = info.accessor; } else if (ctx.isFlyweight) { needSetToWatch = false; } else { const isReady = !ctx.lfc.isBeforeCreate(); const isSystem = info.type === 'system', isField = !isSystem && info.type === 'field'; if (isSystem || isField) { // If property not already watched, don't force the creation of a proxy // eslint-disable-next-line @typescript-eslint/unbound-method needSetToWatch = isReady && Object.isFunction(Object.getOwnPropertyDescriptor(ctx, info.name)?.get); if (isSystem) { // If a component already initialized watchers of system fields, // we have to set these properties directly to the proxy object if (needSetToWatch) { ref = ctx.$systemFields; // Otherwise, we have to synchronize these properties between the proxy object and component instance } else { const name = chunks[0]; sync = () => Object.set(ctx.$systemFields, [name], ref[name]); } } else { ref = ctx.$fields; if (!isReady) { chunks[0] = info.name; } const needSync = ctx.isNotRegular && unwrap(ref) === ref; // If a component does not already initialize watchers of fields, // we have to synchronize these properties between the proxy object and component instance if (needSync) { const name = chunks[0]; sync = () => Object.set(ctx, [name], ref[name]); } } } } } else { chunks = path.split('.'); } let prop; for (let i = 0; i < chunks.length; i++) { prop = keyGetter ? keyGetter(chunks[i], ref) : chunks[i]; if (i + 1 === chunks.length) { break; } let newRef = Object.get(ref, [prop]); if (newRef == null || typeof newRef !== 'object') { newRef = isNaN(Number(chunks[i + 1])) ? {} : []; if (needSetToWatch) { ctx.$set(ref, prop, newRef); } else { Object.set(ref, [prop], newRef); } } ref = Object.get(ref, [prop])!; } if (!needSetToWatch || !Object.isArray(ref) && Object.has(ref, [prop])) { Object.set(ref, [prop], value); } else { ctx.$set(ref, prop, value); } if (sync != null) { sync(); } return value; } /** * Deletes a property from an object by the specified path * * @param path - path to the property (`bla.baz.foo`) * @param keyGetter - function that returns a key name from the passed object * * @example * ```js * this.field.delete('bla.foo'); * this.field.delete('bla.fooBla', String.underscore); * ``` */ delete(path: string, keyGetter?: KeyGetter): boolean; /** * Deletes a property from an object by the specified path * * @param path - path to the property (`bla.baz.foo`) * @param [obj] - source object * @param [keyGetter] - function that returns a key name from the passed object * * @example * ```js * this.field.delete('bla.foo'); * this.field.delete('bla.foo', obj); * this.field.delete('bla.fooBla', obj, String.underscore); * ``` */ delete(path: string, obj?: Nullable<object>, keyGetter?: KeyGetter): boolean; delete( path: string, obj: Nullable<object> = this.ctx, keyGetter?: KeyGetter ): boolean { if (Object.isFunction(obj)) { keyGetter = obj; obj = this.ctx; } if (obj == null) { return false; } let {ctx} = this; let isComponent = false; if ((<Dictionary>obj).instance instanceof iBlock) { ctx = (<iBlock>obj).unsafe; isComponent = true; } let sync, needDeleteToWatch = isComponent; let ref = obj, chunks; if (isComponent) { const info = getPropertyInfo(path, ctx); const isReady = !ctx.lfc.isBeforeCreate(), isSystem = info.type === 'system', isField = !isSystem && info.type === 'field'; ctx = Object.cast(info.ctx); chunks = info.path.split('.'); chunks[0] = info.name; if (ctx.isFlyweight) { needDeleteToWatch = false; } else if (isSystem || isField) { // If property not already watched, don't force the creation of a proxy // eslint-disable-next-line @typescript-eslint/unbound-method needDeleteToWatch = isReady && Object.isFunction(Object.getOwnPropertyDescriptor(ctx, info.name)?.get); if (isSystem) { // If a component already initialized watchers of system fields, // we have to set these properties directly to the proxy object if (needDeleteToWatch) { ref = ctx.$systemFields; // Otherwise, we have to synchronize these properties between the proxy object and component instance } else { const name = chunks[0]; sync = () => Object.delete(ctx.$systemFields, [name]); } } else { ref = ctx.$fields; // If a component does not already initialize watchers of fields, // we have to synchronize these properties between the proxy object and component instance if (ctx.isFunctional && unwrap(ref) === ref) { const name = chunks[0]; sync = () => Object.delete(ctx, [name]); } } } } else { chunks = path.split('.'); } let needDelete = true, prop; for (let i = 0; i < chunks.length; i++) { prop = keyGetter ? keyGetter(chunks[i], ref) : chunks[i]; if (i + 1 === chunks.length) { break; } const newRef = Object.get(ref, [prop]); if (newRef == null || typeof newRef !== 'object') { needDelete = false; break; } ref = newRef!; } if (needDelete) { if (needDeleteToWatch) { ctx.$delete(ref, prop); } else { Object.delete(ref, [prop]); } if (sync != null) { sync(); } return true; } return false; } }