-
Notifications
You must be signed in to change notification settings - Fork 620
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CardView - implement OptionsController (#28540)
- Loading branch information
Showing
8 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
packages/devextreme/js/__internal/grids/new/card_view/options_controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { OptionsController } from '@ts/grids/new/grid_core/options_controller/options_controller_base'; | ||
|
||
import type { defaultOptions, Options } from './options'; | ||
|
||
class CardViewOptionsController extends OptionsController<Options, typeof defaultOptions> {} | ||
|
||
export { CardViewOptionsController as OptionsController }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
...evextreme/js/__internal/grids/new/grid_core/options_controller/options_controller.mock.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import type { defaultOptions, Options } from '../options'; | ||
import { OptionsControllerMock as OptionsControllerBaseMock } from './options_controller_base.mock'; | ||
|
||
export class OptionsControllerMock extends OptionsControllerBaseMock< | ||
Options, typeof defaultOptions | ||
> {} |
7 changes: 7 additions & 0 deletions
7
...ges/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
import type { defaultOptions, Options } from '../options'; | ||
import { OptionsController as OptionsControllerBase } from './options_controller_base'; | ||
|
||
class GridCoreOptionsController extends OptionsControllerBase<Options, typeof defaultOptions> {} | ||
|
||
export { GridCoreOptionsController as OptionsController }; |
26 changes: 26 additions & 0 deletions
26
...reme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.mock.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* eslint-disable max-classes-per-file */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
/* eslint-disable max-len */ | ||
/* eslint-disable spellcheck/spell-checker */ | ||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { Component } from '@js/core/component'; | ||
|
||
import { OptionsController } from './options_controller_base'; | ||
|
||
export class OptionsControllerMock< | ||
TProps, | ||
TDefaultProps extends TProps, | ||
> extends OptionsController<TProps, TDefaultProps> { | ||
private readonly componentMock: Component<TProps>; | ||
constructor(options: TProps) { | ||
const componentMock = new Component(options); | ||
super(componentMock); | ||
this.componentMock = componentMock; | ||
} | ||
|
||
public option(key?: string, value?: unknown): unknown { | ||
// @ts-expect-error | ||
return this.componentMock.option(key, value); | ||
} | ||
} |
99 changes: 99 additions & 0 deletions
99
...reme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* eslint-disable spellcheck/spell-checker */ | ||
/* eslint-disable @typescript-eslint/init-declarations */ | ||
import { | ||
beforeEach, | ||
describe, expect, it, jest, | ||
} from '@jest/globals'; | ||
import { Component } from '@js/core/component'; | ||
|
||
import { OptionsController } from './options_controller_base'; | ||
|
||
interface Options { | ||
value?: string; | ||
|
||
objectValue?: { | ||
nestedValue?: string; | ||
}; | ||
|
||
onOptionChanged?: () => void; | ||
} | ||
|
||
const onOptionChanged = jest.fn(); | ||
let component: Component<Options>; | ||
let optionsController: OptionsController<Options>; | ||
|
||
beforeEach(() => { | ||
component = new Component<Options>({ | ||
value: 'initialValue', | ||
objectValue: { nestedValue: 'nestedInitialValue' }, | ||
onOptionChanged, | ||
}); | ||
optionsController = new OptionsController<Options>(component); | ||
onOptionChanged.mockReset(); | ||
}); | ||
|
||
describe('oneWay', () => { | ||
describe('plain', () => { | ||
it('should have initial value', () => { | ||
const value = optionsController.oneWay('value'); | ||
expect(value.unreactive_get()).toBe('initialValue'); | ||
}); | ||
|
||
it('should update on options changed', () => { | ||
const value = optionsController.oneWay('value'); | ||
const fn = jest.fn(); | ||
|
||
value.subscribe(fn); | ||
|
||
component.option('value', 'newValue'); | ||
expect(fn).toHaveBeenCalledTimes(2); | ||
expect(fn).toHaveBeenCalledWith('newValue'); | ||
}); | ||
}); | ||
|
||
describe('nested', () => { | ||
it('should have initial value', () => { | ||
const a = optionsController.oneWay('objectValue.nestedValue'); | ||
expect(a.unreactive_get()).toBe('nestedInitialValue'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('twoWay', () => { | ||
it('should have initial value', () => { | ||
const value = optionsController.twoWay('value'); | ||
expect(value.unreactive_get()).toBe('initialValue'); | ||
}); | ||
|
||
it('should update on options changed', () => { | ||
const value = optionsController.twoWay('value'); | ||
const fn = jest.fn(); | ||
|
||
value.subscribe(fn); | ||
|
||
component.option('value', 'newValue'); | ||
expect(fn).toHaveBeenCalledTimes(2); | ||
expect(fn).toHaveBeenCalledWith('newValue'); | ||
}); | ||
|
||
it('should return new value after update', () => { | ||
const value = optionsController.twoWay('value'); | ||
value.update('newValue'); | ||
|
||
expect(value.unreactive_get()).toBe('newValue'); | ||
}); | ||
|
||
it('should call optionChanged on update', () => { | ||
const value = optionsController.twoWay('value'); | ||
value.update('newValue'); | ||
|
||
expect(onOptionChanged).toHaveBeenCalledTimes(1); | ||
expect(onOptionChanged).toHaveBeenCalledWith({ | ||
component, | ||
fullName: 'value', | ||
name: 'value', | ||
previousValue: 'initialValue', | ||
value: 'newValue', | ||
}); | ||
}); | ||
}); |
171 changes: 171 additions & 0 deletions
171
...evextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
/* eslint-disable spellcheck/spell-checker */ | ||
import { Component } from '@js/core/component'; | ||
import { getPathParts } from '@js/core/utils/data'; | ||
import type { ChangedOptionInfo } from '@js/events'; | ||
import type { | ||
SubsGets, SubsGetsUpd, | ||
} from '@ts/core/reactive/index'; | ||
import { computed, state } from '@ts/core/reactive/index'; | ||
import type { ComponentType } from 'inferno'; | ||
|
||
import { TemplateWrapper } from '../inferno_wrappers/template_wrapper'; | ||
import type { Template } from '../types'; | ||
|
||
type OwnProperty<T, TPropName extends string> = | ||
TPropName extends keyof Required<T> | ||
? Required<T>[TPropName] | ||
: unknown; | ||
|
||
type PropertyTypeBase<T, TProp extends string> = | ||
TProp extends `${infer TOwnProp}.${infer TNestedProps}` | ||
? PropertyTypeBase<OwnProperty<T, TOwnProp>, TNestedProps> | ||
: OwnProperty<T, TProp>; | ||
|
||
type PropertyType<TProps, TProp extends string> = | ||
unknown extends PropertyTypeBase<TProps, TProp> | ||
? unknown | ||
: PropertyTypeBase<TProps, TProp> | undefined; | ||
|
||
type PropertyWithDefaults<TProps, TDefaults, TProp extends string> = | ||
unknown extends PropertyType<TDefaults, TProp> | ||
? PropertyType<TProps, TProp> | ||
: NonNullable<PropertyType<TProps, TProp>> | PropertyTypeBase<TDefaults, TProp>; | ||
|
||
type TemplateProperty<TProps, TProp extends string> = | ||
NonNullable<PropertyType<TProps, TProp>> extends Template<infer TTemplateProps> | ||
? ComponentType<TTemplateProps> | undefined | ||
: unknown; | ||
|
||
function cloneObjectValue<T extends Record<string, unknown> | unknown[]>( | ||
value: T, | ||
): T { | ||
// @ts-expect-error | ||
return Array.isArray(value) ? [...value] : { ...value }; | ||
} | ||
|
||
function updateImmutable<T extends Record<string, unknown> | unknown[]>( | ||
value: T, | ||
newValue: T, | ||
pathParts: string[], | ||
): T { | ||
const [pathPart, ...restPathParts] = pathParts; | ||
const ret = cloneObjectValue(value); | ||
|
||
ret[pathPart] = restPathParts.length | ||
? updateImmutable(value[pathPart], newValue[pathPart], restPathParts) | ||
: newValue[pathPart]; | ||
|
||
return ret; | ||
} | ||
|
||
function getValue<T>(obj: unknown, path: string): T { | ||
let v: any = obj; | ||
for (const pathPart of getPathParts(path)) { | ||
v = v?.[pathPart]; | ||
} | ||
|
||
return v; | ||
} | ||
|
||
export class OptionsController<TProps, TDefaultProps extends TProps = TProps> { | ||
private isControlledMode = false; | ||
|
||
private readonly props: SubsGetsUpd<TProps>; | ||
|
||
private readonly defaults: TDefaultProps; | ||
|
||
public static dependencies = [Component]; | ||
|
||
constructor( | ||
private readonly component: Component<TProps>, | ||
) { | ||
this.props = state(component.option()); | ||
// @ts-expect-error | ||
this.defaults = component._getDefaultOptions(); | ||
this.updateIsControlledMode(); | ||
|
||
component.on('optionChanged', (e: ChangedOptionInfo) => { | ||
this.updateIsControlledMode(); | ||
|
||
const pathParts = getPathParts(e.fullName); | ||
// @ts-expect-error | ||
this.props.updateFunc((oldValue) => updateImmutable( | ||
// @ts-expect-error | ||
oldValue, | ||
component.option(), | ||
pathParts, | ||
)); | ||
}); | ||
} | ||
|
||
private updateIsControlledMode(): void { | ||
const isControlledMode = this.component.option('integrationOptions.isControlledMode'); | ||
this.isControlledMode = (isControlledMode as boolean | undefined) ?? false; | ||
} | ||
|
||
public oneWay<TProp extends string>( | ||
name: TProp, | ||
): SubsGets<PropertyWithDefaults<TProps, TDefaultProps, TProp>> { | ||
const obs = computed( | ||
(props) => { | ||
const value = getValue(props, name); | ||
/* | ||
NOTE: it is better not to use '??' operator, | ||
because result will be different if value is 'null'. | ||
Some code works differently if undefined is passed instead of null, | ||
for example dataSource's getter-setter `.filter()` | ||
*/ | ||
return value !== undefined ? value : getValue(this.defaults, name); | ||
}, | ||
[this.props], | ||
); | ||
|
||
return obs as any; | ||
} | ||
|
||
public twoWay<TProp extends string>( | ||
name: TProp, | ||
): SubsGetsUpd<PropertyWithDefaults<TProps, TDefaultProps, TProp>> { | ||
const obs = state(this.component.option(name)); | ||
this.oneWay(name).subscribe(obs.update.bind(obs) as any); | ||
return { | ||
subscribe: obs.subscribe.bind(obs) as any, | ||
update: (value): void => { | ||
const callbackName = `on${name}Change`; | ||
const callback = this.component.option(callbackName) as any; | ||
const isControlled = this.isControlledMode && this.component.option(name) !== undefined; | ||
if (isControlled) { | ||
callback?.(value); | ||
} else { | ||
// @ts-expect-error | ||
this.component.option(name, value); | ||
callback?.(value); | ||
} | ||
}, | ||
// @ts-expect-error | ||
unreactive_get: obs.unreactive_get.bind(obs), | ||
}; | ||
} | ||
|
||
public template<TProp extends string>( | ||
name: TProp, | ||
): SubsGets<TemplateProperty<TProps, TProp>> { | ||
return computed( | ||
// @ts-expect-error | ||
(template) => template && TemplateWrapper(this.component._getTemplate(template)) as any, | ||
[this.oneWay(name)], | ||
); | ||
} | ||
|
||
public action<TProp extends string>( | ||
name: TProp, | ||
): SubsGets<PropertyWithDefaults<TProps, TDefaultProps, TProp>> { | ||
return computed( | ||
// @ts-expect-error | ||
() => this.component._createActionByOption(name) as any, | ||
[this.oneWay(name)], | ||
); | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
packages/devextreme/js/__internal/grids/new/grid_core/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import type { template } from '@js/core/templates/template'; | ||
|
||
// TODO | ||
export type Template<T> = (props: T) => HTMLDivElement | template; |