Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CardView - implement #28449

Draft
wants to merge 21 commits into
base: 25_1
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
47fee20
CardView - implement observables (#28422)
pomahtri Nov 28, 2024
565cc76
Merge branch '24_2' into grids/cardview/main
pomahtri Nov 28, 2024
2745cf9
CardView - implement DI (#28450)
pomahtri Nov 29, 2024
a956723
Merge branch '24_2' into grids/cardview/main
pomahtri Dec 2, 2024
e0ad81d
CardView - component base (#28463)
pomahtri Dec 2, 2024
3b8702d
Merge branch '24_2' into grids/cardview/main
pomahtri Dec 2, 2024
17910a6
Merge branch '24_2' into grids/cardview/main
pomahtri Dec 5, 2024
763d32a
CardView - inferno utils (#28510)
pomahtri Dec 11, 2024
f7dd6da
Merge branch '24_2' into grids/cardview/main
pomahtri Dec 11, 2024
967997f
CardView - implement OptionsController (#28540)
pomahtri Dec 16, 2024
b2a9553
Merge branch '24_2' into grids/cardview/main
pomahtri Dec 16, 2024
8c487b7
Merge branch '24_2' into grids/cardview/main
pomahtri Dec 18, 2024
8c49bf5
CardView - add default values for optionsController mock (#28590)
pomahtri Dec 26, 2024
659d50f
CardView - implement ColumnsController (#28591)
pomahtri Dec 26, 2024
689d827
Merge branch '25_1' into grids/cardview/main
pomahtri Dec 26, 2024
51be8e7
Merge branch '25_1' into grids/cardview/main
pomahtri Jan 9, 2025
a9c1925
CardView - implement dataController (#28688)
pomahtri Jan 15, 2025
22b53d9
Merge branch '25_1' into grids/cardview/main
pomahtri Jan 15, 2025
c37e6ad
CardView: Implement Toolbar (#28693)
Alyar666 Jan 20, 2025
d2700f2
Merge branch '25_1' into grids/cardview/main
pomahtri Jan 20, 2025
1cee130
fix: typing
pomahtri Jan 20, 2025
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
186 changes: 186 additions & 0 deletions packages/devextreme/js/__internal/core/di/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/* eslint-disable @typescript-eslint/no-extraneous-class */
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/init-declarations */
/* eslint-disable max-classes-per-file */
/* eslint-disable class-methods-use-this */
import { describe, expect, it } from '@jest/globals';

import { DIContext } from './index';

describe('basic', () => {
describe('register', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

it('should return registered class', () => {
const ctx = new DIContext();
ctx.register(MyClass);

expect(ctx.get(MyClass)).toBeInstanceOf(MyClass);
expect(ctx.get(MyClass).getNumber()).toBe(1);
});

it('should return registered class with tryGet', () => {
const ctx = new DIContext();
ctx.register(MyClass);

expect(ctx.tryGet(MyClass)).toBeInstanceOf(MyClass);
expect(ctx.tryGet(MyClass)?.getNumber()).toBe(1);
});

it('should return same instance each time', () => {
const ctx = new DIContext();
ctx.register(MyClass);

expect(ctx.get(MyClass)).toBe(ctx.get(MyClass));
});
});

describe('registerInstance', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

const ctx = new DIContext();
const instance = new MyClass();
ctx.registerInstance(MyClass, instance);

it('should work', () => {
expect(ctx.get(MyClass)).toBe(instance);
});
});

describe('non registered items', () => {
const ctx = new DIContext();
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}
it('should throw', () => {
expect(() => ctx.get(MyClass)).toThrow();
});
it('should not throw if tryGet', () => {
expect(ctx.tryGet(MyClass)).toBe(null);
});
});
});

describe('dependencies', () => {
class MyUtilityClass {
static dependencies = [] as const;

getNumber(): number {
return 2;
}
}

class MyClass {
static dependencies = [MyUtilityClass] as const;

constructor(private readonly utility: MyUtilityClass) {}

getSuperNumber(): number {
return this.utility.getNumber() * 2;
}
}

const ctx = new DIContext();
ctx.register(MyUtilityClass);
ctx.register(MyClass);

it('should return registered class', () => {
expect(ctx.get(MyClass)).toBeInstanceOf(MyClass);
expect(ctx.get(MyUtilityClass)).toBeInstanceOf(MyUtilityClass);
});

it('dependecies should work', () => {
expect(ctx.get(MyClass).getSuperNumber()).toBe(4);
});
});

describe('mocks', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

class MyClassMock implements MyClass {
static dependencies = [] as const;

getNumber(): number {
return 2;
}
}

const ctx = new DIContext();
ctx.register(MyClass, MyClassMock);

it('should return mock class when they are registered', () => {
expect(ctx.get(MyClass)).toBeInstanceOf(MyClassMock);
expect(ctx.get(MyClass).getNumber()).toBe(2);
});
});

it('should work regardless of registration order', () => {
class MyClass {
static dependencies = [] as const;

getNumber(): number {
return 1;
}
}

class MyDependentClass {
static dependencies = [MyClass] as const;

constructor(private readonly myClass: MyClass) {}

getSuperNumber(): number {
return this.myClass.getNumber() * 2;
}
}

const ctx = new DIContext();
ctx.register(MyDependentClass);
ctx.register(MyClass);
expect(ctx.get(MyDependentClass).getSuperNumber()).toBe(2);
});

describe('dependency cycle', () => {
class MyClass1 {
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-use-before-define
static dependencies = [MyClass2] as const;

constructor(private readonly myClass2: MyClass2) {}
}
class MyClass2 {
static dependencies = [MyClass1] as const;

constructor(private readonly myClass1: MyClass1) {}
}

const ctx = new DIContext();
ctx.register(MyClass1);
ctx.register(MyClass2);

it('should throw', () => {
expect(() => ctx.get(MyClass1)).toThrow();
expect(() => ctx.get(MyClass2)).toThrow();
});
});
90 changes: 90 additions & 0 deletions packages/devextreme/js/__internal/core/di/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable spellcheck/spell-checker */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-explicit-any */

// eslint-disable-next-line @typescript-eslint/ban-types
interface AbstractType<T> extends Function {
prototype: T;
}

type Constructor<T, TDeps extends readonly any[]> = new(...deps: TDeps) => T;

interface DIItem<T, TDeps extends readonly any[]> extends Constructor<T, TDeps> {
dependencies: readonly [...{ [P in keyof TDeps]: AbstractType<TDeps[P]> }];
}

export class DIContext {
private readonly instances: Map<unknown, unknown> = new Map();

private readonly fabrics: Map<unknown, unknown> = new Map();

private readonly antiRecursionSet = new Set();

public register<TId, TFabric extends TId, TDeps extends readonly any[]>(
id: AbstractType<TId>,
fabric: DIItem<TFabric, TDeps>,
): void;
public register<T, TDeps extends readonly any[]>(
idAndFabric: DIItem<T, TDeps>,
): void;
public register<T, TDeps extends readonly any[]>(
id: DIItem<T, TDeps>,
fabric?: DIItem<T, TDeps>,
): void {
// eslint-disable-next-line no-param-reassign
fabric ??= id;
this.fabrics.set(id, fabric);
}

public registerInstance<T>(
id: AbstractType<T>,
instance: T,
): void {
this.instances.set(id, instance);
}

public get<T>(
id: AbstractType<T>,
): T {
const instance = this.tryGet(id);

if (instance) {
return instance;
}

throw new Error('DI item is not registered');
}

public tryGet<T>(
id: AbstractType<T>,
): T | null {
if (this.instances.get(id)) {
return this.instances.get(id) as T;
}

const fabric = this.fabrics.get(id);
if (fabric) {
const res: T = this.create(fabric as any);
this.instances.set(id, res);
this.instances.set(fabric, res);
return res;
}

return null;
}

private create<T, TDeps extends readonly any[]>(fabric: DIItem<T, TDeps>): T {
if (this.antiRecursionSet.has(fabric)) {
throw new Error('dependency cycle in DI');
}

this.antiRecursionSet.add(fabric);

const args = fabric.dependencies.map((dependency) => this.get(dependency));

this.antiRecursionSet.delete(fabric);

// eslint-disable-next-line new-cap
return new fabric(...args as any);
}
}
88 changes: 88 additions & 0 deletions packages/devextreme/js/__internal/core/reactive/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable spellcheck/spell-checker */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */

import { type Subscription, SubscriptionBag } from './subscription';
import type {
Callback, Gettable, Subscribable, Updatable,
} from './types';

export class Observable<T> implements Subscribable<T>, Updatable<T>, Gettable<T> {
private readonly callbacks: Set<Callback<T>> = new Set();

constructor(private value: T) {}

update(value: T): void {
if (this.value === value) {
return;
}
this.value = value;

this.callbacks.forEach((c) => {
c(value);
});
}

updateFunc(func: (oldValue: T) => T): void {
this.update(func(this.value));
}

subscribe(callback: Callback<T>): Subscription {
this.callbacks.add(callback);
callback(this.value);

return {
unsubscribe: () => this.callbacks.delete(callback),
};
}

unreactive_get(): T {
return this.value;
}

dispose(): void {
this.callbacks.clear();
}
}

export class InterruptableComputed<
TArgs extends readonly any[], TValue,
> extends Observable<TValue> {
private readonly depValues: [...TArgs];

private readonly depInitialized: boolean[];

private isInitialized = false;

private readonly subscriptions = new SubscriptionBag();

constructor(
compute: (...args: TArgs) => TValue,
deps: { [I in keyof TArgs]: Subscribable<TArgs[I]> },
) {
super(undefined as any);

this.depValues = deps.map(() => undefined) as any;
this.depInitialized = deps.map(() => false);

deps.forEach((dep, i) => {
this.subscriptions.add(dep.subscribe((v) => {
this.depValues[i] = v;

if (!this.isInitialized) {
this.depInitialized[i] = true;
this.isInitialized = this.depInitialized.every((e) => e);
}

if (this.isInitialized) {
this.update(compute(...this.depValues));
}
}));
});
}

dispose(): void {
super.dispose();
this.subscriptions.unsubscribe();
}
}
3 changes: 3 additions & 0 deletions packages/devextreme/js/__internal/core/reactive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './subscription';
export * from './types';
export * from './utilities';
17 changes: 17 additions & 0 deletions packages/devextreme/js/__internal/core/reactive/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface Subscription {
unsubscribe: () => void;
}

export class SubscriptionBag implements Subscription {
private readonly subscriptions: Subscription[] = [];

add(subscription: Subscription): void {
this.subscriptions.push(subscription);
}

unsubscribe(): void {
this.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
}
}
Loading
Loading