From 32d3f063d816fed5f758caf3784a0d6087190363 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 27 Nov 2024 19:26:51 -0800 Subject: [PATCH 1/2] fix: #501 use arrow functions for all baseline API callers to avoid `this` binding issues --- src/coalesce-vue/src/viewmodel.ts | 135 ++++++++++++++---------------- 1 file changed, 65 insertions(+), 70 deletions(-) diff --git a/src/coalesce-vue/src/viewmodel.ts b/src/coalesce-vue/src/viewmodel.ts index 0054467c6..dd6e248d9 100644 --- a/src/coalesce-vue/src/viewmodel.ts +++ b/src/coalesce-vue/src/viewmodel.ts @@ -141,9 +141,9 @@ export abstract class ViewModel< private _isDirty = ref(false); /** @internal */ - _dirtyProps: Set = IsVue2 - ? new Set() - : reactive(new Set()); + _dirtyProps: Set> = IsVue2 + ? new Set() + : reactive(new Set()); // Backwards-compat with vue2 nonreactive sets. // Typed as any because vue ref unwrapping causes problems with a prop that is a maybe ref. @@ -469,76 +469,71 @@ export abstract class ViewModel< */ get $save() { const $save = this.$apiClient - .$makeCaller( - "item", - async function (this: ViewModel, c, overrideProps?: Partial) { - if (this.$hasError) { - throw Error(joinErrors(this.$getErrors())); - } + .$makeCaller("item", async (c, overrideProps?: Partial) => { + if (this.$hasError) { + throw Error(joinErrors(this.$getErrors())); + } - // Capture the dirty props before we set $isDirty = false; - const dirtyProps = [...this._dirtyProps]; - - // Copy the dirty props into a list that we'll be mutating - // into the list of ALL props that we would need to send - // for surgical saves. - const propsToSave = [...dirtyProps]; - - // If we were passed any override props, send those too. - // Use case of override props is changing some field - // where we don't want the local UI to reflect that new value - // until we know that value has been saved to the server. - // A further example is saving some property that will have sever-determined effects - // on other properties on the model where we don't want an inconsistent intermediate state. - let data: TModel = this as any; - if (overrideProps) { - data = { - ...this.$data, - ...overrideProps, - $metadata: this.$metadata, - }; - propsToSave.push(...Object.keys(overrideProps)); - } + // Capture the dirty props before we set $isDirty = false; + const dirtyProps = [...this._dirtyProps]; + + // Copy the dirty props into a list that we'll be mutating + // into the list of ALL props that we would need to send + // for surgical saves. + const propsToSave: string[] = [...dirtyProps]; + + // If we were passed any override props, send those too. + // Use case of override props is changing some field + // where we don't want the local UI to reflect that new value + // until we know that value has been saved to the server. + // A further example is saving some property that will have sever-determined effects + // on other properties on the model where we don't want an inconsistent intermediate state. + let data: TModel = this as any; + if (overrideProps) { + data = { + ...this.$data, + ...overrideProps, + $metadata: this.$metadata, + }; + propsToSave.push(...Object.keys(overrideProps)); + } - // We always send the PK if it exists, regardless of dirty status or save mode. - if (this.$primaryKey != null) { - propsToSave.push(this.$metadata.keyProp.name); - } + // We always send the PK if it exists, regardless of dirty status or save mode. + if (this.$primaryKey != null) { + propsToSave.push(this.$metadata.keyProp.name); + } - this.$savingProps = new Set(propsToSave); - - // If doing surgical saves, - // only save the props that are dirty and/or explicitly requested. - const fields = - this.$saveMode == "surgical" - ? ([...this.$savingProps] as any) - : null; - - // Before we make the save call, set isDirty = false. - // This lets us detect changes that happen to the model while our save request is pending. - // If the model is dirty when the request completes, we'll not load the response from the server. - this.$isDirty = false; - try { - return await c.save(data, { ...this.$params, fields }); - } catch (e) { - for (const prop of dirtyProps) { - this.$setPropDirty( - prop, - true, - // Don't re-trigger autosave on save failure. - // Wait for next prop change to trigger it again. - // Otherwise, if the wait timeout is zero, - // the save will keep triggering as fast as possible. - // Note that this could be a candidate for a user-customizable option - // in the future. - false - ); - } - this.$savingProps = emptySet; - throw e; + this.$savingProps = new Set(propsToSave); + + // If doing surgical saves, + // only save the props that are dirty and/or explicitly requested. + const fields = + this.$saveMode == "surgical" ? ([...this.$savingProps] as any) : null; + + // Before we make the save call, set isDirty = false. + // This lets us detect changes that happen to the model while our save request is pending. + // If the model is dirty when the request completes, we'll not load the response from the server. + this.$isDirty = false; + try { + return await c.save(data, { ...this.$params, fields }); + } catch (e) { + for (const prop of dirtyProps) { + this.$setPropDirty( + prop, + true, + // Don't re-trigger autosave on save failure. + // Wait for next prop change to trigger it again. + // Otherwise, if the wait timeout is zero, + // the save will keep triggering as fast as possible. + // Note that this could be a candidate for a user-customizable option + // in the future. + false + ); } + this.$savingProps = emptySet; + throw e; } - ) + }) .onFulfilled(function (this: ViewModel) { if (!this.$save.result) { // Can't do anything useful if the save returned no data. @@ -575,7 +570,7 @@ export abstract class ViewModel< get $bulkSave() { const $bulkSave = this.$apiClient.$makeCaller( "item", - async function (this: ViewModel, c, options?: BulkSaveOptions) { + async (c, options?: BulkSaveOptions) => { const { items: itemsToSend, rawItems: dataToSend, @@ -985,7 +980,7 @@ export abstract class ViewModel< */ get $delete() { const $delete = this.$apiClient - .$makeCaller("item", function (this: ViewModel, c) { + .$makeCaller("item", (c) => { if (this._existsOnServer) { return c.delete(this.$primaryKey, this.$params); } else { From 20999855e8568aab71cfd64cb34b86f02dde3637 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 27 Nov 2024 19:32:33 -0800 Subject: [PATCH 2/2] types: fix ref unwrapping issue with _dirtyProps type --- src/coalesce-vue/src/viewmodel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coalesce-vue/src/viewmodel.ts b/src/coalesce-vue/src/viewmodel.ts index dd6e248d9..77cefc15e 100644 --- a/src/coalesce-vue/src/viewmodel.ts +++ b/src/coalesce-vue/src/viewmodel.ts @@ -143,7 +143,7 @@ export abstract class ViewModel< /** @internal */ _dirtyProps: Set> = IsVue2 ? new Set() - : reactive(new Set()); + : (reactive(new Set()) as any); // as any to avoid ref unwrapping madness // Backwards-compat with vue2 nonreactive sets. // Typed as any because vue ref unwrapping causes problems with a prop that is a maybe ref.