diff --git a/.eslintrc.json b/.eslintrc.json index 20fad5e848..9ed1364449 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -95,11 +95,15 @@ "no-useless-escape": 0, "no-console": "error", "no-dupe-class-members": "off", + "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-empty-function": ["error", { "allow": ["methods"] }], - "@typescript-eslint/no-unused-vars": "error" + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-function-return-type": ["warn", { + "allowExpressions": true + }] }, "overrides": [{ // disable typescript rules for JS files diff --git a/src/Ractive/helpers/getComputationSignature.js b/src/Ractive/helpers/getComputationSignature.ts similarity index 59% rename from src/Ractive/helpers/getComputationSignature.js rename to src/Ractive/helpers/getComputationSignature.ts index eab207ac18..d014df90f2 100755 --- a/src/Ractive/helpers/getComputationSignature.js +++ b/src/Ractive/helpers/getComputationSignature.ts @@ -1,17 +1,32 @@ +import { Computation as ComputationType, ComputationDescriptor } from 'types/Computation'; +import { Keypath } from 'types/Keypath'; import bind from 'utils/bind'; import { isFunction, isString, isObjectType } from 'utils/is'; import { fatal } from 'utils/log'; import { createFunctionFromString } from '../config/runtime-parser'; -export default function getComputationSignature(ractive, key, signature) { +export interface ComputationSignature { + // TODO add ractive type on this param + getter: (this: any, context: any, keypath: Keypath) => any; + setter: (value: any, context: any, keypath: Keypath) => void; + getterString: string; + setterString: string; + getterUseStack: boolean; +} + +export default function getComputationSignature( + ractive, // TODO add ractive type + key: string, + signature: ComputationType // TODO add ractive type +): ComputationSignature { let getter; let setter; // useful for debugging - let getterString; - let getterUseStack; - let setterString; + let getterString: string; + let getterUseStack: boolean; + let setterString: string; if (isFunction(signature)) { getter = bind(signature, ractive); @@ -24,7 +39,8 @@ export default function getComputationSignature(ractive, key, signature) { getterString = signature; } - if (isObjectType(signature)) { + // TODO add ractive type + if (isObjectType>(signature)) { if (isString(signature.get)) { getter = createFunctionFromString(signature.get, ractive); getterString = signature.get; diff --git a/src/global/capture.ts b/src/global/capture.ts index a554e97791..c6a25d0bc9 100644 --- a/src/global/capture.ts +++ b/src/global/capture.ts @@ -2,22 +2,23 @@ import ModelBase from 'model/ModelBase'; import KeyModel from 'model/specials/KeyModel'; import { addToArray } from 'utils/array'; +// TODO refine types on this two variables and in stopCapturing function const stack = []; -let captureGroup: CapturableModel[]; - -type CapturableModel = ModelBase | KeyModel; +let captureGroup: []; export function startCapturing(): void { stack.push((captureGroup = [])); } -export function capture(model: CapturableModel): void { +export function capture(model: KeyModel): void; +export function capture(model: ModelBase): void; +export function capture(model: ModelBase | KeyModel): void { if (captureGroup) { addToArray(captureGroup, model); } } -export function stopCapturing(): CapturableModel[] { +export function stopCapturing(): any[] { const dependencies = stack.pop(); captureGroup = stack[stack.length - 1]; return dependencies; diff --git a/src/model/Computation.js b/src/model/Computation.ts similarity index 82% rename from src/model/Computation.js rename to src/model/Computation.ts index c8f4db3253..6aa33fad4d 100755 --- a/src/model/Computation.js +++ b/src/model/Computation.ts @@ -1,17 +1,22 @@ -/* eslint no-console:"off" */ - import { hasConsole } from 'config/environment'; import { capture, startCapturing, stopCapturing } from 'src/global/capture'; import runloop from 'src/global/runloop'; +import { ComputationSignature } from 'src/Ractive/helpers/getComputationSignature'; import { isEqual } from 'utils/is'; import { warnIfDebug } from 'utils/log'; import ComputationChild from './ComputationChild'; import Model, { shared } from './Model'; -import { maybeBind, noVirtual } from './ModelBase'; +import { maybeBind, noVirtual, ModelDependency, ModelGetOpts } from './ModelBase'; + +export default class Computation extends Model implements ModelDependency { + public signature: ComputationSignature; + public dependencies: Model[]; + public pattern: RegExp; -export default class Computation extends Model { - constructor(parent, signature, key) { + private dirty: boolean; + + constructor(parent: Model, signature: ComputationSignature, key: string) { super(parent, key); this.signature = signature; @@ -21,11 +26,6 @@ export default class Computation extends Model { this.dependencies = []; - this.children = []; - this.childByKey = {}; - - this.deps = []; - this.dirty = true; // TODO: is there a less hackish way to do this? @@ -37,7 +37,7 @@ export default class Computation extends Model { return undefined; } - get(shouldCapture, opts) { + get(shouldCapture?: boolean, opts?: ModelGetOpts) { if (shouldCapture) capture(this); if (this.dirty) { @@ -80,6 +80,7 @@ export default class Computation extends Model { } catch (err) { warnIfDebug(`Failed to compute ${this.getKeypath()}: ${err.message || err}`); + /* eslint-disable no-console */ // TODO this is all well and good in Chrome, but... // ...also, should encapsulate this stuff better, and only // show it if Ractive.DEBUG @@ -97,6 +98,7 @@ export default class Computation extends Model { ); if (console.groupCollapsed) console.groupEnd(); } + /* eslint-enable no-console */ } const dependencies = stopCapturing(); @@ -106,16 +108,16 @@ export default class Computation extends Model { return result; } - mark() { + mark(): void { this.handleChange(); } - rebind(next, previous) { + rebind(next, previous): void { // computations will grab all of their deps again automagically if (next !== previous) this.handleChange(); } - set(value) { + set(value): void { if (this.isReadonly) { throw new Error(`Cannot set read-only computed value '${this.key}'`); } @@ -124,7 +126,7 @@ export default class Computation extends Model { this.mark(); } - setDependencies(dependencies) { + setDependencies(dependencies: Model[]): void { // unregister any soft dependencies we no longer have let i = this.dependencies.length; while (i--) { @@ -142,7 +144,9 @@ export default class Computation extends Model { this.dependencies = dependencies; } - teardown() { + handleChange(): void {} + + teardown(): void { let i = this.dependencies.length; while (i--) { if (this.dependencies[i]) this.dependencies[i].unregister(this); @@ -155,6 +159,8 @@ export default class Computation extends Model { const prototype = Computation.prototype; const child = ComputationChild.prototype; prototype.handleChange = child.handleChange; -prototype.joinKey = child.joinKey; + +// function signature do not match return types so use any +prototype.joinKey = child.joinKey as any; shared.Computation = Computation; diff --git a/src/model/ComputationChild.js b/src/model/ComputationChild.ts similarity index 83% rename from src/model/ComputationChild.js rename to src/model/ComputationChild.ts index c89f14fcb0..ce3426c2b8 100755 --- a/src/model/ComputationChild.js +++ b/src/model/ComputationChild.ts @@ -4,9 +4,14 @@ import { isUndefined } from 'utils/is'; import { hasOwn } from 'utils/object'; import Model from './Model'; +import { ModelGetOpts } from './ModelBase'; export default class ComputationChild extends Model { - constructor(parent, key) { + public parent; + + private dirty: boolean; + + constructor(parent, key: string) { super(parent, key); this.isReadonly = !this.root.ractive.syncComputedChildren; @@ -18,13 +23,13 @@ export default class ComputationChild extends Model { return this.parent.setRoot; } - applyValue(value) { + applyValue(value): void { super.applyValue(value); if (!this.isReadonly) { - let source = this.parent; + let source: any = this.parent; // computed models don't have a shuffle method - while (source && source.shuffle) { + while (source?.shuffle) { source = source.parent; } @@ -38,7 +43,7 @@ export default class ComputationChild extends Model { } } - get(shouldCapture, opts) { + get(shouldCapture?: boolean, opts?: ModelGetOpts) { if (shouldCapture) capture(this); if (this.dirty) { @@ -54,7 +59,7 @@ export default class ComputationChild extends Model { : this.value; } - handleChange() { + handleChange(): void { if (this.dirty) return; this.dirty = true; @@ -65,7 +70,7 @@ export default class ComputationChild extends Model { this.children.forEach(handleChange); } - joinKey(key) { + joinKey(key: string): this { if (isUndefined(key) || key === '') return this; if (!hasOwn(this.childByKey, key)) { diff --git a/src/model/LinkModel.js b/src/model/LinkModel.ts similarity index 72% rename from src/model/LinkModel.js rename to src/model/LinkModel.ts index affa958105..68cc431c2b 100755 --- a/src/model/LinkModel.js +++ b/src/model/LinkModel.ts @@ -1,5 +1,7 @@ import { handleChange, marked, markedAll, teardown } from 'shared/methodCallers'; import { rebindMatch } from 'shared/rebind'; +import { Keypath } from 'types/Keypath'; +import { Indexes } from 'utils/array'; import { isUndefined } from 'utils/is'; import noop from 'utils/noop'; import { hasOwn } from 'utils/object'; @@ -7,10 +9,15 @@ import resolveReference from 'view/resolvers/resolveReference'; import { capture } from '../global/capture'; -import ModelBase, { fireShuffleTasks, maybeBind, shuffle } from './ModelBase'; +import Model from './Model'; +import ModelBase, { maybeBind, shuffle, ModelWithShuffle, ModelGetOpts } from './ModelBase'; -// temporary placeholder target for detached implicit links -export const Missing = { +/** + * temporary placeholder target for detached implicit links + * so force it as Model to avoid type warning + */ +export const Missing: Model = ({ + parent: undefined, key: '@missing', animate: noop, applyValue: noop, @@ -26,21 +33,40 @@ export const Missing = { }, mark: noop, registerLink: noop, - shufle: noop, + shuffle: noop, set: noop, unregisterLink: noop -}; +} as unknown) as Model; Missing.parent = Missing; -// todo implements ModelWithRelinking -export default class LinkModel extends ModelBase { - constructor(parent, owner, target, key) { +export default class LinkModel extends ModelBase implements ModelWithShuffle { + private virtual: boolean; + private boundValue: any; + + public owner: ModelBase; + public target: Model; + public sourcePath: Keypath; + public rootLink: boolean; + + public shuffling: boolean; + + public isReadonly: boolean; + + public implicit: boolean; + public mapping: boolean; + + /** @override */ + public children: LinkModel[]; + /** @override */ + public childByKey: { [key: string]: LinkModel }; + + constructor(parent, owner: ModelBase, target, key?: string) { super(parent); this.owner = owner; this.target = target; this.key = isUndefined(key) ? owner.key : key; - if (owner && owner.isLink) this.sourcePath = `${owner.sourcePath}.${this.key}`; + if (owner && owner instanceof LinkModel) this.sourcePath = `${owner.sourcePath}.${this.key}`; if (target) target.registerLink(this); @@ -53,12 +79,12 @@ export default class LinkModel extends ModelBase { return this.target.animate(from, to, options, interpolator); } - applyValue(value) { + applyValue(value): void { if (this.boundValue) this.boundValue = null; this.target.applyValue(value); } - attach(fragment) { + attach(fragment): void { const model = resolveReference(fragment, this.key); if (model) { this.relinking(model, false); @@ -68,11 +94,11 @@ export default class LinkModel extends ModelBase { } } - detach() { + detach(): void { this.relinking(Missing, false); } - get(shouldCapture, opts = {}) { + get(shouldCapture: boolean, opts: ModelGetOpts = {}) { if (shouldCapture) { capture(this); @@ -86,23 +112,24 @@ export default class LinkModel extends ModelBase { return maybeBind(this, this.target.get(false, opts), bind); } - getKeypath(ractive) { + // TODO add ractive type + getKeypath(ractive): Keypath { if (ractive && ractive !== this.root.ractive) return this.target.getKeypath(ractive); return super.getKeypath(ractive); } - handleChange() { + handleChange(): void { this.deps.forEach(handleChange); this.links.forEach(handleChange); this.notifyUpstream(); } - isDetached() { + isDetached(): boolean { return this.virtual && this.target === Missing; } - joinKey(key) { + joinKey(key: string): LinkModel { // TODO: handle nested links if (isUndefined(key) || key === '') return this; @@ -115,11 +142,11 @@ export default class LinkModel extends ModelBase { return this.childByKey[key]; } - mark(force) { + mark(force?: boolean): void { this.target.mark(force); } - marked() { + marked(): void { if (this.boundValue) this.boundValue = null; this.links.forEach(marked); @@ -127,12 +154,12 @@ export default class LinkModel extends ModelBase { this.deps.forEach(handleChange); } - markedAll() { + markedAll(): void { this.children.forEach(markedAll); this.marked(); } - notifiedUpstream(startPath, root) { + notifiedUpstream(startPath, root): void { this.links.forEach(l => l.notifiedUpstream(startPath, this.root)); this.deps.forEach(handleChange); if (startPath && this.rootLink && this.root !== root) { @@ -142,12 +169,12 @@ export default class LinkModel extends ModelBase { } } - relinked() { + relinked(): void { this.target.registerLink(this); this.children.forEach(c => c.relinked()); } - relinking(target, safe) { + relinking(target, safe: boolean): void { if (this.rootLink && this.sourcePath) target = rebindMatch(this.sourcePath, target, this.target); if (!target || this.target === target) return; @@ -171,12 +198,12 @@ export default class LinkModel extends ModelBase { }); } - set(value) { + set(value): void { if (this.boundValue) this.boundValue = null; this.target.set(value); } - shuffle(newIndices) { + shuffle(newIndices: Indexes): void { // watch for extra shuffles caused by a shuffle in a downstream link if (this.shuffling) return; @@ -198,37 +225,9 @@ export default class LinkModel extends ModelBase { else return this.target; } - teardown() { + teardown(): void { if (this._link) this._link.teardown(); this.target.unregisterLink(this); this.children.forEach(teardown); } } - -ModelBase.prototype.link = function link(model, keypath, options) { - const lnk = this._link || new LinkModel(this.parent, this, model, this.key); - lnk.implicit = options && options.implicit; - lnk.mapping = options && options.mapping; - lnk.sourcePath = keypath; - lnk.rootLink = true; - if (this._link) this._link.relinking(model, false); - this.rebind(lnk, this, false); - fireShuffleTasks(); - - this._link = lnk; - lnk.markedAll(); - - this.notifyUpstream(); - return lnk; -}; - -ModelBase.prototype.unlink = function unlink() { - if (this._link) { - const ln = this._link; - this._link = undefined; - ln.rebind(this, ln, false); - fireShuffleTasks(); - ln.teardown(); - this.notifyUpstream(); - } -}; diff --git a/src/model/Model.js b/src/model/Model.ts similarity index 76% rename from src/model/Model.js rename to src/model/Model.ts index 5c829a5bbe..86f8d6e8f5 100755 --- a/src/model/Model.js +++ b/src/model/Model.ts @@ -2,21 +2,62 @@ import { unescapeKey } from 'shared/keypaths'; import { handleChange, mark, markForce, marked, teardown } from 'shared/methodCallers'; import Ticker from 'shared/Ticker'; import { capture } from 'src/global/capture'; +import Ractive from 'src/Ractive'; import getComputationSignature from 'src/Ractive/helpers/getComputationSignature'; +import { AdaptorHandle } from 'types/Adaptor'; +import { Computation as ComputationType } from 'types/Computation'; +import { EasingFunction } from 'types/Easings'; +import { ValueMap } from 'types/ValueMap'; import { buildNewIndices } from 'utils/array'; import { isArray, isEqual, isNumeric, isObjectLike, isUndefined } from 'utils/is'; import { warnIfDebug } from 'utils/log'; import { hasOwn, keys } from 'utils/object'; import './LinkModel'; +import Computation from './Computation'; import getPrefixer from './helpers/getPrefixer'; -import ModelBase, { checkDataLink, maybeBind, shuffle } from './ModelBase'; +import LinkModel from './LinkModel'; +import ModelBase, { + checkDataLink, + maybeBind, + shuffle, + ModelWithShuffle, + ModelGetOpts, + ModelJoinOpts +} from './ModelBase'; + +export const shared: { Computation?: typeof Computation } = {}; + +export type AnimatePromise = Promise & { stop?: Function }; + +export interface ModelAnimateOpts { + duration: number; + easing: EasingFunction; + step: (t: number, value: any) => void; + complete: (to: number) => void; +} + +export default class Model extends ModelBase implements ModelWithShuffle { + /** @override */ + public parent: Model; + + private ticker: Ticker; + protected isReadonly: boolean; + protected isArray: boolean; + public isRoot: boolean; + private rewrap: boolean; + protected boundValue: any; + + protected wrapper: AdaptorHandle; + protected wrapperValue: any; + protected newWrapperValue: any; -export const shared = {}; + public shuffling: boolean; -// todo implements ModelWithRelinking -export default class Model extends ModelBase { - constructor(parent, key) { + /** used to check if model is `Computation` or `ComputationChild` */ + public isComputed: boolean; + + constructor(parent, key: string) { super(parent); this.ticker = null; @@ -33,7 +74,7 @@ export default class Model extends ModelBase { } } - adapt() { + adapt(): void { const adaptors = this.root.adaptors; const len = adaptors.length; @@ -45,7 +86,7 @@ export default class Model extends ModelBase { const value = this.wrapper ? 'newWrapperValue' in this ? this.newWrapperValue - : this.wrapperValue + : (this as Model).wrapperValue : this.value; // TODO remove this legacy nonsense @@ -78,14 +119,13 @@ export default class Model extends ModelBase { } } - let i; - - for (i = 0; i < len; i += 1) { + for (let i = 0; i < len; i += 1) { const adaptor = adaptors[i]; if (adaptor.filter(value, keypath, ractive)) { this.wrapper = adaptor.wrap(ractive, value, keypath, getPrefixer(keypath)); this.wrapperValue = value; - this.wrapper.__model = this; // massive temporary hack to enable array adaptor + // TSRChange - comment since it's not used elsewhere + // this.wrapper.__model = this; // massive temporary hack to enable array adaptor this.value = this.wrapper.get(); @@ -94,11 +134,11 @@ export default class Model extends ModelBase { } } - animate(from, to, options, interpolator) { + animate(_from, to, options: ModelAnimateOpts, interpolator): AnimatePromise { if (this.ticker) this.ticker.stop(); let fulfilPromise; - const promise = new Promise(fulfil => (fulfilPromise = fulfil)); + const promise: AnimatePromise = new Promise(fulfil => (fulfilPromise = fulfil)); this.ticker = new Ticker({ duration: options.duration, @@ -121,7 +161,7 @@ export default class Model extends ModelBase { return promise; } - applyValue(value, notify = true) { + applyValue(value, notify = true): void { if (isEqual(value, this.value)) return; if (this.boundValue) this.boundValue = null; @@ -173,7 +213,7 @@ export default class Model extends ModelBase { } } - compute(key, computed) { + compute(key: string, computed: ComputationType): Computation { const registry = this.computed || (this.computed = {}); if (registry[key]) { @@ -190,14 +230,16 @@ export default class Model extends ModelBase { return registry[key]; } - createBranch(key) { + createBranch(key: number): []; + createBranch(key: string): ValueMap; + createBranch(key: number | string): [] | ValueMap { const branch = isNumeric(key) ? [] : {}; this.applyValue(branch, false); return branch; } - get(shouldCapture, opts) { + get(shouldCapture?: boolean, opts?: ModelGetOpts) { if (this._link) return this._link.get(shouldCapture, opts); if (shouldCapture) capture(this); // if capturing, this value needs to be unwrapped because it's for external use @@ -211,7 +253,7 @@ export default class Model extends ModelBase { ); } - joinKey(key, opts) { + joinKey(key: string, opts?: ModelJoinOpts): this | LinkModel { if (this._link) { if (opts && opts.lastLink !== false && (isUndefined(key) || key === '')) return this; return this._link.joinKey(key); @@ -245,7 +287,7 @@ export default class Model extends ModelBase { if (key === 'data') { const val = this.retrieve(); - if (val && val.viewmodel && val.viewmodel.isRoot) { + if (val?.viewmodel?.isRoot) { child.link(val.viewmodel, 'data'); this.dataModel = val; } @@ -257,13 +299,13 @@ export default class Model extends ModelBase { return child; } - mark(force) { + mark(force?: boolean): void { if (this._link) return this._link.mark(force); const old = this.value; const value = this.retrieve(); - if (this.dataModel || (value && value.viewmodel && value.viewmodel.isRoot)) { + if (this.dataModel || value?.viewmodel?.isRoot) { checkDataLink(this, value); } @@ -292,34 +334,35 @@ export default class Model extends ModelBase { } } - merge(array, comparator) { + merge(array: T[], comparator?: (item: T) => X): void { const newIndices = buildNewIndices( this.value === array ? recreateArray(this) : this.value, array, comparator ); + this.parent.value[this.key] = array; this.shuffle(newIndices, true); } retrieve() { - return this.parent.value ? this.parent.value[this.key] : undefined; + return this.parent.value?.[this.key]; } - set(value) { + set(value): void { if (this.ticker) this.ticker.stop(); this.applyValue(value); } - shuffle(newIndices, unsafe) { + shuffle(newIndices: number[], unsafe?: boolean): void { shuffle(this, newIndices, false, unsafe); } - source() { + source(): this { return this; } - teardown() { + teardown(): void { if (this._link) { this._link.teardown(); this._link = null; @@ -330,7 +373,7 @@ export default class Model extends ModelBase { } } -function recreateArray(model) { +function recreateArray(model: Model): any[] { const array = []; for (let i = 0; i < model.length; i++) { diff --git a/src/model/ModelBase.ts b/src/model/ModelBase.ts index e57b886f14..a85efb7e91 100755 --- a/src/model/ModelBase.ts +++ b/src/model/ModelBase.ts @@ -1,105 +1,143 @@ import { escapeKey, unescapeKey } from 'shared/keypaths'; -import Observer from 'src/Ractive/prototype/observe/Observer'; -import PatternObserver from 'src/Ractive/prototype/observe/Pattern'; import { Keypath } from 'types/Keypath'; -import { addToArray, removeFromArray } from 'utils/array'; +import { addToArray, removeFromArray, Indexes } from 'utils/array'; import bind from 'utils/bind'; import { isArray, isObject, isObjectLike, isFunction } from 'utils/is'; import { create, keys as objectKeys } from 'utils/object'; -import Decorator from 'view/items/element/Decorator'; -import Interpolator from 'view/items/Interpolator'; -import Section from 'view/items/Section'; -import Triple from 'view/items/Triple'; -import ExpressionProxy from 'view/resolvers/ExpressionProxy'; -import ReferenceExpressionProxy from 'view/resolvers/ReferenceExpressionProxy'; import Computation from './Computation'; import LinkModel from './LinkModel'; -import Model from './Model'; import RootModel from './RootModel'; +import RactiveModel from './specials/RactiveModel'; + +interface ShuffleTaskRegistry { + early: T[]; + mark: T[]; +} + +const shuffleTasks: ShuffleTaskRegistry = { early: [], mark: [] }; +const registerQueue: ShuffleTaskRegistry<{ model: ModelBase; item: any }> = { early: [], mark: [] }; -const shuffleTasks = { early: [], mark: [] }; -const registerQueue = { early: [], mark: [] }; export const noVirtual = { virtual: false }; +type ShuffleFunction = (newIndices: Indexes, unsafe?: boolean) => void; + /** - * The following interface can be applied to: + * TODO Implement this interface in the following classes + * - Triple + * - PatternObserver + * - Decorator + * - Observer + * - Section * - ExpressionProxy - * - ReferenceExpressionProxy + * - Interpolator */ -export interface ModelWithRebound extends ModelBase { - rebound: Function; +export interface ModelDependency { + handleChange(path?: unknown): void; + rebind(prev: ModelBase, next: ModelBase, safe: boolean): void; + shuffle: ShuffleFunction; +} + +/** When adding a pattern to the model is also tracked as a depency */ +export interface ModelPattern extends ModelDependency { + notify: (path: string[]) => void; +} + +export interface ModelBinding { + rebind(prev: ModelBase, next: ModelBase, safe: boolean): void; + getValue: Function; } -export interface ModelWithRelinking extends ModelBase { - relinking: Function; +// Options >> +export interface ModelGetOpts { + virtual?: boolean; + unwrap?: boolean; + shouldBind?: boolean; } -type Pattern = RootModel | LinkModel | PatternObserver | Model; -type Link = Model | ReferenceExpressionProxy | LinkModel; -type ModelBaseDependency = - | Triple - | PatternObserver - | Decorator - | Computation - | Observer - | Section - | ExpressionProxy - | Interpolator; +export interface ModelJoinOpts { + lastLink?: boolean; +} + +export interface ModelLinkOpts { + implicit?: boolean; + mapping?: boolean; +} +// Options << // TODO add correct types -// TODO maybe we can convert this class to an abstract class -export default class ModelBase { - public parent: any; - public root: any; +export default abstract class ModelBase { + protected parent: ModelBase; + protected root: RootModel | RactiveModel; - public ractive: any; + public ractive: any; // TODO add ractive type - public deps: ModelBaseDependency[] = []; + public deps: ModelDependency[]; - public children = []; - public childByKey = {}; - public links: Link[] = []; + public children: ModelBase[]; + public childByKey: { [key: string]: any }; - public bindings = []; + public links: LinkModel[]; - public _link: any; + public bindings: ModelBinding[]; + + public _link: LinkModel; public keypath: Keypath; public key: string; public length: number; + public refs: number; - public computed: any; + public computed: { [key: string]: Computation }; public dataModel: any; - public patterns: Pattern[] = []; + public patterns: ModelPattern[]; public value: any; - constructor(parent) { + /** + * isModel a LinkModel? + * Maybe this can be replaced with `instanceof LinkModel` check + */ + public isLink: boolean; + + constructor(parent: ModelBase) { + this.deps = []; + + this.children = []; + this.childByKey = {}; + + this.links = []; + this.bindings = []; + + this.patterns = []; + if (parent) { this.parent = parent; this.root = parent.root; } } - public get: Function; - public set: Function; - public joinKey: Function; - public retrieve: Function; + abstract get(shouldCapture?: boolean, opts?: ModelGetOpts); + abstract set(value: unknown): void; + + abstract joinKey(key: string | number, opts?: ModelJoinOpts): ModelBase; + + retrieve(): any {} - addShuffleTask(task, stage = 'early'): void { + addShuffleTask(task: Function, stage = 'early'): void { shuffleTasks[stage].push(task); } addShuffleRegister(item, stage = 'early'): void { registerQueue[stage].push({ model: this, item }); } - downstreamChanged() {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + downstreamChanged(_path: string[], _depth?: number): void {} - findMatches(keys) { + findMatches(keys: string[]): string[] { const len = keys.length; let existingMatches = [this]; @@ -123,6 +161,7 @@ export default class ModelBase { return matches; } + // TODO add ractive type getKeypath(ractive?): Keypath { if (ractive !== this.ractive && this._link) return this._link.target.getKeypath(ractive); @@ -136,8 +175,9 @@ export default class ModelBase { return this.keypath; } - getValueChildren(value) { + getValueChildren(value: unknown) { let children; + if (isArray(value)) { children = []; if ('length' in this && this.length !== value.length) { @@ -160,7 +200,7 @@ export default class ModelBase { return children; } - getVirtual(shouldCapture) { + getVirtual(shouldCapture?: boolean) { const value = this.get(shouldCapture, { virtual: false }); if (isObjectLike(value)) { const result = isArray(value) ? [] : create(null); @@ -191,10 +231,12 @@ export default class ModelBase { } return result; - } else return value; + } + + return value; } - has(key) { + has(key: string): boolean { if (this._link) return this._link.has(key); const value = this.get(false, noVirtual); @@ -206,7 +248,7 @@ export default class ModelBase { let computed = this.computed; if (computed && key in this.computed) return true; - computed = this.root.ractive && this.root.ractive.computed; + computed = this.root.ractive?.computed; if (computed) { objectKeys(computed).forEach(k => { if (computed[k].pattern && computed[k].pattern.test(this.getKeypath())) return true; @@ -216,25 +258,20 @@ export default class ModelBase { return false; } - joinAll(keys, opts) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - let model = this; + joinAll(keys: string[], opts?: ModelJoinOpts): this { + // add any to avoid warning on below reassign. Maybe we can find a more clean solution? + let model: any = this; // eslint-disable-line @typescript-eslint/no-this-alias for (let i = 0; i < keys.length; i += 1) { - if ( - opts && - opts.lastLink === false && - i + 1 === keys.length && - model.childByKey[keys[i]] && - model.childByKey[keys[i]]._link - ) + if (opts?.lastLink === false && i + 1 === keys.length && this.childByKey[keys[i]]?._link) { return model.childByKey[keys[i]]; + } model = model.joinKey(keys[i], opts); } return model; } - notifyUpstream(startPath) { + notifyUpstream(startPath?: string[]): void { let parent = this.parent; const path = startPath || [this.key]; while (parent) { @@ -247,7 +284,7 @@ export default class ModelBase { } } - rebind(next, previous, safe) { + rebind(next: ModelBase, previous: ModelBase, safe?: boolean): void { if (this._link) { this._link.rebind(next, previous, false); } @@ -263,10 +300,10 @@ export default class ModelBase { i = this.links.length; while (i--) { - const link = this.links[i]; + const link: LinkModel = this.links[i] as LinkModel; // only relink the root of the link tree - if ('owner' in link && link.owner?._link) { - (link as ModelWithRelinking).relinking(next, safe); + if (link.owner?._link) { + link.relinking(next, safe); } } @@ -285,27 +322,25 @@ export default class ModelBase { } } - public refs: number; - reference(): void { const hasRefs = 'refs' in this; hasRefs ? this.refs++ : (this.refs = 1); } - register(dep: ModelBaseDependency): void { + register(dep: ModelDependency): void { this.deps.push(dep); } - registerLink(link: Link): void { + registerLink(link: LinkModel): void { addToArray(this.links, link); } - registerPatternObserver(observer: Pattern): void { + registerPatternObserver(observer: ModelPattern): void { this.patterns.push(observer); - this.register(observer as ModelBaseDependency); + this.register(observer); } - registerTwowayBinding(binding) { + registerTwowayBinding(binding: ModelBinding): void { this.bindings.push(binding); } @@ -313,20 +348,20 @@ export default class ModelBase { if ('refs' in this) this.refs--; } - unregister(dep: ModelBaseDependency): void { + unregister(dep: ModelDependency): void { removeFromArray(this.deps, dep); } - unregisterLink(link: Link): void { + unregisterLink(link: LinkModel): void { removeFromArray(this.links, link); } - unregisterPatternObserver(observer: Pattern): void { + unregisterPatternObserver(observer: ModelPattern): void { removeFromArray(this.patterns, observer); - this.unregister(observer as ModelBaseDependency); + this.unregister(observer); } - unregisterTwowayBinding(binding) { + unregisterTwowayBinding(binding: ModelBinding): void { removeFromArray(this.bindings, binding); } @@ -349,10 +384,47 @@ export default class ModelBase { if (this._link) this._link.updateFromBindings(cascade); } } + + link(model: ModelBase, keypath: Keypath, options?: ModelLinkOpts): LinkModel { + const lnk = this._link || new LinkModel(this.parent, this, model, this.key); + lnk.implicit = options?.implicit; + lnk.mapping = options?.mapping; + lnk.sourcePath = keypath; + lnk.rootLink = true; + if (this._link) this._link.relinking(model, false); + this.rebind(lnk, this, false); + fireShuffleTasks(); + + this._link = lnk; + lnk.markedAll(); + + this.notifyUpstream(); + return lnk; + } + + unlink(): void { + if (this._link) { + const ln = this._link; + this._link = undefined; + ln.rebind(this, ln, false); + fireShuffleTasks(); + ln.teardown(); + this.notifyUpstream(); + } + } +} + +/** + * The following interface can be applied to: + * - ExpressionProxy + * - ReferenceExpressionProxy + */ +export interface ModelWithRebound extends ModelBase { + rebound: Function; } // TODO: this may be better handled by overriding `get` on models with a parent that isRoot -export function maybeBind(model, value, shouldBind) { +export function maybeBind(model, value, shouldBind: boolean) { if (shouldBind && isFunction(value) && model.parent && model.parent.isRoot) { if (!model.boundValue) { model.boundValue = bind(value._r_unbound || value, model.parent.ractive); @@ -381,7 +453,7 @@ export function findBoundValue(list) { } } -export function fireShuffleTasks(stage) { +export function fireShuffleTasks(stage?: keyof ShuffleTaskRegistry): void { if (!stage) { fireShuffleTasks('early'); fireShuffleTasks('mark'); @@ -398,7 +470,20 @@ export function fireShuffleTasks(stage) { } } -export function shuffle(model, newIndices, link, unsafe) { +export interface ModelWithShuffle extends ModelBase { + shuffling: boolean; + source: Function; + shuffle: ShuffleFunction; + mark: (force?: boolean) => void; + marked?: () => void; +} + +export function shuffle( + model: ModelWithShuffle, + newIndices: Indexes, + link: boolean, + unsafe?: boolean +): void { model.shuffling = true; let i = newIndices.length; @@ -425,7 +510,8 @@ export function shuffle(model, newIndices, link, unsafe) { i = model.deps.length; while (i--) { - if (model.deps[i].shuffle) model.deps[i].shuffle(newIndices); + // TSRChange - `model.deps[i].shuffle === 'function'` -> was `model.deps[i].shuffle` + if (typeof model.deps[i].shuffle === 'function') model.deps[i].shuffle(newIndices); } model[link ? 'marked' : 'mark'](); @@ -436,7 +522,7 @@ export function shuffle(model, newIndices, link, unsafe) { model.shuffling = false; } -export function checkDataLink(model, value) { +export function checkDataLink(model: ModelBase, value): void { if (value !== model.dataModel) { if (value && value.viewmodel && value.viewmodel.isRoot && model.childByKey.data) { model.childByKey.data.link(value.viewmodel, 'data'); diff --git a/src/model/RootModel.js b/src/model/RootModel.ts similarity index 74% rename from src/model/RootModel.js rename to src/model/RootModel.ts index f887e19017..20ac5a3d76 100755 --- a/src/model/RootModel.js +++ b/src/model/RootModel.ts @@ -1,10 +1,15 @@ import { splitKeypath, unescapeKey } from 'shared/keypaths'; import { handleChange, mark } from 'shared/methodCallers'; import { capture } from 'src/global/capture'; +import { Adaptor } from 'types/Adaptor'; +import { Keypath } from 'types/Keypath'; import noop from 'utils/noop'; +import Fragment from 'view/Fragment'; import resolveReference from 'view/resolvers/resolveReference'; +import LinkModel from './LinkModel'; import Model from './Model'; +import { ModelGetOpts, ModelLinkOpts, ModelJoinOpts } from './ModelBase'; import RactiveModel from './specials/RactiveModel'; import SharedModel, { GlobalModel, SharedModel as SharedBase } from './specials/SharedModel'; @@ -27,8 +32,20 @@ const specialModels = { }; specialModels['@'] = specialModels['@this']; +export interface RootModelOpts { + // TODO add ractive type + ractive: any; + data: any; + adapt: Adaptor[]; +} + export default class RootModel extends Model { - constructor(options) { + private helpers: SharedBase; + private ractiveModel: RactiveModel; + + public adaptors: Adaptor[]; + + constructor(options: RootModelOpts) { super(null, null); this.isRoot = true; @@ -40,13 +57,14 @@ export default class RootModel extends Model { this.adapt(); } - attached(fragment) { + attached(fragment: Fragment): void { attachImplicits(this, fragment); } - createLink(keypath, target, targetPath, options) { + createLink(keypath: Keypath, target, targetPath, options: ModelLinkOpts): LinkModel { const keys = splitKeypath(keypath); + // eslint-disable-next-line @typescript-eslint/no-this-alias let model = this; while (keys.length) { const key = keys.shift(); @@ -56,11 +74,11 @@ export default class RootModel extends Model { return model.link(target, targetPath, options); } - detached() { + detached(): void { detachImplicits(this); } - get(shouldCapture, options) { + get(shouldCapture: boolean, options: ModelGetOpts) { if (shouldCapture) capture(this); if (!options || options.virtual !== false) { @@ -70,16 +88,16 @@ export default class RootModel extends Model { } } - getHelpers() { + getHelpers(): SharedBase { if (!this.helpers) this.helpers = new SharedBase(this.ractive.helpers, 'helpers', this.ractive); return this.helpers; } - getKeypath() { + getKeypath(): Keypath { return ''; } - getRactiveModel() { + getRactiveModel(): RactiveModel { return this.ractiveModel || (this.ractiveModel = new RactiveModel(this.ractive)); } @@ -97,7 +115,7 @@ export default class RootModel extends Model { return children; } - has(key) { + has(key: string): boolean { if (key[0] === '~' && key[1] === '/') key = key.slice(2); if (specialModels[key] || key === '') return true; @@ -111,7 +129,7 @@ export default class RootModel extends Model { } } - joinKey(key, opts) { + joinKey(key: string, opts?: ModelJoinOpts): this | LinkModel { if (key[0] === '~' && key[1] === '/') key = key.slice(2); if (key[0] === '@') { @@ -122,7 +140,7 @@ export default class RootModel extends Model { } } - set(value) { + set(value): void { // TODO wrapping root node is a baaaad idea. We should prevent this const wrapper = this.wrapper; if (wrapper) { @@ -147,17 +165,19 @@ export default class RootModel extends Model { return this.wrapper ? this.wrapper.get() : this.value; } - teardown() { + teardown(): void { super.teardown(); this.ractiveModel && this.ractiveModel.teardown(); } + + update = noop; } -RootModel.prototype.update = noop; -function attachImplicits(model, fragment) { - if (model._link && model._link.implicit && model._link.isDetached()) { - model.attach(fragment); - } +function attachImplicits(model: RootModel, fragment: Fragment): void { + // TSRChange - attach function doesn't exists on RootModel maybe this code is not longer valid? + // if (model._link && model._link.implicit && model._link.isDetached()) { + // model.attach(fragment); + // } // look for virtual children to relink and cascade for (const k in model.childByKey) { @@ -174,7 +194,7 @@ function attachImplicits(model, fragment) { } } -function detachImplicits(model) { +function detachImplicits(model: RootModel): void { if (model._link && model._link.implicit) { model.unlink(); } diff --git a/src/model/helpers/getPrefixer.ts b/src/model/helpers/getPrefixer.ts index 19ad33f729..7580ad4d0c 100644 --- a/src/model/helpers/getPrefixer.ts +++ b/src/model/helpers/getPrefixer.ts @@ -1,3 +1,4 @@ +import { AdaptorPrefixer } from 'types/Adaptor'; import { Keypath } from 'types/Keypath'; import { ValueMap } from 'types/ValueMap'; import { isString, isObjectType } from 'utils/is'; @@ -6,7 +7,7 @@ import { hasOwn } from 'utils/object'; // TODO this is legacy. sooner we can replace the old adaptor API the better /* istanbul ignore next */ function prefixKeypath(obj: ValueMap, prefix: string): ValueMap { - const prefixed = {}; + const prefixed: ValueMap = {}; if (!prefix) { return obj; @@ -23,21 +24,19 @@ function prefixKeypath(obj: ValueMap, prefix: string): ValueMap { return prefixed; } -type PrefixerFunction = (relativeKeypath: Keypath, value: any) => Keypath | ValueMap; - interface Prefixers { - [key: string]: PrefixerFunction; + [key: string]: AdaptorPrefixer; } const prefixers: Prefixers = {}; -export default function getPrefixer(rootKeypath: Keypath): PrefixerFunction { +export default function getPrefixer(rootKeypath: Keypath): AdaptorPrefixer { let rootDot: Keypath; if (!prefixers[rootKeypath]) { rootDot = rootKeypath ? rootKeypath + '.' : ''; /* istanbul ignore next */ - prefixers[rootKeypath] = function(relativeKeypath: Keypath, value): Keypath | ValueMap { + prefixers[rootKeypath] = function(relativeKeypath, value) { let obj: ValueMap; if (isString(relativeKeypath)) { diff --git a/src/model/specials/CSSModel.js b/src/model/specials/CSSModel.ts similarity index 71% rename from src/model/specials/CSSModel.js rename to src/model/specials/CSSModel.ts index 388917b5a3..4ba0d21e42 100644 --- a/src/model/specials/CSSModel.js +++ b/src/model/specials/CSSModel.ts @@ -3,18 +3,23 @@ import { applyChanges } from '../../Ractive/static/styleSet'; import { SharedModel } from './SharedModel'; export default class CSSModel extends SharedModel { + // TODO define what is this (sometimes is Ractive) + private component: any; + private locked: boolean; + constructor(component) { super(component.cssData, '@style'); + this.component = component; } - downstreamChanged(path, depth) { + downstreamChanged(path: string[], depth: number): void { if (this.locked) return; const component = this.component; component.extensions.forEach(e => { - const model = e._cssModel; + const model: CSSModel = e._cssModel; model.mark(); model.downstreamChanged(path, depth || 1); }); diff --git a/src/model/specials/KeyModel.ts b/src/model/specials/KeyModel.ts index 4ff465dba3..fbfa88c903 100755 --- a/src/model/specials/KeyModel.ts +++ b/src/model/specials/KeyModel.ts @@ -21,7 +21,6 @@ export default class KeyModel { * - RootModel * - Model * - LinkModel - * - Model */ public context: ModelBase; public instance: any; // TODO add ractive type here and in the constructor diff --git a/src/model/specials/RactiveModel.js b/src/model/specials/RactiveModel.ts similarity index 71% rename from src/model/specials/RactiveModel.js rename to src/model/specials/RactiveModel.ts index 8894f0a99b..7b1af231e2 100644 --- a/src/model/specials/RactiveModel.js +++ b/src/model/specials/RactiveModel.ts @@ -1,19 +1,22 @@ import { create } from 'utils/object'; -import { Missing } from '../LinkModel'; +import LinkModel, { Missing } from '../LinkModel'; import { SharedModel } from './SharedModel'; export default class RactiveModel extends SharedModel { + // TODO add ractive type constructor(ractive) { super(ractive, '@this'); this.ractive = ractive; } - joinKey(key) { + joinKey(key: string): this | LinkModel { const model = super.joinKey(key); - if ((key === 'root' || key === 'parent') && !model.isLink) return initLink(model, key); + // TSRChange - `model instanceof LinkModel` -> was `!model.isLink` + if ((key === 'root' || key === 'parent') && !(model instanceof LinkModel)) + return initLink(model, key); else if (key === 'data') return this.ractive.viewmodel; else if (key === 'cssData') return this.ractive.constructor._cssModel; @@ -21,7 +24,7 @@ export default class RactiveModel extends SharedModel { } } -function initLink(model, key) { +function initLink(model: RactiveModel, key: string): LinkModel { model.applyValue = function(value) { this.parent.value[key] = value; if (value && value.viewmodel) { @@ -43,7 +46,7 @@ function initLink(model, key) { }; } - model.applyValue(model.parent.ractive[key], key); + model.applyValue(model.parent.ractive[key], !!key); model._link.set = v => model.applyValue(v); model._link.applyValue = v => model.applyValue(v); diff --git a/src/model/specials/SharedModel.js b/src/model/specials/SharedModel.ts similarity index 70% rename from src/model/specials/SharedModel.js rename to src/model/specials/SharedModel.ts index 3cb34124ad..357efb703a 100644 --- a/src/model/specials/SharedModel.js +++ b/src/model/specials/SharedModel.ts @@ -1,11 +1,16 @@ import { base } from 'config/environment'; +import { Adaptor } from 'types/Adaptor'; +import { Keypath } from 'types/Keypath'; import Model from '../Model'; export const data = {}; export class SharedModel extends Model { - constructor(value, name, ractive) { + public adaptors: Adaptor[]; + + // TODO add ractive type + constructor(value, name: string, ractive?) { super(null, `@${name}`); this.key = `@${name}`; this.value = value; @@ -15,7 +20,7 @@ export class SharedModel extends Model { this.ractive = ractive; } - getKeypath() { + getKeypath(): Keypath { return this.key; } diff --git a/src/model/z-models.md b/src/model/z-models.md new file mode 100644 index 0000000000..2a6cd1d757 --- /dev/null +++ b/src/model/z-models.md @@ -0,0 +1,23 @@ +# Models + +```text +KeyModel + +ModelBase +| +|__ LinkModel +| +|__ Model + | + |__ RootModel + | + |__ ComputationChild + | + |__ Computation + | + |__ SharedModel + | + |__ CSSModel + | + |__ RactiveModel +``` diff --git a/src/shared/rebind.ts b/src/shared/rebind.ts index 4094332636..44b6c8c4c2 100644 --- a/src/shared/rebind.ts +++ b/src/shared/rebind.ts @@ -6,7 +6,7 @@ import { splitKeypath } from './keypaths'; // a particular keypath because in some cases, a dep may be bound // directly to a particular keypath e.g. foo.bars.0.baz and need // to avoid getting kicked to foo.bars.1.baz if foo.bars is unshifted -export function rebindMatch(template, next, previous, fragment) { +export function rebindMatch(template, next, previous, fragment?) { const keypath = template.r || template; // no valid keypath, go with next diff --git a/src/types/Adaptor.ts b/src/types/Adaptor.ts new file mode 100644 index 0000000000..f0cd507eb4 --- /dev/null +++ b/src/types/Adaptor.ts @@ -0,0 +1,35 @@ +import { Keypath } from './Keypath'; +import { ValueMap } from './ValueMap'; + +// TODO replace ractive type with correct type when available + +export interface Adaptor { + /** Called when Ractive gets a new value to see if the adaptor should be applied. + * @param value the value to evaluate + * @param keypath the keypath of the value in the Ractive data + * @param ractive the Ractive instance that is applying the value to the given keypath + * @returns true if the adaptor should be applied, false otherwisej + */ + filter: (value: any, keypath: string, ractive: any) => boolean; + + /** Called when Ractive is applying the adaptor to a value + * @param ractive the Ractive instance that is applying the adaptor + * @param value the value to which the value is being applied + * @param keypath the keypath of the value to which the adaptor is being applied + * @param prefixer a helper function to prefix a value map with the current keypath + * @returns the adaptor + */ + wrap: (ractive: any, value: any, keypath: string, prefixer: AdaptorPrefixer) => AdaptorHandle; +} +export interface AdaptorHandle { + /** Called when Ractive needs to retrieve the adapted value. */ + get: () => any; + /** Called when Ractive needs to set a property of the adapted value e.g. r.set('adapted.prop', {}). */ + set: (prop: string, value: any) => void; + /** Called when Ractive needs to replace the adapted value e.g. r.set('adapted', {}). */ + reset: (value: any) => boolean; + /** Called when Ractive no longer needs the adaptor. */ + teardown: () => void; +} + +export type AdaptorPrefixer = (relativeKeypath: Keypath | ValueMap, value?: any) => ValueMap; diff --git a/src/types/Computation.ts b/src/types/Computation.ts new file mode 100644 index 0000000000..72da7278f0 --- /dev/null +++ b/src/types/Computation.ts @@ -0,0 +1,21 @@ +type Ractive = any; // TODO use ractive type + +export type ComputationFn> = (this: T, context: any, keypath: string) => any; + +export interface ComputationDescriptor> { + /** + * Called when Ractive needs to get the computed value. + * Computations are lazy, so this is only called when a dependency asks for a value. + */ + get: ComputationFn; + + /** + * Called when Ractive is asked to set a computed keypath. + */ + set?: (this: T, value: any, context: any, keypath: string) => void; +} + +export type Computation> = + | string + | ComputationFn + | ComputationDescriptor; diff --git a/src/types/RactiveHTMLElement.ts b/src/types/RactiveHTMLElement.ts index c45ea3417a..cbd6e76149 100644 --- a/src/types/RactiveHTMLElement.ts +++ b/src/types/RactiveHTMLElement.ts @@ -1,6 +1,10 @@ -// todo add correct typings +// TODO add ractive type typings export class RactiveHTMLElement extends HTMLElement { public _ractive: any; public __ractive_instances__: any[]; } + +export interface RactiveHTMLOptionElement extends HTMLOptionElement { + _ractive?: any; +} diff --git a/src/utils/array.ts b/src/utils/array.ts index 3b2b93f76e..c21a662fda 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -1,7 +1,5 @@ import { isArray, isString, isUndefined } from './is'; -// TODO refine types on params and return - export function addToArray(array: T[], value: T): void { const index = array.indexOf(value); @@ -34,7 +32,11 @@ export function arrayContentsMatch(a: T[], b: T[]): boolean { return true; } -export function ensureArray(x) { +export function ensureArray(x: string): [string]; +export function ensureArray(x: undefined): []; +export function ensureArray>(x: T): T; + +export function ensureArray(x: unknown): unknown { if (isString(x)) { return [x]; } @@ -62,7 +64,7 @@ export function removeFromArray(array: T[], member: T): void { } } -export function combine(...arrays) { +export function combine(...arrays: T[]): T[] { const res = arrays.concat.apply([], arrays); let i = res.length; while (i--) { @@ -73,7 +75,7 @@ export function combine(...arrays) { return res; } -export function toArray(arrayLike) { +export function toArray(arrayLike: ArrayLike): T[] { const array = []; let i = arrayLike.length; while (i--) { @@ -83,7 +85,7 @@ export function toArray(arrayLike) { return array; } -export function findMap(array, fn) { +export function findMap(array: T[], fn: (item: T) => X): X { const len = array.length; for (let i = 0; i < len; i++) { const result = fn(array[i]); @@ -91,21 +93,30 @@ export function findMap(array, fn) { } } -export function buildNewIndices(one, two, comparator) { - let oldArray = one; - let newArray = two; - if (comparator) { - oldArray = oldArray.map(comparator); - newArray = newArray.map(comparator); +export interface Indexes extends Array { + oldLen?: number; + newLen?: number; + same?: boolean; +} + +export function buildNewIndices(one: T[], two: T[]): Indexes; +export function buildNewIndices(one: T[], two: T[], mapper: (item: T) => X): Indexes; + +export function buildNewIndices(one: T[], two: T[], mapper?: (item: T) => X): Indexes { + let oldArray: unknown[] = one; + let newArray: unknown[] = two; + if (mapper) { + oldArray = oldArray.map(mapper); + newArray = newArray.map(mapper); } const oldLength = oldArray.length; - const usedIndices = {}; + const usedIndices: { [key: string]: boolean } = {}; let firstUnusedIndex = 0; - const result = oldArray.map(item => { - let index; + const result: Indexes = oldArray.map(item => { + let index: number; let start = firstUnusedIndex; do { diff --git a/src/utils/bind.ts b/src/utils/bind.ts index 3491dd9a1c..d356412877 100644 --- a/src/utils/bind.ts +++ b/src/utils/bind.ts @@ -1,6 +1,9 @@ const fnBind = Function.prototype.bind; -export default function bind(fn: Function, context: unknown): Function { +export default function bind( + fn: T, + context: unknown +): (T extends (...args) => Y ? Y : T) | T { if (!/this/.test(fn.toString())) return fn; const bound = fnBind.call(fn, context); diff --git a/src/utils/is.ts b/src/utils/is.ts index d02311408f..32960ca5c1 100644 --- a/src/utils/is.ts +++ b/src/utils/is.ts @@ -31,7 +31,7 @@ export function isObject(thing: unknown): boolean { return thing && toString.call(thing) === '[object Object]'; } -export function isObjectType(thing: unknown): thing is object { +export function isObjectType(thing: unknown): thing is T { return typeof thing === 'object'; } @@ -50,7 +50,7 @@ export function isArrayLike(obj: unknown): boolean { /* Misc */ -export function isEqual(a: object, b: object): boolean { +export function isEqual(a: unknown, b: unknown): boolean { if (a === null && b === null) { return true; } diff --git a/src/view/items/element/binding/MultipleSelectBinding.ts b/src/view/items/element/binding/MultipleSelectBinding.ts index c96c2b8ad8..91de0b9a61 100755 --- a/src/view/items/element/binding/MultipleSelectBinding.ts +++ b/src/view/items/element/binding/MultipleSelectBinding.ts @@ -1,3 +1,4 @@ +import { RactiveHTMLOptionElement } from 'types/RactiveHTMLElement'; import { arrayContentsMatch } from 'utils/array'; import getSelectedOptions from 'utils/getSelectedOptions'; import { isUndefined } from 'utils/is'; @@ -30,7 +31,7 @@ export default class MultipleSelectBinding extends Binding const selectedValues = []; for (let i = 0; i < len; i += 1) { - const option = options[i]; + const option: RactiveHTMLOptionElement = options[i]; if (option.selected) { const optionValue = option._ractive ? option._ractive.value : option.value; @@ -71,8 +72,7 @@ export default class MultipleSelectBinding extends Binding const result = new Array(i); while (i--) { - // todo add correct type when we will have an inrerface for augmented HTML elements - const option: any = selectedOptions[i]; + const option: RactiveHTMLOptionElement = selectedOptions[i]; result[i] = option._ractive ? option._ractive.value : option.value; } diff --git a/src/view/items/element/specials/Select.ts b/src/view/items/element/specials/Select.ts index b313a999ae..b8e0a3bde7 100755 --- a/src/view/items/element/specials/Select.ts +++ b/src/view/items/element/specials/Select.ts @@ -1,3 +1,4 @@ +import { RactiveHTMLOptionElement } from 'types/RactiveHTMLElement'; import { toArray } from 'utils/array'; import getSelectedOptions from 'utils/getSelectedOptions'; import { isArray, isFunction } from 'utils/is'; @@ -9,6 +10,11 @@ export default class Select extends Element { public options: any[]; private selectedOptions: any[]; + /** + * @override + */ + public node: HTMLSelectElement; + constructor(options: ElementOptions) { super(options); this.options = []; @@ -65,7 +71,7 @@ export default class Select extends Element { if (selectValue !== undefined) { let optionWasSelected; - options.forEach(o => { + options.forEach((o: RactiveHTMLOptionElement) => { const optionValue = o._ractive ? o._ractive.value : o.value; const shouldSelect = isMultiple ? array && this.valueContains(selectValue, optionValue)