create mobx reacting effects

This commit is contained in:
Chika 2020-09-01 21:14:24 +03:00
parent 5f479c1fb6
commit 2109523a52
21 changed files with 214 additions and 170 deletions

View File

@ -29,7 +29,6 @@
"normalize.css": "^8.0.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",

View File

@ -36,7 +36,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Evo Calculator</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,54 +1,46 @@
import CalculationStore from "client/stores/CalculationStore";
import withTitle from "client/hocs/withTitle";
import { testEffectForPrice, testEffectForOne } from "client/Effects";
import Input from "client/Elements/Input";
import CalculationStore from 'client/stores/CalculationStore';
import withTitle from 'client/hocs/withTitle';
import Input from 'client/Elements/Input';
export default [
{
title: "FirstSection",
title: 'FirstSection',
elements: [
{
name: "tbxPrice",
Component: withTitle("Price")(Input),
name: 'tbxPrice',
Component: withTitle('Price')(Input),
props: {
placeholder: "Enter price",
sourceValueName: "price",
Effects: [testEffectForPrice],
},
placeholder: 'Enter price',
sourceValueName: 'price'
}
},
{
name: "tbxOne",
Component: withTitle("One")(Input),
name: 'tbxOne',
Component: withTitle('One')(Input),
props: {
placeholder: "Enter one",
sourceValueName: "one",
Effects: [testEffectForOne],
},
placeholder: 'Enter one',
sourceValueName: 'one'
}
},
{
name: "tbxSecond",
Component: withTitle("Sum")(Input),
name: 'total',
Component: withTitle('Total')(Input),
props: {
readonly: true,
getValue: (calculationStore: CalculationStore) => {
const price = calculationStore.getValue("price");
const one = calculationStore.getValue("one");
return parseInt(price) + parseInt(one);
},
},
},
],
computed: 'total'
}
}
]
},
{
title: "SecondSection",
title: 'SecondSection',
elements: [
{
name: "priceonAnotherTab",
Component: withTitle("Price on another tab")(Input),
name: 'priceonAnotherTab',
Component: withTitle('Price on another tab')(Input),
props: {
sourceValueName: "price",
},
},
],
},
sourceValueName: 'price'
}
}
]
}
];

View File

