👋 Let build a currency converter app:
The application should allow to edit the amount in the input fields and change currency. The amount in another input should change in the base of the conversion rate.
For a working example see this codesandbox (It also contains an advanced example).
First of all, we need to define our data domain. We need to take a currency as a reference point, let use USD:
// USD to currency price
const usdRates = {
USD: 1,
BYN: 2.5,
CAD: 1.260046,
CHF: 0.933058,
EUR: 0.806942,
GBP: 0.719154
};
// list of currency names
const availableCurrencies = Object.keys(usdRates);
Now we can setup the root state:
export default function App() {
const state$ = useSubject({
// entered amount expressed in USD
baseAmount: 0,
// list of currently compared currencies
currencies: ["USD", "EUR"]
});
return (/* jsx here */);
}
Yeah, that's all boilerplate we need. And finally some JSX:
<div className="App">
<Currency
amount$={state$.baseAmount}
currency$={state$.currencies[0]}
/>
<Currency
amount$={state$.baseAmount}
currency$={state$.currencies[1]}
/>
</div>
Operation state$.baseAmount
created a read/write lens to baseAmount
property. Calling state$.baseAmount()
will return its current value and state$.baseAmount(1)
will change the baseAmount
value. The update will bubble to the root state, cause encapsulated object is immutable. Also, you can subscribe to this value. This enables 2-way binding.
Same thing for state$.currencies[0]
, it will read/write the first element of the currency
array.
Now let write an incomplete version of the Currency
component.
const Currency = seal(({ amount$, currency$ }) => {
return (
<div>
<Mlyn.select bindValue={currency$}>
{availableCurrencies.map((c) => (
<option key={c}>{c}</option>
))}
</Mlyn.select>
{/* text input code here */}
</div>
);
});
Mlyn.select
is a wrapper over the plain select
element, it has a property bindValue
which accepts a read/write value, and creates a 2-way binding to it. Internally Mlyn.select
will observe currency$
value, and re-render when it's changed. When a selector option will be selected currency$
(and hence the root state) will be updated.
To write the input we can't just bind amount$
to it, cause we need to display the derived value of the currency:
// will not give the expected result,
// cause USD amount will be displayed
<Mlyn.input bindValue={amount$} />
Ok. This will be the hardest part.
One of good things of 2-way binding, is that you can wrap binded value within a function, that will perform read/write derivation logic. So let create a function that will convert amount in a currency to/from USD amount:
// function that will curry references to `baseAmount$`
// and `currency$` subjects
const convertCurrencyAmount = (baseAmount$, currency$) =>
// returns function to use as 2-way bindable value
(...args) => {
// if function has been invoked with params
// checks if it is a write operation
if (args.length > 0) {
const newAmount = parseFloat(args[0]);
// writes new value to the subject
baseAmount$(newAmount / ratesToUSD[currency$()]);
} else {
// it is a a read operation, return converted value
// note that this code will create subscription and
// routing will rerun whenever baseAmount$ or currency$
// values will changed
return baseAmount$() * ratesToUSD[currency$()];
}
};
The above function is a simplified version, in reality we should do some input validation:
const convertCurrencyAmount = (baseAmount$, currency$) =>
(...args) => {
if (args.length > 0) {
// if user erases all text make value 0.
const value = args[0] === "" ? 0 : parseFloat(args[0]);
// skip non-numeric updates
if (!isNaN(value)) {
baseAmount$(value / usdRates[currency$()]);
}
} else {
const newAmount = baseAmount$() * usdRates[currency$()];
// avoid very long numbers like 0.999999999
return Math.round(newAmount * 100) / 100;
}
};
Now you can use pass the converted currency lens to the amount input:
<Mlyn.input
bindValue={convertCurrencyAmount(baseAmount$, currency$)}
/>
For more examples and docs about mlyn, I invite you to check the github repo page.
Top comments (0)