Skip to content

Commit

Permalink
added ability to pass native boolean to useConditional hook
Browse files Browse the repository at this point in the history
  • Loading branch information
joepuzzo committed Dec 28, 2022
1 parent b17b7c1 commit 8667d4d
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 4 deletions.
26 changes: 26 additions & 0 deletions .storybook/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -666,3 +666,29 @@ strong {
transform: rotate(360deg);
}
}

table,
th,
td {
border: 1px solid;
}

.field-table table {
margin-top: 18px;
margin-bottom: 18px;
}

.field-table td {
padding: 10px;
}

.field-table th {
padding: 10px;
}

.field-table td input {
width: 120px !important;
border-radius: 0px !important;
margin: 0px !important;
margin-bottom: 0px !important;
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 4.38.0 (Dec 28th, 2022)

### Added

- Ability to pass `native` boolean to `useConditional` hook. Best explained in the conditional docs.

## 4.37.1 (Dec 13th, 2022)

- added missing types to formApi
Expand Down
15 changes: 15 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,21 @@ export function useRelevance(params: {
relevanceDeps?: unknown[];
}): boolean;

export type EvaluateParams = {
formState: FormState;
formApi: FormApi;
scope: string;
dependsOn?: unknown[];
};

export function useConditional(params: {
name: string;
evaluate: (params: EvaluateParams) => unknown;
evaluateWhen: string[];
dependsOn?: unknown[];
native: boolean;
}): unknown;

export function FormStateAccessor({
children
}: {
Expand Down
5 changes: 5 additions & 0 deletions src/FormController.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ export class FormController {
meta.onNativeChange(fieldState, e);
}

// Emit native event
if (e) {
this.emit('field-native', name);
}

if (meta.gatherData) {
// Get error to determine if we even want to validateAsync
this.debouncedGatherInfo(name);
Expand Down
10 changes: 7 additions & 3 deletions src/hooks/useConditional.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const useConditional = ({
name,
evaluate,
evaluateWhen = [],
dependsOn = []
dependsOn = [],
native = false
}) => {
// Grab the form controller
const formController = useFormController();
Expand Down Expand Up @@ -50,17 +51,20 @@ export const useConditional = ({
[...check, scope]
);

const event = native ? 'field-native' : 'field-value';

// Example evaluateWhen = ["name", "age"]
useFieldSubscription(
'field-value',
event,
fields,
target => {
logger(`re-evaluating conditional for ${name} because of ${target}`);
const res = evaluate({
formState: formController.getFormState(),
formApi: formController.getFormApi(),
scope: scopeRef.current,
dependsOn
dependsOn,
target
});
setProps(res);
},
Expand Down
165 changes: 165 additions & 0 deletions stories/Conditionals/TableFields/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Table Forms

Sometimes you need very complex forms where fields depend on each-other much like formulas in Excel.

<!-- STORY -->

```jsx
import {
FormProvider,
Input,
Debug,
Scope,
useFieldApi,
useConditional
} from 'informed';

// prettier-ignore
const markets = [
{ supply: 100000, demand: 70000, cost: 32200, price: 46000, market: "United States", margin: 0.3 },
{ supply: 50000, demand: 60000, cost: 15500, price: 31000, market: "United Kingdom", margin: 0.5 },
{ supply: 80000, demand: 70000, cost: 29600, price: 37000, market: "Japan", margin: 0.2 },
{ supply: 70000, demand: 80000, cost: 25200, price: 42000, market: "China", margin: 0.4 },
{ supply: 60000, demand: 90000, cost: 27300, price: 39000, market: "India", margin: 0.3 },
{ supply: 90000, demand: 60000, cost: 28800, price: 48000, market: "Germany", margin: 0.4 }
];

function Cell({
name,
evaluateWhen,
formula,
data,
readOnly,
type,
step,
native
}) {
const { setValue } = useFieldApi(name);

// Define a function to get called with the given formula whenever the specified fields change
const evaluate = useCallback(({ formApi, scope, target }) => {
if (formula) {
console.log(`Evaluating ${name} because ${target} changed`);
// Get this markets data EX: { supply: 90000, demand: 60000, cost: 28800, price: 48000, market: "Germany", margin: 0.4 }
const market = formApi.getValue(scope);
// Pass the entire market definition to the formula
const result = market ? formula(market) : null;
// If we got a result update its value
if (result) setValue(Math.round(result * 100) / 100);
}
}, []);

// Special hook that will trigger our evaluate function whenever specified fields change
useConditional({
evaluate,
evaluateWhen,
native // VERY IMPORTANT ( prevents infinite loops )
});

return (
<td>
<Input
name={name}
initialValue={data}
type={type}
readOnly={readOnly}
step={step}
/>
</td>
);
}

// Formula definitions
const calculatePrice = ({ margin, cost }) => cost / (1 - margin);
const calculateMargin = ({ price, cost }) => (price - cost) / price;
const calculateDemand = ({ price, market, demand }) => {
const originalMarket = markets.find(m => m.market === market);
const originalPrice = originalMarket.price;
const originalDemand = originalMarket.demand;
const percentChange = (originalPrice - price) / originalPrice;
return originalDemand + originalDemand * percentChange;
};

function MarketsTable({ markets }) {
return (
<table>
<thead>
<tr>
<th>Market</th>
<th>Supply</th>
<th>Demand</th>
<th>Cost</th>
<th>Price</th>
<th>Margin</th>
</tr>
</thead>
<tbody>
{markets.map(market => (
<tr key={market.market}>
<Scope scope={market.market}>
<Cell name="market" data={market.market} readOnly />
<Cell name="supply" data={market.supply} type="number" />
<Cell
name="demand"
data={market.demand}
formula={calculateDemand}
evaluateWhen={['price']}
type="number"
step="1000"
/>
<Cell name="cost" data={market.cost} type="number" step="1000" />
<Cell
name="price"
data={market.price}
formula={calculatePrice}
evaluateWhen={['margin', 'cost']}
type="number"
step="1000"
native
/>
<Cell
name="margin"
data={market.margin}
formula={calculateMargin}
evaluateWhen={['price']}
type="number"
step="0.1"
native
/>
</Scope>
</tr>
))}
</tbody>
</table>
);
}

const TableFields = () => {
return (
<div className="field-table">
<h3>Criteria</h3>
<ol>
<li>Recalculate price when margin or cost change</li>
<li>Recalculate margin when the price changes</li>
<li>Recalculate demand when the price changes</li>
</ol>
<h3>Issues</h3>
<ol>
<li>
When you change margin, that will effect the price, which effects the
margin. ( This is a loop we must prevent )
</li>
<li>
When you change the margin it should affect price which effects the
demand ( this is a transitive change we must allow )
</li>
</ol>
<h3>Form Table</h3>
<FormProvider>
<MarketsTable markets={markets} />
{/* <Debug /> */}
</FormProvider>
</div>
);
};
```
Loading

0 comments on commit 8667d4d

Please sign in to comment.