@ -1,9 +1,9 @@
import Background from "client/Elements/Background";
import React, { useState } from "react";
import Sections from "./Sections";
import Background from 'client/Elements/Background';
import React, { useState } from 'react';
import Sections from './Sections';
import { Tabs } from "antd";
import { Box } from "client/UIKit/grid";
import { Tabs } from 'antd';
import { Box } from 'client/UIKit/grid';
const { TabPane } = Tabs;
const Calculation = () => {
@ -14,8 +14,8 @@ const Calculation = () => {
<TabPane tab={title} key={i}>
{elements.map(({ Component, props }, ie) => {
return (
<Box width="250px" my="10px">
<Component {...props} key={ie} />
<Box width="250px" my="10px" key={ie}>
<Component {...props} />
</Box>
);
})}

View File

@ -1,21 +0,0 @@
import CalculationStore from "client/stores/CalculationStore";
import { TEffect } from "core/types/effect";
export const testEffectForPrice: TEffect = async (
calculationStore: CalculationStore
) => {
console.log("priceEffect");
const price = calculationStore.getValue("price");
if (parseInt(price) > 5000) {
calculationStore.setValue("one", 200);
}
};
export const testEffectForOne: TEffect = async (
calculationStore: CalculationStore
) => {
const one = calculationStore.getValue("one");
if (parseInt(one) > 1000) {
calculationStore.setValue("price", 100500);
}
};

View File

@ -1,47 +1,35 @@
import { Input as AntInput } from "antd";
import { useStores } from "client/hooks/useStores";
import runEffects from "client/tools/runEffects";
import { observer } from "mobx-react";
import React, { useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import { Input as AntInput } from 'antd';
import { useStores } from 'client/hooks/useStores';
import { observer } from 'mobx-react';
import React, { useEffect, useState } from 'react';
import { useDebounce } from 'use-debounce';
const Input = ({
readonly,
placeholder,
sourceValueName,
getValue,
Effects,
}) => {
const Input = ({ readonly, placeholder, sourceValueName, computed }) => {
const { calculationStore } = useStores();
const sourceValue = calculationStore.getValue(sourceValueName);
const [currentValue, setCurrentValue] = useState(undefined);
const [debouncedValue] = useDebounce(currentValue, 850);
// get Values
useEffect(() => {
if (sourceValueName) {
if (sourceValue) {
setCurrentValue(sourceValue);
}
}
}, [sourceValue, sourceValueName]);
const sourceValue = calculationStore.values[sourceValueName];
// set Value to global store and run Effects
// get value from store
useEffect(() => {
if (sourceValueName && debouncedValue) {
if (!computed) {
setCurrentValue(sourceValue);
}
}, [computed, sourceValue]);
// set value to store
useEffect(() => {
if (!computed) {
calculationStore.setValue(sourceValueName, debouncedValue);
}
if (Effects && Effects.length > 0) {
runEffects(Effects, calculationStore);
}
}, [debouncedValue]);
}, [calculationStore, computed, debouncedValue, sourceValueName]);
return (
<AntInput
placeholder={placeholder}
value={getValue ? getValue(calculationStore) : currentValue}
onChange={(e) => {
value={computed ? calculationStore[computed]() : currentValue}
onChange={e => {
if (!readonly) {
setCurrentValue(e.target.value);
}

View File

@ -1,9 +1,9 @@
import paths from "client/common/paths";
import { LogoText } from "client/Elements/Text";
import colors from "client/UIKit/colors";
import { Box, Flex } from "client/UIKit/grid";
import React from "react";
import { Route, Switch } from "react-router-dom";
import paths from 'client/common/paths';
import { LogoText } from 'client/Elements/Text';
import colors from 'client/UIKit/colors';
import { Box, Flex } from 'client/UIKit/grid';
import React from 'react';
import { Route, Switch } from 'react-router-dom';
const Header = () => (
<Flex style={styles.header}>
@ -43,18 +43,17 @@ const Layout = () => (
const styles = {
root: {
height: "100%",
height: '100%'
},
flex: { width: "100%" },
flex: { width: '100%' },
header: {
// backgroundColor: colors.violet.shades[700],
background:
"linear-gradient(90deg, rgba(69,0,198,1) 0%, rgba(205,0,231,1) 60%, rgba(163,2,184,1) 100%)",
height: "50px",
padding: "10px 12px",
paddingLeft: "20px",
background: 'linear-gradient(90deg, #1C01A9 0%, #3A0185 50%, #580161 100%)',
height: '50px',
padding: '10px 12px',
paddingLeft: '20px'
// borderRadius: "0 0 10px 10px",
},
}
};
export default Layout;

View File

@ -1,24 +0,0 @@
import initialStatuses from "core/config/initialStatuses";
import initialValues from "core/config/initialValues";
import { SourceValueNames } from "core/types/values";
import { action, observable } from "mobx";
import { Status } from "core/types/elements";
class CalculationStore {
values = observable(initialValues);
statuses = observable(initialStatuses);
getValue = (sourceValueName: SourceValueNames) =>
this.values[sourceValueName];
setValue = action((sourceValueName: SourceValueNames, newValue: any) => {
this.values[sourceValueName] = newValue;
// TODO: Run effect here
});
getStatus = (elementName: string) => this.statuses[elementName];
setStatus = action((elementName: string, status: Status) => {
this.statuses[elementName] = status;
});
}
export default CalculationStore;

View File

@ -0,0 +1,11 @@
import { IAutorunEffect } from 'core/types/effect';
const autorunEffects: IAutorunEffect[] = [
// calculationStore => () => {
// if (parseInt(calculationStore.values.price) > 25) {
// calculationStore.setValue('one', 100500);
// }
// }
];
export default autorunEffects;

View File

@ -0,0 +1,9 @@
const computedEffects = {
total() {
const one = parseInt(this.values.one || 0);
const price = parseInt(this.values.price || 0);
return one + price;
}
};
export default computedEffects;

View File

@ -0,0 +1,20 @@
import CommonStore from '../../CommonStore';
import { autorun, reaction, when } from 'mobx';
import CalculationStore from '..';
import autorunEffects from './autorun';
import reactionEffects from './reaction';
import whenEffects from './when';
autorunEffects.map(autorunEffect =>
autorun(autorunEffect(CalculationStore, CommonStore))
);
reactionEffects.map(reactionEffectBuilder => {
const reactionEffect = reactionEffectBuilder(CalculationStore);
return reaction(reactionEffect.expression, reactionEffect.effect);
});
whenEffects.map(whenEffectBuilder => {
const whenEffect = whenEffectBuilder(CalculationStore);
return when(whenEffect.predicate, whenEffect.effect);
});

View File

@ -0,0 +1,10 @@
import { IReactionEffect } from 'core/types/effect';
const reactionEffects: IReactionEffect[] = [
calculationStore => ({
expression: () => calculationStore.values.one,
effect: value => console.log(value)
})
];
export default reactionEffects;

View File

@ -0,0 +1,10 @@
import { IWhenEffect } from 'core/types/effect';
const whenEffects: IWhenEffect[] = [
calculationStore => ({
predicate: () => parseInt(calculationStore.values.one) === 5,
effect: () => console.log(calculationStore.values.one)
})
];
export default whenEffects;

View File

@ -1,3 +1,35 @@
import CalculationStore from "./CalculationStore";
import assignProperties from 'client/tools/assignProps';
import initialStatuses from 'core/config/initialStatuses';
import initialValues from 'core/config/initialValues';
import { Status } from 'core/types/elements';
import { SourceValueNames } from 'core/types/values';
import { observable } from 'mobx';
import computedEffects from './Effects/computed';
const CalculationStore = observable(
assignProperties(
{
values: initialValues,
statuses: initialStatuses,
getValue(sourceValueName: SourceValueNames) {
return this.values[sourceValueName];
},
getStatus(elementName: string) {
return this.statuses[elementName];
},
setValue(sourceValueName: SourceValueNames, newValue: any) {
this.values[sourceValueName] = newValue;
},
setStatus(elementName: string, status: Status) {
this.statuses[elementName] = status;
}
},
computedEffects
)
);
console.log(CalculationStore);
export default CalculationStore;

View File

@ -1,3 +1,5 @@
class CommonStore {}
import { observable } from 'mobx';
const CommonStore = observable({});
export default CommonStore;

View File

@ -0,0 +1,12 @@
import CalculationStore from './CalculationStore';
import CommonStore from './CommonStore';
import './CalculationStore/Effects';
class RootStore {
constructor() {
this.calculationStore = CalculationStore;
this.commonStore = CommonStore;
}
}
export default RootStore;

View File

@ -1,14 +0,0 @@
import CalculationStore from "./CalculationStore";
import CommonStore from "client/stores/CommonStore";
class RootStore {
calculationStore: CalculationStore;
commonStore: CommonStore;
constructor() {
this.calculationStore = new CalculationStore();
this.commonStore = new CommonStore();
}
}
export default RootStore;

View File

@ -0,0 +1,14 @@
function assignProperties(target, ...sources) {
sources.forEach(source => {
Object.defineProperties(
target,
Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {})
);
});
return target;
}
export default assignProperties;

View File

@ -1,13 +0,0 @@
async function runEffects(effects, calculationStore) {
if (effects && effects.length > 0) {
for (let effect of effects) {
try {
await effect(calculationStore);
} catch (error) {
throw error;
}
}
}
}
export default runEffects;

View File

@ -1,3 +1,24 @@
import CalculationStore from "client/stores/CalculationStore";
import CalculationStore from 'client/stores/CalculationStore';
import CommonStore from 'client/stores/CommonStore';
import { IReactionPublic, Lambda } from 'mobx';
export type TEffect = (calculationStore: CalculationStore) => Promise<void>;
type TCalculationStore = typeof CalculationStore;
type TCommonStore = typeof CommonStore;
export interface IAutorunEffect {
(CalculationStore: TCalculationStore, CommonStore?: TCommonStore): () => void;
}
export interface IReactionEffect {
(CalculationStore: TCalculationStore): {
expression: (r: IReactionPublic) => any;
effect: (arg: any, r: IReactionPublic) => void;
};
}
export interface IWhenEffect {
(CalculationStore: TCalculationStore): {
predicate: () => boolean;
effect: Lambda;
};
}

View File

@ -1,7 +1,4 @@
import CalculationStore from "client/stores/CalculationStore";
import { SourceValueNames } from "core/types/values";
export enum Status {
Default,
Disabled,
Disabled
}