Currency converter app in React and Mlyn

đź‘‹ Let build a currency converter app:
App example
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.

28