зеркало из https://github.com/Azure/BatchExplorer.git
New Record decorator to define models (#362)
* Added missing files * Record specs * Fixes * pool v1 * now works with pool * fix errors and tslint * update doc * Fix tests * Fix tests * Fix tests
This commit is contained in:
Родитель
889acca6bc
Коммит
ad940e97f6
|
@ -1,2 +1,3 @@
|
|||
export * from "./dynamic-form"
|
||||
export * from "./dto"
|
||||
export * from "./dto";
|
||||
export * from "./dynamic-form";
|
||||
export * from "./record";
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import { RecordMissingExtendsError } from "./errors";
|
||||
import { setProp, updateTypeMetadata } from "./helpers";
|
||||
import { Record } from "./record";
|
||||
|
||||
// tslint:disable:only-arrow-functions
|
||||
|
||||
/**
|
||||
* Model attribute decorator.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* @Prop
|
||||
* public foo: string = "default";
|
||||
*
|
||||
* @Prop
|
||||
* public bar:string; // Default will be null
|
||||
* ```
|
||||
*/
|
||||
export function Prop<T>(...args) {
|
||||
return (target, attr, descriptor?: TypedPropertyDescriptor<T>) => {
|
||||
const ctr = target.constructor;
|
||||
const type = Reflect.getMetadata("design:type", target, attr);
|
||||
if (!type) {
|
||||
throw `Cannot retrieve the type for RecordAttribute ${target.constructor.name}#${attr}`
|
||||
+ "Check your nested type is defined in another file or above this DtoAttr";
|
||||
}
|
||||
|
||||
updateTypeMetadata(ctr, attr, { type, list: false });
|
||||
if (descriptor) {
|
||||
descriptor.writable = false;
|
||||
} else {
|
||||
setProp(ctr, attr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Model list attribute decorator. Use this if the attribute is an array
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* @ListProp(Bar)
|
||||
* public foos: List<Bar> = List([]);
|
||||
* ```
|
||||
*/
|
||||
export function ListProp<T>(type: any) {
|
||||
return (target, attr, descriptor?: TypedPropertyDescriptor<T>) => {
|
||||
const ctr = target.constructor;
|
||||
updateTypeMetadata(ctr, attr, { type, list: true });
|
||||
|
||||
if (descriptor) {
|
||||
descriptor.writable = false;
|
||||
} else {
|
||||
setProp(ctr, attr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function Model() {
|
||||
return <T extends { new (...args: any[]): {} }>(ctr: T) => {
|
||||
if (!(ctr.prototype instanceof Record)) {
|
||||
throw new RecordMissingExtendsError(ctr);
|
||||
}
|
||||
// save a reference to the original constructor
|
||||
const original = ctr;
|
||||
|
||||
// the new constructor behaviour
|
||||
const f: any = function (this: T, data, ...args) {
|
||||
if (data instanceof ctr) {
|
||||
return data;
|
||||
}
|
||||
const obj = original.apply(this, [data, ...args]);
|
||||
obj._completeInitialization();
|
||||
return obj;
|
||||
};
|
||||
|
||||
// copy prototype so intanceof operator still works
|
||||
f.prototype = original.prototype;
|
||||
|
||||
// return new constructor (will override original)
|
||||
return f;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Execption to be thrown if the user created a model with the @Model decorator but forgot to extend the Record class.
|
||||
*/
|
||||
export class RecordMissingExtendsError extends Error {
|
||||
constructor(ctr: Function) {
|
||||
super(`Class ${ctr.name} with the @Model Decorator should also extends the Record class`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execption to be thrown if the user tries to call setter of attribute.
|
||||
*/
|
||||
export class RecordSetAttributeError extends Error {
|
||||
constructor(ctr: Function, attr: string) {
|
||||
super(`Cannot set attribute ${attr} of immutable Record ${ctr.name}!`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { Type } from "@angular/core";
|
||||
|
||||
import { RecordSetAttributeError } from "./errors";
|
||||
import { Record } from "./record";
|
||||
|
||||
const attrMetadataKey = "record:attrs";
|
||||
export const primitives = new Set(["Array", "Number", "String", "Object", "Boolean"]);
|
||||
|
||||
export function metadataForRecord(record: Record<any>) {
|
||||
return Reflect.getMetadata(attrMetadataKey, record.constructor) || {};
|
||||
}
|
||||
|
||||
interface TypeMetadata {
|
||||
list: boolean;
|
||||
type: Type<any>;
|
||||
|
||||
}
|
||||
|
||||
export function updateTypeMetadata(ctr: Type<any>, attr: string, type: TypeMetadata) {
|
||||
const metadata = Reflect.getMetadata(attrMetadataKey, ctr) || {};
|
||||
metadata[attr] = type;
|
||||
Reflect.defineMetadata(attrMetadataKey, metadata, ctr);
|
||||
}
|
||||
|
||||
export function setProp(ctr: Type<any>, attr: string) {
|
||||
Object.defineProperty(ctr.prototype, attr, {
|
||||
get: function (this: Record<any>) {
|
||||
return (this as any)._map.get(attr) || (this as any)._defaultValues[attr];
|
||||
},
|
||||
set: function <T>(this: Record<any>, value: T) {
|
||||
if ((this as any)._initialized) {
|
||||
throw new RecordSetAttributeError(this.constructor, attr);
|
||||
} else {
|
||||
const defaults = (this as any)._defaultValues;
|
||||
defaults[attr] = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./record";
|
||||
export * from "./decorators";
|
||||
export * from "./errors";
|
|
@ -0,0 +1,63 @@
|
|||
import { List, Map } from "immutable";
|
||||
|
||||
import { metadataForRecord, primitives } from "./helpers";
|
||||
|
||||
/**
|
||||
* Base class for a record.
|
||||
* @template TInput Interface of the data returned by the server.
|
||||
*/
|
||||
export class Record<TInput> {
|
||||
private _map: Map<string, any> = Map({});
|
||||
private _defaultValues = {};
|
||||
private _initialized = false;
|
||||
private _keys: Set<string>;
|
||||
|
||||
constructor(data: Partial<TInput> = {}) {
|
||||
this._init(data);
|
||||
}
|
||||
|
||||
public equals(other: this) {
|
||||
return this === other || this._map.equals(other._map);
|
||||
}
|
||||
|
||||
public get(key: string) {
|
||||
return this._map.get(key);
|
||||
}
|
||||
|
||||
public toJS(): any {
|
||||
return Object.assign({}, this._defaultValues, this._map.toJS());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will be called by the decorator.
|
||||
*/
|
||||
private _init(data: any) {
|
||||
const attrs = metadataForRecord(this);
|
||||
const obj = {};
|
||||
const keys = Object.keys(attrs);
|
||||
this._keys = new Set(keys);
|
||||
for (let key of keys) {
|
||||
this._defaultValues[key] = null;
|
||||
const typeMetadata = attrs[key];
|
||||
if (!(key in data)) {
|
||||
continue;
|
||||
}
|
||||
const value = (data as any)[key];
|
||||
if (value && typeMetadata && !primitives.has(typeMetadata.type.name)) {
|
||||
if (typeMetadata.list) {
|
||||
obj[key] = List(value && value.map(x => new typeMetadata.type(x)));
|
||||
} else {
|
||||
obj[key] = new typeMetadata.type(value);
|
||||
}
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
}
|
||||
this._map = Map(obj);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private _completeInitialization() {
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { List, Record } from "immutable";
|
||||
import { List } from "immutable";
|
||||
import { Duration } from "moment";
|
||||
|
||||
import { ListProp, Model, Prop, Record } from "app/core";
|
||||
import { ModelUtils } from "app/utils";
|
||||
import { CloudServiceConfiguration } from "./cloud-service-configuration";
|
||||
import { Metadata, MetadataAttributes } from "./metadata";
|
||||
|
@ -9,36 +10,6 @@ import { StartTask } from "./start-task";
|
|||
import { UserAccount, UserAccountAttributes } from "./user-account";
|
||||
import { VirtualMachineConfiguration, VirtualMachineConfigurationAttributes } from "./virtual-machine-configuration";
|
||||
|
||||
const PoolRecord = Record({
|
||||
allocationState: null,
|
||||
allocationStateTransitionTime: null,
|
||||
applicationPackageReferences: [],
|
||||
certificateReferences: [],
|
||||
cloudServiceConfiguration: null,
|
||||
creationTime: null,
|
||||
currentDedicated: 0,
|
||||
displayName: null,
|
||||
enableAutoScale: false,
|
||||
enableInterNodeCommunication: false,
|
||||
id: null,
|
||||
lastModified: null,
|
||||
maxTasksPerNode: 1,
|
||||
resizeError: null,
|
||||
resizeTimeout: null,
|
||||
state: null,
|
||||
stateTransitionTime: null,
|
||||
targetDedicated: 0,
|
||||
autoScaleEvaluationInterval: null,
|
||||
autoScaleFormula: null,
|
||||
taskSchedulingPolicy: null,
|
||||
url: null,
|
||||
virtualMachineConfiguration: null,
|
||||
vmSize: null,
|
||||
startTask: null,
|
||||
metadata: List([]),
|
||||
userAccounts: List([]),
|
||||
});
|
||||
|
||||
export interface PoolAttributes {
|
||||
allocationState: string;
|
||||
allocationStateTransitionTime: Date;
|
||||
|
@ -70,34 +41,62 @@ export interface PoolAttributes {
|
|||
/**
|
||||
* Class for displaying Batch pool information.
|
||||
*/
|
||||
export class Pool extends PoolRecord {
|
||||
@Model()
|
||||
export class Pool extends Record<PoolAttributes> {
|
||||
@Prop()
|
||||
public allocationState: string;
|
||||
@Prop()
|
||||
public allocationStateTransitionTime: Date;
|
||||
@Prop()
|
||||
public applicationPackageReferences: any[];
|
||||
@Prop()
|
||||
public certificateReferences: any[];
|
||||
@Prop()
|
||||
public cloudServiceConfiguration: CloudServiceConfiguration;
|
||||
@Prop()
|
||||
public creationTime: Date;
|
||||
@Prop()
|
||||
public currentDedicated: number;
|
||||
@Prop()
|
||||
public displayName: string;
|
||||
@Prop()
|
||||
public enableAutoScale: boolean;
|
||||
@Prop()
|
||||
public enableInterNodeCommunication: boolean;
|
||||
@Prop()
|
||||
public id: string;
|
||||
@Prop()
|
||||
public lastModified: Date;
|
||||
public maxTasksPerNode: number;
|
||||
@Prop()
|
||||
public maxTasksPerNode: number = 1;
|
||||
@Prop()
|
||||
public resizeError: ResizeError;
|
||||
@Prop()
|
||||
public resizeTimeout: Duration;
|
||||
@Prop()
|
||||
public state: string;
|
||||
@Prop()
|
||||
public stateTransitionTime: Date;
|
||||
public targetDedicated: number;
|
||||
@Prop()
|
||||
public targetDedicated: number = 0;
|
||||
@Prop()
|
||||
public autoScaleFormula: string;
|
||||
@Prop()
|
||||
public autoScaleEvaluationInterval: Duration;
|
||||
@Prop()
|
||||
public taskSchedulingPolicy: any;
|
||||
@Prop()
|
||||
public url: string;
|
||||
@Prop()
|
||||
public virtualMachineConfiguration: VirtualMachineConfiguration;
|
||||
@Prop()
|
||||
public vmSize: string;
|
||||
@Prop()
|
||||
public startTask: StartTask;
|
||||
public metadata: List<Metadata>;
|
||||
public userAccounts: List<UserAccount>;
|
||||
@ListProp(Metadata)
|
||||
public metadata: List<Metadata> = List([]);
|
||||
@ListProp(UserAccount)
|
||||
public userAccounts: List<UserAccount> = List([]);
|
||||
|
||||
/**
|
||||
* Tags are computed from the metadata using an internal key
|
||||
|
@ -105,12 +104,7 @@ export class Pool extends PoolRecord {
|
|||
public tags: List<string> = List([]);
|
||||
|
||||
constructor(data: Partial<PoolAttributes> = {}) {
|
||||
super(Object.assign({}, data, {
|
||||
resizeError: data.resizeError && new ResizeError(data.resizeError),
|
||||
startTask: data.startTask && new StartTask(data.startTask),
|
||||
metadata: List(data.metadata && data.metadata.map(x => new Metadata(x))),
|
||||
userAccounts: List(data.userAccounts && data.userAccounts.map(x => new UserAccount(x))),
|
||||
}));
|
||||
super(data);
|
||||
this.tags = ModelUtils.tagsFromMetadata(this.metadata);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { ObjectUtils, SecureUtils } from "app/utils";
|
||||
import { Map } from "immutable";
|
||||
import { BehaviorSubject, Observable, Subject } from "rxjs";
|
||||
|
||||
import { ObjectUtils, SecureUtils } from "app/utils";
|
||||
import { PollService } from "./poll-service";
|
||||
import { QueryCache } from "./query-cache";
|
||||
|
||||
|
|
|
@ -2,20 +2,19 @@
|
|||
|
||||
This is a documentation to help create models which are DataStructure that maps entities returned by apis.
|
||||
|
||||
All models should be immmutable using `immutable.Record` otherwise the `RxProxy` that is using immutable `List` and `Map` will not handle those correctly.
|
||||
All models should be immmutable using the record api defined in `app/core`.
|
||||
|
||||
Immutable.js will convert those to a Map automatically which then lose the ability to run `myModel.myAttr`
|
||||
|
||||
If you are just making a model that is internal to a component:
|
||||
* doesn't it really need to be shared with others
|
||||
* if yes maybe just export from the component file/folder
|
||||
Note: Before writting a model double check this is the best option:
|
||||
* Models should be for containg data returned from remote APIs.
|
||||
* Models are immutable which means it should not be for a structure containing user edit.
|
||||
* Don't use models for internal data structure.(For a component or a small set of components)
|
||||
|
||||
### Step 1: Create file
|
||||
Create model file `my-new-model.ts` in `app/models`
|
||||
add this to `app/models/index.ts`
|
||||
|
||||
```typescript
|
||||
export * from "./my-new-model"
|
||||
export * from "./my-model"
|
||||
```
|
||||
|
||||
Then you should be able to have
|
||||
|
@ -28,25 +27,7 @@ import { MyNewModel } from "app/models"
|
|||
import { MyNewModel } from "app/models/myNewModel"
|
||||
```
|
||||
|
||||
### Step 2: Write the Record
|
||||
Use this to specify default values for each input. This is quite useful for inputs which are array for example and prevent a null check later in the code
|
||||
**It is important to have every input defined here otherwise they will be ignored**
|
||||
|
||||
|
||||
```typescript
|
||||
import { List, Record } from "immutable";
|
||||
|
||||
|
||||
// Note the record is not being exported
|
||||
const FooRecord = Record({
|
||||
id: null,
|
||||
state: null,
|
||||
files: List([]),
|
||||
bar: null,
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Write the attribute interface
|
||||
### Step 2: Write the attribute interface
|
||||
|
||||
In this interface define all the attributes of the model
|
||||
This may look like we are creating a lot of duplicate code here but it makes it worth it when using the model later as you'll have typing when creating a new model.
|
||||
|
@ -57,43 +38,36 @@ import { Partial } from "app/utils"
|
|||
export interface MyModelAttributes {
|
||||
id: string;
|
||||
state: string;
|
||||
files: List<string>;
|
||||
files: Partial<BarAttributes>[];
|
||||
bar: Partial<BarAttributes>
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Step 4: Write the model class
|
||||
### Step 3: Write the model class
|
||||
|
||||
You'll need to redefine the inputs. This is for typing purposes.
|
||||
You need to do the following for the class:
|
||||
- Decorate the class with the `@Model` decorator
|
||||
- Extend the class with the `Record` class
|
||||
- Decorate all attributes of the model with `@Prop` and all list attributes with `@ListProp`. Note: `@Prop` will be able to get the type of a nested model automatically. However you need to pass the type of the model in the list decorator.
|
||||
- For default values just set the value in the class body `@Prop public a: string = "abc"`
|
||||
|
||||
```typescript
|
||||
import { ListProp, Model, Prop, Record } from "app/core";
|
||||
import { Bar, BarAttributes } from "./bar"
|
||||
|
||||
export class Foo extends FooRecord implements MyModelAttributes {
|
||||
public id: string;
|
||||
@Model()
|
||||
export class MyModel extends Record<MyModelAttributes> {
|
||||
@Prop()
|
||||
public id: string = "default-value";
|
||||
@Prop()
|
||||
public state: string;
|
||||
public files: List<string>;
|
||||
@Prop()
|
||||
public bar: Bar;
|
||||
|
||||
constructor(data: Partial<MyModelAttributes> = {}) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4(If applicable): created nested Record
|
||||
In the case some of the attributes are other models(Record). Then you'll need to do the following to make sure they are initialized correctly
|
||||
|
||||
**Important If the model has some attributes with complex object as type. Write another model(Extending record) for it.**
|
||||
|
||||
```typescript
|
||||
// Add this constructor
|
||||
constructor(data: Partial<MyModelAttributes> = {}) {
|
||||
super(Object.assign({}, data, {
|
||||
files: List(data.files),
|
||||
bar: data.bar && new Bar(data.bar),
|
||||
}));
|
||||
@ListProp(Bar)
|
||||
public files: List<Bar>;
|
||||
}
|
||||
```
|
||||
|
||||
The record api will make all attributes with `@Prop` immutable. If you have a nested object it will automatically create it. And when using `@ListProp` it will automatically create a immutable list of items.
|
||||
|
|
|
@ -26,7 +26,7 @@ const pool1 = new Pool({ id: "pool-1", targetDedicated: 3, virtualMachineConfigu
|
|||
const pool2 = new Pool({ id: "pool-2", targetDedicated: 1, virtualMachineConfiguration: config });
|
||||
const pool3 = new Pool({ id: "pool-3", targetDedicated: 19, virtualMachineConfiguration: config });
|
||||
|
||||
describe("PoolPickerComponenent", () => {
|
||||
describe("PoolPickerComponent", () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let testComponent: TestComponent;
|
||||
let component: PoolPickerComponent;
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import { ListProp, Model, Prop, Record, RecordMissingExtendsError, RecordSetAttributeError } from "app/core";
|
||||
import { List } from "immutable";
|
||||
|
||||
@Model()
|
||||
class NestedRec extends Record<any> {
|
||||
@Prop()
|
||||
public name: string = "default-name";
|
||||
}
|
||||
|
||||
@Model()
|
||||
class TestRec extends Record<any> {
|
||||
@Prop()
|
||||
public id: string = "default-id";
|
||||
|
||||
@Prop()
|
||||
public nested: NestedRec;
|
||||
|
||||
@ListProp(NestedRec)
|
||||
public nestedList: List<NestedRec> = List([]);
|
||||
}
|
||||
|
||||
@Model()
|
||||
class SimpleTestRec extends Record<any> {
|
||||
@Prop()
|
||||
public id: string;
|
||||
|
||||
@Prop()
|
||||
public a: number;
|
||||
|
||||
@Prop()
|
||||
public b: number;
|
||||
|
||||
@Prop()
|
||||
public c: number;
|
||||
}
|
||||
|
||||
describe("Record", () => {
|
||||
it("should throw an expecption when record doesn't extends Record class", () => {
|
||||
try {
|
||||
@Model()
|
||||
class MissingExtendRecord {
|
||||
@Prop()
|
||||
public name: string;
|
||||
}
|
||||
expect(true).toBe(false, "Should have thrown an expecption");
|
||||
} catch (e) {
|
||||
expect(true).toBe(true, "Throw an excpetion as expected");
|
||||
expect(e instanceof RecordMissingExtendsError).toBe(true, "Throw an excpetion as expected");
|
||||
}
|
||||
});
|
||||
|
||||
it("should set the defaults", () => {
|
||||
const record = new TestRec();
|
||||
expect(record.id).toEqual("default-id");
|
||||
expect(record.nested).toBe(null);
|
||||
expect(record.nestedList).toEqual(List([]));
|
||||
});
|
||||
|
||||
it("should set basic value", () => {
|
||||
const record = new TestRec({ id: "some-id" });
|
||||
expect(record.id).toEqual("some-id");
|
||||
});
|
||||
|
||||
it("should set nested value", () => {
|
||||
const record = new TestRec({ nested: { name: "some-name" } });
|
||||
expect(record.nested).not.toBeFalsy();
|
||||
expect(record.nested instanceof NestedRec).toBe(true);
|
||||
expect(record.nested.name).toEqual("some-name");
|
||||
});
|
||||
|
||||
it("should set list attributes", () => {
|
||||
const record = new TestRec({ nestedList: [{ name: "name-1" }, { name: "name-2" }, {}] });
|
||||
const list = record.nestedList;
|
||||
expect(list).not.toBeFalsy();
|
||||
expect(list instanceof List).toBe(true);
|
||||
expect(list.size).toBe(3);
|
||||
expect(list.get(0) instanceof NestedRec).toBe(true, "Item 0 in list should be of nested type");
|
||||
expect(list.get(0).name).toEqual("name-1");
|
||||
expect(list.get(1) instanceof NestedRec).toBe(true, "Item 1 in list should be of nested type");
|
||||
expect(list.get(1).name).toEqual("name-2");
|
||||
expect(list.get(2) instanceof NestedRec).toBe(true, "Item 2 in list should be of nested type");
|
||||
expect(list.get(2).name).toEqual("default-name");
|
||||
});
|
||||
|
||||
it("should not allow to set attributes after created", () => {
|
||||
const record = new SimpleTestRec({ a: 1, b: 2 });
|
||||
|
||||
try {
|
||||
record.a = 3;
|
||||
expect(false).toBe(true, "Should have caught an error");
|
||||
} catch (e) {
|
||||
expect(e instanceof RecordSetAttributeError).toBe(true, "Throw an excpetion as expected");
|
||||
}
|
||||
});
|
||||
|
||||
it("should pass record of the same type", () => {
|
||||
const a = new TestRec({ id: "some-id" });
|
||||
const c = new SimpleTestRec(a);
|
||||
expect(new TestRec(a)).toBe(a, "IF applying constructor again it should just return the same object");
|
||||
expect(c instanceof SimpleTestRec).toBe(true);
|
||||
expect(c.id).toEqual(a.id);
|
||||
});
|
||||
|
||||
it("is a value type and equals other similar Records", () => {
|
||||
let t1 = new SimpleTestRec({ a: 10 });
|
||||
let t2 = new SimpleTestRec({ a: 10, b: 2 });
|
||||
expect(t1.equals(t2));
|
||||
});
|
||||
|
||||
it("skips unknown keys", () => {
|
||||
let record = new SimpleTestRec({ a: 29, d: 12, u: 2 });
|
||||
|
||||
expect(record.a).toBe(29);
|
||||
expect(record.get("d")).toBeUndefined();
|
||||
expect(record.get("u")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("toJS() returns correct values", () => {
|
||||
let a = new SimpleTestRec({ a: 29, b: 12, u: 2 });
|
||||
expect(a.toJS()).toEqual({ id: null, a: 29, b: 12, c: null });
|
||||
|
||||
let b = new TestRec({ id: "id-1", nested: { name: "name-1", other: "invalid" }, nestedList: [{}] });
|
||||
expect(b.toJS()).toEqual({ id: "id-1", nested: { name: "name-1" }, nestedList: [{ name: "default-name" }] });
|
||||
});
|
||||
|
||||
it("should have access to values in constructor", () => {
|
||||
@Model()
|
||||
class ComputedValueRec extends Record<any> {
|
||||
@Prop()
|
||||
public a = 1;
|
||||
|
||||
@Prop()
|
||||
public b = 2;
|
||||
|
||||
public computedA;
|
||||
public computedB;
|
||||
constructor(data: any) {
|
||||
super(data);
|
||||
this.computedA = `A${this.a}`;
|
||||
this.computedB = `B${this.b}`;
|
||||
}
|
||||
}
|
||||
const rec1 = new ComputedValueRec({});
|
||||
expect(rec1.computedA).toEqual("A1");
|
||||
expect(rec1.computedB).toEqual("B2");
|
||||
|
||||
const rec2 = new ComputedValueRec({ a: 3, b: 50 });
|
||||
expect(rec2.computedA).toEqual("A3");
|
||||
expect(rec2.computedB).toEqual("B50");
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче