Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions packages/dts-generator/src/resources/typed-json-model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ declare module "sap/ui/model/json/TypedJSONModel" {
import JSONModel from "sap/ui/model/json/JSONModel";
import TypedJSONContext from "sap/ui/model/json/TypedJSONContext";
import Context from "sap/ui/model/Context";
import ClientContextBinding from "sap/ui/model/ClientContextBinding";

/**
* TypedJSONModel is a subclass of JSONModel that provides type-safe access to the model data. It is only available when using UI5 with TypeScript.
Expand All @@ -18,6 +19,19 @@ declare module "sap/ui/model/json/TypedJSONModel" {
fnCallBack?: Function,
bReload?: boolean,
): TypedJSONContext<Data, Path>;
bindContext<Path extends AbsoluteObjectBindingPath<Data>>(
sPath: Path,
oContext?: undefined,
mParameters?: object,
): ClientContextBinding;
bindContext<
Path extends RelativeObjectBindingPath<Data, Root>,
Root extends AbsoluteObjectBindingPath<Data>,
>(
sPath: Path,
oContext?: TypedJSONContext<Data, Root>,
mParameters?: object,
): ClientContextBinding;
getData(): Data;
getProperty<Path extends AbsoluteBindingPath<Data>>(
sPath: Path,
Expand Down Expand Up @@ -82,6 +96,24 @@ declare module "sap/ui/model/json/TypedJSONModel" {
: // if T is not of type object:
never;

/**
* Valid absolute binding path for underlying object types (excludes arrays and primitives).
*
* @example
* type Order = { customer: { address: { city: string } }, items: string[], total: number };
* type ObjectPaths = AbsoluteObjectBindingPath<Order>; // "/customer" | "/customer/address"
*/
export type AbsoluteObjectBindingPath<Type> = {
[Path in AbsoluteBindingPath<Type>]: PropertyByAbsoluteBindingPath<
Type,
Path
> extends Array<unknown>
? never
: PropertyByAbsoluteBindingPath<Type, Path> extends object
? Path
: never;
}[AbsoluteBindingPath<Type>];

/**
* Valid relative binding path in a JSONModel.
* The root of the path is defined by the given root string.
Expand All @@ -98,6 +130,32 @@ declare module "sap/ui/model/json/TypedJSONModel" {
? Rest
: never;

/**
* Valid relative binding path for underlying object types (excludes arrays and primitives).
* The root of the path is defined by the given root string.
*
* @example
* type SalesOrder = { buyer: { id: string, name: string }, items: string[] };
* type PathRelativeToSalesOrder = RelativeObjectBindingPath<SalesOrder, "/buyer">; // never (no nested objects)
*
* type Order = { customer: { address: { city: string } }, total: number };
* type PathInOrder = RelativeObjectBindingPath<Order, "/">; // "customer" | "customer/address"
*/
export type RelativeObjectBindingPath<
Type,
Root extends AbsoluteBindingPath<Type>,
> = {
[Path in RelativeBindingPath<Type, Root>]: PropertyByRelativeBindingPath<
Type,
Root,
Path
> extends Array<unknown>
? never
: PropertyByRelativeBindingPath<Type, Root, Path> extends object
? Path
: never;
}[RelativeBindingPath<Type, Root>];

/**
* The type of a property in a JSONModel identified by the given path.
* Counterpart to {@link AbsoluteBindingPath}.
Expand Down
2 changes: 1 addition & 1 deletion test-packages/typed-json-model/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"ci": "npm run lint && npm run ui5lint && npm run ts-typecheck && npm run test"
},
"devDependencies": {
"@types/openui5": "1.136.0",
"@openui5/types": "^1.146.0",
"@ui5/cli": "^4.0.30",
"@ui5/linter": "^1.20.2",
"eslint": "^9.37.0",
Expand Down
23 changes: 23 additions & 0 deletions test-packages/typed-json-model/webapp/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import Context from "sap/ui/model/Context";
import JSONModel from "sap/ui/model/json/JSONModel";
import {
AbsoluteBindingPath,
AbsoluteObjectBindingPath,
PropertyByAbsoluteBindingPath,
PropertyByRelativeBindingPath,
RelativeBindingPath,
RelativeObjectBindingPath,
} from "./typing";
import ClientContextBinding from "sap/ui/model/ClientContextBinding";

export class TypedJSONContext<Data extends object, Root extends AbsoluteBindingPath<Data>> extends Context {
constructor(oModel: TypedJSONModel<Data>, sPath: Root) {
Expand Down Expand Up @@ -39,6 +42,26 @@ export class TypedJSONModel<Data extends object> extends JSONModel {
return super.createBindingContext(sPath, oContext, mParameters, fnCallBack, bReload) as TypedJSONContext<Data, Path>;
}

// Overload for absolute paths
bindContext<Path extends AbsoluteObjectBindingPath<Data>>(
sPath: Path,
oContext?: undefined,
mParameters?: object,
): ClientContextBinding;
// Overload for relative paths
bindContext<Path extends RelativeObjectBindingPath<Data, Root>, Root extends AbsoluteObjectBindingPath<Data>>(
sPath: Path,
oContext?: TypedJSONContext<Data, Root>,
mParameters?: object,
): ClientContextBinding;
// Implementation
bindContext<
Path extends AbsoluteObjectBindingPath<Data> | RelativeObjectBindingPath<Data, Root>,
Root extends AbsoluteObjectBindingPath<Data>,
>(sPath: Path, oContext?: TypedJSONContext<Data, Root>, mParameters?: object): ClientContextBinding {
return super.bindContext(sPath, oContext, mParameters);
}

getData(): Data {
return super.getData() as Data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { JSONSafe, objectLikeByInference, Placeholder } from "../input";

import { TypedJSONModel } from "../../model";
import ClientContextBinding from "sap/ui/model/ClientContextBinding";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warum dieser import?


/***********************************************************************************************************************
* Check model.setProperty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* @file Various edge cases to test the TypedJSONModel
*/

import ClientContextBinding from "sap/ui/model/ClientContextBinding";
import { TypedJSONModel } from "../../model";
import { Placeholder } from "../input";

/***********************************************************************************************************************
* Edge Case: The underlying data structure is an array (rather unusual)
Expand Down Expand Up @@ -65,3 +67,61 @@ const model4 = new TypedJSONModel(edgeCase);
/** @expect ok */ model4.setProperty("/anIntersection", { a: 1, b: "b" });
/** @expect ts2345 */ model4.setProperty("/anIntersection", { a: 1 });
/** @expect ts2322 */ model4.setProperty("/anIntersection", { a: 1, b: 2 });

/***********************************************************************************************************************
* Check model.bindContext
**********************************************************************************************************************/

// Absolute paths
const data = {
aString: "string",
anObject: { a: "foo" },
anArray: [],
anArrayOfObjects: [{ aNumber: 1 }],
aPlaceholder: new Placeholder(),
anArrayOfPlaceholders: [new Placeholder()],
aTuple: ["string", 1],
};

const model5 = new TypedJSONModel(data);

/** @expect ok */ let clientContextBindingAbsolute: ClientContextBinding = model5.bindContext("/anObject");
/** @expect ok */ model5.bindContext("/anArrayOfObjects/0");
/** @expect ok */ model5.bindContext("/aPlaceholder");
/** @expect ok */ model5.bindContext("/anArrayOfPlaceholders/0");

/** @expect ts2769 */ model5.bindContext("/anArray");
/** @expect ts2769 */ model5.bindContext("/aTuple");
/** @expect ts2769 */ model5.bindContext("/aTuple/0");
/** @expect ts2769 */ model5.bindContext("/aJsonSafeArray/0");
/** @expect ts2769 */ model5.bindContext("/anArrayOfObjects/0/aNumber");
/** @expect ts2769 */ model5.bindContext("/anArray/0/doesNotExist");

// Relative paths

const dataForRelativeCase = {
root: {
aString: "string",
anObject: { a: "foo" },
anArray: [],
anArrayOfObjects: [{ aNumber: 1 }],
aPlaceholder: new Placeholder(),
anArrayOfPlaceholders: [new Placeholder()],
aTuple: ["string", 1],
},
};

const model6 = new TypedJSONModel(dataForRelativeCase);
const context = model6.createBindingContext("/root");

/** @expect ok */ let clientContextBindingRelative: ClientContextBinding = model6.bindContext("anObject", context);
/** @expect ok */ model6.bindContext("anArrayOfObjects/0", context);
/** @expect ok */ model6.bindContext("aPlaceholder", context);
/** @expect ok */ model6.bindContext("anArrayOfPlaceholders/0", context);

/** @expect ts2769 */ model6.bindContext("anArray", context);
/** @expect ts2769 */ model6.bindContext("aTuple", context);
/** @expect ts2769 */ model6.bindContext("aTuple/0", context);
/** @expect ts2769 */ model6.bindContext("aJsonSafeArray/0", context);
/** @expect ts2769 */ model6.bindContext("anArrayOfObjects/0/aNumber", context);
/** @expect ts2769 */ model6.bindContext("anArray/0/doesNotExist", context);
34 changes: 34 additions & 0 deletions test-packages/typed-json-model/webapp/model/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ export type AbsoluteBindingPath<Type> =
: // if T is not of type object:
never;

/**
* Valid absolute binding path for underlying object types (excludes arrays and primitives).
*
* @example
* type Order = { customer: { address: { city: string } }, items: string[], total: number };
* type ObjectPaths = AbsoluteObjectBindingPath<Order>; // "/customer" | "/customer/address"
*/
export type AbsoluteObjectBindingPath<Type> = {
[Path in AbsoluteBindingPath<Type>]: PropertyByAbsoluteBindingPath<Type, Path> extends Array<unknown>
? never
: PropertyByAbsoluteBindingPath<Type, Path> extends object
? Path
: never;
}[AbsoluteBindingPath<Type>];

/**
* Valid relative binding path in a JSONModel.
* The root of the path is defined by the given root string.
Expand Down Expand Up @@ -87,6 +102,25 @@ export type PropertyByRelativeBindingPath<
RelativePath extends string,
> = PropertyByAbsoluteBindingPath<Type, `${Root}/${RelativePath}`>;

/**
* Valid relative binding path for underlying object types (excludes arrays and primitives).
* The root of the path is defined by the given root string.
*
* @example
* type SalesOrder = { buyer: { id: string, name: string }, items: string[] };
* type PathRelativeToSalesOrder = RelativeObjectBindingPath<SalesOrder, "/buyer">; // never (no nested objects)
*
* type Order = { customer: { address: { city: string } }, total: number };
* type PathInOrder = RelativeObjectBindingPath<Order, "/">; // "customer" | "customer/address"
*/
export type RelativeObjectBindingPath<Type, Root extends AbsoluteBindingPath<Type>> = {
[Path in RelativeBindingPath<Type, Root>]: PropertyByRelativeBindingPath<Type, Root, Path> extends Array<unknown>
? never
: PropertyByRelativeBindingPath<Type, Root, Path> extends object
? Path
: never;
}[RelativeBindingPath<Type, Root>];

/***********************************************************************************************************************
* Helper types to split the types above into separate parts
* to make it easier to read and understand.
Expand Down
Loading
Loading