Categories
JavaScript React

How to Build Ionic Apps with React

With the latest version of the Ionic framework, a tool that lets you write mobile apps with web technologies, you can use React to build your mobile application. Before v4, Ionic only provided components for Angular, but now React has complete support for the components.

Ionic 4 is a mobile app framework and a component library. You can build mobile apps, progressive web apps, and normal web apps. The component library can also be used on its own. Of course, the hardware support remains in place for you to use if you wish.

The full reference for the React version of Ionic is at https://ionicframework.com/docs.

In this article, we will build a React web app with Ionic that does currency conversion. The home page will display the list of the latest exchange rates and another page will have a form to convert the currencies of your choice. To get the data, we use the Foreign exchange rates API located at https://exchangeratesapi.io/. We use the Open Exchange Rates API located at https://openexchangerates.org/ to get the list of currencies.

Getting Started

To start, we will install the Ionic CLI. We run:

npm install -g ionic@latest

Next, we run:

ionic start currency-converter --type=react

Then we select the Sidenav option to create an Ionic project with a left sidebar.

We will also need to install some other packages. We need Axios to make HTTP requests and MobX for state management. Run npm i axios mobx mobx-react in our project folder to install them.

Now we are ready to create some pages. In the pages folder, create ConvertCurrencyPage.jsx and add:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonList,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonInput,
  IonLabel,
  IonSelect,
  IonSelectOption,
  IonButton
} from "@ionic/react";
import React from "react";
import { observer } from "mobx-react";
import { getExchangeRates } from "../requests";
const ConvertCurrencyPage = ({ currenciesStore }) => {
  const [fromCurrencies, setFromCurrencies] = React.useState({});
  const [toCurrencies, setToCurrencies] = React.useState({});
  const [values, setValues] = React.useState({ amount: 0 } as any);
  const [submitting, setSubmitting] = React.useState(false);
  const [toAmount, setToAmount] = React.useState(0);
const convertCurrency = async () => {
    setSubmitting(true);
    if (values.amount <= 0 || !values.from || !values.to) {
      return;
    }
    const { data } = await getExchangeRates(values.from);
    const rate = data.rates[values.to];
    setToAmount(values.amount * rate);
  };

  React.useEffect(() => {
    const fromCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.to) {
        fromCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setFromCurrencies(fromCurrencies);

  const toCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.from) {
        toCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setToCurrencies(toCurrencies);
  }, [currenciesStore.currencies, values.from, values.to]);

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Convert Currency</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonItem>
            <IonInput
              type="number"
              value={values.amount}
              color={!values.amount && submitting ? "danger" : undefined}
              min="0"
              onIonChange={ev =>
                setValues({ ...values, amount: (ev.target as any).value })
              }
            ></IonInput>
          </IonItem>
          <IonItem>
            <IonLabel>Currency to Convert From</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.from && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, from: (ev.target as any).value })
              }
            >
              {Object.keys(fromCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(fromCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
          <IonItem>
            <IonLabel>Currency to Convert To</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.to && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, to: (ev.target as any).value })
              }
            >
              {Object.keys(toCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(toCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
          <IonItem>
            <IonButton size="default" fill="solid" onClick={convertCurrency}>
              Convert
            </IonButton>
          </IonItem>
          {toAmount ? (
            <IonItem>
              {values.amount} {values.from} is {toAmount} {values.to}.
            </IonItem>
          ) : (
            undefined
          )}
        </IonList>
      </IonContent>
    </IonPage>
  );
};
export default observer(ConvertCurrencyPage);

This adds a form to convert currency from one to another. In this form, we filter by choices by excluding from the currency being converted to from the first drop down and exclude the currency being converted from in the second dropdown. Also, we have an ion-input for the amount being converted. We get the values of the currency from the currenciesStore, which is the MobX store that gets the list of currencies from. In the IonSelect components we set the onIonChange props to the handler functions that set the drop down’s values. We also set the placeholder for all the inputs and selects. In the IonInput component, we do the same with the onIonChange handler. We show the correct value by using the variables set in the values object during change events.

When the user clicks Convert, we run the convertCurrency function. We check if the values are set correctly before running the rest of the code. If that succeeds, then we run the getExchangeRates function imported from requests.js, then we set the final toAmount by multiplying the rate with the amount.

The useEffect callback is used for excluding the currency to convert from the currency to convert to list and vice versa. The array in the second argument useEffect specifies which values to watch for.

The observer function in the last line is for designating the component to watch the latest values from the MobX stores.

Next in Home.tsx, we replace the existing code with:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonLabel,
  IonList,
  IonListHeader,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonSelect,
  IonSelectOption
} from "@ionic/react";
import React from "react";
import "./Home.css";
import { getExchangeRates } from "../requests";
import { CurrencyStore } from "../stores";
import { observer } from "mobx-react";
const HomePage = ({ currencyStore, currenciesStore }) => {
  const [rates, setRates] = React.useState({});
  const getRates = async () => {
    const { data } = await getExchangeRates(
      (currencyStore as CurrencyStore).currency
    );
    setRates(data.rates);
  };

  React.useEffect(() => {
    getRates();
  }, [(currencyStore as CurrencyStore).currency]);
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Home</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonListHeader>Latest Exchange Rates</IonListHeader>
          <IonItem>
            <IonLabel>Currency</IonLabel>
            <IonSelect
              placeholder="Select One"
              onIonChange={ev => {
                (currencyStore as CurrencyStore).setCurrency(
                  ev.target && (ev.target as any).value
                );
              }}
            >
              {Object.keys(currenciesStore.currencies).map(key => {
                return (
                  <IonSelectOption
                    value={key}
                    key={key}
                    selected={
                      (currencyStore as CurrencyStore).currency
                        ? key == (currencyStore as CurrencyStore).currency
                        : key == "AUD"
                    }
                  >
                    {(currenciesStore.currencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
        </IonList>
        <IonList lines="none">
          <IonListHeader>Exchange Rates</IonListHeader>
          {Object.keys(rates).map(key => {
            console.log(rates);
            return (
              <IonItem>
                {key}: {rates[key]}
              </IonItem>
            );
          })}
        </IonList>
      </IonContent>
    </IonPage>
  );
};
export default observer(HomePage);

In this file, we display the exchange rates from an API. We get the currencies from the currenciesStore so that users can see exchange rates based on different currencies. The items are displayed in an IonList provided by Ionic.

The observer function in the last line is for designating the component to watches the latest values from the MobX stores.

Next in App.tsx, replace the following code with:

import React from "react";
import { Redirect, Route } from "react-router-dom";
import { IonApp, IonRouterOutlet, IonSplitPane } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { AppPage } from "./declarations";
import Menu from "./components/Menu";
import Home from "./pages/Home";
import { home, cash } from "ionicons/icons";
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
/* Theme variables */
import "./theme/variables.css";
import ConvertCurrencyPage from "./pages/ConvertCurrencyPage";
import { CurrencyStore, CurrenciesStore } from "./stores";
import { getCurrenciesList } from "./requests";
const currencyStore = new CurrencyStore();
const currenciesStore = new CurrenciesStore();
const appPages: AppPage[] = [
  {
    title: "Home",
    url: "/home",
    icon: home
  },
  {
    title: "Convert Currency",
    url: "/convertcurrency",
    icon: cash
  }
];
const App: React.FC = () => {
  const [initialized, setInitialized] = React.useState(false);
  const getCurrencies = async () => {
    const { data } = await getCurrenciesList();
    currenciesStore.setCurrencies(data);
    setInitialized(true);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCurrencies();
    }
  });
  return (
    <IonApp>
      <IonReactRouter>
        <IonSplitPane contentId="main">
          <Menu appPages={appPages} />
          <IonRouterOutlet id="main">
            <Route
              path="/home"
              render={() => (
                <Home
                  currencyStore={currencyStore}
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route
              path="/convertcurrency"
              render={() => (
                <ConvertCurrencyPage
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route exact path="/" render={() => <Redirect to="/home" />} />
          </IonRouterOutlet>
        </IonSplitPane>
      </IonReactRouter>
    </IonApp>
  );
};
export default App;

We modified the routes so that we use the render prop instead of the component prop since we want to pass in our MobX stores into the components. The stores contain the currently selected currency for the home page and the list of currencies for both pages.

We get/set the list of currencies here for both components.

Next, create requests.ts in the src folder and add:

const axios = require('axios');
const APIURL = 'https://api.exchangeratesapi.io';
const OPEN_EXCHANGE_RATES_URL = 'http://openexchangerates.org/api/currencies.json';

export const getExchangeRates = (baseCurrency: string) => axios.get(`${APIURL}/latest?base=${baseCurrency}`)
export const getCurrenciesList = () => axios.get(OPEN_EXCHANGE_RATES_URL)

This the code for making the HTTP requests to get the currencies and exchange rates.

Next create store.ts and add:

import { observable, action } from "mobx";
class CurrencyStore {
    @observable currency: string = 'AUD';
    @action setCurrency(currency: string) {
        this.currency = currency;
    }
}
class CurrenciesStore {
    @observable currencies = {};
    @action setCurrencies(currencies) {
        this.currencies = currencies;
    }
}
export { CurrencyStore, CurrenciesStore };

The CurrencyStore is for storing the selected currency in the home page and to show the exchange rates based on the selected currency. currency is the value observed by the home page and setCurrency sets the currency value. Similarly, CurrenciesStore stores a list of currencies retrieved in App.tsx where setCurrencies is called.

Next in tsconfig.json, we replace the existing code with:

{
  "compilerOptions": {
      "experimentalDecorators": true,
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "noImplicitAny": false
  },
  "include": [
    "src"
  ]
}

This changes noImplicitAny and sets it to true.

Categories
JavaScript

Using the URLSearchParams Object in JavaScript — Part 2

Working with query strings can be a pain if we have to write the code from scratch to parse and modify it. Fortunately, most recent browsers have the URLSearchParams object that can let us deal with query strings easily. With it, is makes it easy to parse and manipulate query strings. We no longer need a third party library or write code from scratch to deal with query strings anymore. In this article, we continue from part 1 of this guide.

We can create a URLSearchParams object with the URLSearchParams constructor. The URLSearchParams constructor takes one optional argument, which is a USVString argument that has the query string. USVString objects correspond to the set of all possible sequences of Unicode scalar values. In our code, we can treat them the same as regular strings. It can be a sequence of USVStrings or a record that contains USVStrings. In our code, we don’t have to concerned with USVStrings. For all practical purposes, they’re treated as like strings. We can construct a URLSearchParams object like the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
console.log(params.get('key1'));
console.log(params.get('key2'));

There are other ways to create URLSearchParams object which involves passing in other kinds of objects. To see the details see the Part 1 of this guide.

More Methods

Each URLSearchParams instance has multiple methods available to it to get the keys and values and to manipulate them by adding, changing and removing keys or values. With these methods, we can construct query strings easily without having to do string manipulating directly. If we want to get back the full query string after manipulating, we can get it with the toString() method.

get

The get method of the URLSearchParams object let us get a value of the query string by the given key. It takes one argument, which is the string with the key name. It returns the USVString of the value if the search parameter is found, otherwise, it returns null . For example, if we have the following query string and URLSearchParams object like in the code below:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
console.log(params.get('key1'));

Then we get 1 from the console.log statement we have above. If we have more than one key-value pair with the same key like in the following example:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 2);
params.append('key1', 3);
console.log(params.get('key1'));

Then we get the first value that’s present as the value returned from the get method. If we want to get all the values associated with the given key, we can use the getAll method. It’s the same even if we didn’t use the append method to add more key-value pairs to the query string:

const url = new URL('https://example.com?key1=1&key1=2&key1=3&key2=2');
const params = new URLSearchParams(url.search);
console.log(params.get('key1'));

In the above example, we still get 1 from the console.log output above.

If we try to get a value with a key that didn’t exist, then we get null returned, like in the example below:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
console.log(params.get('abc'));

We get null from the console.log statement above since we don’t have a search parameter with the key 'abc' .

getAll

With the getAll method, we can get all the values with the associated key name. It takes one argument, which is a string with the key name and returns USVString array with all the values associated with the key name. For example, if we have the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 2);
params.append('key1', 3);
console.log(params.getAll('key1'));

Then we get [“1”, “2”, “3”] from the console.log statement above.

has

The has method returns a boolean value that is true if the value with the given key exists. It takes one argument, which is a string with the key name that we want to look up. For example, we can use the has method like in the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 2);
params.append('key1', 3);
console.log(params.has('key1'));
console.log(params.has('abc'));

When we run the code above, we get that the first console.log statement will output true while the second one will output false . This is what we expect since we have ‘key1’ as the key of one or more search parameters, but we don’t have any search parameter with 'abc' as the key.

keys

We can use the keys method to get an iterator which let us iterate through all the search parameter keys in our query string. The keys are all USVString objects. It takes no arguments and returns an iterator which let us iterate through the search parameter keys. For example, we can write:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
for (const key of params.keys()) {
  console.log(key);
}

Then we get the following out from the console.log statements above:

key1  
key2

set

The set method of the URLSeacrchParam instance let us set the value with the given search parameter key. It takes 2 arguments. The first is a string with the search parameter key, and the second is the value that you want to set as the value with the given key. If there’re several values with the same key, then they’ll be deleted. If the search parameter with the given key doesn’t exist, then this method will create it.

For example, we can use it like the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 2);
params.append('key1', 3);
params.set('key1', 5);
console.log(params.getAll('key1'));
console.log(params.toString());

If we run the code above, will get the array[“5”] from the first console.log statement above. All the other values for key1 are removed as we expected. The second console.log statement will get us key1=5&key2=2 which matches what we have from the getAll method. We can also use it to create a new search parameter key-value pair, like we do with the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 2);
params.append('key1', 3);
params.set('abc', 1);
console.log(params.getAll('abc'));
console.log(params.toString());

After we run the code above, we get back [“1”] from the first console.log statement, which means the search parameter with the key 'abc' and the value 1 is created, like we expected. The query string that we get when we use the toString() method is key1=1&key2=2&key1=2&key1=3&abc=1 , which is consistent with what we get from the getAll() method.

sort

We can sort the key-value pairs of a URLSearchParams object by its keys with the sort() method. The sort order is determined by the Unicode code points of the keys. The relative order between key-value pairs with the same keys will be preserved. This method takes no arguments and returns undefined . For instance, we can use it like in the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 2);
params.append('key1', 3);
params.set('abc', 1);
params.sort();
console.log(params.toString());

If we run the code above, we get:

abc=1&key1=1&key1=2&key1=3&key2=2

from the console.log statement’s output, which is what we expect since 'abc' has lower code points than 'key1' and ‘key1' has lower code point value than ‘key2' .

toString

As we can see from the earlier examples, the toString() method returns the query string which we can put in a URL. The query string returned will be include the results after manipulation of the URLSearchParams object are done. Note that the returned query string will not have the question mark in front of it, unlike what’s returned from window.location.searh. For example, if we have:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key3', 3);
params.append('key4', 4);
params.set('key5', 5);
console.log(params.toString());

Then we get back:

'key1=1&key2=2&key3=3&key4=4&key5=5'

Which is what we expected. We have all the search parameters we parsed from the URL object and whatever we append with the URLSearchParams object’s append() method and also what we added with the set() method.

values

The values method returns an iterator that let us iterate through all the values contained in the URLSearchParams object. Iterators can be iterated through by the for...of loop and operated on by the spread operator. It takes no arguments. For example, if we have the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key3', 3);
params.append('key4', 4);
params.set('key5', 5);
for (const value of params.values()) {
  console.log(value);
}

Then we get the following output from the console.log statements in the loop:

1  
2  
3  
4  
5

Which are the values that we set in the search parameters.

Catches When Using URLSearchParams Object

The URLSearchParams constructor can’t parse full URLs. So something like:

const params = new URLSearchParams('https://example.com?key1=1&key2=2');
for (const [key, value] of params.entries()) {
  console.log(key, value);
}

will not work. When we run the code above, we get the following outputs from the console.log statements:

https://example.com?key1 1
key2 2

As we can see, the whole URL before the first equal sign was parsed as the key of the first search parameter, which is wrong. This means that we have to use the URL object’s search property to get the query string and pass it in like we do in the example below:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
for (const [key, value] of params.entries()) {
  console.log(key, value);
}

If we run the code above, we get the following output from the console.log statements:

key1 1  
key2 2

The output above is what we want since these are the actual search parameter key-value pairs.

Most recent browsers have the URLSearchParams object that can let us deal with query strings easily. Now working with query strings is no longer a pain since we don’t have to write the code from scratch to parse and modify it. With URLSearchParams objects, is makes it easy to parse and manipulate query strings. We no longer need a third party library or write code from scratch to deal with query strings anymore. The only catch is that we have to pass in a query string to the constructor, or sequences like Maps and arrays with key-value pairs as an array with key as the first element and the value as the second element. See Part 1 for more details on how to construct URLSearchParams objects.

Categories
JavaScript

Using the URLSearchParams Object in JavaScript — Part 1

Working with query strings can be a pain if we have to write the code from scratch to parse and modify it. Fortunately, most recent browsers have the URLSearchParams object that can let us deal with query strings easily. With it, is makes it easy to parse and manipulate query strings. We no longer need a third party library or write code from scratch to deal with query strings anymore. In the part 1 of this guide, we will look at how to create URLSearchParams objects and look at a few handy methods which let us get and set key-value pairs of a query string.

Create URLSearchParam Objects

We can create a URLSearchParams object with the URLSearchParams constructor. The URLSearchParams constructor takes one optional argument, which is a USVString argument that has the query string. USVString objects correspond to the set of all possible sequences of Unicode scalar values. In our code, we can treat them the same as regular strings. It can be a sequence of USVStrings or a record that contains USVStrings. In our code, we don’t have to concerned with USVStrings. For all practical purposes, they’re treated as like strings. We can construct a URLSearchParams object like the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
console.log(params.get('key1'));
console.log(params.get('key2'));

With the code above, we create a URL object, then we get the query string with the search property of the URL object, then we pass that into the URLSearchParams constructor. Then we can use the URLSearchParams object to get the value of the key using the get method of the URLSearchParams object. The console.log statements above should get us the values 1 and 2 respectively.

We can also constructor a URLSearchParams object using an array with entries that are arrays with the key as the first element and the value as the second element. For example, we can construct a URLSearchParams objects with arrays like in the following code:

const params = new URLSearchParams([
  ["key1", 1],
  ["key2", 2]
]);
console.log(params.get('key1'));
console.log(params.get('key2'));
console.log(params.toString());

With the code above, we pass in an array with the key and value in the entries. With the toString() instance method, we can get the full query string easily. The console.log statements above should get us the values 1 and 2 respectively with the first 2 statements and the last one should get us key1=1&key2=2 . Creating query strings have never been easier with this object.

A third way to construct a URLSearchParams object is with a plain JavaScript object passed into the constructor. The keys would be in the keys of the object, and the values are in the values of the object. For example, we can construct a URLSearchParams object like in the following code:

const params = new URLSearchParams({key1: 1, key2: 2});
console.log(params.get('key1'));
console.log(params.get('key2'));
console.log(params.toString());

The console.log statements above should get us the values 1 and 2 respectively with the first 2 statements and the last one should get us key1=1&key2=2 . Creating query strings have never been easier with this object.

We can also pass in Maps into the constructor and get back a query string. With Maps, we can pass it in like in the following code:

const params = new URLSearchParams(new Map([['key1', 1], ['key2', 2]]));
console.log(params.get('key1'));
console.log(params.get('key2'));
console.log(params.toString());

We should get the same output as the earlier examples with the console.log statements above.

After constructing a URLSearchParams object, we can iterate through it since it’s an array like object. An array like object are objects that have an iterator method included. The method is identified with the Symbol.iterator Symbol. We can iterate through the entries of an array like objects with the for...of loop. For example, if we have the following URLSearchParams object:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
for (const [key, value] of params) {
  console.log(key, value);
}

Then we get back the following console.log outputs:

key1 1  
key2 2

As we can see, we get back all the query string keys and values with with for...of loop. This is another reason to use the URLSearchParams object to parse query strings. It is an iterable object that lets us get the keys and values easily. This feature is even more useful is we have a long list of query string key-value pairs. Alternatively, we can use the entries method, which is an instance method of an URLSearchParams object. to get back the key-value pairs of a query string, like we do in the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
for (const [key, value] of params.entries()) {
  console.log(key, value);
}

We should get back the same output as the first example with where we loop through the params object directly with the for...of loop.

Methods

Each URLSearchParams instance has multiple methods available to it to get the keys and values and to manipulate them by adding, changing and removing keys or values. With these methods, we can construct query strings easily without having to do string manipulating directly. If we want to get back the full query string after manipulating, we can get it with the toString() method.

append

The append method lets us add an key-value pair to the URLSearchParams object as a new search parameter. The method takes 2 arguments. One for the key name and one for the value. Both arguments will be converted to strings when we pass them in. We can append the same key multiple times with or without the same corresponding value. For example, we can use the append method like in the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 1);
params.append('key1', 2);
console.log(params.toString());

With the code above, we should get back key1=1&key2=2&key1=1&key1=2 from the console.log statement we have in the code above. As we can see, we can append the same key-value pair as many times as we want and the append method will not attempt to merge them together.

delete

The delete method of the URLSearchParams object will let us delete the key-value pair with the given key. If there’s more than one with the same key, then they’ll all be deleted. If takes one argument, which is the key name as a string. For example, we can write the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key1', 1);
params.append('key1', 2);
params.delete('key1');
console.log(params.toString());

If we run console.log above like we did on the last of the code above, we get back key2=2 . This means that all the key-value pairs with key name key1 were removed from the URLSearchParams object and the corresponding query string.

entries

The entries method gets the key-value pairs from the URLSearchParams object. It takes no arguments and returns an iterator that let us iterate through the key-value pairs. The key-value pairs will be in the form of an array with the key as the first element and the corresponding value as the second element. For example, we can use the entries method like in the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
for (const [key, value] of params.entries()) {
  console.log(key, value);
}

In the code above, we used the destructuring assignment syntax to decompose the key-value array into their own variables. This is a great way to access the keys and values since we don’t have to use the indexes or assign them to variables with our own code.

forEach

The forEach method let us iterate the values directly without using the entries method. It takes a callback function where can can access the key and value of each URLSearchParams entry. The callback function has the value as the first parameter and the key as the second parameter. For example, if we have the following code:

const url = new URL('https://example.com?key1=1&key2=2');
const params = new URLSearchParams(url.search);
params.append('key3', 3);
params.append('key4', 4);
params.append('key5', 5);
params.forEach((value, key) => {
  console.log(key, value);
})

When we run the code above, we should get the following output from the console.log statements:

key1 1  
key2 2  
key3 3  
key4 4  
key5 5

It’s important to note the value parameter comes before the key, so we don’t reverse them like in most other places.

There are more methods in the URLSearchParams object, we will continue the list of methods and look at the caveats for using the URLSearchParams objects in part 2 of this guide.

Most recent browsers have the URLSearchParams object that can let us deal with query strings easily. Now working with query strings is no longer a pain since we don’t have to write the code from scratch to parse and modify it. With URLSearchParams objects, is makes it easy to parse and manipulate query strings. We no longer need a third party library or write code from scratch to deal with query strings anymore. There are more methods in the URLSearchParams object and some caveats when using it, so stay tuned for Part 2.

Categories
JavaScript JavaScript Basics

How to Merge Arrays in JavaScript

Oftentimes, we need to combine multiple arrays into one array. With multiple arrays, combining them is pain without some help from JavaScript’s standard libraries. With ES6, there’s also the spread syntax to help us combine arrays.

There are a few ways to merge arrays in JavaScript.

Array.concat

We can all Array.concat on an array to combine 2 arrays and return the new one. For example:

const a = [1,2,3];
const b = [4,5];
const c = a.concat(b) // [1,2,3,4,5]

It also works with multiple arrays. You can pass in as many arrays as you want in the arguments of the concat function. For example:

const a = [1,2,3];
const b = [4,5];
const c = [6,7,8];
const d = a.concat(b, c) // [1,2,3,4,5,6,7]

Array.push

We can push elements of one array into another.

const a = [1,2,3];
const b = [4,5];
let c = Object.assign([], a);
for (let i = 0; i < b.length; i++){
  c.push(b[i]);
}
console.log(c); // [1,2,3,4,5]

What we did is make a copy of a and assigned it to c , then pushed the elements of b by looping through it and adding them to the end of c .

You can use the apply function available with all objects to combine 2 arrays. For example:

const a = [1,2];
const b = [3,4];
a.push.apply(a, b);
console.log(a); // [1,2,3,4];

Spread Operator

With ES6 or later, we can use the spread operator to spread the items from another array into a new array by doing the following:

const a = [1,2,3];
const b = [4,5];
const c = [...a, ...b];
console.log(c); // [1,2,3,4,5]
Categories
JavaScript React

How to Make a Calendar App with React

For many applications, recording dates is an important feature. Having a calendar is often a handy feature to have. Fortunately, many developers have made calendar components that other developers can easily add to their apps.

React has many calendar widgets that we can add to our apps. One of them is React Big Calendar. It has a lot of features. It has a month, week, and day calendar. Also, you can navigate easily to today or any other days with back and next buttons. You can also drag over a date range in the calendar to select the date range. With that, you can do any manipulation you want with the dates.

In this article, we will make a simple calendar app where users can drag over a date range and add a calendar entry. Users can also click on an existing calendar entry and edit the entry. Existing entries can also be deleted. The form for adding and editing the calendar entry will have date and time pickers to select the date and time.

We will save the data on the back end in a JSON file.

We will use React to build our app. To start, we run:

npx create-react-app calendar-app

to create the project.

Next we have to install a few packages. We will use Axios for HTTP requests to our back end, Bootstrap for styling, MobX for simple state management, React Big Calendar for our calendar component, React Datepicker for the date and time picker in our form , and React Router for routing.

To install them, we run:

npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom

With all the packages installed, we can start writing the code. First, we replace the existing code in App.js with:

import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "react-datepicker/dist/react-datepicker.css";
const history = createHistory();
function App({ calendarStore }) {
  return (
    <div>
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Calendar App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} calendarStore={calendarStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;

We add the React Bootstrap top bar in here with a link to the home page. Also, we add the route for the home page in here with the MobX calendarStore passed in.

Also, we import the styles for the date picker and calendar here so that we can use them throughout the app.

Next in App.css , replace the existing code with:

.page {
  padding: 20px;
}
.form-control.react-datepicker-ignore-onclickoutside,
.react-datepicker-wrapper {
  width: 465px !important;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header,
.react-datepicker__day-name,
.react-datepicker__day,
[class^="react-datepicker__day--*"],
.react-datepicker__time-list-item {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
    "Droid Sans", "Helvetica Neue", sans-serif;
}

to add some padding to our page, change the width of the datepicker input and change the font of the datepicker.

Next create a file called CalendarForm.js in the src folder and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import DatePicker from "react-datepicker";
import Button from "react-bootstrap/Button";
import {
  addCalendar,
  editCalendar,
  getCalendar,
  deleteCalendar
} from "./requests";
import { observer } from "mobx-react";
const buttonStyle = { marginRight: 10 };
function CalendarForm({ calendarStore, calendarEvent, onCancel, edit }) {
  const [start, setStart] = React.useState(null);
  const [end, setEnd] = React.useState(null);
  const [title, setTitle] = React.useState("");
  const [id, setId] = React.useState(null);
React.useEffect(() => {
    setTitle(calendarEvent.title);
    setStart(calendarEvent.start);
    setEnd(calendarEvent.end);
    setId(calendarEvent.id);
  }, [
    calendarEvent.title,
    calendarEvent.start,
    calendarEvent.end,
    calendarEvent.id
  ]);
const handleSubmit = async ev => {
    ev.preventDefault();
    if (!title || !start || !end) {
      return;
    }
if (+start > +end) {
      alert("Start date must be earlier than end date");
      return;
    }
    const data = { id, title, start, end };
    if (!edit) {
      await addCalendar(data);
    } else {
      await editCalendar(data);
    }
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
  const handleStartChange = date => setStart(date);
  const handleEndChange = date => setEnd(date);
  const handleTitleChange = ev => setTitle(ev.target.value);
const deleteCalendarEvent = async () => {
    await deleteCalendar(calendarEvent.id);
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    onCancel();
  };
return (
    <Form noValidate onSubmit={handleSubmit}>
      <Form.Row>
        <Form.Group as={Col} md="12" controlId="title">
          <Form.Label>Title</Form.Label>
          <Form.Control
            type="text"
            name="title"
            placeholder="Title"
            value={title || ""}
            onChange={handleTitleChange}
            isInvalid={!title}
          />
          <Form.Control.Feedback type="invalid">{!title}</Form.Control.Feedback>
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="start">
          <Form.Label>Start</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={start}
            onChange={handleStartChange}
          />
        </Form.Group>
      </Form.Row>
<Form.Row>
        <Form.Group as={Col} md="12" controlId="end">
          <Form.Label>End</Form.Label>
          <br />
          <DatePicker
            showTimeSelect
            className="form-control"
            selected={end}
            onChange={handleEndChange}
          />
        </Form.Group>
      </Form.Row>
      <Button type="submit" style={buttonStyle}>
        Save
      </Button>
      <Button type="button" style={buttonStyle} onClick={deleteCalendarEvent}>
        Delete
      </Button>
      <Button type="button" onClick={onCancel}>
        Cancel
      </Button>
    </Form>
  );
}
export default observer(CalendarForm);

This is the form for adding and editing the calendar entries. We add the React Bootstrap form in here by adding the Form component. The Form.Control is also from the same library. We use it for the title text input.

The other 2 fields are the start and end dates. We use React Datepicker in here to let use select the start and end dates of a calendar entry. In addition, we enable the time picker to let users pick the time.

There are change handlers in each field to update the values in the state so users can see what they entered and let them submit the data later. The change handlers are handleStartChange , handleEndChange and handleTitleChange . We set the states with the setter functions generated by the useState hooks.

We use the useEffect callback to set the fields in the calendarEvent prop to the states. We pass all the fields we want to set to the array in the second argument of the useEffect function so that the states will be updated whenever the latest value of the calendarEvent prop is passed in.

In the handleSubmit function, which is called when the form Save button is clicked. we have to call ev.preventDefault so that we can use Ajax to submit our form data.

If data validation passes, then we submit the data and get the latest and store them in our calendarStore MobX store.

We wrap observer outside the CalendarForm component so that we always get the latest values from calendarStore .

Next we create our home page. Create a HomePage.js file in the src folder and add:

import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import Modal from "react-bootstrap/Modal";
import CalendarForm from "./CalendarForm";
import { observer } from "mobx-react";
import { getCalendar } from "./requests";
const localizer = momentLocalizer(moment);
function HomePage({ calendarStore }) {
  const [showAddModal, setShowAddModal] = React.useState(false);
  const [showEditModal, setShowEditModal] = React.useState(false);
  const [calendarEvent, setCalendarEvent] = React.useState({});
  const [initialized, setInitialized] = React.useState(false);
  const hideModals = () => {
    setShowAddModal(false);
    setShowEditModal(false);
  };
  const getCalendarEvents = async () => {
    const response = await getCalendar();
    const evs = response.data.map(d => {
      return {
        ...d,
        start: new Date(d.start),
        end: new Date(d.end)
      };
    });
    calendarStore.setCalendarEvents(evs);
    setInitialized(true);
  };
  const handleSelect = (event, e) => {
    const { start, end } = event;
    const data = { title: "", start, end, allDay: false };
    setShowAddModal(true);
    setShowEditModal(false);
    setCalendarEvent(data);
  };
  const handleSelectEvent = (event, e) => {
    setShowAddModal(false);
    setShowEditModal(true);
    let { id, title, start, end, allDay } = event;
    start = new Date(start);
    end = new Date(end);
    const data = { id, title, start, end, allDay };
    setCalendarEvent(data);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCalendarEvents();
    }
  });
  return (
    <div className="page">
      <Modal show={showAddModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Add Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={false}
          />
        </Modal.Body>
      </Modal>
      <Modal show={showEditModal} onHide={hideModals}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Calendar Event</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <CalendarForm
            calendarStore={calendarStore}
            calendarEvent={calendarEvent}
            onCancel={hideModals.bind(this)}
            edit={true}
          />
        </Modal.Body>
      </Modal>
      <Calendar
        localizer={localizer}
        events={calendarStore.calendarEvents}
        startAccessor="start"
        endAccessor="end"
        selectable={true}
        style={{ height: "70vh" }}
        onSelectSlot={handleSelect}
        onSelectEvent={handleSelectEvent}
      />
    </div>
  );
}
export default observer(HomePage);

We get the calendar entries and populate them in the calendar here. The entries are retrieved from back end and then saved into the store. In the useEffect callback, we set get the items when the page loads. We only do it when initialized is false so we won’t be reloading the data every time the page renders.

To open the modal for adding calendar entries, we set the onSelectSlot prop with our handler so that we can call setShowAddModal and setCalendarEvent to set open the modal and set the dates before opening the add calendar event modal.

Similarly, we set the onSelectEvent modal with the handleSelectEvent handler function so that we open the edit modal and set the calendar event data of the existing entry.

Each Modal have the CalendarForm component inside. We pass in the function for closing the modals into the form so that we can close them from the form. Also, we pass in the calendarStore and calendarEvent so that they can be manipulated in the CalendarForm.

We wrap observer outside the CalendarForm component so that we always get the latest values from calendarStore .

Next in index.js , we replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { CalendarStore } from "./store";
const calendarStore = new CalendarStore();
ReactDOM.render(
  <App calendarStore={calendarStore} />,
  document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

so that we can pass in the MobX calendarStore into the root App component.

Next create a requests.js file in the src folder and add:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getCalendar = () => axios.get(`${APIURL}/calendar`);
export const addCalendar = data => axios.post(`${APIURL}/calendar`, data);
export const editCalendar = data =>
  axios.put(`${APIURL}/calendar/${data.id}`, data);
export const deleteCalendar = id => axios.delete(`${APIURL}/calendar/${id}`);

These are the functions for making the HTTP calls to manipulate the calendar entries.

Next createstore.js in the src folder and add:

import { observable, action, decorate } from "mobx";
class CalendarStore {
  calendarEvents = [];
setCalendarEvents(calendarEvents) {
    this.calendarEvents = calendarEvents;
  }
}
CalendarStore = decorate(CalendarStore, {
  calendarEvents: observable,
  setCalendarEvents: action
});
export { CalendarStore };

to save the items in the store for access by all of our components.

Next in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      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>Calendar App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and rename the title.

Now all the hard work is done. All we have to do is use JSON Server NPM package located at https://github.com/typicode/json-server for our back end.

Install it by running:

npm i -g json-server

Then run it by running:

json-server --watch db.json

In db.json , replace the existing content with:

{  
  "calendar": []  
}

Next we run our app by running npm start in our app’s project folder and when the program asks you to run in a different port, select yes.