Unverified Commit 4e2995f9 authored by Janne Savolainen's avatar Janne Savolainen
Browse files

Extract business logic from component while trying to solve re-rendering issue

Co-authored-by: default avatarMikko Aspiala <mikko.aspiala@gmail.com>
Signed-off-by: default avatarJanne Savolainen <janne.savolainen@live.fi>
parent 06379b08
Showing with 111 additions and 128 deletions
+111 -128
......@@ -20,9 +20,12 @@
*/
import { NamespaceSelectFilterModel } from "./namespace-select-filter-model";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import namespaceStoreInjectable from "../namespace-store/namespace-store.injectable";
const NamespaceSelectFilterModelInjectable = getInjectable({
instantiate: () => new NamespaceSelectFilterModel(),
instantiate: (di) => new NamespaceSelectFilterModel({
namespaceStore: di.inject(namespaceStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
......
......@@ -18,17 +18,22 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { observable, makeObservable, action } from "mobx";
import { observable, makeObservable, action, untracked } from "mobx";
import type { NamespaceStore } from "../namespace-store/namespace.store";
import type { SelectOption } from "../../select";
import { isMac } from "../../../../common/vars";
interface Dependencies {
namespaceStore: NamespaceStore;
}
export class NamespaceSelectFilterModel {
constructor() {
constructor(private dependencies: Dependencies) {
makeObservable(this, {
menuIsOpen: observable,
closeMenu: action,
openMenu: action,
toggleMenu: action,
isMultiSelection: observable,
setIsMultiSelection: action,
reset: action,
});
}
......@@ -42,13 +47,65 @@ export class NamespaceSelectFilterModel {
this.menuIsOpen = true;
};
toggleMenu = () => {
this.menuIsOpen = !this.menuIsOpen;
get selectedNames() {
return untracked(() => this.dependencies.namespaceStore.selectedNames);
}
isSelected = (namespace: string | string[]) =>
this.dependencies.namespaceStore.hasContext(namespace);
selectSingle = (namespace: string) => {
this.dependencies.namespaceStore.selectSingle(namespace);
};
selectAll = () => {
this.dependencies.namespaceStore.selectAll();
};
onChange = ([{ value: namespace }]: SelectOption[]) => {
if (namespace) {
if (this.isMultiSelection) {
this.dependencies.namespaceStore.toggleSingle(namespace);
} else {
this.dependencies.namespaceStore.selectSingle(namespace);
}
} else {
this.dependencies.namespaceStore.selectAll();
}
};
isMultiSelection = false;
onClick = () => {
if (!this.menuIsOpen) {
this.openMenu();
} else if (!this.isMultiSelection) {
this.closeMenu();
}
};
private isMultiSelection = false;
setIsMultiSelection = (isMultiSelection: boolean) => {
this.isMultiSelection = isMultiSelection;
onKeyDown = (event: React.KeyboardEvent) => {
if (isSelectionKey(event)) {
this.isMultiSelection = true;
}
};
onKeyUp = (event: React.KeyboardEvent) => {
if (isSelectionKey(event)) {
this.isMultiSelection = false;
}
};
reset = () => {
this.isMultiSelection = false;
this.closeMenu();
};
}
const isSelectionKey = (event: React.KeyboardEvent): boolean => {
if (isMac) {
return event.key === "Meta";
}
return event.key === "Control"; // windows or linux
};
......@@ -22,155 +22,80 @@
import "./namespace-select-filter.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { observer } from "mobx-react";
import { components, PlaceholderProps } from "react-select";
import { action, makeObservable, observable, reaction } from "mobx";
import { Icon } from "../icon";
import { NamespaceSelect } from "./namespace-select";
import type { NamespaceStore } from "./namespace-store/namespace.store";
import type { SelectOption, SelectProps } from "../select";
import { isMac } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
import type { NamespaceSelectFilterModel } from "./namespace-select-filter-model/namespace-select-filter-model";
import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model/namespace-select-filter-model.injectable";
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
interface Dependencies {
model: NamespaceSelectFilterModel,
namespaceStore: NamespaceStore
model: NamespaceSelectFilterModel;
}
@observer
class NonInjectedNamespaceSelectFilter extends React.Component<SelectProps & Dependencies> {
/**
* Only updated on every open
*/
private selected = observable.set<string>();
private didToggle = false;
constructor(props: SelectProps & Dependencies) {
super(props);
makeObservable(this);
}
get model() {
return this.props.model;
}
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.model.menuIsOpen, newVal => {
if (newVal) { // rising edge of selection
this.selected.replace(this.props.namespaceStore.selectedNames);
this.didToggle = false;
}
}),
]);
}
formatOptionLabel = ({ value: namespace, label }: SelectOption) => {
if (namespace) {
const isSelected = this.props.namespaceStore.hasContext(namespace);
return (
<div className="flex gaps align-center">
<Icon small material="layers" />
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right" />}
</div>
);
}
return label;
};
@action
onChange = ([{ value: namespace }]: SelectOption[]) => {
if (namespace) {
if (this.model.isMultiSelection) {
this.didToggle = true;
this.props.namespaceStore.toggleSingle(namespace);
} else {
this.props.namespaceStore.selectSingle(namespace);
}
} else {
this.props.namespaceStore.selectAll();
}
};
private isSelectionKey(e: React.KeyboardEvent): boolean {
if (isMac) {
return e.key === "Meta";
}
return e.key === "Control"; // windows or linux
}
@action
onKeyDown = (e: React.KeyboardEvent) => {
if (this.isSelectionKey(e)) {
this.model.setIsMultiSelection(true);
}
};
@action
onKeyUp = (e: React.KeyboardEvent) => {
if (this.isSelectionKey(e)) {
this.model.setIsMultiSelection(false);
}
if (!this.model.isMultiSelection && this.didToggle) {
this.model.closeMenu();
}
};
@action
onClick = () => {
if (!this.model.menuIsOpen) {
this.model.openMenu();
} else if (!this.model.isMultiSelection) {
this.model.toggleMenu();
}
};
reset = () => {
this.model.setIsMultiSelection(true);
this.model.closeMenu();
};
class NonInjectedNamespaceSelectFilter extends React.Component<
SelectProps & Dependencies
> {
render() {
return (
<div onKeyUp={this.onKeyUp} onKeyDown={this.onKeyDown} onClick={this.onClick}>
<div
onKeyUp={this.props.model.onKeyUp}
onKeyDown={this.props.model.onKeyDown}
onClick={this.props.model.onClick}
>
<NamespaceSelect
isMulti={true}
menuIsOpen={this.model.menuIsOpen}
menuIsOpen={this.props.model.menuIsOpen}
components={{ Placeholder }}
showAllNamespacesOption={true}
closeMenuOnSelect={false}
controlShouldRenderValue={false}
placeholder={""}
onChange={this.onChange}
onBlur={this.reset}
formatOptionLabel={this.formatOptionLabel}
onChange={this.props.model.onChange}
onBlur={this.props.model.reset}
formatOptionLabel={formatOptionLabelFor(this.props.model)}
className="NamespaceSelectFilter"
menuClass="NamespaceSelectFilterMenu"
sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)}
sort={(left, right) =>
+this.props.model.selectedNames.has(right.value) -
+this.props.model.selectedNames.has(left.value)
}
/>
</div>
);
}
}
const formatOptionLabelFor =
(model: NamespaceSelectFilterModel) =>
({ value: namespace, label }: SelectOption) => {
if (namespace) {
const isSelected = model.isSelected(namespace);
return (
<div className="flex gaps align-center">
<Icon small material="layers" />
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right" />}
</div>
);
}
return label;
};
export const NamespaceSelectFilter = withInjectables<Dependencies, SelectProps>(
NonInjectedNamespaceSelectFilter,
observer(NonInjectedNamespaceSelectFilter),
{
getProps: (di, props) => ({
model: di.inject(namespaceSelectFilterModelInjectable),
namespaceStore: di.inject(namespaceStoreInjectable),
...props,
}),
},
......@@ -179,7 +104,7 @@ export const NamespaceSelectFilter = withInjectables<Dependencies, SelectProps>(
type CustomPlaceholderProps = PlaceholderProps<any, boolean>;
interface PlaceholderDependencies {
namespaceStore: NamespaceStore
namespaceStore: NamespaceStore;
}
const NonInjectedPlaceholder = observer(
......@@ -206,7 +131,6 @@ const NonInjectedPlaceholder = observer(
},
);
const Placeholder = withInjectables<PlaceholderDependencies, CustomPlaceholderProps>(
NonInjectedPlaceholder,
......@@ -217,4 +141,3 @@ const Placeholder = withInjectables<PlaceholderDependencies, CustomPlaceholderPr
}),
},
);
......@@ -89,7 +89,7 @@ class NonInjectedNamespaceSelect extends React.Component<Props & Dependencies> {
};
render() {
const { className, showIcons, customizeOptions, components = {}, ...selectProps } = this.props;
const { className, showIcons, customizeOptions, components = {}, namespaceStore, ...selectProps } = this.props;
return (
<Select
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment